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.
- You are working in a controlled environment where you can ensure Bash is located at
#!/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
[[((declarelocalsource
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
.shextension - Top of the script
- No shebang
- No
setflags - Source guard: to prevent direct execution
- Idempotent import: to prevent multiple imports
- Use
printfinstead ofecho
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