Cloud Tech by Victor
DevOpsBeginner

Bash Fundamentals

The scripting layer built on top of the shell - variables, conditionals, loops, and functions - and the quoting and exit-status habits that separate a script that looks right from one that fails safely.

Updated 2026-07-245 min read

Overview

Bash-the-scripting-language sits directly on top of Bash-the-interactive-shell: the same commands you'd type at a prompt become a script once you add variables, conditionals, loops, and functions to sequence and reuse them. That reuse is exactly where scripts stop looking like a transcript of commands and start behaving like real programs, with the same failure modes worth taking seriously: unquoted variables that word-split unexpectedly, exit statuses that silently get ignored, and subshells whose variables don't leak back out the way a newcomer expects. None of this replaces a real programming language for genuinely complex logic, but for gluing commands together, it's usually the fastest path from "a sequence of steps I keep typing" to "a script I can trust."

Quick Reference

ConceptWhat it isNote
ShebangThe first line, declaring which interpreter runs the script#!/usr/bin/env bash is the portable form
Positional parameterAn argument passed to a script or function$1, $2, ... $9, ${10} and beyond need braces
Exit statusA command's numeric result; 0 means successChecked via $?, or directly in if/while
SubshellA child shell running a command substitution or (...) groupVariables set inside it don't leak back to the parent

The commands below are grouped by what you're trying to do, not an exhaustive reference (see the official GNU Bash Reference Manual for every detail), but the syntax that covers almost all everyday scripting.

bash
#!/usr/bin/env bash
set -euo pipefail   # exit on error, unset variable, or failed pipeline stage

echo "Script name: $0"
echo "First argument: $1"
echo "All arguments: $@"
echo "Argument count: $#"

Syntax

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

backup_dir="/backups/$(date +%Y-%m-%d)"
mkdir -p "$backup_dir"

for file in /data/*.db; do
  cp "$file" "$backup_dir/"
done

echo "Backed up $(ls "$backup_dir" | wc -l) files to $backup_dir"

Examples

bash
set -euo pipefail
count=0
while read -r line; do
  [[ "$line" == *ERROR* ]] && ((++count))
done < app.log
echo "Found $count error lines"
find . -name "*.tmp" -mtime +7 -delete

A common cleanup one-liner: find files matching a pattern older than 7 days and delete them, no script file needed.

find . -name "*.tmp" -mtime +7 -delete

Visual Diagram

Common Mistakes

  • Leaving variables unquoted ($file instead of "$file"), so a value with a space or glob character silently word-splits or expands into multiple arguments.
  • Looping over $(ls) output instead of a glob (for f in *.log), which breaks on filenames containing spaces or newlines.
  • Omitting set -euo pipefail, so a failed command partway through a script is silently ignored and the script keeps running against a now-inconsistent state.
  • Forgetting local inside a function, so a variable meant to be scoped to that function overwrites a same-named variable in the caller.
  • Assuming a pipeline's exit status reflects every stage; without pipefail, false | true reports success because only the last command's status is checked.

Performance

  • Every subshell ($(...), a (...) group, or a pipeline stage) forks a new process; a tight loop that spawns one per iteration is measurably slower than an equivalent loop using Bash builtins.
  • Prefer parameter expansion (${var//search/replace}) over piping through sed/awk for simple string substitutions; one is a builtin, the other forks an external process.
  • A while read -r line; do ... done < file loop streams a file line by line without loading it entirely into memory, which matters once files get large.

Best Practices

  • Start every script with #!/usr/bin/env bash and set -euo pipefail, and treat leaving either out as something that needs a specific reason.
  • Quote every variable expansion unless you specifically want word-splitting or globbing to happen.
  • Prefer [[ ]] over [ ] for conditionals in Bash-specific scripts; reach for [ ] only when a script genuinely needs POSIX /bin/sh portability.
  • Run scripts through ShellCheck before relying on them; it catches most of the quoting and word-splitting mistakes above automatically.
  • Use local for every variable declared inside a function, so functions don't leak state into the caller's scope.

Interview questions

What is the difference between `[ ]` and `[[ ]]` in Bash conditionals?

`[ ]` is the POSIX test command, an actual command whose arguments undergo the shell's normal word-splitting and globbing before it ever sees them, which is why an unquoted variable inside it can break in surprising ways. `[[ ]]` is a Bash keyword with special parsing: it does not word-split or glob its arguments, supports pattern matching (`==`, `=~`) and logical operators (`&&`, `||`) directly inside the brackets, and is generally the safer, more predictable choice in Bash-specific scripts, at the cost of not being portable to a strict POSIX `/bin/sh`.

Why does `set -euo pipefail` matter at the top of a script?

By default, Bash keeps executing after a command fails, treats referencing an unset variable as an empty string instead of an error, and reports a pipeline's exit status as only its last command's; all three hide real failures. `-e` exits on a non-zero status from most simple commands, but only in contexts where errexit actually applies; it does not trigger inside `if`/`while`/`until` conditions, for any but the last command in a pipeline (unless combined with `pipefail`), or for a non-final command in a `&&`/`||` list, whose status is checked by the operator itself rather than causing an exit. The final command in that list is not exempt, though: if it fails and the list isn't itself acting as an `if`/`while`/`until` condition or the left side of another `&&`/`||`, errexit still triggers. `-u` turns an unset-variable reference into an error, and `-o pipefail` makes a pipeline fail if any stage fails, not just the last one. Together they turn a script that silently continues past errors into one that fails loudly in the cases where errexit applies, which is almost always what you want for anything beyond a one-off interactive command, but it is not a blanket guarantee that catches every failure everywhere.

Why should variables almost always be quoted, e.g. `"$name"` instead of `$name`?

An unquoted variable expansion undergoes word-splitting (on whitespace) and globbing (on `*`, `?`, etc.) before the command sees it, so a value containing a space or a shell metacharacter silently becomes multiple arguments or an unintended file-glob expansion instead of one literal string. Quoting (`"$name"`) suppresses both, so the variable's value is always passed through as exactly one argument, which is why nearly every Bash style guide treats an unquoted variable expansion as a latent bug rather than a style preference.

References

ShareXLinkedInReddit
Was this page helpful?
Suggest an improvement