Bash · lesson 2 of 22
Bash Setup and First Script
Install Bash on Linux, macOS and Windows WSL, write and run your first script, and fix chmod, /bin/sh vs bash, and CRLF line-ending errors.
By SkillByExample · Updated
The runnable examples below were captured on Ubuntu 24.04 with bash 5.2. The install
commands for other systems are for reference only.
Check What You Have
Bash is already installed on every mainstream Linux distribution. Check the version before
you start, because associative arrays, mapfile and ${var,,} all need Bash 4 or newer.
bash --version | head -1
echo "bash major version: ${BASH_VERSINFO[0]}"
echo "login shell: $(getent passwd "$USER" | cut -d: -f7)"
echo "bash lives at: $(command -v bash)"
grep -c bash /etc/shells
$ bash check.sh
GNU bash, version 5.2.21(1)-release (x86_64-pc-linux-gnu)
bash major version: 5
login shell: /bin/bash
bash lives at: /usr/bin/bash
4
BASH_VERSINFO holds the version as numbers, which makes it easy to test in a script
(Practice task 1). /etc/shells lists the shells users are allowed to log in with.
Installing or Upgrading
Debian/Ubuntu: sudo apt update && sudo apt install bash · Fedora/RHEL:
sudo dnf install bash
macOS ships Bash 3.2 from 2007, which has no associative arrays and no mapfile. The
default login shell is zsh. Install a current Bash with Homebrew:
brew install bash
echo "$(brew --prefix)/bin/bash" | sudo tee -a /etc/shells # allow it as a login shell
chsh -s "$(brew --prefix)/bin/bash" # optional
Windows: run wsl --install in an administrator PowerShell to get Ubuntu with a real
Linux kernel, the most faithful option. Git Bash (from Git for Windows) is lighter, but it
lacks many Linux tools and differs in places such as paths, ps and signals.
Your First Script and chmod +x
Save this as hello.sh:
#!/usr/bin/env bash
# hello.sh — my first bash script
set -euo pipefail
name="${1:-World}"
echo "Hello, $name!"
echo "Today is $(date '+%A')"
echo "Running bash ${BASH_VERSION%%(*}"
Then try to run it directly, both before and after making it executable:
$ ./hello.sh; echo "exit $?"; chmod +x hello.sh; stat -c '%A %n' hello.sh; ./hello.sh; ./hello.sh Alice
bash: line 1: ./hello.sh: Permission denied
exit 126
-rwxr-xr-x hello.sh
Hello, World!
Today is Wednesday
Running bash 5.2.21
Hello, Alice!
Today is Wednesday
Running bash 5.2.21
Exit code 126 means “found but not executable”. chmod +x added the x bits shown in
-rwxr-xr-x, and after that the kernel read the shebang and started bash. bash hello.sh
works without the execute bit, because then bash is the program being run and the script
is just its input. The day of the week in the output is whatever day you run it.
Shebangs: sh Is Not Bash
The shebang picks the interpreter. #!/bin/sh does not mean Bash on Debian or Ubuntu:
#!/bin/sh
name="Alice"
if [[ $name == A* ]]; then echo "starts with A"; fi
echo "done"
$ ./greet.sh; readlink -f /bin/sh; bash greet.sh
./greet.sh: 3: [[: not found
done
/usr/bin/dash
starts with A
done
On Ubuntu, /bin/sh is dash, a minimal POSIX shell that has no [[. The test failed,
the script printed done anyway, and it gave the wrong answer. Run explicitly with
bash, the same file works. Use #!/usr/bin/env bash for Bash scripts. It finds whichever
bash comes first on PATH, which on a Mac is the Homebrew one, not the old 3.2. Keep
#!/bin/sh for scripts you have deliberately written for POSIX sh.
The Windows Line-Ending Trap
A script saved on Windows usually has CRLF line endings. cat -A makes them visible as
^M before each line end:
$ cat -A winscript.sh
#!/usr/bin/env bash^M$
echo "hello from windows"^M$
On Linux, those carriage returns break the shebang:
$ ./winscript.sh; echo "exit $?"; file winscript.sh; sed -i 's/\r$//' winscript.sh; ./winscript.sh
/usr/bin/env: ‘bash\r’: No such file or directory
/usr/bin/env: use -[v]S to pass options in shebang lines
exit 127
winscript.sh: Bourne-Again shell script, ASCII text executable, with CRLF line terminators
hello from windows
The kernel looked for an interpreter named bash followed by an invisible carriage return
(\r). file shows the cause, and removing the \r fixes it. To stop it happening again,
add *.sh text eol=lf to .gitattributes and set your editor to save shell scripts with LF
endings.
ShellCheck
ShellCheck is a linter for shell scripts. Install it with sudo apt install shellcheck or
brew install shellcheck, and run it on every script you write:
#!/usr/bin/env bash
src=$1
cp -r $src /tmp/backup
echo "Backed up $src"
$ shellcheck backup.sh
In backup.sh line 3:
cp -r $src /tmp/backup
^--^ SC2086 (info): Double quote to prevent globbing and word splitting.
Did you mean:
cp -r "$src" /tmp/backup
For more information:
https://www.shellcheck.net/wiki/SC2086 -- Double quote to prevent globbing ...
It exits non-zero when it finds problems, so a CI step fails automatically. The VS Code ShellCheck extension shows the same warnings as you type.
Practice
-
Make a script refuse to run under anything older than Bash 4, including when someone runs it with
sh.Solution
Use only POSIX syntax for the check itself, so that
shcan run it too:if [ -z "${BASH_VERSION:-}" ] || [ "${BASH_VERSINFO[0]}" -lt 4 ]; then echo "this script needs bash 4+; run it with bash, not sh" >&2 exit 1 fi declare -A ok=([check]=passed) echo "version check ${ok[check]} on bash ${BASH_VERSION%%(*}"$ bash p1.sh; sh p1.sh; echo "exit $?" version check passed on bash 5.2.21 this script needs bash 4+; run it with bash, not sh exit 1 -
Fix
backup.shso ShellCheck is clean and a missing argument produces a usage message.Solution
#!/usr/bin/env bash set -euo pipefail src=${1:?usage: backup.sh <dir>} cp -r -- "$src" /tmp/backup echo "Backed up $src"$ shellcheck backup.sh && echo "clean" clean -
In a directory of
.shfiles, list the ones that have Windows line endings.Solution
$ grep -l $'\r' *.sh bad.sh$'\r'is Bash’s ANSI-C quoting for a literal carriage return.grep -lprints only the names of files that contain a match.