# The shell stack we would set up today

> Source: https://learn-bash.net/tools/
> Part of Learn Bash, free to read.

Shell is the language you write least deliberately and run most often. A small amount of tooling changes it from a source of 3am surprises into something you can trust.

:::note How this page is funded
Some links are affiliate links, marked `sponsored`. We earn a commission if you buy; it costs you nothing and does not buy placement. Everything in the first section is free.
:::

## Install these

### `shellcheck` — non-negotiable

The single highest-value tool in this ecosystem. It catches unquoted expansions, wrong test operators, useless `cat`, subshell variable loss, and about three hundred other things — including nearly every mistake in generated shell.

```bash
brew install shellcheck        # or apt install shellcheck
shellcheck -S warning scripts/*.sh
```

Wire it into your agent's post-edit hook and generated scripts get fixed before you see them. See [shell scripts as agent guardrails](/ai/hooks-and-guardrails/).

### `shfmt`

Formatting, so diffs are about substance.

```bash
shfmt -w -i 2 -ci -bn scripts/
```

### `bats` for anything important

If a script does something you would be upset to get wrong, test it.

```bash bats test/deploy.bats
@test "refuses to deploy without a target" {
  run ./deploy.sh
  [ "$status" -eq 2 ]
  [[ "$output" == *"usage"* ]]
}
```

### `trash` instead of `rm`

```bash
brew install trash
```

Then alias `rm` to it in your interactive shell. Not in scripts — in your shell, where the accidents happen. Recoverable deletes cost nothing and have saved a great many afternoons.

## Modern replacements worth having

None of these are required and all of them make the terminal better. All free.

| Old | New | Why |
|---|---|---|
| `grep` | `ripgrep` (`rg`) | much faster, respects `.gitignore` |
| `find` | `fd` | sane syntax, fast |
| `cat` | `bat` | syntax highlighting, paging |
| `ls` | `eza` | git status in the listing |
| `cd` | `zoxide` | jumps to frecent directories |
| `sed`/`awk` for JSON | `jq` | the right tool, and agents use it correctly |
| `sed`/`awk` for YAML | `yq` | same |
| `du` | `dust` | readable output |

`jq` deserves particular mention: it is how agent hooks parse their input, and generated `jq` is generally correct because the language is small.

## Terminal

:::promo warp
:::

Warp's case is the block model — each command and its output are a discrete unit, which makes agent-driven sessions far easier to follow and to scroll back through. There is a good free tier.

Ghostty, WezTerm, Alacritty and Kitty are all excellent, all free, and we earn nothing from any of them. If you are happy with your terminal, this is not a problem worth solving.

## Where to run it

:::promo hetzner
:::

For a box to run scripts, cron jobs and self-hosted agent runners on, Hetzner is the price-performance answer and has been for years.

:::promo digitalocean
:::

Droplets if you want the better dashboard, the managed databases and the $200 of credit to experiment with.

## When to stop writing shell

This is the most useful advice on the page.

**Stop at about 100 lines, or at the first of these:**

- You need an array of anything other than strings
- You need to parse JSON that is more than one `jq` call
- You need error handling more subtle than "exit non-zero"
- You are writing a function that returns a value
- You have written `eval`

At that point, Python or Go will be shorter, clearer, testable, and much easier to review. A 400-line bash script is a program written in a language that does not have data structures — and generated shell at that length is genuinely hard to verify.

The shell's job is orchestration: run these commands, in this order, stop if one fails. It is very good at that, and it should mostly do only that.

## The script header, one more time

```bash
#!/usr/bin/env bash
set -Eeuo pipefail
IFS=$'\n\t'
trap 'echo "failed at line $LINENO: $BASH_COMMAND" >&2' ERR
```

If you take one thing from this site, take those four lines.

## Common questions

### Is `set -e` enough on its own?

No — it has well-known gaps, particularly in pipelines and inside conditionals. `set -Eeuo pipefail` together with an `ERR` trap covers far more, and `set -u` in particular prevents the unset-variable-in-a-path accident that causes the worst outcomes.

### bash or POSIX sh?

`bash` unless you genuinely need to run on Alpine or a BSD without it, in which case `sh` and `shellcheck -s sh` to enforce it. Writing POSIX-only by default costs you arrays and `[[ ]]` for a portability you probably do not need.

### Should I use zsh or fish for scripts?

No. Use them interactively if you like them; write scripts in `bash` with an explicit shebang. Scripts get run by things that are not your shell.
