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.

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
forloops for simple filename patterns. - Use
while read -r linewhen processing lines from a file. The-roption prevents backslashes from receiving special treatment. - Use
casewhen 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:
- Prefer full paths or validated input. Do not assume the script starts in a particular directory.
- Quote expansions. Write
"$file","$@", and"$HOME"unless you have a specific reason not to. - Preview destructive actions. Print the proposed
rm, move, or overwrite operation first, then test it on copies. - Choose a deliberate failure policy. For scripts where continuing after an error is unsafe,
set -euo pipefailcan 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. - Do not execute input as code. Avoid
evalwhen 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.

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.
