NexusCS

sh (POSIX Shell)

CLI
POSIX sh scripting reference. Portable shell commands that work across all UNIX systems without bash/zsh dependencies. Essential for cross-platform scripts and containers.
featured

Getting started

What is POSIX sh?

POSIX sh is the portable shell scripting standard. Works on any UNIX system without bash/zsh dependencies.

#!/bin/sh
# NOT #!/bin/bash

set -eu  # Exit on error, undefined vars

Use sh when:

  • Writing build scripts
  • Maximum portability needed
  • Targeting minimal environments (containers, embedded)
  • CI/CD pipelines across platforms

Script header template

#!/bin/sh
#
# Script Name - Brief description
#

set -eu  # Exit on error, undefined variables

# Optional: Allow pipefail if available
set -o pipefail 2>/dev/null || true

Testing for POSIX compliance

# Validate syntax
sh -n script.sh

# Run with POSIX shell
dash script.sh    # Debian Almquist Shell
ash script.sh     # Alpine Shell

# Check for bashisms
checkbashisms script.sh
shellcheck -s sh script.sh

Variables & Expansion

Variable assignment

var="value"              # No spaces around =
readonly CONST="value"   # Constants
export PATH="/bin:$PATH" # Environment vars

Never:

var = "value"   # ❌ Syntax error
let var=5       # ❌ Bashism

Variable expansion

"$var"           # Always quote
"${var}"         # Explicit braces
"${var:-default}" # Default if unset
"${var:=default}" # Assign default
"${var:?error}"   # Error if unset
"${var:+replace}" # Replace if set

Quoting rules

"$var"           # ✅ Prevent word splitting
'$var'           # ✅ Literal (no expansion)
`command`        # ✅ Command substitution
$(command)       # ✅ Preferred command sub

$var             # ❌ Unquoted (unsafe)

Control Flow

if/then/else

if [ "$x" = "value" ]; then
    echo "match"
elif [ "$x" = "other" ]; then
    echo "other"
else
    echo "default"
fi

case statement

case "$var" in
    pattern1)
        echo "match 1"
        ;;
    pattern2|pattern3)
        echo "match 2 or 3"
        ;;
    *)
        echo "default"
        ;;
esac

while loop

while [ "$count" -lt 10 ]; do
    echo "$count"
    count=$((count + 1))
done

# Read file line-by-line
while IFS= read -r line; do
    echo "$line"
done < file.txt

for loop

# Iterate over words
for item in one two three; do
    echo "$item"
done

# Iterate over files
for file in *.txt; do
    [ -e "$file" ] || continue  # Skip if no match
    echo "$file"
done

until loop

count=0
until [ "$count" -ge 10 ]; do
    echo "$count"
    count=$((count + 1))
done

Test Operators

String tests

Test Description
[ -z "$s" ] String is empty
[ -n "$s" ] String not empty
[ "$a" = "$b" ] Strings equal
[ "$a" != "$b" ] Strings not equal

Always use = not ==

Numeric tests

Test Description
[ "$a" -eq "$b" ] Equal
[ "$a" -ne "$b" ] Not equal
[ "$a" -lt "$b" ] Less than
[ "$a" -le "$b" ] Less or equal
[ "$a" -gt "$b" ] Greater than
[ "$a" -ge "$b" ] Greater or equal

File tests

Test Description
[ -e "$f" ] Exists
[ -f "$f" ] Regular file
[ -d "$f" ] Directory
[ -L "$f" ] Symlink
[ -r "$f" ] Readable
[ -w "$f" ] Writable
[ -x "$f" ] Executable
[ -s "$f" ] Non-empty
[ "$a" -nt "$b" ] a newer than b
[ "$a" -ot "$b" ] a older than b

Logical operators

[ "$a" = "x" ] && [ "$b" = "y" ]  # AND
[ "$a" = "x" ] || [ "$b" = "y" ]  # OR
! [ "$a" = "x" ]                  # NOT

# Combine tests
[ "$a" = "x" -a "$b" = "y" ]      # AND (deprecated)
[ "$a" = "x" -o "$b" = "y" ]      # OR (deprecated)

Use [ ] not [[ ]] (bash-specific)

Functions

Basic functions

my_function() {
    echo "arg1: $1"
    echo "arg2: $2"
    echo "all args: $*"
    return 0
}

my_function "hello" "world"

Local variables

# ❌ POSIX sh has NO local keyword
my_function() {
    local var="value"  # ❌ Bashism
}

# ✅ Use naming conventions
my_function() {
    _my_function_var="value"  # Prefix with function name
}

Return values

get_value() {
    echo "result"  # Return via stdout
}

result=$(get_value)
echo "$result"

# Or use exit codes
check_status() {
    [ -f "$1" ] && return 0 || return 1
}

if check_status "file.txt"; then
    echo "exists"
fi

String Operations

Parameter expansion

"${var#pattern}"   # Remove shortest match from start
"${var##pattern}"  # Remove longest match from start
"${var%pattern}"   # Remove shortest match from end
"${var%%pattern}"  # Remove longest match from end

# Examples
file="path/to/file.txt"
"${file##*/}"      # file.txt (basename)
"${file%/*}"       # path/to (dirname)
"${file%.txt}"     # path/to/file (strip extension)
"${file##*.}"      # txt (extension only)

String length

var="hello"
echo "${#var}"     # 5

Substring extraction

# ❌ POSIX sh has NO substring syntax
"${var:0:5}"       # ❌ Bashism

# ✅ Use cut/sed instead
echo "$var" | cut -c 1-5

String replacement

# ❌ POSIX sh has NO substitution syntax
"${var/old/new}"   # ❌ Bashism
"${var//old/new}"  # ❌ Bashism

# ✅ Use sed instead
echo "$var" | sed 's/old/new/'
echo "$var" | sed 's/old/new/g'

File Operations

Reading files

# Read entire file
content=$(cat file.txt)

# Read line-by-line (preferred)
while IFS= read -r line; do
    echo "$line"
done < file.txt

# Read with custom delimiter
while IFS=: read -r user pass uid gid; do
    echo "User: $user"
done < /etc/passwd

Writing files

# Overwrite
echo "content" > file.txt
printf "content\n" > file.txt

# Append
echo "more" >> file.txt

# Heredoc
cat > file.txt << 'EOF'
Line 1
Line 2
EOF

File manipulation

# Create directory
mkdir -p path/to/dir

# Copy files
cp source dest
cp -r source_dir/ dest_dir/

# Move/rename
mv old new

# Remove files
rm file.txt
rm -rf directory/

# Create temp file
tmpfile=$(mktemp)
trap 'rm -f "$tmpfile"' EXIT

Process Management

Running commands

# Run command
command arg1 arg2

# Run in background
command &

# Wait for background jobs
wait

# Run with different exit code
command || true

# Chain commands
command1 && command2  # Run if success
command1 || command2  # Run if failure
command1; command2    # Run always

Command substitution

result=$(command)      # ✅ Preferred
result=`command`       # ✅ Alternate syntax

# Nested (prefer $())
result=$(echo "$(date)")

Exit codes

# Check last exit code
echo $?

# Set exit code
exit 0   # Success
exit 1   # Failure

# Use exit codes
if command; then
    echo "success"
else
    echo "failed"
fi

Signal handling

# Trap signals
trap 'cleanup' EXIT
trap 'echo "interrupted"' INT TERM

cleanup() {
    rm -f "$tmpfile"
    echo "cleaned up"
}

Arithmetic

POSIX arithmetic

# ✅ POSIX: $(( ))
result=$((1 + 2))
count=$((count + 1))
result=$((x * y / z))

# Operators: + - * / % ( )
# Comparisons: < > <= >= == !=

# ❌ Bashisms to AVOID
let count++           # ❌
((count++))           # ❌
result=$[1 + 2]       # ❌ Deprecated

Increment/decrement

# ✅ POSIX way
count=$((count + 1))
count=$((count - 1))

# ❌ Bashisms
let count++           # ❌
((count++))           # ❌
count+=1              # ❌

Common Bashisms to AVOID

Comparison table

❌ Bashism ✅ POSIX sh Notes
[[ ]] [ ] Always use single brackets
== = String equality
=~ case or grep Regex matching
let x++ x=$((x + 1)) Arithmetic
((x++)) x=$((x + 1)) Arithmetic
${var:0:5} cut/sed Substring
${var//x/y} sed Substitution
local Naming convention No local vars
echo -e printf Escape sequences
echo -n printf No newline
source . Source files
$'string' "string" ANSI quoting
function f() f() Function syntax
{1..10} seq 1 10 Brace expansion
**/*.txt find Globstar
read -a Multiple reads Arrays
${!var} eval Indirect expansion

[[]] vs [ ]

# ❌ Bash-specific
if [[ "$x" == "value" ]]; then
    echo "match"
fi

# ✅ POSIX
if [ "$x" = "value" ]; then
    echo "match"
fi

Arrays

# ❌ Bash arrays
arr=(one two three)    # ❌
echo "${arr[0]}"       # ❌

# ✅ POSIX: Use positional parameters
set -- one two three
echo "$1"              # one
echo "$@"              # all items

# Or use a string with delimiters
items="one:two:three"

echo vs printf

# ❌ echo is NOT portable
echo -n "no newline"   # ❌ Not POSIX
echo -e "tab\there"    # ❌ Not POSIX

# ✅ printf is portable
printf "no newline"
printf "tab\there\n"
printf "%s\n" "$var"

source vs .

# ❌ Bashism
source script.sh       # ❌

# ✅ POSIX
. script.sh            # ✅

function keyword

# ❌ Bashism
function myfunc() {    # ❌
    echo "hello"
}

# ✅ POSIX
myfunc() {             # ✅
    echo "hello"
}

Build Script Patterns

Standard build script

#!/bin/sh
set -eu

# Configuration
BUILD_DIR="build"
SRC_DIR="src"

# Clean
clean() {
    rm -rf "$BUILD_DIR"
}

# Build
build() {
    mkdir -p "$BUILD_DIR"
    # Build commands here
}

# Test
test() {
    build
    # Test commands here
}

# Main
case "${1:-build}" in
    clean)
        clean
        ;;
    build)
        build
        ;;
    test)
        test
        ;;
    *)
        echo "Usage: $0 {clean|build|test}"
        exit 1
        ;;
esac

Error handling

#!/bin/sh
set -eu

# Fail function
fail() {
    printf "Error: %s\n" "$1" >&2
    exit 1
}

# Check prerequisites
command -v make >/dev/null 2>&1 || fail "make not found"

# Validate input
[ $# -eq 1 ] || fail "Usage: $0 <arg>"
[ -f "$1" ] || fail "File not found: $1"

Detecting features

# Check if command exists
if command -v git >/dev/null 2>&1; then
    echo "git available"
fi

# Check shell features
if (set -o pipefail 2>/dev/null); then
    set -o pipefail
fi

# Platform detection
case "$(uname -s)" in
    Linux*)     OS=linux ;;
    Darwin*)    OS=macos ;;
    *)          OS=unknown ;;
esac

Parallel execution

# Run jobs in background
job1 &
job2 &
job3 &

# Wait for all
wait

# Or wait individually
job1 &
pid1=$!
job2 &
pid2=$!

wait "$pid1" || fail "job1 failed"
wait "$pid2" || fail "job2 failed"

Configuration files

# Load config if exists
if [ -f .buildrc ]; then
    . .buildrc
fi

# Set defaults
BUILD_TYPE="${BUILD_TYPE:-release}"
VERBOSE="${VERBOSE:-0}"

# Use configuration
if [ "$VERBOSE" -eq 1 ]; then
    set -x  # Enable debug output
fi

Portability Tips

Always quote

# ✅ Always quote variables
[ -f "$file" ]
echo "$var"
for item in "$@"; do

# ❌ Unquoted (word splitting)
[ -f $file ]           # ❌
echo $var              # ❌

Use command not which

# ✅ POSIX
command -v git >/dev/null 2>&1

# ❌ Not portable
which git              # ❌

Prefer printf

# ✅ Portable
printf "%s\n" "$var"
printf "Line 1\nLine 2\n"

# ❌ Not portable
echo -e "Line 1\nLine 2"  # ❌
echo -n "no newline"      # ❌

Avoid external commands

# ✅ Built-in parameter expansion
basename="${file##*/}"
dirname="${file%/*}"

# ❌ External commands (slower)
basename=$(basename "$file")  # Slower
dirname=$(dirname "$file")    # Slower

Use || true carefully

# Allow command to fail
set -e
command || true        # Won't exit

# Better: Check explicitly
if command; then
    echo "success"
else
    echo "failed (expected)"
fi

Common Gotchas

Word splitting

# ❌ Unquoted expands
var="a b c"
[ $var = "a b c" ]     # ❌ Error: too many args

# ✅ Quoted preserves
[ "$var" = "a b c" ]   # ✅ Works

Empty variables

# ❌ Fails if var unset (with set -u)
echo $var              # ❌

# ✅ Use defaults
echo "${var:-}"        # Empty string if unset
echo "${var:-default}" # Default if unset

Glob expansion

# ❌ Expands to files
for item in *.txt; do
    # Runs once with "*.txt" if no match!
done

# ✅ Check if file exists
for item in *.txt; do
    [ -e "$item" ] || continue
    echo "$item"
done

Command substitution

# ❌ Loses exit code
result=$(command)
echo $?                # ❌ Always 0

# ✅ Check before assignment
command > tmpfile || fail "command failed"
result=$(cat tmpfile)

Test operators

# ❌ Wrong operator
[ "$x" == "y" ]        # ❌ Use = not ==
[ "$x" -eq "y" ]       # ❌ -eq is numeric only

# ✅ Correct
[ "$x" = "y" ]         # ✅ String equality
[ "$x" -eq "$y" ]      # ✅ Numeric (no quotes needed)

Debugging

Debug mode

# Enable debug output
set -x

# Disable debug output
set +x

# Debug from start
#!/bin/sh -x

Verbose mode

# Show commands as executed
set -v

# Disable verbose
set +v

Check syntax

# Validate without running
sh -n script.sh

# With shellcheck
shellcheck -s sh script.sh

# With checkbashisms
checkbashisms script.sh

Trace execution

# Show all commands
PS4='+ ${LINENO}: '
set -x

# Example output:
# + 42: echo "hello"

Also see