Skip to main content

Shell

Guide to writing good shell scripts.

TL;DR starter template

Script

#!/usr/bin/env bash
set -xeuo pipefail

Library script

# Source guard: Exit if executed directly
(return 0 2>/dev/null) || exit 0

# Idempotent import: Prevent multiple imports
[ "${__LOG_SH_LOADED:-}" = "1" ] && return 0
__LOG_SH_LOADED=1

What shell to use?

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)
  • If you're writing a utility library script that will be sourced by different shells

Otherwise, use Bash.

Shebang line

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

  • #!/usr/bin/env bash
    • 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.
  • #!/bin/bash
    • 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.
  • #!/bin/sh
    • You're writing a POSIX-compliant script.
  • (No shebang)
    • The script is intended to be sourced by other scripts or run in an interactive shell.

Bash-only features

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

set flags

Most commonly used flags:

  • -x: Print commands before executing, you can remove it after debugging
  • -e: Exit on error
  • -u: Exit on unset variable
  • -o pipefail: Exit on pipe fail

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

Library script

  • Write with POSIX compliance, use .sh extension
  • Top of the script
    • No shebang
    • No set flags
    • Source guard: to prevent direct execution
    • Idempotent import: to prevent multiple imports
  • Use printf instead of echo

See the starter template above.

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