# bash-style-guide: explicit rules for bash scripts, including the ones it cannot settle

> Dave Eddy's bash-style-guide is a written style contract for shell scripts, not a tool. It explains why each rule exists, refuses to cover sh, and links enforcement to a separate style checker. Useful for teams tired of arguing about quoting and semicolons, less useful if you need a linter config you can paste today.

**bahamas10/bash-style-guide** — A style guide for writing safe, predictable, and portable bash scripts (not sh!)

- Repository: https://github.com/bahamas10/bash-style-guide
- Website: https://style.ysap.sh
- Stars: 997 · Forks: 102
- Language: CSS
- License: MIT
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/bahamas10-bash-style-guide

## A style guide that refuses to cover sh

bash-style-guide is a document, not a program. It is a working style guide written by Dave Eddy as part of the YSAP (You Suck at Programming) series, and it makes a deliberate bet: that a written set of rules beats an unwritten sense of taste. The preface concedes that good style alone will not make a script free of errors, but argues it narrows the range of places where bugs can hide. The guide's stated purpose is to set out the author's choices explicitly rather than relying on a vibe.

The scope is narrow by design. The repository description says the guide is for writing safe, predictable and portable bash scripts, and the text repeats the constraint in the first word of its second major section: this is a bash guide, not sh. Every rule that follows is judged against that. If your script also has to run under dash or a strict POSIX shell, a good share of the advice here is wrong for you, and the guide does not pretend otherwise.

The intended reader is the engineer who writes bash for a living and keeps having the same arguments with reviewers about quoting, indentation and which test operator to use. It is much less useful to someone hunting for a linter configuration to paste into a repository, because the enforcement tool lives in a separate project.

## Tabs, eighty columns, and the semicolon rule

Aesthetic choices are deliberately quarantined in their own section, and the guide is honest that some of them are not universally agreeable. Tabs for indentation. No more than 80 columns. At most one blank line in a row. No semicolons, unless a control statement requires one.

The semicolon rule has a mechanical argument attached to it, which is why it survives contact with a real codebase. A trailing semicolon adds a character that carries no meaning, and it is accepted only inside constructs such as if and while where the syntax needs a terminator.

``` bash
# wrong
name='dave';
echo "hello $name";

# right
name='dave'
echo "hello $name"
```

Comments are handled separately from the aesthetic bucket, and the position is unusually practical: there is no explicit style guide for comments, and you should not change somebody's comments for looks unless you are rewriting or updating the code anyway. For a team, that removes a common source of diff noise, since comment reflowing is the classic way to turn a one line fix into a twenty line pull request.

## Functions and block statements where a mistake leaks state

Two rules here are about scope and about layout, and the first one is the more important.

Do not use the function keyword, and every variable created inside a function should be made local. The example makes the reason plain, because an assignment without local writes into the global namespace, and whether that is a defect depends entirely on what the author intended.

``` bash
# wrong
function foo {
    i=foo # this is now global, wrong depending on intent
}

# right
foo() {
    local i=foo # this is local, preferred
}
```

The second rule covers the shape of control flow. The then keyword belongs on the same line as the if, and the do keyword on the same line as the while.

``` bash
# right
if true; then
    ...
fi
```

The guide rejects the two line form outright and also rejects the `true && {` variant, noting only that the second one admittedly looks kinda cool. That last clause is worth reading as a signal about tone. This is a document that expects you to argue with it, and it does not pretend that every rule is a law of nature.

## Every rule that removes a fork from the process table

The Bashisms section carries the guide's real argument, and its organising principle is stated up front: when given the choice, always prefer bash builtins or keywords instead of external commands or sh(1) syntax.

Conditional testing is the first case. The guide wants `[[ ... ]]`, and it treats both `[ -d /etc ]` and `test -d /etc` as wrong, linking the rule to YSAP066 and to the BashFAQ031 entry on the subject.

``` bash
# correct
[[ -d /etc ]]
```

Command substitution follows the same pattern, with backticks marked as wrong.

``` bash
foo=`date`  # wrong
foo=$(date) # right
```

Math and integer manipulation are covered by the arithmetic builtins `((...))` and `$((...))`, and the guide adds a single flat prohibition on the let command. Parameter expansion gets the strongest treatment, since it replaces both an external command and a pipeline with a single expansion.

``` bash
# right
prog=${0##*/}
nonumbers=${name//[0-9]/}
```

Each of these rules links to a numbered YSAP page, so the short form in the guide and the long form online are two layers of the same argument. If you adopt one habit from this document, make it the parameter expansion one: it is the rule with the widest reach across ordinary scripts.

## Arrays and the read builtin instead of space separated strings

The guide asks you to use bash arrays rather than a string separated by spaces, tabs or newlines whenever possible. The example is short enough to be memorable.

``` bash
# right
modules=(json httpserver jshint)
for module in "${modules[@]}"; do
    npm install -g "$module"
done
```

The guide then notes that in many cases the loop is unnecessary, since a command that accepts multiple arguments lets you write `npm install -g "${modules[@]}"` directly, but only if the command takes several arguments and you are not interested in catching individual failures. That caveat is the kind of detail most style guides leave out, and it is the reason to keep the slower form when a package install can fail partway through.

The read builtin gets its own subsection, framed as a way to avoid forking external commands for parsing.

``` bash
fqdn='computer1.daveeddy.com'

IFS=. read -r hostname domain tld <<< "$fqdn"
echo "$hostname is in $domain.$tld"
# => "computer1 is in daveeddy.com"
```

Here the -r flag keeps backslashes intact, the here string feeds the builtin without a temporary file, and the temporary variable names are left with no declaration at all, which is fine because read assigns them.

## The cases the guide says you cannot win

Three sections of this guide are about failure, and they are the parts most worth reading before you adopt it wholesale.

The first is finding the path of the running program. The guide is blunt about it: you cannot know this for sure, and if you are trying to find the full path of the executing program you should rethink your software design. It points to BashFAQ028 and to a case study on `__dirname` across languages.

The second is file listing. Do not parse ls, the guide says, and the reason is not formatting but safety, since word splitting on ls output breaks as soon as a filename contains a space.

``` bash
# right
for f in *; do
    ...
done
```

The third is portability of external commands. The world does not run on GNU or on Linux, so GNU specific options for awk, sed and grep are to be avoided. The reasoning is that bash builtins already cover most simple string manipulation, so the external fork is rarely necessary in the first place.

The related rule about cat is about process count rather than correctness. If a program supports reading from stdin, pass data by redirection, and if the program can read a file passed by name, that is likely faster than piping the same file into it.

## The style checker that checks the guide itself

Enforcement is mentioned only in passing, in the references section, where the guide points to the YSAP Style Checker and says the tool can check for basic style, in fact it is used to check this document itself. The reference is a link to a separate repository, so the checker is not bundled into this one.

That self reference is the interesting part. A guide that only asks to be believed is easy to ignore, and a guide that is linted by a tool derived from it has a working answer to the objection that nobody would follow all of it. It also sets a realistic expectation: the word basic is doing work in that sentence. This is a checker for a subset of the rules, not a shell correctness analyser.

The repository itself carries the tool wrapper. The top level entries are a .github directory, the LICENSE file, a Makefile, the README, a tools directory and a website directory, and the Makefile exposes a single check target that runs the checker against README.md.

``` make
.PHONY: check
check:
	./tools/style-check README.md
```

The help target describes the same thing in one line, `make check     style check files`. Note what this does and does not cover: the target lints the guide's own README, so running it as shipped tells you about the guide, not about your scripts.

On maintenance and licensing, the facts are simple. The repository is not archived, the licence field on the repository says MIT, and the last push was on 2026-10-20. There are no GitHub releases, so the repository is the only distribution channel, and upgrades arrive as edits to a single document rather than as versioned artefacts with migration notes.

## Conclusion

Adopt the guide as a written review contract if your team writes bash and currently resolves style by memory or by whoever reviews fastest; do not adopt it wholesale if your scripts must also run under a POSIX shell, and do not treat it as a substitute for shellcheck. Before adopting, run the linked YSAP Style Checker against your existing scripts and count how many findings are taste disagreements rather than real defects.

## FAQ

### How do I check my bash scripts against the rules in bahamas10/bash-style-guide?

The guide links to a separate YSAP Style Checker in its references section and states that it can check for basic style, and it is the tool used to check the guide itself. The Makefile in this repository exposes a check target that runs the checker against README.md, so the shipped target validates the guide rather than your own files.

### Does bahamas10/bash-style-guide also apply to POSIX sh scripts?

No. The guide is explicitly for bash, not sh, and it says that when given the choice you should always prefer bash builtins or keywords instead of external commands or sh(1) syntax. Rules such as using `[[ ... ]]` and bash arrays assume a bash interpreter.

### How does the guide tell me to find the directory of a running bash script?

It says you cannot know this for sure, and that if you are trying to find the full path of the executing program you should rethink your software design. It points to BashFAQ028 and to a dirname case study covering bash and node.

### What licence is bahamas10/bash-style-guide released under, and is it still being updated?

The licence field on the repository says MIT. The repository is not archived and the last push was on 2026-10-20, and there are no GitHub releases, so changes arrive as direct edits to the document.

## Sources

- [bahamas10/bash-style-guide on GitHub](https://github.com/bahamas10/bash-style-guide)
- [Issues](https://github.com/bahamas10/bash-style-guide/issues)
- [License: MIT](https://github.com/bahamas10/bash-style-guide/blob/master/LICENSE)
- [Project website](https://style.ysap.sh)
- [README](https://github.com/bahamas10/bash-style-guide/blob/master/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/bahamas10-bash-style-guide
