Skip to main content
Bash · lesson 2 of 22
Bash beginner 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

  1. 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 sh can 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
  2. Fix backup.sh so 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
  3. In a directory of .sh files, 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 -l prints only the names of files that contain a match.

Frequently Asked Questions

Does Windows have Bash?
Not natively, but WSL (Windows Subsystem for Linux) gives you a full Linux environment with Bash on Windows 10/11. Git Bash is another lightweight option for basic scripting.
What does chmod +x do?
It adds the execute permission bit to a file, allowing it to be run directly as a program rather than needing to be passed explicitly to the bash interpreter.
What is a shebang line?
The first line of a script, like #!/usr/bin/env bash, tells the kernel which interpreter to use when the file is executed directly. Without it, the kernel doesn't know how to run the file.
Why do I get 'bash\r: No such file or directory'?
The script was saved with Windows CRLF line endings, so the shebang asks for an interpreter literally named 'bash' followed by a carriage return. Convert the file to LF endings with dos2unix or sed -i 's/\r$//' file, and set your editor or .gitattributes to use LF for shell scripts.