1042 lines
23 KiB
Plaintext
1042 lines
23 KiB
Plaintext
# Platform-Specific Bash Scripting
|
|
|
|
Comprehensive guide to handling platform differences in bash scripts across Linux, macOS, Windows (Git Bash/WSL), and containers.
|
|
|
|
---
|
|
|
|
## Table of Contents
|
|
|
|
1. [Platform Detection](#platform-detection)
|
|
2. [Linux Specifics](#linux-specifics)
|
|
3. [macOS Specifics](#macos-specifics)
|
|
4. [Windows (Git Bash)](#windows-git-bash)
|
|
5. [Windows (WSL)](#windows-wsl)
|
|
6. [Container Environments](#container-environments)
|
|
7. [Cross-Platform Patterns](#cross-platform-patterns)
|
|
8. [Command Compatibility Matrix](#command-compatibility-matrix)
|
|
|
|
---
|
|
|
|
## 🚨 CRITICAL GUIDELINES
|
|
|
|
### Windows File Path Requirements
|
|
|
|
**MANDATORY: Always Use Backslashes on Windows for File Paths**
|
|
|
|
When using Edit or Write tools on Windows, you MUST use backslashes (`\`) in file paths, NOT forward slashes (`/`).
|
|
|
|
**Examples:**
|
|
- ❌ WRONG: `D:/repos/project/file.tsx`
|
|
- ✅ CORRECT: `D:\repos\project\file.tsx`
|
|
|
|
This applies to:
|
|
- Edit tool file_path parameter
|
|
- Write tool file_path parameter
|
|
- All file operations on Windows systems
|
|
|
|
|
|
### Documentation Guidelines
|
|
|
|
**NEVER create new documentation files unless explicitly requested by the user.**
|
|
|
|
- **Priority**: Update existing README.md files rather than creating new documentation
|
|
- **Repository cleanliness**: Keep repository root clean - only README.md unless user requests otherwise
|
|
- **Style**: Documentation should be concise, direct, and professional - avoid AI-generated tone
|
|
- **User preference**: Only create additional .md files when user specifically asks for documentation
|
|
|
|
|
|
---
|
|
|
|
## Platform Detection
|
|
|
|
### Comprehensive Detection Script
|
|
|
|
```bash
|
|
#!/usr/bin/env bash
|
|
|
|
detect_os() {
|
|
case "$OSTYPE" in
|
|
linux-gnu*)
|
|
if grep -qi microsoft /proc/version 2>/dev/null; then
|
|
echo "wsl"
|
|
else
|
|
echo "linux"
|
|
fi
|
|
;;
|
|
darwin*)
|
|
echo "macos"
|
|
;;
|
|
msys*|mingw*|cygwin*)
|
|
echo "gitbash"
|
|
;;
|
|
*)
|
|
echo "unknown"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
detect_distro() {
|
|
# Only for Linux
|
|
if [[ -f /etc/os-release ]]; then
|
|
# shellcheck source=/dev/null
|
|
source /etc/os-release
|
|
echo "$ID"
|
|
elif [[ -f /etc/redhat-release ]]; then
|
|
echo "rhel"
|
|
elif [[ -f /etc/debian_version ]]; then
|
|
echo "debian"
|
|
else
|
|
echo "unknown"
|
|
fi
|
|
}
|
|
|
|
detect_container() {
|
|
if [[ -f /.dockerenv ]]; then
|
|
echo "docker"
|
|
elif grep -q docker /proc/1/cgroup 2>/dev/null; then
|
|
echo "docker"
|
|
elif [[ -n "$KUBERNETES_SERVICE_HOST" ]]; then
|
|
echo "kubernetes"
|
|
else
|
|
echo "none"
|
|
fi
|
|
}
|
|
|
|
# Usage
|
|
OS=$(detect_os)
|
|
DISTRO=$(detect_distro)
|
|
CONTAINER=$(detect_container)
|
|
|
|
echo "OS: $OS"
|
|
echo "Distro: $DISTRO"
|
|
echo "Container: $CONTAINER"
|
|
```
|
|
|
|
### Environment Variables for Detection
|
|
|
|
```bash
|
|
# Check various environment indicators
|
|
check_environment() {
|
|
echo "OSTYPE: $OSTYPE"
|
|
echo "MACHTYPE: $MACHTYPE"
|
|
echo "HOSTTYPE: $HOSTTYPE"
|
|
|
|
# Kernel info
|
|
uname -s # Operating system name
|
|
uname -r # Kernel release
|
|
uname -m # Machine hardware
|
|
uname -p # Processor type
|
|
|
|
# More detailed
|
|
uname -a # All information
|
|
}
|
|
|
|
# Platform-specific variables
|
|
# Linux: OSTYPE=linux-gnu
|
|
# macOS: OSTYPE=darwin20.0
|
|
# Git Bash: OSTYPE=msys
|
|
# Cygwin: OSTYPE=cygwin
|
|
# WSL: OSTYPE=linux-gnu (but with Microsoft in /proc/version)
|
|
```
|
|
|
|
---
|
|
|
|
## Linux Specifics
|
|
|
|
### Linux-Only Features
|
|
|
|
```bash
|
|
# /proc filesystem
|
|
get_process_info() {
|
|
local pid=$1
|
|
|
|
if [[ -d "/proc/$pid" ]]; then
|
|
echo "Command: $(cat /proc/$pid/cmdline | tr '\0' ' ')"
|
|
echo "Working dir: $(readlink /proc/$pid/cwd)"
|
|
echo "Executable: $(readlink /proc/$pid/exe)"
|
|
fi
|
|
}
|
|
|
|
# systemd
|
|
check_systemd() {
|
|
if command -v systemctl &> /dev/null; then
|
|
systemctl status my-service
|
|
systemctl is-active my-service
|
|
systemctl is-enabled my-service
|
|
fi
|
|
}
|
|
|
|
# cgroups
|
|
check_cgroups() {
|
|
if [[ -d /sys/fs/cgroup ]]; then
|
|
cat /sys/fs/cgroup/memory/memory.limit_in_bytes
|
|
fi
|
|
}
|
|
|
|
# inotify for file watching
|
|
watch_directory() {
|
|
if command -v inotifywait &> /dev/null; then
|
|
inotifywait -m -r -e modify,create,delete /path/to/watch
|
|
fi
|
|
}
|
|
```
|
|
|
|
### Distribution-Specific Commands
|
|
|
|
```bash
|
|
# Package management
|
|
install_package() {
|
|
local package=$1
|
|
|
|
if command -v apt-get &> /dev/null; then
|
|
# Debian/Ubuntu
|
|
sudo apt-get update
|
|
sudo apt-get install -y "$package"
|
|
elif command -v yum &> /dev/null; then
|
|
# RHEL/CentOS
|
|
sudo yum install -y "$package"
|
|
elif command -v dnf &> /dev/null; then
|
|
# Fedora
|
|
sudo dnf install -y "$package"
|
|
elif command -v pacman &> /dev/null; then
|
|
# Arch
|
|
sudo pacman -S --noconfirm "$package"
|
|
elif command -v zypper &> /dev/null; then
|
|
# openSUSE
|
|
sudo zypper install -y "$package"
|
|
elif command -v apk &> /dev/null; then
|
|
# Alpine
|
|
sudo apk add "$package"
|
|
else
|
|
echo "Error: No supported package manager found" >&2
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Service management
|
|
manage_service() {
|
|
local action=$1
|
|
local service=$2
|
|
|
|
if command -v systemctl &> /dev/null; then
|
|
# systemd (most modern distros)
|
|
sudo systemctl "$action" "$service"
|
|
elif command -v service &> /dev/null; then
|
|
# SysV init
|
|
sudo service "$service" "$action"
|
|
else
|
|
echo "Error: No supported service manager found" >&2
|
|
return 1
|
|
fi
|
|
}
|
|
```
|
|
|
|
### GNU Coreutils (Linux Standard)
|
|
|
|
```bash
|
|
# GNU-specific features
|
|
# These work on Linux but may not work on macOS/BSD
|
|
|
|
# sed with -i (in-place editing)
|
|
sed -i 's/old/new/g' file.txt # Linux
|
|
sed -i '' 's/old/new/g' file.txt # macOS requires empty string
|
|
|
|
# date with flexible parsing
|
|
date -d "yesterday" +%Y-%m-%d # Linux
|
|
date -v-1d +%Y-%m-%d # macOS
|
|
|
|
# stat with -c format
|
|
stat -c "%s" file.txt # Linux (file size)
|
|
stat -f "%z" file.txt # macOS
|
|
|
|
# readlink with -f (canonicalize)
|
|
readlink -f /path/to/file # Linux
|
|
# macOS doesn't have -f, use greadlink or:
|
|
python -c "import os; print(os.path.realpath('$file'))"
|
|
|
|
# GNU find with -printf
|
|
find . -type f -printf "%p %s\n" # Linux
|
|
find . -type f -exec stat -f "%N %z" {} \; # macOS
|
|
```
|
|
|
|
---
|
|
|
|
## macOS Specifics
|
|
|
|
### BSD vs GNU Commands
|
|
|
|
```bash
|
|
# Detect and use GNU versions if available
|
|
setup_commands_macos() {
|
|
# Install GNU commands: brew install coreutils gnu-sed gnu-tar findutils
|
|
if command -v gsed &> /dev/null; then
|
|
SED=gsed
|
|
else
|
|
SED=sed
|
|
fi
|
|
|
|
if command -v ggrep &> /dev/null; then
|
|
GREP=ggrep
|
|
else
|
|
GREP=grep
|
|
fi
|
|
|
|
if command -v greadlink &> /dev/null; then
|
|
READLINK=greadlink
|
|
else
|
|
READLINK=readlink
|
|
fi
|
|
|
|
if command -v gdate &> /dev/null; then
|
|
DATE=gdate
|
|
else
|
|
DATE=date
|
|
fi
|
|
|
|
if command -v gstat &> /dev/null; then
|
|
STAT=gstat
|
|
else
|
|
STAT=stat
|
|
fi
|
|
|
|
export SED GREP READLINK DATE STAT
|
|
}
|
|
|
|
# Usage
|
|
setup_commands_macos
|
|
$SED -i 's/old/new/g' file.txt # Works on both platforms
|
|
```
|
|
|
|
### macOS-Specific Features
|
|
|
|
```bash
|
|
# macOS filesystem (case-insensitive by default on APFS/HFS+)
|
|
check_case_sensitivity() {
|
|
touch /tmp/test_case
|
|
if [[ -f /tmp/TEST_CASE ]]; then
|
|
echo "Filesystem is case-insensitive"
|
|
else
|
|
echo "Filesystem is case-sensitive"
|
|
fi
|
|
rm -f /tmp/test_case /tmp/TEST_CASE
|
|
}
|
|
|
|
# macOS extended attributes
|
|
# Set extended attribute
|
|
xattr -w com.example.myattr "value" file.txt
|
|
|
|
# Get extended attribute
|
|
xattr -p com.example.myattr file.txt
|
|
|
|
# List all extended attributes
|
|
xattr -l file.txt
|
|
|
|
# Remove extended attribute
|
|
xattr -d com.example.myattr file.txt
|
|
|
|
# macOS Spotlight
|
|
# Disable indexing for directory
|
|
mdutil -i off /path/to/directory
|
|
|
|
# Search with mdfind (Spotlight from command line)
|
|
mdfind "kMDItemFSName == 'filename.txt'"
|
|
|
|
# macOS clipboard
|
|
# Copy to clipboard
|
|
echo "text" | pbcopy
|
|
|
|
# Paste from clipboard
|
|
pbpaste
|
|
|
|
# macOS notifications
|
|
# Display notification
|
|
osascript -e 'display notification "Build complete" with title "Build Status"'
|
|
|
|
# macOS open command
|
|
# Open file with default application
|
|
open file.pdf
|
|
|
|
# Open URL
|
|
open https://example.com
|
|
|
|
# Open current directory in Finder
|
|
open .
|
|
```
|
|
|
|
### Homebrew Package Management
|
|
|
|
```bash
|
|
# Check if Homebrew is installed
|
|
if command -v brew &> /dev/null; then
|
|
# Install package
|
|
brew install package-name
|
|
|
|
# Update Homebrew
|
|
brew update
|
|
|
|
# Upgrade packages
|
|
brew upgrade
|
|
|
|
# Search for package
|
|
brew search package-name
|
|
|
|
# Get package info
|
|
brew info package-name
|
|
fi
|
|
```
|
|
|
|
---
|
|
|
|
## Windows (Git Bash)
|
|
|
|
### Git Bash Environment
|
|
|
|
```bash
|
|
# Git Bash uses MSYS2 runtime
|
|
# Provides Unix-like environment on Windows
|
|
|
|
# Path handling
|
|
convert_path() {
|
|
local path=$1
|
|
|
|
if command -v cygpath &> /dev/null; then
|
|
# Convert Unix path to Windows
|
|
windows_path=$(cygpath -w "$path")
|
|
echo "$windows_path"
|
|
|
|
# Convert Windows path to Unix
|
|
unix_path=$(cygpath -u "C:\\Users\\user\\file.txt")
|
|
echo "$unix_path"
|
|
else
|
|
# Manual conversion (Git Bash)
|
|
# /c/Users/user → C:\Users\user
|
|
echo "${path//\//\\}" | sed 's/^\\//'
|
|
fi
|
|
}
|
|
|
|
# Git Bash path conventions
|
|
# C:\Users\user → /c/Users/user
|
|
# D:\data → /d/data
|
|
|
|
# Home directory
|
|
echo "$HOME" # /c/Users/username
|
|
echo "$USERPROFILE" # Windows-style path
|
|
|
|
# Temp directory
|
|
echo "$TEMP" # Windows temp
|
|
echo "$TMP" # Windows temp
|
|
echo "/tmp" # Git Bash temp (usually C:\Users\username\AppData\Local\Temp)
|
|
```
|
|
|
|
### Limited Features in Git Bash
|
|
|
|
```bash
|
|
# Features NOT available in Git Bash:
|
|
|
|
# 1. No systemd
|
|
# Use Windows services instead:
|
|
# sc query ServiceName
|
|
# net start ServiceName
|
|
|
|
# 2. Limited signal support
|
|
# SIGTERM works, but some signals behave differently
|
|
|
|
# 3. No /proc filesystem
|
|
# Use wmic or PowerShell:
|
|
# wmic process get processid,commandline
|
|
|
|
# 4. Process handling differences
|
|
# ps command is available but limited
|
|
ps -W # Show Windows processes
|
|
|
|
# 5. File permissions are simulated
|
|
# chmod works but doesn't map directly to Windows ACLs
|
|
|
|
# 6. Symbolic links require administrator privileges
|
|
# Or Developer Mode enabled in Windows 10+
|
|
```
|
|
|
|
### Windows-Specific Workarounds
|
|
|
|
```bash
|
|
# Run PowerShell commands from Git Bash
|
|
run_powershell() {
|
|
local command=$1
|
|
powershell.exe -Command "$command"
|
|
}
|
|
|
|
# Example: Get Windows version
|
|
run_powershell "Get-ComputerInfo | Select-Object WindowsVersion"
|
|
|
|
# Run cmd.exe commands
|
|
run_cmd() {
|
|
local command=$1
|
|
cmd.exe /c "$command"
|
|
}
|
|
|
|
# Example: Set Windows environment variable
|
|
run_cmd "setx MY_VAR value"
|
|
|
|
# Check if running with admin privileges
|
|
is_admin() {
|
|
net session &> /dev/null
|
|
return $?
|
|
}
|
|
|
|
if is_admin; then
|
|
echo "Running with administrator privileges"
|
|
else
|
|
echo "Not running as administrator"
|
|
fi
|
|
|
|
# Windows line endings (CRLF vs LF)
|
|
fix_line_endings() {
|
|
local file=$1
|
|
|
|
# Convert CRLF to LF
|
|
dos2unix "$file"
|
|
|
|
# Or with sed
|
|
sed -i 's/\r$//' "$file"
|
|
|
|
# Convert LF to CRLF
|
|
unix2dos "$file"
|
|
|
|
# Or with sed
|
|
sed -i 's/$/\r/' "$file"
|
|
}
|
|
```
|
|
|
|
### Git Bash Best Practices
|
|
|
|
```bash
|
|
# Always handle spaces in Windows paths
|
|
process_file() {
|
|
local file="$1" # Always quote!
|
|
|
|
# Windows paths often have spaces
|
|
# C:\Program Files\...
|
|
}
|
|
|
|
# Use forward slashes when possible
|
|
cd /c/Program\ Files/Git # Works
|
|
cd "C:\Program Files\Git" # Also works, but...
|
|
cd C:\\Program\ Files\\Git # Avoid
|
|
|
|
# Set Git config for line endings
|
|
git config --global core.autocrlf true # Windows
|
|
git config --global core.autocrlf input # Linux/macOS
|
|
|
|
# Check Git Bash version
|
|
bash --version
|
|
uname -a # Shows MINGW or MSYS
|
|
```
|
|
|
|
---
|
|
|
|
## Windows (WSL)
|
|
|
|
### WSL1 vs WSL2
|
|
|
|
```bash
|
|
# Detect WSL version
|
|
detect_wsl_version() {
|
|
if grep -qi microsoft /proc/version; then
|
|
if [[ $(uname -r) =~ microsoft ]]; then
|
|
echo "WSL 1"
|
|
elif [[ $(uname -r) =~ WSL2 ]]; then
|
|
echo "WSL 2"
|
|
else
|
|
# Check kernel version
|
|
if [[ $(uname -r) =~ ^4\. ]]; then
|
|
echo "WSL 1"
|
|
else
|
|
echo "WSL 2"
|
|
fi
|
|
fi
|
|
else
|
|
echo "Not WSL"
|
|
fi
|
|
}
|
|
|
|
# WSL1 limitations:
|
|
# - No full syscall compatibility
|
|
# - File I/O slower on Windows filesystem
|
|
# - No Docker/containers (needs WSL2)
|
|
|
|
# WSL2 improvements:
|
|
# - Full Linux kernel
|
|
# - Better filesystem performance
|
|
# - Docker/container support
|
|
# - Near-native Linux performance
|
|
```
|
|
|
|
### Windows Filesystem Access
|
|
|
|
```bash
|
|
# Access Windows drives from WSL
|
|
# Mounted at /mnt/c, /mnt/d, etc.
|
|
|
|
# List Windows drives
|
|
ls /mnt/
|
|
|
|
# Access Windows user directory
|
|
WINDOWS_HOME="/mnt/c/Users/$USER"
|
|
cd "$WINDOWS_HOME"
|
|
|
|
# File permissions on Windows filesystem
|
|
# Files on /mnt/c are owned by root but accessible
|
|
# Permissions are simulated
|
|
|
|
# Best practice: Use WSL filesystem for Linux files
|
|
# Use /home/username, not /mnt/c/...
|
|
# Much faster, especially in WSL1
|
|
```
|
|
|
|
### WSL Interoperability
|
|
|
|
```bash
|
|
# Run Windows executables from WSL
|
|
# .exe files are automatically executable
|
|
|
|
# Run Windows commands
|
|
cmd.exe /c dir
|
|
notepad.exe file.txt
|
|
explorer.exe . # Open current directory in Windows Explorer
|
|
|
|
# Run PowerShell
|
|
powershell.exe -Command "Get-Date"
|
|
|
|
# Pipe between Linux and Windows
|
|
cat file.txt | clip.exe # Copy to Windows clipboard
|
|
|
|
# Environment variables
|
|
# Windows environment is accessible with WSLENV
|
|
|
|
# Share environment variable from Windows to WSL
|
|
# In PowerShell:
|
|
# $env:WSLENV = "MYVAR/p"
|
|
# This converts Windows paths to WSL paths
|
|
```
|
|
|
|
### WSL-Specific Configuration
|
|
|
|
```bash
|
|
# /etc/wsl.conf configuration
|
|
cat > /etc/wsl.conf << 'EOF'
|
|
[automount]
|
|
enabled = true
|
|
root = /mnt/
|
|
options = "metadata,umask=22,fmask=11"
|
|
|
|
[network]
|
|
generateHosts = true
|
|
generateResolvConf = true
|
|
|
|
[interop]
|
|
enabled = true
|
|
appendWindowsPath = true
|
|
EOF
|
|
|
|
# Apply: wsl.exe --shutdown (from PowerShell)
|
|
|
|
# Network differences
|
|
# WSL1: Shares network with Windows
|
|
# WSL2: NAT network, different IP
|
|
|
|
# Get WSL IP address
|
|
ip addr show eth0 | grep -oP '(?<=inet\s)\d+(\.\d+){3}'
|
|
|
|
# Access Windows services from WSL2
|
|
# Use Windows IP, not localhost
|
|
# Or use: localhost (WSL2 has localhost forwarding)
|
|
```
|
|
|
|
---
|
|
|
|
## Container Environments
|
|
|
|
### Docker Considerations
|
|
|
|
```bash
|
|
# Minimal base images often lack bash
|
|
# alpine: Only has /bin/sh by default
|
|
# debian:slim: Has bash
|
|
# ubuntu: Has bash
|
|
|
|
# Check if bash is available
|
|
if [ -f /bin/bash ]; then
|
|
exec /bin/bash "$@"
|
|
else
|
|
exec /bin/sh "$@"
|
|
fi
|
|
|
|
# Container detection
|
|
is_docker() {
|
|
if [[ -f /.dockerenv ]] || grep -q docker /proc/1/cgroup 2>/dev/null; then
|
|
return 0
|
|
else
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# PID 1 problem in containers
|
|
# Your script might be PID 1, which means:
|
|
# - Zombie process reaping is your responsibility
|
|
# - Signals behave differently
|
|
|
|
# Solution: Use tini or dumb-init
|
|
# Or handle signals explicitly
|
|
handle_sigterm() {
|
|
# Forward to child processes
|
|
kill -TERM "$child_pid" 2>/dev/null
|
|
wait "$child_pid"
|
|
exit 0
|
|
}
|
|
|
|
trap handle_sigterm SIGTERM
|
|
|
|
# Start main process
|
|
main_process &
|
|
child_pid=$!
|
|
wait "$child_pid"
|
|
```
|
|
|
|
### Kubernetes Considerations
|
|
|
|
```bash
|
|
# Kubernetes-specific environment variables
|
|
if [[ -n "$KUBERNETES_SERVICE_HOST" ]]; then
|
|
echo "Running in Kubernetes"
|
|
|
|
# Access Kubernetes API
|
|
KUBE_TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
|
|
KUBE_CA=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
|
|
|
|
# Get pod name
|
|
POD_NAME=${POD_NAME:-$(hostname)}
|
|
|
|
# Get namespace
|
|
NAMESPACE=$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)
|
|
fi
|
|
|
|
# Health checks
|
|
# Kubernetes expects:
|
|
# - HTTP probe on specific port
|
|
# - Or command that exits 0 for success
|
|
|
|
# Liveness probe handler
|
|
handle_health_check() {
|
|
# Check if application is healthy
|
|
if check_health; then
|
|
exit 0
|
|
else
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# Readiness probe handler
|
|
handle_readiness_check() {
|
|
# Check if ready to serve traffic
|
|
if is_ready; then
|
|
exit 0
|
|
else
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# Graceful shutdown for rolling updates
|
|
# Kubernetes sends SIGTERM, waits (default 30s), then SIGKILL
|
|
trap 'graceful_shutdown' SIGTERM
|
|
|
|
graceful_shutdown() {
|
|
echo "Received SIGTERM, shutting down gracefully..."
|
|
|
|
# Stop accepting new connections
|
|
# Finish processing existing requests
|
|
# Close connections
|
|
# Exit
|
|
|
|
exit 0
|
|
}
|
|
```
|
|
|
|
### Container Best Practices
|
|
|
|
```bash
|
|
# Don't assume specific users/groups exist
|
|
# Many containers run as non-root or random UID
|
|
|
|
# Check current user
|
|
if [[ $EUID -eq 0 ]]; then
|
|
echo "Running as root"
|
|
else
|
|
echo "Running as user $EUID"
|
|
fi
|
|
|
|
# Handle arbitrary UIDs (OpenShift)
|
|
# Files in mounted volumes may not be owned by container user
|
|
# Solution: Add current user to group, use group permissions
|
|
|
|
# Minimal dependencies
|
|
# Container images should be small
|
|
# Don't install unnecessary packages
|
|
|
|
# Use absolute paths or set PATH explicitly
|
|
export PATH=/usr/local/bin:/usr/bin:/bin
|
|
|
|
# Environment variables for configuration
|
|
# Don't hardcode values, use env vars
|
|
DATABASE_URL=${DATABASE_URL:-postgres://localhost/db}
|
|
|
|
# Logging to stdout/stderr
|
|
# Container orchestrators capture these
|
|
echo "Log message" # To stdout
|
|
echo "Error message" >&2 # To stderr
|
|
|
|
# Don't write to filesystem (except for tmpfs)
|
|
# Containers are ephemeral
|
|
# Use volumes for persistent data
|
|
```
|
|
|
|
---
|
|
|
|
## Cross-Platform Patterns
|
|
|
|
### Portable Command Wrapper
|
|
|
|
```bash
|
|
# Create wrappers for platform-specific commands
|
|
setup_portable_commands() {
|
|
local os
|
|
os=$(detect_os)
|
|
|
|
case "$os" in
|
|
linux)
|
|
SED=sed
|
|
READLINK="readlink -f"
|
|
DATE=date
|
|
STAT="stat -c"
|
|
GREP=grep
|
|
;;
|
|
macos)
|
|
# Prefer GNU versions if available
|
|
SED=$(command -v gsed || echo sed)
|
|
READLINK=$(command -v greadlink || echo "echo") # No -f on BSD
|
|
DATE=$(command -v gdate || echo date)
|
|
STAT=$(command -v gstat || echo stat)
|
|
GREP=$(command -v ggrep || echo grep)
|
|
;;
|
|
gitbash)
|
|
SED=sed
|
|
READLINK=readlink # Git Bash has GNU tools
|
|
DATE=date
|
|
STAT=stat
|
|
GREP=grep
|
|
;;
|
|
esac
|
|
|
|
export SED READLINK DATE STAT GREP
|
|
}
|
|
|
|
# Use the wrappers
|
|
setup_portable_commands
|
|
$SED -i 's/old/new/g' file.txt
|
|
```
|
|
|
|
### Cross-Platform Temp Files
|
|
|
|
```bash
|
|
# Portable temporary file creation
|
|
create_temp_file() {
|
|
# Works on all platforms
|
|
local temp_file
|
|
temp_file=$(mktemp) || {
|
|
# Fallback if mktemp doesn't exist
|
|
temp_file="/tmp/script.$$.$RANDOM"
|
|
touch "$temp_file"
|
|
}
|
|
|
|
echo "$temp_file"
|
|
}
|
|
|
|
# Portable temporary directory
|
|
create_temp_dir() {
|
|
local temp_dir
|
|
temp_dir=$(mktemp -d) || {
|
|
# Fallback
|
|
temp_dir="/tmp/script.$$.$RANDOM"
|
|
mkdir -p "$temp_dir"
|
|
}
|
|
|
|
echo "$temp_dir"
|
|
}
|
|
|
|
# Clean up temp files on exit
|
|
TEMP_DIR=$(create_temp_dir)
|
|
trap 'rm -rf "$TEMP_DIR"' EXIT
|
|
```
|
|
|
|
### Cross-Platform File Paths
|
|
|
|
```bash
|
|
# Normalize paths across platforms
|
|
normalize_path() {
|
|
local path="$1"
|
|
|
|
# Remove trailing slashes
|
|
path="${path%/}"
|
|
|
|
# Convert backslashes to forward slashes (Windows)
|
|
path="${path//\\//}"
|
|
|
|
# Resolve . and ..
|
|
# Use Python for reliable normalization
|
|
if command -v python3 &> /dev/null; then
|
|
path=$(python3 -c "import os; print(os.path.normpath('$path'))")
|
|
elif command -v python &> /dev/null; then
|
|
path=$(python -c "import os; print(os.path.normpath('$path'))")
|
|
fi
|
|
|
|
echo "$path"
|
|
}
|
|
|
|
# Get absolute path (cross-platform)
|
|
get_absolute_path() {
|
|
local path="$1"
|
|
|
|
# Try readlink -f (Linux, Git Bash)
|
|
if readlink -f "$path" &> /dev/null; then
|
|
readlink -f "$path"
|
|
# Try realpath (most platforms)
|
|
elif command -v realpath &> /dev/null; then
|
|
realpath "$path"
|
|
# Fallback to Python
|
|
elif command -v python3 &> /dev/null; then
|
|
python3 -c "import os; print(os.path.abspath('$path'))"
|
|
# Fallback to cd
|
|
elif [[ -d "$path" ]]; then
|
|
(cd "$path" && pwd)
|
|
else
|
|
(cd "$(dirname "$path")" && echo "$(pwd)/$(basename "$path")")
|
|
fi
|
|
}
|
|
```
|
|
|
|
### Cross-Platform Process Management
|
|
|
|
```bash
|
|
# Find process by name (cross-platform)
|
|
find_process() {
|
|
local process_name="$1"
|
|
|
|
if command -v pgrep &> /dev/null; then
|
|
pgrep -f "$process_name"
|
|
else
|
|
ps aux | grep "$process_name" | grep -v grep | awk '{print $2}'
|
|
fi
|
|
}
|
|
|
|
# Kill process by name (cross-platform)
|
|
kill_process() {
|
|
local process_name="$1"
|
|
|
|
if command -v pkill &> /dev/null; then
|
|
pkill -f "$process_name"
|
|
else
|
|
local pids
|
|
pids=$(find_process "$process_name")
|
|
if [[ -n "$pids" ]]; then
|
|
kill $pids
|
|
fi
|
|
fi
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Command Compatibility Matrix
|
|
|
|
| Command | Linux | macOS | Git Bash | Notes |
|
|
|---------|-------|-------|----------|-------|
|
|
| `sed -i` | ✓ | ✓* | ✓ | macOS needs `sed -i ''` |
|
|
| `date -d` | ✓ | ✗ | ✓ | macOS uses `-v` |
|
|
| `readlink -f` | ✓ | ✗ | ✓ | macOS needs `greadlink` |
|
|
| `stat -c` | ✓ | ✗ | ✓ | macOS uses `-f` |
|
|
| `grep -P` | ✓ | ✗ | ✓ | macOS doesn't support PCRE |
|
|
| `find -printf` | ✓ | ✗ | ✓ | macOS doesn't have `-printf` |
|
|
| `xargs -r` | ✓ | ✗ | ✓ | macOS doesn't have `-r` |
|
|
| `ps aux` | ✓ | ✓ | ✓* | Git Bash has limited output |
|
|
| `ls --color` | ✓ | ✗ | ✓ | macOS uses `-G` |
|
|
| `du -b` | ✓ | ✗ | ✓ | macOS doesn't support bytes |
|
|
| `mktemp` | ✓ | ✓ | ✓ | Works on all platforms |
|
|
| `timeout` | ✓ | ✗ | ✓ | macOS needs `gtimeout` |
|
|
|
|
**Legend:**
|
|
- ✓ = Supported
|
|
- ✗ = Not supported
|
|
- ✓* = Supported with limitations
|
|
|
|
---
|
|
|
|
## Testing Across Platforms
|
|
|
|
```bash
|
|
# Test script on multiple platforms
|
|
test_platforms() {
|
|
local script="$1"
|
|
|
|
echo "Testing on current platform: $(detect_os)"
|
|
bash -n "$script" || {
|
|
echo "Syntax error!"
|
|
return 1
|
|
}
|
|
|
|
# Run ShellCheck
|
|
if command -v shellcheck &> /dev/null; then
|
|
shellcheck "$script" || return 1
|
|
fi
|
|
|
|
# Run the script
|
|
bash "$script" || return 1
|
|
|
|
echo "Tests passed on $(detect_os)"
|
|
}
|
|
|
|
# CI/CD matrix testing
|
|
# Use GitHub Actions, GitLab CI, etc. to test on multiple platforms
|
|
```
|
|
|
|
**Example GitHub Actions matrix:**
|
|
```yaml
|
|
jobs:
|
|
test:
|
|
runs-on: ${{ matrix.os }}
|
|
strategy:
|
|
matrix:
|
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
steps:
|
|
- uses: actions/checkout@v3
|
|
- name: Test script
|
|
run: bash test.sh
|
|
```
|
|
|
|
---
|
|
|
|
## Summary
|
|
|
|
**Key takeaways for cross-platform bash scripts:**
|
|
|
|
1. **Always detect the platform** before using platform-specific features
|
|
2. **Use portable commands** or provide fallbacks
|
|
3. **Test on all target platforms** (CI/CD with matrix builds)
|
|
4. **Avoid platform-specific assumptions** (file paths, users, services)
|
|
5. **Use ShellCheck** to catch portability issues
|
|
6. **Prefer POSIX compliance** when possible for maximum portability
|
|
7. **Document platform requirements** in script comments
|
|
8. **Provide GNU alternatives** on macOS when needed
|
|
9. **Handle path differences** carefully (especially Windows)
|
|
10. **Test in containers** if that's your deployment target
|
|
|
|
For maximum portability: stick to POSIX shell (`#!/bin/sh`) and avoid bashisms unless you control the deployment environment.
|