Good shell scripts should come with documentation that is easy to access. Usually some-script -h, some-script --help will print such help — if the developer was nice enough to include docs anyway. I think it’s a really important thing to include in any script you’re going to share with the public. I’ve seen and tried a few different methods over the years. Here are a few, how they work, and what I’m using today.

0. Intercepting the -h and --help flags

Before anything, your script needs to detect the -h and --help flags in order to know when to print the help text. This will vary depending on the way your script is written. For example, if you are already handling flags via getopt or similar, you would likely want to place this logic there. I typically put something like this near the very top of my scripts:

#!/usr/bin/env bash

# Print help text and exit.
if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then
  ... # some logic to print the help text
  exit 1
fi

How does it work?

$1 refers to the first argument passed to the script. If that is -h, or --help, print the help text and exit.

1. echo

A simple approach is to just echo your help text. This works fine, but it’s always had a slight smell to me. You may have to escape characters like ` or swap around single ' and double quotes ". Depending on your $EDITOR, it could be difficult to make large edits to the help text or shift indentation since you have echo and quotes to deal with. Still, for simple cases, it is often sufficient.

The implementation is as follows:

#!/usr/bin/env bash

# Print help text and exit.
if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then
  echo "Usage: some-script [options]"
  echo
  echo "Examples:"
  echo
  echo "  - some-script foo"
  echo "  - some-script bar"
  exit 1
fi

How does it work?

The given text for each echo call is printed.

The output looks like:

Usage: some-script [options]

Examples:
  - some-script foo
  - some-script bar

2. cat

Similar to using echo, you can also use cat with a herestring. This has an improvement over echo since it makes editing the actual help text much easier. Personally, I’m not a fan of how the indentation looks in this approach, but that’s purely stylistic and a personal preference.

The implementation is as follows:

#!/usr/bin/env bash

# Print help text and exit.
if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then
  cat <<HELP
Usage: some-script [options]

Examples:

  - some-script foo
  - some-script bar
HELP
  exit 1
fi

How does it work?

The herestring (i.e. the text between <<HELP and HELP) is printed.

The output looks like:

Usage: some-script [options]

Examples:
  - some-script foo
  - some-script bar

3. grep with embedded help text

I’ve grown to like the idea of keeping help text as source comments at the top of a script. This makes it easy to edit the help text, and more importantly, it is simple to read when editing the script.

The first implementation of this I saw was something like this:

#!/usr/bin/env bash
#/ Usage: some-script [options]
#/
#/ Examples:
#/
#/   - some-script foo
#/   - some-script bar

# Print help text and exit.
if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then
  grep "#/" $0 | cut -c 4-
  exit 1
fi

I prefer this over echo or cat, but I find the magic #/ comment a bit distracting.

How does it work?

Let’s split these commands up.

grep "#/" $0"

This runs the grep utility for the script itself (i.e. $0), printing any lines starting with #/. These characters should be stripped out before presenting the help test to the user, so this output is piped (i.e. |) to the next command.

cut -c 4-

This strips all characters before the 4th in each line.

The output looks like:

Usage: some-script [options]

Examples:
  - some-script foo
  - some-script bar

4. sed with embedded help text

To avoid a magic comment, sed can be used instead. I came across this trick a while back (maybe here).

#!/usr/bin/env bash
# Usage: some-script [options]
#
# Examples:
#
#   - some-script foo
#   - some-script bar

# Print help text and exit.
if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then
  sed -ne '/^#/!q;s/.\{1,2\}//;1d;p' < "$0"
  exit 1
fi

I love the simplicity of this method. Editing the help text is easy, and the extra two # characters don’t affect things much when I wrap text at 80 columns. It’s easy to read the text when editing the script. Most of all, it is a small 4-line snippet that can be dropped in any bash/sh script.

I used a variation of this snippet in most of my shell scripts for several years.

How does it work?

This one uses the sed utility on the script itself (again, $0). Let’s break down the sed command:

-ne: This is two separate switches, -n (don’t print each line), and -e to specify the sed commands to run.

The commands are /^#/!q;s/.\{1,2\}//;1d;p. These are actually 4 separate commands, each is separated by ;. Every line in the script is passed to each command before proceeding to the next; that is: /^#/!q runs on each line, then s/.\{1,2\}// runs on each line, 1d runs on each line, and p runs on each line.

Let’s break down the commands.

/^#/!q: If the line doesn’t start with #, quit sed immediately. This serves two purposes. First, only the initial block of text starting with # is considered as part of the help text. Second, any other comments that come after this block aren’t included as part of the help text. In the example above, this line is true and causes sed to quit on the blank line. Otherwise, the # Print help text and exit. line would be included in the help text. After this runs for each line, the matched text is sent to the next command.

s/.\{1,2\}//: Delete the first 1 or two characters. For lines starting with # only, this makes them completely blank. Other lines, such as the shebang (#!/usr/bin/env bash) and the help text # Examples: have their first two characters removed (becoming /usr/bin/env bash and Examples:). After this runs for each line, the updated text is sent to the next command.

1d: Delete the first line. At this point, the first line is the script’s shebang. That isn’t part of the help text, so delete it. After this runs, the rest of the lines are sent to the next command.

p: If we made it this far, print each line.

The output looks as expected:

Usage: some-script [options]

Examples:
  - some-script foo
  - some-script bar

5. sed + awk, so -h and --help behave differently

All of these methods are fine, but one minor UX issue is that -h and --help both return the same long form help text. Typically, -h is reserved for a shorter variant, that would explain the most common options, while --help would be reserved for printing the full help text.

This method is based on rbenv-help.

It allows printing just the Usage section when a script is called with -h, and the entire help text when called with --help.

#!/usr/bin/env bash
# Usage: some-script foo [options]
#        some-script bar [options]
#
# Examples:
#
#   - some-script foo
#   - some-script bar

# Print help text and exit.
if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then
  sed -ne '/^#/!q;s/^#$/# /;/^# /s/^# //p' < "$0" |
    awk -v f="${1#-h}" '!f && /^Usage:/ || u { u=!/^\s*$/; if (!u) exit } u || f'
  exit 1
fi

This one is a bit more complicated. Let’s break down the sed command first:

-ne: Same as above, don’t automatically print lines and take the given string as sed commands.

/^#/!q: Same as above, if the line doesn’t start with # then quit sed immediately. The help text is passed to the next command.

s/^#$/# /: If the line is just # with no trailing spaces, add a trailing space. The updated text is passed to the next command.

/^# /s/^# //p: This is three separate commands that run on the same line (notice they aren’t separated by ;, that would make them run on all lines in order like explained above). /^# / matches a line starting with # , this is passed to the next action; s/^# // deletes the leading # characters, the updated text is passed to the next action; p prints the updated text.

This is exactly what we need for the --help long help text. To get -h to print shorter text, the formatted help text is piped to awk. This is how we will strip away the unnecessary examples for the short help text.

The awk command works as follows:

-v f="${1#-h}": Sets an internal awk variable named f to the value of $1, with a -h prefix deleted. For -h, this sets f="". For --help, this sets f="-help".

!f && /^Usage:/ || u { u=!/^\s*$/; if (!u) exit } u || f: is the actual awk program. Broken down:

!f && /^Usage:/ || u { ... }: If the f variable set above is empty (i.e. the user passed -h), and the line starts with Usage: or a variable u is set, run the code in between { and }. This variable, u, will be used to track whether or not the Usage text section is still being printed. It is false at first.

u=!/^\s*$/: If the line isn’t blank or just spaces, set u to true. Otherwise, set u to false.

if (!u) { exit }: If u is not set (e.g., the line is blank), quit awk now. This is expected to happen once the Usage text has been completely printed, and stops printing the remaining help text.

u || f: If u is true, the line is a Usage line and it is printed. If the user passed --help, nothing has been printed yet, so print the line. (The awk print call is implicit, this is similar to u || f { print }

Phew! With that done, now -h and --help provide different output.

With -h, the output is short:

Usage: some-script foo [options]
       some-script bar [options]

With --help, the output is long:

Usage: some-script foo [options]
       some-script bar [options]

Examples:

  - some-script foo
  - some-script bar

I love having these separate options in my scripts. One great use-case is when you are handling invalid arguments, a pattern I sometimes use is:

# Something happened, we detected an invalid command!
echo "Error: Invalid command!" >&2
$0 -h

This would give output like:

Error: Invalid command!
Usage: some-script foo [options]
       some-script bar [options]

But really, just use anything please!

I really like the simplicity of the last two examples above, and found I couldn’t justify not shipping docs with my scripts anymore.

But maybe you don’t. That’s okay. Just make sure to provide something when the user tries -h and --help — even if it is just a one line echo "some-script <cmd1|cmd2>". They probably won’t thank you, but at least they won’t rage 😀.