1 of 4

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.

  • bash
  • chmod
  • bash -n
  • bash -x
  • $1
  • $#
  • $?
  • exit

40 min5 exercises

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

VariableHolds
$0The 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.

  1. Warm-up

    Make a script that runs

    Create ~/lab/hello.sh that prints Hello, 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.sh prints Hello, world. Without the execute bit you get Permission denied.

    Hint

    The first line is #!/bin/bash. chmod +x adds 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
  2. Practice

    Greet the name you are given

    Change the script so ./hello.sh Ali prints Hello, Ali, and with no argument it prints Hello, world. It should also print how many arguments it received.

    Check your result

    ./hello.sh prints Hello, world and args: 0. ./hello.sh Ali extra prints Hello, Ali and args: 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
  3. Practice

    Fail loudly when called wrongly

    Write ~/lab/copy2.sh that 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 prints copy SOURCE to DEST. Prove the exit status with echo $?.

    Check your result

    ./copy2.sh a prints Usage: ./copy2.sh SOURCE DEST and echo $? shows 2. ./copy2.sh a b prints copy a to b and echo $? shows 0.

    Hint

    if [ $# -ne 2 ]; then echo "..." >&2; exit 2; fi. Redirecting with >&2 sends 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 $?
  4. Practice

    Check and trace a script

    Check copy2.sh for 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 final fi, and read the error.

    Check your result

    bash -n copy2.sh prints nothing when the script is valid. bash -x ./copy2.sh a b shows lines starting with +. Without fi, bash -n reports syntax error: unexpected end of file.

    Hint

    bash -n FILE parses only. bash -x FILE ARGS prints 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
  5. Challenge

    Handle any number of arguments

    Write ~/lab/count.sh that 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 with n=$((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