# How to write better scripts

***

## Writing High-Quality Bash Scripts

Here's what separates solid bash scripts from fragile ones:

***

### Always Start with a Strict Header

```bash
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
```

* `set -e` — exit immediately on error
* `set -u` — treat unset variables as errors
* `set -o pipefail` — catch errors in pipelines (e.g., `cmd1 | cmd2`)
* `IFS=$'\n\t'` — safer word splitting (avoids space-splitting bugs)

***

### Quote Everything

Unquoted variables are a common source of bugs:

```bash
# Bad
cp $file $destination

# Good
cp "$file" "$destination"
```

This matters when filenames contain spaces or special characters.

***

### Use Functions

Break logic into named functions. It makes scripts readable and testable:

```bash
log() {
  echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >&2
}

cleanup() {
  rm -f "$tmpfile"
  log "Cleaned up."
}

trap cleanup EXIT  # Always runs on exit
```

***

### Handle Errors Explicitly

Don't rely solely on `set -e`. Check critical commands:

```bash
if ! cp "$src" "$dst"; then
  echo "Error: failed to copy $src to $dst" >&2
  exit 1
fi
```

Print errors to **stderr** (`>&2`), not stdout.

***

### Use `trap` for Cleanup

```bash
tmpfile=$(mktemp)
trap 'rm -f "$tmpfile"' EXIT
```

This ensures temp files are removed even if the script crashes.

***

### Validate Inputs Early

```bash
if [[ $# -lt 2 ]]; then
  echo "Usage: $(basename "$0") <source> <destination>" >&2
  exit 1
fi

src="$1"
dst="$2"

[[ -f "$src" ]] || { echo "Error: $src not found" >&2; exit 1; }
```

***

### Use `[[ ]]` Instead of `[ ]`

`[[ ]]` is the modern bash conditional — it handles edge cases better:

```bash
# Safer string comparison
if [[ "$name" == "admin" ]]; then ...

# Safe file existence check (no issue with empty vars)
if [[ -f "$file" ]]; then ...
```

***

### Prefer `$(...)` Over Backticks

```bash
# Old style (hard to nest, hard to read)
result=`date`

# Modern style
result=$(date)
```

***

### Name Variables Clearly

```bash
# Bad
a="$1"
b="$2"

# Good
input_file="$1"
output_dir="$2"
```

Use `UPPER_CASE` for exported/global variables and `lower_case` for locals.

***

### Add a Usage/Help Function

```bash
usage() {
  cat <<EOF
Usage: $(basename "$0") [OPTIONS] <input>

Options:
  -h, --help     Show this help message
  -v, --verbose  Enable verbose output
EOF
}
```

***

### Use `local` in Functions

```bash
process_file() {
  local file="$1"
  local count=0
  # ...
}
```

Without `local`, variables leak into global scope.

***

### Lint Your Scripts

Use [**ShellCheck**](https://www.shellcheck.net/) — it catches an enormous range of bugs:

```bash
shellcheck myscript.sh
```

It's available in most package managers (`apt install shellcheck`, `brew install shellcheck`) and has VS Code/Vim/Emacs plugins.

***

### Putting It Together — A Template

```bash
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'

# ── Constants ──────────────────────────────────────────
SCRIPT_NAME="$(basename "$0")"

# ── Helpers ────────────────────────────────────────────
log()   { echo "[$SCRIPT_NAME] $*" >&2; }
die()   { echo "[$SCRIPT_NAME] ERROR: $*" >&2; exit 1; }
usage() {
  echo "Usage: $SCRIPT_NAME <input_file> <output_dir>"
  exit 0
}

# ── Cleanup ────────────────────────────────────────────
tmpfile=$(mktemp)
trap 'rm -f "$tmpfile"' EXIT

# ── Argument Parsing ───────────────────────────────────
[[ $# -lt 2 ]] && usage

input_file="$1"
output_dir="$2"

[[ -f "$input_file" ]] || die "Input file not found: $input_file"
[[ -d "$output_dir" ]] || die "Output directory not found: $output_dir"

# ── Main Logic ─────────────────────────────────────────
main() {
  log "Processing $input_file..."
  # your logic here
  log "Done."
}

main "$@"
```

***

The single biggest improvement you can make is running **ShellCheck** on everything you write. It'll teach you more than almost anything else.

***
