Everything covered in Bash Basics works identically inside a script file - the difference is that a script also needs to handle cases you'd notice and correct interactively, like a missing argument or a failed command, without a human watching.

The shebang and making a script executable

#!/usr/bin/env bash

The first line tells the kernel which interpreter to run the file with. #!/usr/bin/env bash finds bash via PATH rather than assuming it's at /bin/bash, which is more portable across systems. Then make it executable and run it:

chmod +x deploy.sh
./deploy.sh

See File Permissions and Ownership for what chmod +x is actually changing.

Variables and arguments

#!/usr/bin/env bash
name="$1"           # first argument
echo "Hello, $name"
echo "Called with $# argument(s): $*"

$1, $2, … are positional arguments; $# is the argument count; $@ and $* both expand to all arguments, but "$@" (quoted) preserves argument boundaries when one contains spaces, while "$*" collapses them into one string - prefer "$@" when forwarding arguments to another command.

Conditionals

if [[ -f "$file" ]]; then
  echo "exists"
elif [[ -d "$file" ]]; then
  echo "is a directory"
else
  echo "not found"
fi

[[ ]] is a Bash keyword with more forgiving syntax and no word-splitting surprises; [ ] (a synonym for the test command) is the POSIX-portable form needed if the script must also run under sh. Common tests: -f (regular file exists), -d (directory exists), -z/-n (string is empty/non-empty), -eq/-lt/-gt (numeric comparison - [[ ]] doesn't overload </> for numbers the way it does for strings).

Loops

for f in *.log; do
  echo "processing $f"
done

while read -r line; do
  echo "got: $line"
done < input.txt

while read -r line is the standard idiom for processing a file line by line; -r prevents backslashes in the input from being interpreted.

Functions

log() {
  echo "[$(date '+%H:%M:%S')] $1"
}

log "starting deploy"

Functions in Bash don't return values the way most languages do - return sets the function's exit status (0–255), not a value. To get data out of a function, either echo it and capture with $(...), or write to a variable directly.

Exit codes and error handling

Every command sets $? to its exit status: 0 for success, non-zero for failure. Scripts should check this rather than assuming a command succeeded:

if ! curl -sf https://example.com/health; then
  echo "health check failed" >&2
  exit 1
fi

For anything beyond a trivial script, start with these three options, which turn silent failures into loud ones:

set -euo pipefail
  • -e - exit immediately if any command fails (without this, a script keeps running after an error by default, which is rarely what you want)
  • -u - treat an unset variable as an error, instead of silently expanding to an empty string
  • -o pipefail - make a pipeline (cmd1 | cmd2) fail if any command in it fails, not just the last one

See Pipes and Redirection for what a pipeline is, and Signals for handling SIGINT/ SIGTERM in longer-running scripts.

A minimal complete example

#!/usr/bin/env bash
set -euo pipefail

log_dir="${1:?usage: $0 <log-dir>}"

for f in "$log_dir"/*.log; do
  [[ -f "$f" ]] || continue
  echo "compressing $f"
  gzip "$f"
done

${1:?message} exits with message printed to stderr if $1 is unset - a compact way to validate required arguments up front instead of failing confusingly later in the script.