Skip to main content

Shell

Guide to writing good shell scripts.

Shebang

Put at the first line of the script. It tells the system which interpreter to use.

  • Bash: #!/bin/bash or #!/usr/bin/env bash
  • POSIX: #!/bin/sh

If you meet any of the following criteria, use POSIX:

  • Executing on a minimal environment
  • Bash is not available
  • You want to ensure high portability (Think again if you really need this)

Otherwise, use Bash.

Bash-only features

  • Arrays
  • Associative arrays
  • [[
  • ((
  • declare
  • local
  • source

env or bash?

  • Use #!/usr/bin/env bash if:
    • You need your script to be portable and run on various systems.
    • You don't have control over the environment where the script will be executed.
  • Use #!/bin/bash if:
    • You are working in a controlled environment where you can ensure Bash is located at /bin/bash.
    • You need the slight performance benefit of directly invoking the interpreter without using env.

set flags

Most commonly used flags:

  • -x: Print commands before executing
  • -e: Exit on error
  • -u: Exit on unset variable
  • -o pipefail: Exit on pipe fail

TL;DR: Use this starter template:

#!/bin/bash
set -xeuo pipefail

after script is done, you can remove the -x flag.

Bracketing

TODO

Piping remote script

curl

bash -c "$(curl -fsSL https://init.tomy.tech)"

wget

bash -c "$(wget -O - https://example.com)"
danger

Don't ever curl | bash or wget -O - | bash. Malicious server could potentially change the script if they observe the response is being piped to bash.

The above examples are safe because the script is fetched first and then executed.

Tips

Don't use cd in scripts

Refrain from using cd in scripts. Because:

  • It makes the script harder to understand and maintain
  • The script becomes less portable and highly dependent on the environment

If you really need to change the directory, use subshell and handle missing directory error:

(
cd /path/to/dir || exit
# do something
)

Use mktemp for temporary files

tempfile=$(mktemp)
echo "Hello, world!" > "${tempfile}"

Don't read and write to the same file

Beacuse it will be cleared before the read operation is done.

head -1 some-file > some-file

Use pipe:

head -1 some-file | sponge some-file # `sponge` is from moreutils, or use `tee`

or temporary file:

head -1 some-file > some-file.tmp
mv some-file.tmp some-file

References