Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

To write a Bash script, put commands in a text file that starts with #!/usr/bin/env bash, save it, and run it with Bash (or make it executable and run the file). Bash then parses each line, expands variables and patterns, runs commands, and returns a status code. This guide builds a reliable first script and explains the syntax you need for variables, quoting, conditions, loops, functions, pipelines, and error handling.

Your first Bash script

Bash is both a command interpreter and a programming language. The examples below target Bash 5.3, documented in the GNU Bash Reference Manual, Edition 5.3 (updated 18 May 2025): GNU Bash Reference Manual.

  1. Create a file named backup-note.sh.
  2. Add this content:
#!/usr/bin/env bash
# Print a note for the selected project.
project="My Documents"
printf 'Preparing: %sn' "$project"

mkdir -p -- "$HOME/archive/$project"

The first line is the shebang. It tells the operating system to find an interpreter named bash through env; it is not a comment when the file is started as a program. The assignment creates a variable without spaces around =. The quoted expansion preserves the space in My Documents. mkdir is an external utility; Bash starts it and records its exit status.

Save and run it

You can always select the interpreter explicitly:

bash backup-note.sh

To run the file directly, give it execute permission and invoke its path:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
chmod u+x backup-note.sh
./backup-note.sh

A filename extension does not make a script executable. Direct execution needs execute permission and a valid shebang; bash backup-note.sh needs read access and uses the Bash binary you named, regardless of the file’s mode.

How Bash processes a command

Bash reads input, divides it into words and operators, parses the command, performs expansions, applies redirections, runs the command, and collects its status. Quoting matters during this process: it controls whether spaces, wildcard characters, variable references, and command substitutions retain their literal meaning.

Single and double quotes

name="Ada Lovelace"
printf '%sn' "$name"       # one argument: Ada Lovelace
printf '%sn' '$name'        # literal text: $name
printf '%sn' "$HOME"       # expands HOME

Double quotes allow parameter and command substitutions while protecting the resulting spaces and wildcard characters. Single quotes preserve everything literally until the next single quote. As a default, quote variable expansions: an unquoted expansion can be split into several words and then treated as a filename pattern.

Variables and command-line arguments

Use $variable or ${variable} to read a value. Braces make boundaries clear:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
prefix="report"
printf '%sn' "${prefix}_2026.txt"

Arguments supplied after the script name are positional parameters. $0 is the script name, $1 through $9 are individual arguments, "$@" expands to one quoted word per argument, and $# is the argument count.

#!/usr/bin/env bash
if (( $# != 1 )); then
    printf 'Usage: %s DIRECTORYn' "$0" >&2
    exit 2
fi

printf 'Scanning: %sn' "$1"

Use "$@" when forwarding arguments without losing spaces:

for item in "$@"; do
    printf 'Item: %sn' "$item"
done

Tests and conditional logic

An if command runs its body when the test command returns status zero. Bash offers the Bash-specific [[ ... ]] compound conditional, arithmetic evaluation with (( ... )), and the portable [ ... ] (also called test).

path="$1"
if [[ -f "$path" ]]; then
    printf 'Regular file: %sn' "$path"
elif [[ -d "$path" ]]; then
    printf 'Directory: %sn' "$path"
else
    printf 'Not found: %sn' "$path" >&2
    exit 1
fi

count=3
if (( count > 0 )); then
    printf 'Remaining: %dn' "$count"
fi

Inside [[ ... ]], use operators such as ==, !=, -f (regular file), and -d (directory). Arithmetic conditions use numbers rather than string operators. Quote expansions in ordinary test expressions unless you deliberately need pattern matching.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Loops for repeated work

Iterating over arguments or a known list

for file in "$@"; do
    printf 'Would process %sn' "$file"
done

Reading lines safely

while IFS= read -r line; do
    printf '> %sn' "$line"
done < input.txt

IFS= prevents trimming leading and trailing whitespace, and read -r leaves backslashes unchanged.

Counting with arithmetic

n=1
while (( n <= 3 )); do
    printf '%dn' "$n"
    ((n++))
done

Functions and Bash arrays

Functions group commands for reuse and run in the current shell context. Unless you explicitly return another status, a function returns the status of its last command.

say_status() {
    local label=$1
    printf '%s: %sn' "$label" "${2:-unknown}"
}

say_status "Database" "ready"

local limits a variable to the function. Use return for a function status, and use printf or output variables for results.

Indexed arrays are Bash-specific, so these examples require Bash rather than an arbitrary /bin/sh:

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
files=("notes.txt" "photo archive.jpg")
for file in "${files[@]}"; do
    printf '%sn' "$file"
done

Redirection and pipelines

Syntax Effect
command > file Write standard output to a file, replacing it.
command >> file Append standard output.
command 2> errors.log Write standard error separately.
command &> all.log Write standard output and standard error together (Bash syntax).
command1 | command2 Connect the first command’s standard output to the second command’s standard input.
grep -i 'failed' application.log | sort > failed-sorted.txt

Utilities such as grep, find, and sed are separate programs, and their options can differ between operating systems. Commands in a multi-command pipeline generally run in separate subshells, so a variable changed inside a pipeline may not change the calling shell.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Exit statuses and dependable failure handling

By convention, status 0 means success and a nonzero status means failure. $? contains the status of the most recently completed command, so capture or test it immediately:

if ! mkdir -p -- "$target"; then
    printf 'Cannot create %sn' "$target" >&2
    exit 1
fi

A missing command normally returns status 127; a command that was found but cannot be executed returns 126, according to the Bash manual.

Pipeline status and pipefail

Setting Pipeline result
Default The status of the last command in the pipeline.
set -o pipefail The status of the rightmost command that exits nonzero, or zero when every command succeeds.
set -o pipefail
if ! grep -i 'failed' application.log | sort > failed-sorted.txt; then
    printf 'The pipeline failedn' >&2
    exit 1
fi

What set -e does—and does not do

set -e asks Bash to exit when a simple command returns nonzero, but it is not a universal error-trapping switch. Bash suppresses that exit behavior in documented contexts, including commands used as tests in if, commands in parts of && and || lists (with context-dependent exceptions), and non-final pipeline commands; pipefail changes pipeline status but does not remove every exception. For important operations, write an explicit check such as if ! command; then ... fi. If you use strict-mode combinations, understand each option and test the exact control flow rather than assuming every failure stops the script.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Bash versus POSIX sh

Choice Use when Examples
Bash script You control the interpreter and want Bash features. #!/usr/bin/env bash, [[ ... ]], arrays, pipefail.
POSIX shell script The script must run under different /bin/sh implementations. Use POSIX syntax and avoid Bash-only constructs.

/bin/sh is not guaranteed to be Bash. A script using arrays, [[ ... ]], or other Bash-specific syntax should name Bash in its shebang and be started with Bash. If portability is the priority, write to the POSIX shell language instead and test on every target shell.

A practical checklist before sharing a script

  • Start with the intended interpreter, such as #!/usr/bin/env bash.
  • Quote expansions unless you intentionally need splitting or pattern expansion.
  • Validate required arguments and quote path arguments, including those beginning with - when a utility supports --.
  • Check statuses for operations whose failure matters; decide whether pipefail matches your pipeline requirements.
  • Keep Bash-only syntax out of scripts that claim POSIX sh portability.
  • Test with filenames containing spaces, empty input, missing files, and commands that fail.

Further reference

The complete language reference, including expansions, redirections, builtins, and option details, is available in the GNU Bash Reference Manual. GNU also provides a Bash manual landing page with information about the manual and printed editions.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.