Shell Fundamentals and Execution Environment
Shell Fundamentals and Execution Environment¶
Next: 02_Parameter_Expansion.md
This lesson explores the shell execution environment, different shell types, startup mechanisms, and fundamental concepts that affect how your scripts behave in different contexts.
1. Types of Shells¶
Different shells offer varying features and compatibility levels. Understanding these differences is crucial for writing portable scripts.
Shell Comparison Table¶
| Feature | bash | sh (dash) | zsh | fish |
|---|---|---|---|---|
| POSIX compliant | Mostly | Yes | Mostly | No |
| Arrays | Yes | No | Yes (better) | Yes |
| Associative arrays | Yes (4.0+) | No | Yes | Yes |
| [[ ]] test | Yes | No | Yes | No |
| Process substitution | Yes | No | Yes | Yes |
| Here strings | Yes | No | Yes | Yes |
| Arithmetic (( )) | Yes | No | Yes | No |
| Command completion | Good | Basic | Excellent | Excellent |
| Startup performance | Medium | Fast | Slow | Medium |
| Scripting focus | Yes | Yes | Yes | No |
| Default on Debian/Ubuntu | bash | dash (/bin/sh) | bash | fish |
| Configuration syntax | bash | POSIX sh | zsh-extended | Fish-specific |
When to Use Each Shell¶
bash: General-purpose scripting, most systems have it, good balance of features and portability.
#!/bin/bash
# Use bash for scripts needing arrays, [[ ]], or process substitution
declare -A config
config[host]="localhost"
config[port]=8080
if [[ -n "${config[host]}" ]]; then
echo "Host: ${config[host]}"
fi
sh (POSIX): Maximum portability, embedded systems, minimal environments.
#!/bin/sh
# POSIX-compliant script - no bash-isms
# No arrays, no [[, no process substitution
if [ -n "$HOST" ]; then
echo "Host: $HOST"
fi
# Use case instead of [[ with regex
case "$filename" in
*.txt) echo "Text file" ;;
*.log) echo "Log file" ;;
esac
zsh: Interactive use, advanced completion, better array handling.
#!/bin/zsh
# zsh has more powerful array features
array=(one two three)
echo $array[1] # zsh arrays are 1-indexed (bash uses 0-indexed)
# Advanced globbing
setopt extended_glob
files=(^*.txt) # all files except .txt
fish: Interactive shell, user-friendly, NOT for portable scripts.
#!/usr/bin/fish
# Fish has different syntax - not POSIX compatible
set host localhost
set port 8080
if test -n "$host"
echo "Host: $host"
end
2. POSIX Compliance¶
POSIX (Portable Operating System Interface) defines a standard for shell behavior. POSIX-compliant scripts run on any POSIX shell (sh, bash, dash, ksh, etc.).
POSIX vs Bash-isms¶
| Feature | POSIX sh | bash Extension |
|---|---|---|
| Test command | [ ] |
[[ ]] |
| String comparison | [ "$a" = "$b" ] |
[[ $a == $b ]] |
| Regex matching | (use grep) | [[ $str =~ regex ]] |
| Arrays | Not supported | arr=(1 2 3) |
| Functions | func() { } |
function func { } |
| Arithmetic | expr, $(( )) |
let, (( )) |
| Process substitution | Not supported | <(cmd), >(cmd) |
| Here strings | Not supported | <<< "string" |
| Local variables | Not in POSIX | local var=value |
Writing Portable POSIX Scripts¶
#!/bin/sh
# POSIX-compliant script example
# Use [ ] instead of [[ ]]
if [ "$1" = "start" ]; then
echo "Starting service..."
fi
# Use $(( )) for arithmetic (this IS POSIX)
count=0
count=$((count + 1))
# Use case for pattern matching
case "$filename" in
*.tar.gz|*.tgz)
echo "Compressed tarball"
;;
*.zip)
echo "ZIP archive"
;;
*)
echo "Unknown format"
;;
esac
# Avoid arrays - use positional parameters or temporary files
set -- "item1" "item2" "item3"
for item in "$@"; do
echo "$item"
done
# Use command substitution $(cmd) not backticks
current_dir=$(pwd)
# Check command existence portably
if command -v docker >/dev/null 2>&1; then
echo "Docker is installed"
fi
3. Shell Modes¶
Shells operate in different modes depending on how they are invoked. This affects which startup files are read.
Login vs Non-Login Shells¶
Login shell: Started when you log in (SSH, console login, bash --login).
Non-login shell: Started from an existing session (opening terminal in GUI, running bash from bash).
Test if shell is login shell:
#!/bin/bash
# Check if running as login shell
if shopt -q login_shell; then
echo "This is a login shell"
else
echo "This is a non-login shell"
fi
# Alternative method
case "$-" in
*l*) echo "Login shell" ;;
*) echo "Non-login shell" ;;
esac
Interactive vs Non-Interactive Shells¶
Interactive: Terminal attached, accepts user input (normal terminal session).
Non-interactive: Running scripts, no terminal interaction.
Test if shell is interactive:
#!/bin/bash
# Check if running interactively
if [[ $- == *i* ]]; then
echo "Interactive shell"
else
echo "Non-interactive shell (script)"
fi
# Alternative method
case "$-" in
*i*) echo "Interactive" ;;
*) echo "Non-interactive" ;;
esac
# Check if stdin is a terminal
if [ -t 0 ]; then
echo "stdin is a terminal"
else
echo "stdin is not a terminal (piped/redirected)"
fi
4. Startup Files Loading Order¶
The order in which bash reads configuration files depends on the shell mode.
Startup Sequence Diagram¶
Login Shell (bash --login or SSH)
├── /etc/profile (system-wide)
│ └── /etc/profile.d/*.sh (if sourced by /etc/profile)
└── First found:
├── ~/.bash_profile
├── ~/.bash_login (if ~/.bash_profile not found)
└── ~/.profile (if neither above found)
└── (many .bash_profile files source ~/.bashrc)
Non-Login Interactive Shell (terminal window)
├── /etc/bash.bashrc (Debian/Ubuntu)
└── ~/.bashrc
Non-Interactive Shell (scripts)
├── $BASH_ENV (if set, file path to source)
└── (typically nothing)
Login Shell Exit
└── ~/.bash_logout
Startup Files Purpose¶
| File | Purpose | Typical Contents |
|---|---|---|
/etc/profile |
System-wide login settings | PATH, LANG, umask |
/etc/bash.bashrc |
System-wide interactive settings | PS1, aliases (Debian/Ubuntu) |
~/.bash_profile |
User login settings | Source ~/.bashrc, set PATH |
~/.bashrc |
User interactive settings | Aliases, functions, PS1 |
~/.profile |
POSIX login settings | Portable login settings |
~/.bash_logout |
Cleanup on logout | Clear screen, clean temp files |
Example Startup File Structure¶
~/.bash_profile (login shell entry point):
# ~/.bash_profile - loaded by login shells
# Set PATH for user binaries
export PATH="$HOME/bin:$HOME/.local/bin:$PATH"
# Load .bashrc if it exists (for interactive login shells)
if [ -f ~/.bashrc ]; then
. ~/.bashrc
fi
# Login-specific settings
echo "Last login: $(date)" >> ~/.login_log
~/.bashrc (interactive shell settings):
# ~/.bashrc - loaded by interactive non-login shells
# If not running interactively, don't do anything
case $- in
*i*) ;;
*) return;;
esac
# History settings
HISTCONTROL=ignoreboth
HISTSIZE=10000
HISTFILESIZE=20000
shopt -s histappend
# Prompt
PS1='\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ '
# Aliases
alias ll='ls -lah'
alias grep='grep --color=auto'
# Load functions from separate file
if [ -f ~/.bash_functions ]; then
. ~/.bash_functions
fi
~/.profile (POSIX-compatible login settings):
# ~/.profile - POSIX-compatible login settings
# Used when bash is invoked as sh, or by other POSIX shells
# Set PATH
PATH="$HOME/bin:$PATH"
export PATH
# Environment variables
export EDITOR=vim
export PAGER=less
# If bash, source .bashrc
if [ -n "$BASH_VERSION" ]; then
if [ -f "$HOME/.bashrc" ]; then
. "$HOME/.bashrc"
fi
fi
5. Exit Codes¶
Exit codes indicate whether a command succeeded or failed. By convention:
Exit Code Conventions¶
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | General error |
| 2 | Misuse of shell builtin |
| 126 | Command found but not executable |
| 127 | Command not found |
| 128 | Invalid exit argument |
| 128+N | Fatal error signal N (130 = Ctrl+C (SIGINT=2)) |
| 255 | Exit status out of range |
Using Exit Codes¶
#!/bin/bash
# Check exit code with $?
grep "pattern" file.txt
if [ $? -eq 0 ]; then
echo "Pattern found"
else
echo "Pattern not found"
fi
# Better: use command directly in if
if grep "pattern" file.txt > /dev/null; then
echo "Pattern found"
fi
# Return custom exit codes from functions
validate_input() {
local input="$1"
if [ -z "$input" ]; then
echo "Error: input is empty" >&2
return 1
fi
if ! [[ "$input" =~ ^[0-9]+$ ]]; then
echo "Error: input must be numeric" >&2
return 2
fi
if [ "$input" -lt 0 ] || [ "$input" -gt 100 ]; then
echo "Error: input must be between 0 and 100" >&2
return 3
fi
return 0
}
# Use function and check exit code
if validate_input "42"; then
echo "Input is valid"
else
case $? in
1) echo "Empty input" ;;
2) echo "Not numeric" ;;
3) echo "Out of range" ;;
esac
fi
# Exit script with specific code
check_prerequisites() {
if ! command -v docker >/dev/null 2>&1; then
echo "Error: docker not found" >&2
exit 127
fi
if [ ! -r /etc/config.conf ]; then
echo "Error: config file not readable" >&2
exit 1
fi
}
Exit Code Best Practices¶
#!/bin/bash
set -e # Exit on error (but be careful with this)
# Explicitly handle errors
perform_backup() {
local source="$1"
local dest="$2"
if ! tar czf "$dest" "$source" 2>/dev/null; then
echo "Backup failed" >&2
return 1
fi
echo "Backup successful"
return 0
}
# Chain commands with proper error handling
if perform_backup "/data" "/backup/data.tar.gz"; then
echo "Cleaning up old backups..."
find /backup -name "*.tar.gz" -mtime +7 -delete
else
echo "Backup failed, keeping old backups"
exit 1
fi
6. Shell Options Overview¶
Shell options control shell behavior. Two commands manage options:
- set: POSIX-standard options
- shopt: Bash-specific extended options
Important set Options¶
#!/bin/bash
# Show current options
echo "$-" # e.g., "himBH" (each letter is an active option)
# Enable options
set -e # Exit on error (errexit)
set -u # Error on undefined variables (nounset)
set -o pipefail # Pipeline fails if any command fails
set -x # Print commands before executing (xtrace)
# Disable options
set +e # Don't exit on error
set +x # Stop printing commands
# Combine options
set -euo pipefail # Common "strict mode"
# Example: noclobber prevents overwriting files
set -o noclobber
echo "test" > file.txt # Creates file
echo "test" > file.txt # Error: file exists
echo "test" >| file.txt # Override noclobber with >|
Common set Options¶
| Option | Short | Description |
|---|---|---|
-e (errexit) |
-e |
Exit if command fails |
-u (nounset) |
-u |
Error on undefined variable |
-x (xtrace) |
-x |
Print commands before execution |
-o pipefail |
(long only) | Pipeline fails if any command fails |
-o noclobber |
-C |
Prevent > from overwriting files |
-o noglob |
-f |
Disable pathname expansion |
-o vi |
(long only) | Vi-style command line editing |
-o emacs |
(long only) | Emacs-style editing (default) |
Important shopt Options¶
#!/bin/bash
# Enable bash extended options
shopt -s extglob # Extended pattern matching
shopt -s globstar # ** for recursive glob
shopt -s nullglob # Non-matching globs expand to nothing
shopt -s dotglob # Include hidden files in globs
shopt -s nocaseglob # Case-insensitive globbing
# Disable options
shopt -u dotglob # Exclude hidden files
# Check if option is set
if shopt -q nullglob; then
echo "nullglob is enabled"
fi
# Example: nullglob
shopt -s nullglob
files=(*.txt)
if [ ${#files[@]} -eq 0 ]; then
echo "No .txt files found"
else
echo "Found ${#files[@]} .txt files"
fi
# Example: globstar
shopt -s globstar
# Find all Python files recursively
for file in **/*.py; do
echo "$file"
done
# Example: extglob (covered more in Lesson 04)
shopt -s extglob
rm !(*.txt|*.log) # Remove all except .txt and .log files
Useful shopt Options¶
| Option | Description |
|---|---|
extglob |
Extended pattern matching (!(pat), *(pat), etc.) |
globstar |
** matches recursively |
nullglob |
Non-matching globs expand to null, not literal |
dotglob |
Include hidden files in pathname expansion |
nocaseglob |
Case-insensitive pathname expansion |
failglob |
Unmatched globs cause error |
checkjobs |
Check running jobs before exiting |
autocd |
Change directory by typing directory name |
cdspell |
Autocorrect minor cd errors |
Strict Mode Example¶
#!/bin/bash
# Strict mode for safer scripts
set -euo pipefail
IFS=$'\n\t'
# -e: exit on error
# -u: error on undefined variable
# -o pipefail: pipeline fails if any command fails
# IFS: safer word splitting
# Now errors will stop the script
command_that_fails # Script exits here
echo "This won't execute"
# To handle errors explicitly:
if ! command_that_might_fail; then
echo "Command failed, handling error"
# Do cleanup
exit 1
fi
7. The env Command and #!/usr/bin/env bash¶
Why Use #!/usr/bin/env bash¶
The shebang line tells the system which interpreter to use. Two approaches:
Direct path: #!/bin/bash
- Fast (no PATH search)
- Not portable (bash might be in /usr/local/bin)
env approach: #!/usr/bin/env bash
- Portable (finds bash in PATH)
- Standard in modern scripts
- Works across different systems
#!/usr/bin/env bash
# This finds bash wherever it is in PATH
# Check where bash is located
which bash
# On macOS: /bin/bash
# On FreeBSD: /usr/local/bin/bash
# On Nix: /nix/store/.../bin/bash
Using env to Set Environment¶
#!/usr/bin/env -S bash -euo pipefail
# -S flag allows passing multiple arguments (GNU env 8.30+)
# Alternative for older env:
#!/usr/bin/env bash
set -euo pipefail
env for Clean Environment¶
# Run command with clean environment
env -i bash --norc --noprofile
# Run with specific variables only
env -i HOME=/tmp USER=testuser bash
# Remove specific variables
env -u DISPLAY firefox
# Add variables
env FOO=bar ./script.sh
# Inspect environment
env | sort
Script Template with Best Practices¶
#!/usr/bin/env bash
# Script: example.sh
# Description: Example script with best practices
# Author: Your Name
# Created: 2026-02-13
# Strict mode
set -euo pipefail
IFS=$'\n\t'
# Trap errors
trap 'echo "Error on line $LINENO" >&2' ERR
# Useful shell options
shopt -s nullglob globstar
# Constants
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")"
# Functions
usage() {
cat <<EOF
Usage: $SCRIPT_NAME [OPTIONS] <argument>
Options:
-h, --help Show this help message
-v, --verbose Enable verbose output
-d, --debug Enable debug mode
Examples:
$SCRIPT_NAME input.txt
$SCRIPT_NAME -v input.txt
EOF
}
main() {
# Main script logic
echo "Running from: $SCRIPT_DIR"
echo "Script name: $SCRIPT_NAME"
}
# Parse arguments
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help)
usage
exit 0
;;
-v|--verbose)
set -x
shift
;;
-d|--debug)
set -x
shift
;;
-*)
echo "Unknown option: $1" >&2
usage >&2
exit 1
;;
*)
break
;;
esac
done
# Run main function
main "$@"
Practice Problems¶
Problem 1: Shell Detection Script¶
Write a script that detects: - Which shell it's running in (bash, zsh, sh, etc.) - Whether it's a login or non-login shell - Whether it's interactive or non-interactive - The version of the shell
Expected output:
Shell: bash
Version: 5.1.16
Type: non-login, interactive
Problem 2: Startup File Analyzer¶
Create a script that: - Lists all bash startup files that exist on the system - Shows the order they would be loaded for login vs non-login shells - Displays the first 5 lines of each file - Checks for common mistakes (like setting aliases in .bash_profile instead of .bashrc)
Problem 3: Exit Code Logger¶
Write a function that wraps any command and:
- Logs the command being executed
- Captures and logs the exit code
- Logs execution time
- Appends to a log file: timestamp | command | exit_code | duration
Example usage:
log_command ls -la /nonexistent
# Should log: 2026-02-13 10:30:45 | ls -la /nonexistent | 2 | 0.003s
Problem 4: Portable Script Checker¶
Create a script that analyzes another bash script and reports: - Non-POSIX constructs used ([[ ]], arrays, etc.) - Bashisms that would fail in sh - Suggestions for making it more portable - A "portability score" (0-100%)
Hint: Search for patterns like [[, declare, function keyword, etc.
Problem 5: Environment Snapshot¶
Write a script that: - Saves current environment variables to a file - Saves current shell options (set -o, shopt -p) to a file - Can restore the environment from the saved state - Shows diff between saved and current state
Usage:
./envsnap.sh save snapshot.env
# ... make changes ...
./envsnap.sh diff snapshot.env
./envsnap.sh restore snapshot.env