Skip to main content

What Is Bash? Versions, Startup Files and Your First Script

What Bash is, which version you have and why it matters (5.2, 5.3, macOS 3.2), sh versus Bash, which startup files load when, and your first script.

Bash (the Bourne Again SHell) is the program that reads what you type in a Linux terminal, works out what you meant, and runs it. It is also a programming language: the same commands, saved in a file, become a script. That double role is why Bash is everywhere in DevOps work. It is the login shell on most Linux servers, CI systems run your pipeline steps with it, and container entrypoints, Git hooks, cloud-init files and install scripts are very often Bash.

This part explains what Bash is, which version you have and why the version matters, how sh differs from Bash, and which startup files Bash reads when. At the end you set up the practice directory the rest of the guide uses and run your first script.

Prerequisites

  • A Linux, macOS or Windows computer with a terminal. The tabs below show how to get Bash on each.
  • Basic terminal use: moving around with cd and ls, and editing a file. The Linux command line basics part of the Linux guide covers it.
  • ShellCheck, which this guide runs on every script: sudo apt install shellcheck on Debian and Ubuntu, sudo dnf install ShellCheck on Fedora, brew install shellcheck on macOS. Later parts also use jq, curl and nc (on Debian and Ubuntu: sudo apt install jq curl netcat-openbsd), and Part 10 adds shfmt and bats.

The output in this guide comes from Bash 5.2.21 on Ubuntu 24.04.5 LTS, run as a normal user called dev on a machine called devbox. Blocks that use another version say so: Bash 5.3.9 on Ubuntu 26.04.1 LTS for the new 5.3 features, and Bash 3.2.57, the release macOS still ships, for portability checks.

What a Shell Does

A terminal (GNOME Terminal, Windows Terminal, iTerm2, the VS Code panel) is the window: it draws text and sends your keystrokes on. The shell is the program running inside it. Bash reads a line, expands variables, wildcards and other special syntax, finds the program you named, starts it, waits for it to finish and shows the next prompt.

Most of what makes Bash tricky happens in the second box. Before a command runs, Bash splits the line into words, replaces $name with values, turns *.log into file names and removes quotes. Part 2 walks through that process step by step, because almost every Bash bug is a surprise in it.

A Short History, and Why the Version Matters

The original Unix shell for scripts was the Bourne shell (sh), written by Stephen Bourne at Bell Labs and released with Version 7 Unix in 1979. Brian Fox wrote Bash for the GNU Project as a free replacement and released it in 1989. Chet Ramey has maintained it since the early 1990s. Bash runs almost every Bourne shell script and adds arrays, [[ ]], brace expansion, better interactive editing and much more.

New Bash versions add features that scripts come to depend on, so the version on the machine that runs your script matters:

Version Released Features this guide uses
3.2 October 2006 The last GPLv2 release, and still /bin/bash on macOS
4.0 February 2009 Associative arrays, mapfile, ** with globstar, ${var,,} and ${var^^}, ;& in case
4.2 February 2011 [[ -v name ]], printf '%(...)T' for dates, lastpipe, ${a[-1]} for the last array element
4.3 February 2014 Namerefs (declare -n), wait -n
4.4 September 2016 ${var@Q} transformations, local -, inherit_errexit, empty arrays allowed under set -u
5.0 January 2019 $EPOCHSECONDS and $EPOCHREALTIME
5.1 December 2020 $SRANDOM, ${var@U} and ${var@L}
5.2 September 2022 & in ${var/pattern/replacement} inserts the match (patsub_replacement), globskipdots
5.3 July 2025 ${ command; } command substitution without a subshell, GLOBSORT, source -p

Upstream Bash is at 5.3 with patch 20 (5.3.20, September 2026). What you get depends on your system:

System Bash version (October 2026)
Ubuntu 24.04 LTS 5.2.21
Ubuntu 26.04 LTS 5.3.9
Debian 13 (trixie) 5.2.37
Fedora 44 5.3.9
RHEL 10, AlmaLinux 10, Rocky 10 5.2.26
RHEL 9 and its rebuilds 5.1.8
Alpine Linux 3.24 5.3.9, but only after apk add bash
macOS 3.2.57 as /bin/bash; zsh is the default shell
Homebrew (macOS and Linux) 5.3.20

The practical rule: target Bash 4.4 or newer, test on the oldest version you deploy to, and treat macOS's 3.2 as a different language unless you install a newer Bash there. Part 10 shows what breaks under 3.2.

Which Bash Do You Have?

Two checks trip people up. $SHELL is set at login from your account's entry in /etc/passwd; it does not change when you start another shell, so it cannot tell you what you are running now. $BASH_VERSION is set only by Bash, so an empty value means you are in another shell.

If you would rather practise on a fresh Linux server than on your own machine, any small cloud server with Ubuntu 24.04 LTS works. On DigitalOcean, you can create a Droplet with the smallest size and connect with ssh. That is a referral link: new accounts that sign up through it get DigitalOcean's standard signup credit ($5, valid for 90 days, checked in October 2026) and must add a payment method before creating a Droplet, and if you later spend $25 there, DevOps Daily gets $25 in credit (referral program). Delete the Droplet when you are done, so it stops costing money.

sh Is Not Bash

Many tools run commands with /bin/sh, not Bash, and on Debian and Ubuntu /bin/sh is dash, a small and fast POSIX shell. Bash features such as [[ ]], arrays and {1..5} do not exist there:

On Fedora and RHEL, /bin/sh is Bash running in POSIX mode, which hides the problem until the script runs on Debian, Ubuntu or Alpine (BusyBox ash). This matters in places you might not think of as "running a script":

  • cron runs each crontab line with /bin/sh.
  • Dockerfile RUN lines use /bin/sh -c unless you add a SHELL ["/bin/bash", "-o", "pipefail", "-c"] instruction.
  • Makefile recipes use /bin/sh unless you set SHELL := /bin/bash.
  • GitHub Actions runs a run: step on Linux with bash -e {0} by default, and with bash --noprofile --norc -eo pipefail {0} when you write shell: bash. The second one also catches failures in the middle of a pipeline.

The fix is simple: start every Bash script with a Bash shebang (#!/usr/bin/env bash or #!/bin/bash) and run it directly or with bash script.sh, never with sh script.sh. Part 7 covers shebangs in depth, and the difference between sh and Bash lists more differences.

Login, Interactive and Non-Interactive Shells

Bash reads different startup files depending on how it starts. This explains two classic puzzles: "my alias works in the terminal but not in my script" and "the command works when I type it but not from cron".

You can watch this happen. Add a line that prints a message to the end of ~/.profile and ~/.bashrc, then start each kind of shell:

The login shell ran ~/.profile, which on Ubuntu sources ~/.bashrc, but the default Ubuntu ~/.bashrc starts with a check that stops it early in a non-interactive shell, so its last line never ran. The script read neither file. Two rules follow:

  • Put environment variables such as PATH changes in ~/.profile (or ~/.bash_profile), and interactive settings such as aliases, prompt and completion in ~/.bashrc.
  • Do not rely on aliases or functions from ~/.bashrc inside scripts. A script should set up everything it needs itself, including PATH when it runs from cron or systemd.

One exception: Debian and Ubuntu build Bash so that when sshd runs a command for you (ssh devbox 'uptime'), it reads ~/.bashrc even though the shell is not interactive. That is one reason the default ~/.bashrc returns early for non-interactive shells, and why anything that prints text belongs below that check: output from ~/.bashrc breaks tools such as scp and rsync that talk over the same connection.

Be careful with ~/.bash_profile: if it exists, Bash reads it instead of ~/.profile. If you create one, add [[ -f ~/.profile ]] && . ~/.profile to it, or the settings in ~/.profile stop loading. After editing ~/.bashrc, apply the changes to your current terminal with source ~/.bashrc (see how to reload .bashrc).

Remove the two test lines when you are done. The # startup-test comment marks them, so sed deletes exactly those lines and nothing else:

Set Up the Practice Directory

The examples in this guide work with a small set of files in ~/bash-practice: a web server log with 40 requests, an application log, a config file, a server list, a CSV inventory and a directory of files with awkward names. Create them with this script. Open an editor in your home directory with nano setup-practice.sh, paste the script, save with Ctrl+O and Enter, and exit with Ctrl+X:

#!/usr/bin/env bash
# setup-practice.sh: create ~/bash-practice, the files this guide works with
set -euo pipefail

dir="$HOME/bash-practice"
if [[ -e "$dir" ]]; then
    echo "$dir already exists; remove it first to start fresh" >&2
    exit 1
fi
mkdir -p "$dir/logs" "$dir/config" "$dir/files"
cd "$dir"

# 40 web server requests in nginx's "combined" format, the same on every run
ips=(203.0.113.10 198.51.100.23 192.0.2.44 203.0.113.77 198.51.100.5)
requests=("GET /" "GET /about.html" "POST /api/orders" "GET /api/orders/1042"
          "GET /api/users" "POST /login" "GET /favicon.ico" "GET /wp-login.php"
          "GET /api/health" "GET /index.html")
statuses=(200 200 201 200 200 302 404 404 500 200)
sizes=(612 418 88 1290 734 0 162 162 21 612)
for (( i = 0; i < 40; i++ )); do
    ip=${ips[(i * i + i / 3) % 5]}
    r=$(( (i * 7 + i / 4) % 10 ))
    t=$(( 8 * 3600 + 15 * 60 + i * 97 ))
    printf '%s - - [23/Oct/2026:%02d:%02d:%02d +0000] "%s HTTP/1.1" %s %s "-" "curl/8.5.0"\n' \
        "$ip" $(( t / 3600 )) $(( t % 3600 / 60 )) $(( t % 60 )) \
        "${requests[r]}" "${statuses[r]}" "${sizes[r]}"
done > logs/access.log

cat > logs/app.log <<'LOG'
2026-10-23T08:15:02Z INFO  server started on port 8080
2026-10-23T08:16:40Z WARN  slow query took 1250 ms
2026-10-23T08:17:03Z ERROR payment provider timeout order=1042
2026-10-23T08:21:19Z INFO  order created order=1043
2026-10-23T08:24:55Z ERROR database connection refused
2026-10-23T08:25:00Z INFO  database connection restored
LOG

cat > config/app.env <<'ENV'
# settings for the notes API
APP_NAME="notes api"
APP_PORT=8080
LOG_LEVEL=info
ENV

printf '%s\n' "# web tier" web-1.example.com web-2.example.com "" "# database" db-1.example.com > servers.txt
printf '%s\n' name,role,port web-1,web,80 web-2,web,80 db-1,db,5432 cache-1,cache,6379 > inventory.csv
printf 'quarterly numbers\n' > "files/monthly report.txt"
printf 'buy milk\n' > files/notes.txt
printf 'id,total\n1,9.50\n' > files/data.csv
printf 'not a real photo\n' > files/photo.JPG
printf 'API_TOKEN=change-me\n' > files/.env

echo "created $dir"

Do not worry if parts of the script look unfamiliar. By Part 6 you will have seen every line of it explained. The script refuses to overwrite an existing directory. To get back to the starting state, run rm -rf ~/bash-practice && bash ~/setup-practice.sh, but note that this deletes everything in ~/bash-practice, including the scripts, libraries and tests you write in later parts. To keep them, rename the directory instead (mv ~/bash-practice ~/bash-practice.old) before running the setup script again.

Your First Script

Save this as ~/bash-practice/hello.sh:

#!/usr/bin/env bash
# hello.sh: say hello and show a few facts about this shell

name="${1:-world}"                     # the first argument, or "world"
log_lines=$(wc -l < logs/access.log)   # command substitution: capture output

printf 'Hello, %s!\n' "$name"
printf 'This is Bash %s on %s.\n' "$BASH_VERSION" "$HOSTNAME"
printf 'The practice log has %d requests.\n' "$log_lines"

Line by line:

  • #!/usr/bin/env bash is the shebang. When you run ./hello.sh, the kernel reads it and starts bash to run the file. It must be the very first line.
  • # starts a comment, both on its own line and after a command.
  • name="${1:-world}" stores the first argument, or world when there is none. There are no spaces around =; with spaces, Bash would try to run a command called name.
  • $(...) runs a command and captures its output. wc -l < logs/access.log counts lines read from standard input, so the output is just the number. Because the script uses a relative path, run it from ~/bash-practice.
  • printf prints formatted text. It is more predictable than echo, as Part 9 shows.

bash -n checks the syntax without running anything, and ShellCheck finds common bugs. Use both on every script you write; this guide does. Part 7 explains chmod +x and the different ways to run a script.

New in Bash 5.3

Bash 5.3 (July 2025) is the default on Ubuntu 26.04 LTS, Fedora 43 and later, Alpine 3.23 and later, and Homebrew. Its biggest addition is a form of command substitution that runs in the current shell instead of a subshell, so it is faster and can change variables:

On Bash 5.2 the same line is a syntax error:

Other 5.3 additions include GLOBSORT to sort wildcard results by size or modification time, source -p to search a custom path, and BASH_MONOSECONDS for timing. Debian 13, Ubuntu 24.04 and RHEL 10 stay on 5.2, so use 5.3 features only in scripts that will never run there. This guide marks every feature that needs a newer Bash.

Common Mistakes

  • Checking $SHELL to see which shell you are in. It shows your login shell. Use echo "$BASH_VERSION" or ps -p $$.
  • Running Bash scripts with sh script.sh. On Debian and Ubuntu, that is dash. Run them as ./script.sh or bash script.sh.
  • Expecting a script to see your aliases and ~/.bashrc settings. Non-interactive shells do not read it.
  • Creating ~/.bash_profile and wondering why ~/.profile stopped working. Bash reads only the first of the two it finds.
  • Writing for your laptop's Bash. A script that uses 5.3 syntax fails on a server with 5.2, and one that uses associative arrays fails on macOS's 3.2.

Summary

  • Bash is both an interactive shell and a scripting language, and it is the default shell on most Linux systems.
  • Check the version with $BASH_VERSION or $BASH_VERSINFO. Ubuntu 24.04 and Debian 13 ship 5.2, Ubuntu 26.04 and Fedora ship 5.3, and macOS ships 3.2.
  • /bin/sh is often not Bash. Give Bash scripts a Bash shebang.
  • Login shells read ~/.profile or ~/.bash_profile, interactive shells read ~/.bashrc, and scripts read neither.
  • bash -n and shellcheck catch mistakes before a script runs.

Next: Bash Basics, where you follow a command line through every step Bash takes before it runs it.

Keep Learning

Found an issue?