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.
- You are working in a controlled environment where you can ensure Bash is located at
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)"
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