gifPaper for Mac
Guide

Bash Script Cheat Sheet: 8 Must-Know Sections

· 19 min read

Stop copy-pasting half a Bash script from Stack Overflow like it's a magical sandwich. Bash is one of those tools that looks tiny from a distance, then reveals a command surface big enough to swallow your afternoon, which is exactly why a bash script cheat sheet works better when it groups the useful stuff by workflow instead of dumping every spell in the grimoire at once. The practical version lives in the boring places: variables, tests, loops, redirection, error handling, and the kind of small habits that keep a script from behaving like a startled raccoon.

That's also why the best references don't just list commands. They show how Bash is used, with patterns for files, process checks, loops, and the little one-liners people lean on when they'd rather not type the same thing twelve times in a row. Bash adoption landed at 49% in 2025, up about 15 percentage points from 2024, and Bash/Shell ranked 5th overall among languages used extensively in the past year (commandlinux.com). That makes this less like trivia and more like survival gear.

Variables and Parameter Expansion

The first Bash habit worth nailing is simple: a variable is just a named place to keep a value without retyping it like a caffeinated squirrel. In real scripts, that usually means paths, filenames, config values, or the odd command substitution when you want Bash to do the typing for you. A clean example looks like this:

WALLPAPER_DIR='~/Library/Application Support/gifPaper'
TIMESTAMP=$(date +%Y%m%d)

Curly braces matter more than people expect. ${var} keeps Bash from guessing where the variable name ends, which matters when you glue text onto the end or use defaults like CONFIG_FILE=${1:-'config.sh'}. The same trick handles quick text surgery too, like FILENAME=${SOURCE_FILE/.png/.jpg} or a simple array such as COLORS=('neon' 'lofi' 'fireplace'); echo ${COLORS[0]}.

Practical rule: quote your variables unless you enjoy accidental word splitting and mysterious bug reports at 11:47 p.m.

A few habits pay off immediately:

  • Use double quotes for expansion: "$var" preserves spaces, punctuation, and your dignity.
  • Prefer descriptive names: SCRIPT_DIR and CONFIG_PATH beat a and stuff.
  • Use defaults for optional input: ${var:-default} keeps empty values from wrecking a run.
  • Use readonly for constants: it prevents a future you from "just tweaking one thing" into a mess.

The best part is that variables aren't just storage—they're how Bash becomes reusable. A script that reads a config file, builds a destination path, and passes values into functions is already doing the grown-up version of shell work. It's not glamorous, but neither is finding out your filename with spaces got split into three cursed pieces.

Gears
A script needs structure, like gears need teeth.
Get this wallpaper

Conditional Statements That Don't Act Haunted

Bash conditionals are less about drama and more about asking: "Did that thing succeed?" If a command returns exit code 0, Bash treats it as true. That's why if command; then ... fi is often cleaner than wrapping every check in ceremony like a Victorian inheritance dispute.

File checks, string matches, and numeric comparisons all fit naturally into [[ ]], which is generally the safer and more flexible choice in Bash scripts. A config load might look like:

if [[ -f ~/.config/gifpaper.conf ]]; then 
  source ~/.config/gifpaper.conf
fi

A theme branch could be:

if [[ $THEME == 'neon' ]]; then 
  echo 'Purple mode activated'
fi

For numbers, if (( COUNT > 10 )); then echo 'Limit exceeded'; fi reads like actual intent instead of punctuation cosplay.

The useful trade-off is this: [[ ]] is stronger for Bash-specific work, while [ ] is more old-school and easier to misuse. For scripts that might grow teeth later, [[ ]] and (( )) usually make the logic clearer. A quick regex check can stay tidy too:

if [[ $FILENAME =~ \.(jpg|png)$ ]]; then 
  echo 'Valid image'
fi

if command; then is usually enough. If the command failed, Bash already told you. You don't need to interrogate it twice like it owes you money.

A compact pattern helps in real automation:

  • Check directories before writing: [[ -d $DIR && -w $DIR ]] saves you from permissions surprises.
  • Use numeric syntax for math: (( num > 5 )) is clearer than string-style comparisons.
  • Prefer direct command success: if curl -s https://gifpaper.com > /dev/null; then ... fi keeps the flow readable.
  • Avoid overusing -n and -z: bare [[ $var ]] or [[ -z $var ]] is usually enough.

Conditional logic is where Bash stops being a bag of commands and starts acting like a script. That's useful when the script has to decide whether to source a file, skip a missing directory, or refuse to run when a prerequisite is absent. The shell may be old, but at least it still knows how to say no.

Loops That Handle Repetition Without Whining

Loops are where Bash stops making you repeat yourself like a customer service chatbot. A for loop fits a list, while keeps going while a condition stays true, and until does the slightly more contrarian version, repeating until the condition finally cooperates. In practice, that means batch jobs, file conversion, retries, and all the little maintenance tasks nobody wants to click through by hand.

A file loop often looks like:

for file in *.png; do 
  convert "$file" "${file%.png}.jpg"
done

This is about as direct as shell work gets. Numeric ranges can be dead simple too:

for i in {1..5}; do 
  echo "Task $i"
done

Or more explicit with:

for (( i=0; i<10; i++ )); do 
  echo $i
done

If you're processing a list of categories or wallpapers, arrays make the intent obvious:

COLORS=('neon' 'lofi' 'fireplace')
for COLOR in "${COLORS[@]}"; do 
  echo "Category: $COLOR"
done

Use while when the stopping point depends on the data, not just a neat counter. A counting loop like:

COUNT=0
while (( COUNT < 10 )); do 
  echo $COUNT
  (( COUNT++ ))
done

is easy to read, and until flips the condition when that makes the logic cleaner. If you need to read input without losing shell state, process substitution is often safer than piping directly into the loop.

Practical rule: if you need loop variables later, don't pipe into the loop unless you like discovering subshell behavior the hard way.

A few loop habits keep scripts sane:

  • Quote array items: for item in "${ITEMS[@]}" avoids word splitting.
  • Prefer continue for skips: it reduces nesting and keeps the path obvious.
  • Use globbing first when it fits: for file in *.png is often simpler than calling find.
  • Use {1..N} for ranges: it keeps the syntax compact.

This is the part of Bash where people accidentally write five lines to do what a loop already knows. Don't fight the machine. Make it repeat the boring part and move on with your day.

Record Player
A loop is just a record player for your data.
Get this wallpaper

Functions and Return Values

Functions are the difference between a script and a script that's trying to be a responsible adult. They bundle reusable logic, make the file easier to read, and let you give meaningful names to the little operations that keep showing up. Bash accepts both function NAME { CODE; } and NAME() { CODE; }, which means you can pick the style that annoys your future self the least.

Arguments come in as positional parameters, so $1, $2, and friends work inside the function just like they do at the script level. For a quick validation helper:

validate_file() { 
  [[ -f "$1" ]] && echo 'Valid' || echo 'Invalid'
}

A stricter boolean-style check might be:

is_wallpaper_valid() { 
  [[ "$1" =~ \.(mp4|mov)$ ]] && return 0 || return 1
}

The big trade-off here is output versus status. Use return for success or failure, and use echo when you want data back. That means:

get_config_value() { 
  grep "^$1=" ~/.gifpaper.conf | cut -d'=' -f2
}

can feed command substitution, while a function that validates input should usually return an exit code and let the caller decide what happens next.

Here's a useful habit with production scripts:

Practical rule: keep reusable functions from calling exit unless the whole script should die right there. return is kinder, and kinder code is easier to reuse.

A few small choices make functions less messy:

  • Use local for temporary variables: it keeps the global scope from turning into a junk drawer.
  • Quote arguments defensively: "$@" preserves each argument exactly as passed.
  • Name functions for intent: validate_config tells you more than check.
  • Check return codes at the call site: if is_valid "$file"; then ... fi keeps control flow obvious.

For practical Bash work, functions are how you keep a long automation script from becoming a comment-supported collapse. They're also the easiest way to make one chunk of shell logic testable without treating the whole file like a sacred text.

If you're wiring Bash into other tooling, you'll run into tasks that need a clean install path or a setup step. For a Mac-side example, the installation flow for installing Ruby on Mac shows the same general idea: a small reusable function beats copy-pasting setup steps into three different places.

String Manipulation and Pattern Matching

Bash can do a surprising amount of text work without launching external tools, which is good because every external process is one more chance for a script to feel dramatic. Parameter expansion can trim file extensions, remove path prefixes, replace chunks of text, or slice substrings straight from the variable. That means less sed, less cut, and less waiting around while your shell pretends to be a pipeline manager.

A few examples carry most real-world needs:

FILENAME="image.png"
echo "${FILENAME%.png}"  # drops the extension

PATH="/home/user/wallpapers/neon.mp4"
DIR="${PATH%/*}"  # pulls the directory

URL="https://example.com"
echo "${URL/example/gifpaper}"  # swaps one substring for another

VERSION="v2.1.3"
echo "${VERSION:1:3}"  # extracts a slice

THEME="neon"
echo "${THEME^}"  # capitalizes the first letter

Pattern matching in [[ ]] is handy when you want to test shape, not just exact text. A filename check like:

if [[ $EMAIL =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then 
  echo 'Valid email'
fi

is a good example of where Bash can stay compact without becoming cryptic. If you need capture groups:

if [[ $STR =~ ([0-9]+)-([0-9]+) ]]; then
  echo "${BASH_REMATCH[1]} and ${BASH_REMATCH[2]}"
fi

Bash gives you a tiny text factory. Use it for filenames, paths, and lightweight checks, then reach for heavier tools only when the job actually needs them.

A few details are easy to forget:

  • # trims from the left: useful for removing prefixes.
  • % trims from the right: perfect for extensions and suffixes.
  • Single versus double operators matter: one is shortest match, two is longest.
  • Substring extraction stays readable: ${var:offset:length} often beats piping into cut.

This is one of Bash's nicer surprises. You can take a weird filename, normalize a path, and test a pattern without spawning three separate commands and a cloud of unnecessary ceremony. That's the kind of efficiency that keeps scripts boring in the best possible way.

Crystal Ball
The future of your script depends on tight string handling now.
Get this wallpaper

Input, Output, Redirection, and Pipes

Redirection is where Bash starts acting like plumbing, which is either satisfying or mildly alarming depending on your mood. Input redirection < reads from a file, > writes, and >> appends. Pipes | pass output from one command to the next, which is how a bunch of humble tools can behave like a mini assembly line.

A few examples are worth memorizing because they show up constantly:

echo 'neon' > theme.txt  # writes a file
echo 'wallpaper' >> log.txt  # appends to one
curl https://gifpaper.com 2> errors.log  # sends errors away from standard output
ls *.png | wc -l  # counts files
cat << EOF > config.sh  # creates a heredoc
# config goes here
EOF
diff <(sort file1.txt) <(sort file2.txt)  # compares outputs

The subtle part is standard error. If you want both output streams together, command > output.log 2>&1 is the classic pattern, though &> is usually clearer when Bash supports it. If your pipe is getting noisy, command 2>/dev/null | next_command keeps the junk out of the downstream tool. That matters when you'd rather process clean data than debug a parade of warnings.

For logging and inspection, tee earns its keep:

command | tee output.log | grep error

This lets you watch the stream and keep a copy, which is the shell version of "I'd like receipts, please." Herestrings also save time:

grep 'neon' <<< "$WALLPAPER_LIST"

This is cleaner than turning a tiny string into a file just to feed it right back into a command.

One important link in the workflow is background handling, especially when scripts split work between what the user sees and what the script writes. The differences between foreground and background behavior matter whenever you're building shell automation, and the guide on background and foreground handling fits naturally with that distinction.

Practical rule: pipes create subshells, so if you change a variable inside a piped loop, don't expect the outer shell to remember it. Use process substitution or a different structure when the loop needs to keep its memory.

That one bites people a lot. Bash is happy to stream data around like a clever little post office, but it won't always preserve your local state the way you hoped.

Error Handling and Exit Codes

Bash error handling starts with a tiny number that does a big job. Exit code 0 means success, and any non-zero value means failure, so scripts can branch, stop early, and keep bad input from leaking into later steps. The quickest habit is plain enough: check the result right away, or use if command; then and let Bash do the branch for you.

The strict mode trio is the practical baseline for production scripts:

set -o errexit
set -o nounset
set -o pipefail

In the shorter form, many people write set -euo pipefail, which tells Bash to fail fast on errors, unset variables, and hidden pipeline failures. That is a real improvement over a script that keeps marching after the first broken command.

A solid pattern is to fail loudly and clean up cleanly:

trap 'rm -f "$TEMP_FILE"' EXIT
set -x  # prints each command before execution
echo "Error: Failed to download wallpaper from $URL" >&2  # send errors to stderr

For user-facing problems, send errors to stderr so logs and normal output stay separate.

Here is the part people skip and then regret later. set -e alone does not cover every case, especially not pipelines or commands hidden inside more complex structures. pipefail closes that gap, and scripts that need to survive real automation should also be checked with bash -n and reviewed with ShellCheck, which is stronger when you use the official guidance at shellcheck.net. The point is not paranoia. It is making failure obvious before it ships.

A compact pattern helps when things break:

  • Use fallback logic with ||: command || { echo 'Error'; exit 1; } keeps failure handling close to the command.
  • Keep context in messages: telling the user what failed is more useful than a lonely "failed".
  • Check code immediately: $? only means something before the next command runs.
  • Debug with bash -x script.sh: it is often faster than guessing.

If Bash scripts live in cron, CI, or a deploy hook, this section matters more than the syntax candy. Silent failure is the main villain. The shell is happy to dress it up and send it out the door.

Volcano
Error handling is like lava containment: you have to be deliberate about where it flows.
Get this wallpaper

Script Structure and Shebang

A Bash script should start by saying exactly how it wants to run. #!/bin/bash points straight at Bash, while #!/bin/sh asks for POSIX shell behavior instead. Red Hat's Bash scripting cheat sheet covers both startup styles, whether you put the shebang on the first line or launch the script directly with bash script.sh, so the interpreter choice stays clear from the start (developers.redhat.com).

Structure matters because Bash is easier to trust when it reads top to bottom in a predictable order. A practical layout is shebang first, safety settings next, then helper functions, main logic, and main "$@" at the end. #!/usr/bin/env bash is the friendlier portable form because it finds Bash through PATH, which helps when the interpreter lives somewhere different across machines. If you are wiring that script into a wallpaper task, the same setup applies whether you are launching a local helper or following how to set a desktop background on Mac.

A script that runs directly also needs execute permission:

chmod +x wallpaper-installer.sh
./wallpaper-installer.sh

This route is cleaner than spraying chmod 777 around like confetti. If you want to keep an eye on how these scripts are launched in real deployments, you can also monitor scripts with Fivenines.

A small usage() function earns its place early:

A tiny usage() function saves debugging time because the script can explain itself before anyone starts guessing arguments in the dark.

Useful structure habits include the following:

  • Set safety flags right after the shebang: set -euo pipefail belongs near the top.
  • Use usage() for argument help: it gives the script a clear front door.
  • Keep main() separate: it makes testing and reading easier.
  • Add a comment header: purpose, usage, and author all belong there.

This turns a quick one-off into something you can hand to another person without apologizing first. If the script is going to live longer than the shortcut that launched it, the structure needs to carry its weight. Bash can be scrappy, but a little discipline goes a long way.

8-Point Bash Scripting Comparison

Item Implementation complexity Resource requirements Expected outcomes Ideal use cases Key advantages
Variables and Parameter Expansion Low, simple assignment; advanced expansions moderately tricky Minimal, built-in shell features; no externals Reusable config values; dynamic behavior Storing configs, capturing command output, passing data to child processes Lightweight, flexible string ops, defaults via parameter expansion
Conditional Statements (if/else/elif) Low–Moderate, basic tests easy; complex expressions require care Minimal, shell builtins; may call commands for tests Branching logic; input and state validation File checks, input validation, conditional flows Direct use of exit codes, readable logic, [[ ]] supports regex
Loops (for, while, until) Low–Moderate, simple loops easy; nested/large loops add complexity Varies, efficient for small lists; large datasets may tax CPU/memory; pipes spawn subshells Batch processing and repeated tasks Iterating files, ranges, retries, collection processing Flexible iteration patterns, globbing, C-style numeric loops
Functions and Return Values Moderate, definition simple; scope and return semantics require attention Minimal, promotes code reuse and smaller scripts Modular, testable, reusable components; clearer structure Encapsulating logic, utilities, validation helpers Encapsulation, local scope, output capture and exit-code signaling
String Manipulation and Pattern Matching Moderate, terse syntax but powerful; pattern rules can be non-intuitive Minimal, built-in (faster than external sed/awk) Efficient filename/text transformations and validations Trimming extensions, extracting substrings, regex checks Fast built-in ops, avoids external tools, regex via =~
Input/Output Redirection and Pipes Low–Moderate, basic redirection simple; process substitution advanced Varies, pipelines may spawn subshells; avoids temp files with process substitution Chained processing, logging, selective error handling Command chains, logging, diff/sort via process substitution Powerful chaining, separate stdout/stderr, heredocs and tee for debugging
Error Handling and Exit Codes Moderate, requires understanding set/trap nuances Minimal, shell features; careful global options usage Robust failure handling; clear diagnostics and cleanup Production scripts, installers, automation requiring reliability set -euo pipefail, trap for cleanup, consistent exit-code signaling
Script Structure and Shebang / Script Execution Low, shebang and layout straightforward; portability choices add nuance Minimal, choose interpreter availability; executable permission required Portable, maintainable, easy-to-run scripts Distributable scripts, team-maintained tools, production deployment Explicit interpreter, consistent structure, usage/help patterns

Script Smarter, Not Harder

Keep this bash script cheat sheet close when you're working through the usual shell nonsense. Variables, tests, loops, functions, string tricks, redirection, error handling, and a clean script structure cover most of the actual work, and the rest is usually just you and a stubborn filename having a disagreement.

The fastest way to get better at Bash is to stop memorizing isolated commands and start noticing patterns. A variable becomes useful when it survives quoting. A conditional becomes useful when it branches on a real exit code. A loop becomes useful when it handles a whole directory instead of one lonely file. A function becomes useful when it turns repeated glue code into something you can trust twice.

That's why the best cheat sheets feel less like a dictionary and more like a condensed field manual. Bash has plenty of built-ins, but production scripts still live or die on a few habits: checking errors early, quoting carefully, and keeping the structure obvious enough that your future self doesn't need a lantern and a rescue team.

If you're using Bash for automation, CI, setup tasks, or the recurring little jobs that nobody wants to click through manually, this is the stuff that saves time without turning the script into a ceremonial artifact. It also explains why modern cheat sheets focus on workflow-ready patterns instead of just command recall—because the audience isn't tiny, and the problems aren't theoretical. They're the sort of problems that show up when a script runs at 2 a.m. and nobody's there to babysit it.

And yes, the shell is still a glorified text machine at heart. That's part of the charm. It takes a few strong habits, a bit of structure, and the willingness to let Bash do the repetitive bits while you stay human.

Neon Vibes Purple
A well-structured script is like neon light—clean, purposeful, and nothing wasted.
Get this wallpaper

If you want the screen you stare at all day to look less like a holding pen for spreadsheets, try gifPaper. It turns your Mac lock screen, desktop, and screen saver into live wallpapers, so the same kind of thoughtful structure you want in a script can finally show up on your monitor too.