You are currently viewing Bash Scripting for Beginners: Variables, Loops, and Safer Automation

Bash Scripting for Beginners: Variables, Loops, and Safer Automation

A Bash script is a plain-text file containing commands Linux could otherwise run one at a time in a terminal. Saving those commands makes repeatable tasks less error-prone: creating a dated backup folder, checking for a configuration file, or processing files in a directory. Bash, short for Bourne Again Shell, is the interpreter most commonly used for this purpose.

Begin with tasks on your own machine and files you are authorized to manage. Use harmless sample directories, print planned actions before making changes, and keep a backup before any script removes or overwrites data.

What Bash does and where scripts run

Bash reads commands, expands variables and filename patterns, then either starts programs or runs built-in commands such as cd, echo, and read. A script lets you combine these small command-line operations without compiling a program first.

Most Linux distributions include Bash, but Bash scripts are not automatically portable to every shell. Arrays and some conditional expressions, for example, are Bash-specific. Put this line at the top of a Bash script:

#!/usr/bin/env bash

This is called a shebang. When you launch the file directly, it tells the operating system to find Bash through the user’s environment. While learning, you can also run a script explicitly with bash filename.sh.

The shell is useful because it connects command-line tools through arguments, pipes, redirects, and exit codes. That same flexibility means unquoted text can be interpreted in ways you did not intend. Treat filenames and user input as data, never as pieces of a command.

A Bash script open in a Linux terminal

Create and run a first script

Create a file named greet.sh in a code editor or terminal editor:

#!/usr/bin/env bash

name="${1:-friend}"
printf 'Hello, %s!n' "$name"
printf 'Working directory: %sn' "$PWD"

Run it like this:

bash greet.sh Ada

$1 is the first command-line argument. The expression ${1:-friend} uses friend when no first argument is supplied. For formatted output, printf is generally a better choice than echo because its behavior is more predictable in common cases.

To run the file as ./greet.sh Ada, first make it executable:

chmod u+x greet.sh
./greet.sh Ada

The ./ is important. It explicitly identifies the file in the current directory. Linux does not normally search the current directory for commands, which reduces the chance of accidentally running a similarly named file.

Variables, quoting, and command substitution

Variable assignments do not have spaces around the equals sign:

project="demo-site"
backup_dir="$HOME/backups/$project"

Quote variable expansions unless you intentionally need word splitting or filename pattern expansion. A directory named March reports shows why:

mkdir -p "$backup_dir"
cp "$HOME/March reports/notes.txt" "$backup_dir/"

Without quotes, Bash can treat spaces as argument separators and pass several arguments to cp. Double quotes keep the text together while still expanding variables. Single quotes keep text literal:

printf '%sn' '$HOME will not expand here'

Use command substitution to store a command’s output:

today=$(date +%F)
printf 'Backup date: %sn' "$today"

The $(...) form is easier to read than older backticks and is safer to nest. For a solid introduction to command-line basics, Mozilla’s command line introduction covers navigation, file operations, and command execution in accessible language.

Conditions and loops for practical decisions

Scripts become more useful when they can make decisions. An if statement checks a command’s exit status; by convention, a status of zero means success.

#!/usr/bin/env bash

file="$HOME/Documents/todo.txt"

if [[ -f "$file" ]]; then
  printf 'Found: %sn' "$file"
else
  printf 'No file found at: %sn' "$file" >&2
fi

[[ ... ]] is Bash’s conditional syntax. -f checks for a regular file, and -d checks for a directory. The &2 redirect sends the error-style message to standard error instead of normal output.

Loop through files safely

A simple loop can work with selected files. This example lists text files without changing them:

#!/usr/bin/env bash

folder="${1:-.}"

for file in "$folder"/*.txt; do
  [[ -e "$file" ]] || continue
  printf 'Text file: %sn' "$file"
done

The [[ -e "$file" ]] || continue check handles directories with no matching files. Quotes around "$folder" protect spaces in the directory name. Keep the wildcard outside the quotes so Bash can expand it.

  • Use for loops for simple filename patterns.
  • Use while read -r line when processing lines from a file. The -r option prevents backslashes from receiving special treatment.
  • Use case when a value may match several fixed options.
  • Use functions to name repeated operations and make scripts easier to follow.

Arguments, input, and exit codes

Useful scripts state what they expect and fail clearly when input is missing. This compact pattern checks for one directory argument:

#!/usr/bin/env bash

if [[ $# -ne 1 ]]; then
  printf 'Usage: %s DIRECTORYn' "$0" >&2
  exit 2
fi

if [[ ! -d "$1" ]]; then
  printf 'Not a directory: %sn' "$1" >&2
  exit 1
fi

printf 'Directory accepted: %sn' "$1"

$# holds the number of arguments, while $0 is the name used to invoke the script. A nonzero exit status lets a person, another script, or an automated task recognize failure. By convention, 0 means success. The meaning of other values is up to the script unless a tool documents them.

Expression Meaning Typical use
$1 First argument A file or directory supplied by the user
$# Argument count Checking required inputs
$? Previous command’s status Checking whether a command succeeded
"$@" All arguments, kept separate Passing arguments to another command

Safer scripting habits

Automation can magnify small mistakes. Before a script touches important files, follow these habits:

  1. Prefer full paths or validated input. Do not assume the script starts in a particular directory.
  2. Quote expansions. Write "$file", "$@", and "$HOME" unless you have a specific reason not to.
  3. Preview destructive actions. Print the proposed rm, move, or overwrite operation first, then test it on copies.
  4. Choose a deliberate failure policy. For scripts where continuing after an error is unsafe, set -euo pipefail can help. It stops on many errors, rejects unset variables, and detects failures inside pipelines. Learn how it behaves before relying on it, since some conditional commands intentionally return nonzero statuses.
  5. Do not execute input as code. Avoid eval when processing filenames or user-supplied text.

Permissions are part of script safety too. A script containing API tokens or local paths should not be readable by unrelated accounts on a shared machine. Keep secrets outside source files where possible, restrict file permissions, and never commit real credentials in examples to version control.

Checking a script before automated file operations

Debugging without guesswork

Run bash -x script.sh to display each command after Bash expands it. Use tracing carefully because it can expose sensitive values. For a syntax-only check, use bash -n script.sh; it catches many structural errors without running commands.

Explicit diagnostics are often better than guessing:

printf 'Processing file: %qn' "$file"

The %q format prints a shell-escaped representation, which makes spaces and special characters easier to spot. Test in a temporary directory with one sample path, inspect what the script prints, and only then replace the diagnostic line with the intended file operation.