sh and bash
sh and bash
The shell most often encountered on Unix-flavored systems is bash. Its roots trace back to sh (Bourne shell).
1. The relationship between sh and bash
- sh (Bourne shell) — the shell Stephen Bourne built in 1977 for Unix V7. Later POSIX standardized sh against it.
- bash (Bourne Again Shell) — the sh-compatible shell Brian Fox wrote for the GNU project in 1989. A superset of POSIX sh, with extensions like arrays, associative arrays, and
[[ ]].
/bin/sh points at different actual shells depending on the system. Some Linux distros wire it to dash (a lightweight POSIX sh), others wire it to bash's sh-compatible mode. On macOS, /bin/sh is bash's sh mode.
The same script behaves differently. Starting it with #!/bin/sh allows only POSIX features, while #!/bin/bash or #!/usr/bin/env bash enables bash extensions.
2. Shebang and execute permission
The first line #! of a script file is the shebang. When the script runs, the kernel reads this line to decide which interpreter handles the file.
#!/usr/bin/env bash
echo "hello"
The /usr/bin/env bash form lets the system find bash flexibly when its absolute path differs (/bin/bash vs /usr/local/bin/bash).
| Task | macOS · Linux | Windows (Git Bash · WSL) |
|---|---|---|
| Grant permission | chmod +x script.sh |
(same inside WSL · Git Bash) |
| Run | ./script.sh |
./script.sh or bash script.sh |
| Invoke interpreter directly | bash script.sh |
bash script.sh |
Calling the interpreter directly with bash script.sh works even without the executable bit.
3. Variables and conditionals
NAME="world"
echo "hello, $NAME"
echo "hello, ${NAME}!"
No spaces around =. NAME = "world" is parsed as a command invocation and errors out.
if [ -f config.json ]; then
echo "found"
elif [ -d config ]; then
echo "directory"
else
echo "not found"
fi
In bash the [[ ... ]] form is also available. [[ ]] does no word splitting or globbing, which is safer, but it does not exist in POSIX sh.
if [[ "$NAME" == "world" ]]; then
echo "match"
fi
4. Loops and functions
for f in *.txt; do
echo "$f"
done
i=0
while [ $i -lt 5 ]; do
echo $i
i=$((i + 1))
done
greet() {
local name="$1"
echo "hello, $name"
}
greet "alice"
local is a bash extension and absent from POSIX sh. In strict POSIX scope every variable is global.
5. Pipes and redirection
ls -la | grep "\.md$" # send stdout into the next command
echo "log entry" >> app.log # append
command 2> errors.txt # redirect stderr
command > out.txt 2>&1 # both into one place
command 2>/dev/null # discard errors
6. set -euo pipefail
A line that often appears at the top of scripts.
| Option | Meaning |
|---|---|
-e |
Exit immediately if any command exits with non-zero. |
-u |
Error on referencing undefined variables. |
-o pipefail |
Fail the whole pipeline if any stage in it fails. |
Without these, a mid-pipeline failure can leave the script running on broken state. That said, -e also catches some legitimately non-zero exits (for example, grep returning 1 when it finds nothing), so handle those explicitly with || true.
#!/usr/bin/env bash
set -euo pipefail
count=$(grep -c "ERROR" app.log || true)
echo "errors: $count"
7. dash vs bash incompatibility traps
Debian and Ubuntu wiring /bin/sh to dash is a frequent source of breakage. Common cases where a bash-friendly script fails under dash:
[[ ... ]]does not exist in dash. Only[ ... ].- Arrays (
arr=(a b c)) do not exist in dash. - The
functionkeyword infunction name() { }does not exist in dash. Onlyname() { }. <<<(here-string) does not exist in dash.- The
==comparison is not POSIX. Only=is safe. localexists in dash but is not in the POSIX standard.
If a script uses bash extensions, declare it with #!/usr/bin/env bash. If staying in POSIX is the intent, write #!/bin/sh and validate against dash once.
8. Invocation in both environments
# macOS · Linux
#!/usr/bin/env bash
set -euo pipefail
chmod +x deploy.sh
./deploy.sh
# Windows (Git Bash · WSL)
bash deploy.sh
# or WSL
wsl bash deploy.sh
Double-clicking or invoking .sh directly from cmd.exe or PowerShell does not work. The shell itself is not bash.
9. Common pitfalls
Forgetting to quote variables — rm $FILE works (and ends in an error) when $FILE is empty, but with whitespace it deletes unintended files. Always quote with "$FILE".
== vs = — both work inside [[ ]], but [ ] only accepts = under POSIX.
Using > or < for numeric comparison — [ $a > $b ] is redirection, not comparison. Use -gt, -lt.
A .sh saved with Windows line endings (CRLF) failing because of \r — fix with dos2unix or git's text eol=lf setting.
Forgetting command || true under set -e and exiting unexpectedly.
Closing thoughts
Once a script settles on shebang + set -euo pipefail + variable quoting + eol=lf, OS differences barely show up. Running it once under strict dash catches compatibility surprises early. ShellCheck is also a great starting point.
Next
- powershell-basics
- cmd-and-bat
GNU Bash Reference Manual · POSIX Shell · BashGuide · ShellCheck · Dash manual for reference.