# A beginner-friendly introduction to Bash scripting

Bash scripts turn command-line commands into small programs. This guide covers
the basics: variables, arguments, functions, conditions, loops, and exit codes.

Bash is the Toyota Corolla of programming languages. It is not glamorous, but it
is already on most servers and gets everyday jobs done. If the command line is
new to you,
[the beginner's guide to the Unix-style command line](/os-x-and-ubuntu-command-prompt-demystified)
covers the surrounding shell basics first.

## Your first script

Create a file named `hello.sh`:

```bash
#!/usr/bin/env bash

name=${1:-world}
printf 'Hello, %s!\n' "$name"
```

Make it executable and run it:

```shell
$ chmod +x hello.sh
$ ./hello.sh Bob
Hello, Bob!
```

You can also pass the script directly to Bash. This does not require executable
permissions:

```shell
$ bash hello.sh Bob
Hello, Bob!
```

`${1:-world}` means “use the first argument, or `world` if it is missing.”

## The shebang

The first line is called the **shebang**:

```bash
#!/usr/bin/env bash
```

It tells the operating system to run the file with Bash. It must be the first
line in the file.

You may also see `#!/bin/bash`. That uses Bash at one exact path.
`#!/usr/bin/env bash` finds Bash through your `PATH`, which is often more
portable.

## Script arguments

Bash numbers arguments from left to right:

```bash
#!/usr/bin/env bash

printf 'Script: %s\n' "$0"
printf 'First:  %s\n' "$1"
printf 'Second: %s\n' "$2"
printf 'Count:  %s\n' "$#"
```

- `$0` is the script name
- `$1`, `$2`, and so on are arguments
- `$#` is the number of arguments
- `"$@"` expands to all arguments while keeping each one separate

Use `"$@"` when passing arguments to another command or looping over them:

```bash
for argument in "$@"; do
  printf 'Argument: %s\n' "$argument"
done
```

Quoting matters. If an argument is `Good morning`, `"$@"` keeps it as one
argument instead of splitting it into two words.

## Variables

Assign variables without spaces around `=`:

```bash
greeting="Hello"
name="Ada Lovelace"

printf '%s, %s!\n' "$greeting" "$name"
```

Write `"$name"` to read a variable. Double quotes prevent spaces and wildcard
characters in its value from being treated as shell syntax.

Use braces when text touches the variable name:

```bash
file_name="photo"
printf '%s\n' "images/${file_name}.jpg"
```

Use `readonly` for a value that should not change:

```bash
readonly backup_dir="/var/backups"
```

By convention, environment variables and constants often use uppercase names.
Regular script variables usually use lowercase names.

## Functions

Define a function, then call it by name:

```bash
say_hello() {
  printf 'Hello!\n'
}

say_hello
```

Functions receive arguments the same way scripts do:

```bash
say_hello() {
  local name=$1
  printf 'Hello, %s!\n' "$name"
}

say_hello "Ada Lovelace"
```

`local` keeps the variable inside the function. This avoids accidentally
changing a variable elsewhere in the script.

### Getting a value from a function

In Bash, `return` sets a function's exit status. It does not return a string.
Print the value and capture it with command substitution instead:

```bash
make_greeting() {
  local name=$1
  printf 'Hello, %s!' "$name"
}

greeting=$(make_greeting "Ada")
printf '%s\n' "$greeting"
```

Use `return 0` for success and a non-zero status for failure:

```bash
is_directory() {
  [[ -d $1 ]]
}

if is_directory "images"; then
  printf 'The images directory exists\n'
fi
```

## Conditions

For Bash scripts, `[[ ... ]]` is the clearest way to test strings and files:

```bash
if [[ -z $name ]]; then
  printf 'Name is empty\n'
elif [[ $name == "Ada" ]]; then
  printf 'Hello, Ada!\n'
else
  printf 'Hello, %s!\n' "$name"
fi
```

Some useful tests:

| Test               | Meaning                        |
| ------------------ | ------------------------------ |
| `[[ -z $value ]]`  | The string is empty            |
| `[[ -n $value ]]`  | The string is not empty        |
| `[[ $a == $b ]]`   | The strings are equal          |
| `[[ $a != $b ]]`   | The strings are different      |
| `[[ -e $path ]]`   | The path exists                |
| `[[ -f $path ]]`   | The path is a regular file     |
| `[[ -d $path ]]`   | The path is a directory        |
| `[[ -r $path ]]`   | The path is readable           |
| `[[ -w $path ]]`   | The path is writable           |
| `[[ -x $path ]]`   | The path is executable         |
| `(( count > 10 ))` | The numeric comparison is true |

Combine tests with `&&` for “and” and `||` for “or”:

```bash
if [[ -n $name && -d $backup_dir ]]; then
  printf 'Ready to back up %s\n' "$name"
fi
```

Use `!` to negate a test:

```bash
if [[ ! -f $config_file ]]; then
  printf 'Config file not found: %s\n' "$config_file" >&2
fi
```

`>&2` sends the message to standard error, which is where errors belong.

## Loops

A `for` loop works well for a known list of values:

```bash
for name in Ada Grace Linus; do
  printf 'Hello, %s!\n' "$name"
done
```

A `while` loop repeats as long as its condition succeeds:

```bash
count=1

while (( count <= 3 )); do
  printf 'Count: %d\n' "$count"
  ((count += 1))
done
```

## Arrays

Bash arrays hold ordered lists. When you need text keys instead of numeric
positions, [Bash associative arrays](/associative-arrays-in-bash) are the next
step:

```bash
names=("Ada Lovelace" "Grace Hopper" "Linus Torvalds")

printf 'First: %s\n' "${names[0]}"

for name in "${names[@]}"; do
  printf 'Hello, %s!\n' "$name"
done
```

Like `"$@"`, `"${names[@]}"` keeps every item separate.

## Choosing with `case`

`case` is easier to read than a long chain of string comparisons:

```bash
case ${1:-} in
  start)
    printf 'Starting\n'
    ;;
  stop)
    printf 'Stopping\n'
    ;;
  *)
    printf 'Usage: %s {start|stop}\n' "$0" >&2
    exit 1
    ;;
esac
```

`${1:-}` safely expands to an empty string when no first argument exists.

## Exit codes

Every command ends with a numeric status. Zero means success; any other value
means failure. The script's status is the status of its last command unless you
use `exit` explicitly.

```bash
if [[ $# -ne 2 ]]; then
  printf 'Usage: %s SOURCE DESTINATION\n' "$0" >&2
  exit 1
fi
```

You can inspect the previous command's status with `$?`, but testing the command
directly is usually clearer:

```bash
if cp "$1" "$2"; then
  printf 'Copy complete\n'
else
  printf 'Copy failed\n' >&2
  exit 1
fi
```

## A small complete script

This script checks its arguments and creates a backup of one file:

```bash
#!/usr/bin/env bash

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

file=$1
backup_dir=$2

if [[ ! -f $file ]]; then
  printf 'File not found: %s\n' "$file" >&2
  exit 1
fi

if ! mkdir -p "$backup_dir"; then
  printf 'Could not create directory: %s\n' "$backup_dir" >&2
  exit 1
fi

if ! cp "$file" "$backup_dir/"; then
  printf 'Copy failed\n' >&2
  exit 1
fi

printf 'Backed up %s to %s\n' "$file" "$backup_dir"
```

That is enough Bash for many everyday scripts. Keep commands simple, quote
variable expansions, and treat non-zero exit codes as failures.
