keepcalculator

06 Numbers and Powers

Turn Bash Scripts Into a CLI Tool: Step-by-Step

Card No. 06·64440 · Updated · Standard formula, shown below

This guide shows you how to take a folder of scattered bash scripts and turn them into a single, reliable command-line tool with subcommands, consistent help, and proper error handling. You’ll learn how to structure the project, add a main entry point, manage dependencies, and test everything so the tool works predictably on any machine.

What You’ll Need

  • A folder containing your bash scripts (each should be executable, i.e., have a shebang line like #!/bin/bash).
  • Basic familiarity with the terminal and editing files (e.g., using nano or vim).
  • Optional: git for version control, and shellcheck to lint your scripts.

Step-by-Step

  1. Inventory your scripts. List every script in the folder. Note what each does, what arguments it takes, and what output it produces. This will help you design the subcommands. For example, if you have backup.sh, restore.sh, and status.sh, your tool will have backup, restore, and status subcommands.
  2. Create a project directory. Move all your scripts into a new folder named after your tool (e.g., my-tool). Inside, create a bin/ directory for the main executable and a lib/ directory for shared functions. This keeps the tool self-contained.
  3. Write a main entry point. Create a file named my-tool (no extension) in bin/. Start with a shebang #!/usr/bin/env bash and add a case statement to dispatch to subcommands. For example:

    #!/usr/bin/env bash
    case "$1" in
      backup) shift; exec "$SCRIPT_DIR/lib/backup.sh" "$@" ;;
      restore) shift; exec "$SCRIPT_DIR/lib/restore.sh" "$@" ;;
      status) shift; exec "$SCRIPT_DIR/lib/status.sh" "$@" ;;
      *) echo "Usage: my-tool {backup|restore|status}"; exit 1 ;;
    esac

    Set SCRIPT_DIR to the directory of the script so it can find the lib files even when called from elsewhere.

  4. Make everything executable. Run chmod +x bin/my-tool lib/*.sh so the scripts can be executed directly.
  5. Add a help command. Create a help subcommand that prints usage for the whole tool and for each subcommand. This is essential for usability. For example, my-tool help backup should show the backup script’s options.
  6. Standardize argument parsing. For each subcommand, use a consistent pattern: either getopts for short flags or a simple loop for long flags. Avoid relying on global variables; pass options as parameters to functions. This makes scripts testable and less error-prone.
  7. Create a shared library. Move common functions (e.g., logging, error handling, configuration loading) into lib/common.sh. Source this file at the top of each subcommand with source "$SCRIPT_DIR/lib/common.sh". This reduces duplication and ensures consistent behavior.
  8. Add proper error handling. Use set -euo pipefail at the top of each script to exit on errors, undefined variables, and pipeline failures. Also trap errors to print a friendly message. For example:

    trap 'echo "Error on line $LINENO" >&2' ERR
  9. Test each subcommand. Run your tool with various arguments, including invalid ones. Check exit codes: 0 for success, non-zero for failure. Use shellcheck to catch common mistakes. Write a simple test script that runs each subcommand and checks the output.
  10. Install the tool. To make it available globally, symlink bin/my-tool to /usr/local/bin/my-tool. Or add the bin directory to your PATH in your .bashrc. This lets you run my-tool from anywhere.
  11. Write documentation. Create a README.md with installation instructions, usage examples, and a list of subcommands. Include a man page if you want to be fancy. This is crucial for others (and future you) to use the tool.

Common Mistakes

  • Hardcoding paths. Using absolute paths like /home/user/scripts breaks the tool on other machines. Always use relative paths or derive them from the script’s location.
  • Not handling arguments. Forgetting to check for missing or extra arguments leads to confusing errors. Always validate input and show usage when needed.
  • Ignoring exit codes. If a subcommand fails, the main script should exit with a non-zero code so that other tools can detect the failure. Use exit 1 on error.
  • Using global variables. Global variables can be overwritten by subcommands, causing bugs. Prefer local variables in functions.
  • Not testing on a clean environment. Your tool might work on your machine but fail on a fresh install because of missing dependencies. Test in a container or on a different user account.

Tips

  • Use set -euo pipefail in every script to catch errors early. It’s a best practice for robust bash scripts.
  • Keep subcommand scripts small and focused. If a script grows beyond a few hundred lines, consider splitting it further.
  • Use a consistent naming convention for functions: prefix_function_name. For example, log_info, backup_create.
  • Add color to output only if you detect a terminal (using tput or -t test). This avoids garbage in logs.
  • Version your tool. Use a --version flag that prints a version number. This helps with debugging.

Troubleshooting

  • “command not found” when running the tool. This usually means the tool is not in your PATH. Check if the symlink is correct or if the bin directory is in your PATH. Run which my-tool to see where it’s located.
  • Subcommand not found. If you get “my-tool: no such subcommand”, the case statement in the main script doesn’t match. Ensure the subcommand name is spelled exactly as in the case pattern.
  • Script runs but does nothing. Add set -x at the top of the script to trace execution. This prints each command before running it, helping you see where it stops.
  • Permission denied. The script is not executable. Run chmod +x on the file. Also check the shebang line is correct.
  • Environment differences. If the tool works on your machine but not on another, compare the bash version and available commands. Use command -v to check for dependencies and provide clear error messages if they’re missing.

Frequently Asked Questions

How do I add a new subcommand later?

Create a new script in lib/, add a case entry in bin/my-tool, and update the help and README. That’s it.

Can I use this with other shells like zsh?

Yes, as long as the shebang is #!/usr/bin/env bash, it will run in bash regardless of the user’s interactive shell. However, if you use bash-specific features, they won’t work in sh.

How do I make the tool work on macOS?

Bash is available on macOS, but the default version is 3.2. Avoid using bash 4+ features like associative arrays. Alternatively, install a newer bash via Homebrew and set the shebang to its path.

What if I have a script that takes a long time to run?

Make sure to provide progress feedback. Use echo to show what’s happening, and consider adding a --verbose flag. Also handle interrupts with trap to clean up temporary files.

Should I use a framework like bashly or docopts?

These can help, but they add dependencies. For a small set of scripts, a simple case statement is fine. If you have many subcommands with complex options, a framework might save time.

How do I debug a subcommand that fails only when run through the tool?

Run the subcommand directly with the same arguments to see if it works. If it does, the issue is in the main script—likely in how arguments are passed. Add echo "$@" in the main script to see what’s being passed.

Related

Method: the standard formula for this calculation, rounded as each result states. See how the calculators work or report an error.

More cards in this drawer

The whole Numbers and Powers drawer →