Portable Shell Scripting: Write Once, Run Anywhere
Making Scripts Portable Across Unix Platforms
The Need for Portable Scripts
Unix operating systems have many differences between shells, builtin commands, and external utilities. Scripts written for one Unix may not work on another due to discrepancies in shells, tools, and filesystem layouts. Writing portable scripts that function across Unix platforms can save considerable time and effort compared to writing a separate script for each system.
While Linux, macOS, and other Unix-like operating systems share common ancestry and concepts, they have diverged significantly over decades of separate evolution. Each platform has its own default shell, set of preinstalled tools, directory structure, and environmental assumptions. A script filled with Bash-specific syntax or Linux-centric filesystem paths may fail outright on macOS or BSD systems. By sticking to the standardized feature subset common to all POSIX-compliant shells, scripts can achieve broad portability across diverse Unix environments with minimal extra effort.
Writing Cross-Platform Scripts
Sticking to Standard POSIX Shell Features
The POSIX standard defines a baseline set of features that all conforming shells must support, including:
- Flow control constructs like if/elif/else, for/while loops, case statements
- Variable assignments, expansions, and substitutions
- Quoting, piping, redirection, compound commands
- Simple built-in commands like cd, exit, kill, pwd, read, true/false
- Filename expansion via globbing, including standard wildcards ?,*
- Standard command line option parsing conventions
By coding scripts to this cross-platform POSIX subset, many portability issues can be avoided. Straying outside this baseline feature set into non-standard extensions may impact script compatibility on alternate platforms. Always develop first to POSIX, then add in compatibility checks before using fancy shell addons.
Checking Shell Version and Features
To adapt to different target shells, scripts can inspect the SHELL environment variable or $BASH_VERSION for the current shell type and version. Feature detection can allow adjusting behavior based on what is or isn’t present:
if [ "$SHELL" = "/bin/bash" ]; then # bash-only features elif [ "$SHELL" = "/bin/zsh" ]; then # zsh-only features else # Generic POSIX features only fi
Beyond checking $SHELL, other helpers like command -v
can test for existence of commands, builtins, or utilities before usage in a script. This allows graceful degradation when a desired tool is unavailable:
if command -v curl >/dev/null 2>&1; then # Use curl else # Fall back to wget fi
Avoiding Platform-Specific Commands
Some common commands differ significantly across platforms in syntax, behavior, or default options. For example, Linux distro commands like apt, dnf or pacman for package management will fail outright on macOS. Similarly, macOS delivers much core functionality through builtins that have no direct equivalent on Linux like open
or say
.
When writing cross-platform scripts, avoid reliance on OS-specific utilities like these in favor of more widely standardized tools. If equivalents exist in the POSIX standard, like echo
, kill
, printf
use those instead of platform extras. For higher level tasks lacking POSIX entries, wrap platform-specific calls in compatibility checks that provide alternate workflows on other operating systems.
Handling Filesystem Differences
Normalizing Paths
Unix-like systems arrange the filesystem hierarchy differently, with disagreements on locations of system files, user data, temporary storage, and more. Path normalization can abstract these layout quirks away through variables or programmatic manipulation:
TMPDIR="${TMPDIR:-/tmp}" # Check env var, fall back as needed INSTALL_DIR="$HOME/apps" mkdir -p "$INSTALL_DIR"
By coding defensively around uncertainty in environment values or filesystem layout, scripts remain agnostic to environment differences between platforms. But always provide sane POSIX-compliant defaults rather than assuming specifics like a Linux /home directory tree.
Being Careful with Slashes
The forward slash (/) separator is universal in Unix paths, but Windows influence can lead to backslashes (\) on some platforms. Standardizing on / regardless of host OS helps avoid bugs:
DEST="/usr/local/bin" echo "Copying to $DEST" cp myscript.sh "$DEST"
Leakage from Windows-style paths only gets worse when exchanging removable media. Metadata and subdirectories may transparently bridge Unix and Windows worlds via \ path continuations. Carefully inspecting scripts for rogue backslashes avoids mysterious “No such file or directory” errors.
Testing Scripts on Multiple Platforms
Verifying portable script behavior across operating systems can be challenging without access to a diversity of hardware environments. Emulation, simulation, containers, and virtual machines can provide low cost isolation for different Unix testing targets. Solutions include:
- Docker containers to mimic Linux/BSD userspace packages
- VirtualBox, VMware, QEMU, Parallels VMs
- On Linux, Bash for Windows (WSL), Cygwin
Constructing a matrix of supported Unix platforms on real or simulated hardware allows reproducible tests during development and CI. Mocking critical differences like filesystem layout in key areas like $HOME ensures portability edge cases get covered.
Achieving Portable Scripts
Summary of Main Portability Considerations
Crafting shell scripts for broad Unix portability revolves around a few key practices:
- Stick to standard POSIX sh features, utilities, and arguments
- Detect shell type and version to fine tune functionality
- Insulate filesystem dependencies through normalization
- Isolate platform-specific calls to simplify substitution
- Test on diverse platforms early and often
Assuming POSIX compatibility as the baseline, fine tuning scripts to adapt to higher level shells and utilities only enhances functionality. Wrapping platform-specific logic prevents degradation when better tools are unavailable.
The Benefits of Portable Scripts
Crafting portable Unix scripts requires more initial effort but pays ongoing dividends through code reuse. The ability to write once yet run anywhere reduces duplication between operating systems and lowers maintenance overhead as projects grow. Platform variations cease to block script distribution when support for POSIX foundations and incremental enhancement is baked in from the start.
Portable scripts also encourage healthier cross-pollination between Unix ecosystems. Sharing solid examples written against open standards raises the bar for utility scripting as a whole. Portability best practices lend scripts long term resilience against platform churn, while opening doors to many more potential users. The investment in broad compatibility catalyzes innovation across all Unix environments.