Home / Alt manpages / dash(1)

  • dash(1)
  • User command
  • linux

Write /bin/sh Scripts That Behave Under dash

Your script runs fine on your laptop, then dies on Ubuntu because sh there is dash, not Bash. This guide ends with a small, testable /bin/sh script that handles arguments safely and fails loudly, on a system where sh resolves to dash from package version 0.5.12-6ubuntu5.

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

A script whose first line is #!/bin/sh should stick to portable shell language, not features copied from Bash. dash provides POSIX shell features plus some extensions, but the installed man page says outright 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 a clear entry point

Make a private working directory and write a script that accepts an input path and an output path. The first line picks 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
  • Quoted here-document delimiter. It keeps the script text literal while it is created.
  • Executable bit. It is ordinary file state, so it does not need sudo.
  • The trap. It 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 want those things. The brackets below are part of the command, and the quoted expansion stays 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.

The exceptions catch people out: a command used as an if condition, or on the left side of && or ||, counts as tested by -e.

So 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. Redirect without clobbering 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 write to a temporary output, then rename it deliberately:

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=

Warning

mv changes the destination, and replacing an existing path may be irreversible. Confirm $output is the exact intended path before using this pattern.

Tip

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

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.

Warning

A trace can expose passwords, tokens and private file names. Remove tracing before sharing logs or deploying the script.

Now run the script once with a missing argument, to check 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 give a non-zero result under set -u. Check the status, not the exact path in the message.

Done means

  • Shell identified. sh was checked on the target host, not assumed to be Bash.
  • Portable entry point. The script has a #!/bin/sh first line and uses portable shell syntax.
  • Everything quoted. Input paths and values are quoted, including arguments passed to dash -c.
  • Failures handled. Expected failures are tested, and unexpected ones return a useful non-zero status.
  • Syntax checked. You ran dash -n and removed any tracing before sharing output.
  • Clean up done. Temporary files are gone, and destructive redirections were treated as deliberate changes.