Shell ScriptingLesson 1 of 4
Your first script: shebang, arguments and exit status
Turn commands you type into a file you can run, give it inputs with $1 and $2, and make it report success or failure.
bashchmodbash -nbash -x$1$#$?exit
After this lesson you can
- Create an executable script with a shebang line and run it by path.
- Read arguments with $1, $2, $# and $@, and give a default when one is missing.
- Set and read an exit status, and debug a script with bash -n and bash -x.
The idea
A shell script is a text file of commands that Bash runs in order. Anything you type often, or must type identically on ten servers, belongs in a script. What separates a script from a pile of commands is three things: it can be run by name, it takes inputs from its arguments, and it reports success or failure through its exit status.
The first line, the shebang, tells the kernel which program should interpret the file:
#!/bin/bash
The file also needs the execute permission (chmod +x), and you run it by path, ./script.sh, because the current directory is not on your PATH (a good thing, on purpose).
Special variables you will use constantly
| Variable | Holds |
|---|---|
$0 | The name the script was called by |
$1, $2, … | The first, second, … argument |
$# | How many arguments were given |
"$@" | All arguments, each kept as a separate word |
$? | The exit status of the last command: 0 is success |
Every command returns a number when it finishes. Zero means success, anything else means failure. exit N ends your script with that number, so the caller (another script, cron, a monitoring tool) can tell whether it worked. Convention: 1 for general failure, 2 for wrong usage.
Try it
mkdir -p ~/lab && cd ~/lab
cat > hello.sh <<'EOT'
#!/bin/bash
name="${1:-world}"
echo "Hello, $name"
echo "Script: $0, args: $#"
EOT
chmod +x hello.sh
./hello.sh
./hello.sh Ali extra
Hello, world
Script: ./hello.sh, args: 0
Hello, Ali
Script: ./hello.sh, args: 2
${1:-world} is a default value: use $1, but if it is empty or unset use world. Always put variables in double quotes ("$name") so a value with spaces stays one word.
Insist on the right inputs
cat > copy2.sh <<'EOT'
#!/bin/bash
if [ $# -ne 2 ]; then
echo "Usage: $0 SOURCE DEST" >&2
exit 2
fi
echo "copy $1 to $2"
EOT
chmod +x copy2.sh
./copy2.sh a; echo "status=$?"
./copy2.sh a b; echo "status=$?"
Usage: ./copy2.sh SOURCE DEST
status=2
copy a to b
status=0
>&2 sends the message to standard error, so it never mixes with normal output that another program may be reading. [ $# -ne 2 ] is a test: the number of arguments is not equal to 2. The next lesson covers tests properly.
Find your mistakes
bash -n copy2.sh && echo syntax-ok
bash -x ./copy2.sh a b
syntax-ok
+ '[' 2 -ne 2 ']'
+ echo 'copy a to b'
copy a to b
bash -n catches missing fi, done and unbalanced quotes before you run anything. bash -x prints each command after the shell has expanded it (lines start with +), which is the quickest way to see why a test did not do what you expected.
Going further
set -u makes the script stop when it reads an unset variable, and set -e makes it stop at the first failing command; add set -eu at the top of anything you rely on. shift discards $1 and moves the others down, which is how scripts consume options in a loop. Use $(command) rather than backticks for command substitution, and prefer printf over echo when the text might start with a dash. A script can also be run as bash script.sh without the execute bit, but then the shebang is ignored.
Practice
0 of 5 done
Do these on your own system. They get harder. Check each result, then tick it off.
- Warm-up
Make a script that runs
Create
~/lab/hello.shthat printsHello, world. Make it executable and run it by path. Then show what happens if you forget to make it executable.Check your result
~/lab/hello.shprintsHello, world. Without the execute bit you getPermission denied.Hint
The first line is
#!/bin/bash.chmod +xadds the execute bit.Solution
mkdir -p ~/lab && cd ~/lab printf '#!/bin/bash\necho "Hello, world"\n' > hello.sh ./hello.sh chmod +x hello.sh ./hello.sh - Practice
Greet the name you are given
Change the script so
./hello.sh AliprintsHello, Ali, and with no argument it printsHello, world. It should also print how many arguments it received.Check your result
./hello.shprintsHello, worldandargs: 0../hello.sh Ali extraprintsHello, Aliandargs: 2.Hint
${1:-world}means "$1, or world if it is empty".$#is the argument count.Solution
cat > ~/lab/hello.sh <<'EOT' #!/bin/bash name="${1:-world}" echo "Hello, $name" echo "args: $#" EOT ~/lab/hello.sh ~/lab/hello.sh Ali extra - Practice
Fail loudly when called wrongly
Write
~/lab/copy2.shthat needs exactly two arguments, a source and a destination. With any other count it prints a usage message to standard error and exits with status 2. Otherwise it printscopy SOURCE to DEST. Prove the exit status withecho $?.Check your result
./copy2.sh aprintsUsage: ./copy2.sh SOURCE DESTandecho $?shows2../copy2.sh a bprintscopy a to bandecho $?shows0.Hint
if [ $# -ne 2 ]; then echo "..." >&2; exit 2; fi. Redirecting with>&2sends the message to standard error.Solution
cat > ~/lab/copy2.sh <<'EOT' #!/bin/bash if [ $# -ne 2 ]; then echo "Usage: $0 SOURCE DEST" >&2 exit 2 fi echo "copy $1 to $2" EOT chmod +x ~/lab/copy2.sh ~/lab/copy2.sh a; echo $? ~/lab/copy2.sh a b; echo $? - Practice
Check and trace a script
Check
copy2.shfor syntax errors without running it, then run it with tracing on so you can see each line as it executes. Break the script on purpose by deleting the finalfi, and read the error.Check your result
bash -n copy2.shprints nothing when the script is valid.bash -x ./copy2.sh a bshows lines starting with+. Withoutfi,bash -nreportssyntax error: unexpected end of file.Hint
bash -n FILEparses only.bash -x FILE ARGSprints every command after expansion.Solution
cd ~/lab bash -n copy2.sh && echo syntax-ok bash -x ./copy2.sh a b sed '/^fi$/d' copy2.sh > broken.sh bash -n broken.sh - Challenge
Handle any number of arguments
Write
~/lab/count.shthat prints each argument on its own line, numbered from 1, and then the total. It must work correctly when an argument contains a space, as in./count.sh "two words" three. Explain why"$@"and$*behave differently here.Check your result
The output is
1: two words,2: three,total: 2. With unquoted$@the first argument would split into two.Hint
Loop over
"$@"(quoted). A counter can be an ordinary variable you increase withn=$((n+1)).Solution
cat > ~/lab/count.sh <<'EOT' #!/bin/bash n=0 for arg in "$@"; do n=$((n+1)) echo "$n: $arg" done echo "total: $n" EOT chmod +x ~/lab/count.sh ~/lab/count.sh "two words" three