Write Reliable /bin/sh Scripts with dash
You will finish with a small, testable /bin/sh script that behaves predictably under dash, including safe argument handling and visible failure checks. On this system, sh resolves to dash from package version 0.5.12-6ubuntu5.
The route
Jump straight to the step you need, or tick off Done means at the end.
Allow about fifteen minutes. You need a normal user account, a terminal and a temporary directory. No elevated privileges are required. The examples create only files under /tmp and remove them at the end.
1. Confirm which shell you are running
Do not infer a shell from its prompt. Check both the link and the installed package:
$ command -v sh
/usr/bin/sh
$ readlink -f /usr/bin/sh
/usr/bin/dash
$ dpkg-query -W -f='${Package} ${Version}\n' dash
dash 0.5.12-6ubuntu5
The sh name is an interface choice. A script whose first line is #!/bin/sh should use the portable shell language, not features copied from Bash. dash provides POSIX shell features plus some extensions, but the installed man page explicitly says that it is not a Korn shell clone.
Checkpoint: if /bin/sh points somewhere else on another host, test there before relying on dash-specific details.
2. Create a script with an unambiguous entry point
Make a private working directory and write a script that accepts one input path. The first line selects the interpreter when the file is executed directly:
$ work=$(mktemp -d)
$ trap 'rm -rf "$work"' EXIT HUP INT TERM
$ cat >"$work/report.sh" <<'EOF'
#!/bin/sh
set -eu
input=$1
output=$2
if [ ! -r "$input" ]; then
printf 'cannot read: %s\n' "$input" >&2
exit 1
fi
lines=$(wc -l <"$input")
printf 'lines: %s\n' "$lines" >"$output"
EOF
$ chmod 755 "$work/report.sh"
$ printf '%s\n' alpha beta >"$work/input.txt"
$ "$work/report.sh" "$work/input.txt" "$work/result.txt"
$ cat "$work/result.txt"
lines: 2
The quoted here-document delimiter keeps the script text literal while it is created. The executable bit is ordinary file state, so it does not require sudo. The trap removes the temporary directory when the shell exits, including after an interrupt where dash can run the trap.
3. Keep data separate from shell syntax
dash splits unquoted expansions and performs pathname expansion. Quote variables unless you deliberately need those operations. The brackets below are part of the command, and the quoted expansion remains one argument even when the file name contains spaces:
if [ -r "$input" ]; then
printf 'reading %s\n' "$input"
fi
Single quotes preserve every character until the next single quote. Double quotes preserve most characters but still allow parameter expansion, command substitution and arithmetic expansion. A backslash preserves the next character, except that backslash followed by a newline continues the input line.
Never build a command by concatenating untrusted text and passing it to sh -c. If you need a shell command, pass fixed code as the command string and data as positional parameters:
$ dash -c 'printf "name=%s\n" "$1"' dash 'file with spaces.txt'
name=file with spaces.txt
The first argument after the command string becomes $0, which is why the example supplies dash before the data argument.
4. Make failure behaviour explicit
The example uses set -e and set -u. In a non-interactive shell, -e exits when an untested command fails, while -u reports an unset variable and exits. Their exceptions can surprise readers: a command used as an if condition, or on the left side of && or ||, is considered tested by -e.
Do not use -e as a substitute for checking important results. Capture and report the status when the distinction matters:
if output=$(some_command); then
printf '%s\n' "$output"
else
status=$?
printf 'some_command failed with status %s\n' "$status" >&2
exit "$status"
fi
For a command whose failure is expected, test it directly rather than hiding it behind an unconditional success:
if grep -q 'enabled' "$input"; then
printf '%s\n' 'feature is enabled'
else
status=$?
if [ "$status" -eq 1 ]; then
printf '%s\n' 'feature is not enabled'
else
printf 'grep failed with status %s\n' "$status" >&2
exit "$status"
fi
fi
5. Use redirection without overwriting the wrong file
The shell opens redirections before it runs the command. A plain > truncates an existing file, so treat it as a destructive operation. The earlier example writes only inside a directory created by mktemp. For a real destination, check the path and use a temporary output followed by a deliberate rename:
temporary_output=$(mktemp "${output}.XXXXXX")
trap 'rm -f "$temporary_output"; rm -rf "$work"' EXIT HUP INT TERM
printf 'lines: %s\n' "$lines" >"$temporary_output"
mv "$temporary_output" "$output"
temporary_output=
mv changes the destination, and replacing an existing path may be irreversible. Confirm $output is the exact intended path before using this pattern. If you want dash to refuse an existing target for ordinary > redirection, enable set -C, also called noclobber; >| overrides it.
6. Test syntax, tracing and arguments safely
Before running a changed script, ask dash to parse it without executing commands:
$ dash -n "$work/report.sh"
$ dash -c 'printf "status=%s\n" "$?"'
status=0
For a trace, use dash -x or add set -x temporarily. Trace output goes to standard error and can expose passwords, tokens and private file names. Remove tracing before sharing logs or deploying the script.
Run the script once with a missing argument to verify that its contract fails clearly:
$ "$work/report.sh" "$work/input.txt" 2>"$work/error.txt" || status=$?
$ printf 'status=%s\n' "${status:-0}"
status=2
$ sed -n '1p' "$work/error.txt"
/tmp/.../report.sh: 4: 2: parameter not set
The temporary path and diagnostic wording can vary, but a missing required positional parameter should produce a non-zero result under set -u. Check the status, not the exact path in the message.
Done means
shwas identified on the target host, rather than assumed to be Bash.- The script has a
#!/bin/shentry point and uses portable shell syntax. - Input paths and values are quoted, including arguments passed to
dash -c. - Expected failures are tested and unexpected failures return a useful non-zero status.
- Syntax was checked with
dash -n, and any tracing was removed before sharing output. - Temporary files are cleaned up, and destructive redirections were treated as deliberate changes.