codingstairs
NotesEDULifeContact
⌕Search⌘K
koen

Navigation

  • Intro
  • Blog
  • Life

Get in touch

Send without signing in. Add your email if you'd like a reply.

  • Leave a message anonymously →
  • ✉ warragon112@gmail.com
  • KakaoTalk Open Chat ↗

© 2026 codingstairs

  • Notes
  • EDU
  • Search
  • Life
  • Contact
  • Legal
  • RSS
  • GitHub
Notes›environment

sh and bash

Published 2026-04-28· Updated 2026-05-18·0 views

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 function keyword in function name() { } does not exist in dash. Only name() { }.
  • <<< (here-string) does not exist in dash.
  • The == comparison is not POSIX. Only = is safe.
  • local exists 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.

More in environment

All in this category →
  • WSL2 — Linux on top of Windows
  • Data formats — JSON · YAML · TOML · XML
  • First day with the terminal
  • Text encoding and line endings
  • Markdown
  • Cross-platform scripts