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
nanoorvim). - Optional:
gitfor version control, andshellcheckto lint your scripts.
Step-by-Step
-
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, andstatus.sh, your tool will havebackup,restore, andstatussubcommands. -
Create a project directory. Move all your scripts into a new folder named after your tool (e.g.,
my-tool). Inside, create abin/directory for the main executable and alib/directory for shared functions. This keeps the tool self-contained. -
Write a main entry point. Create a file named
my-tool(no extension) inbin/. Start with a shebang#!/usr/bin/env bashand 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 ;; esacSet
SCRIPT_DIRto the directory of the script so it can find the lib files even when called from elsewhere. -
Make everything executable. Run
chmod +x bin/my-tool lib/*.shso the scripts can be executed directly. -
Add a help command. Create a
helpsubcommand that prints usage for the whole tool and for each subcommand. This is essential for usability. For example,my-tool help backupshould show the backup script’s options. -
Standardize argument parsing. For each subcommand, use a consistent pattern: either
getoptsfor 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. -
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 withsource "$SCRIPT_DIR/lib/common.sh". This reduces duplication and ensures consistent behavior. -
Add proper error handling. Use
set -euo pipefailat 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 -
Test each subcommand. Run your tool with various arguments, including invalid ones. Check exit codes: 0 for success, non-zero for failure. Use
shellcheckto catch common mistakes. Write a simple test script that runs each subcommand and checks the output. -
Install the tool. To make it available globally, symlink
bin/my-toolto/usr/local/bin/my-tool. Or add thebindirectory to yourPATHin your.bashrc. This lets you runmy-toolfrom anywhere. -
Write documentation. Create a
README.mdwith 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/scriptsbreaks 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 1on 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 pipefailin 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
tputor-ttest). This avoids garbage in logs. - Version your tool. Use a
--versionflag 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
bindirectory is in your PATH. Runwhich my-toolto 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 -xat 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 +xon the file. Also check the shebang line is correct. - Environment differences. If the tool works on your machine but not on another, compare the
bashversion and available commands. Usecommand -vto 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
- Hide PHP Version HTTP Header: How To
- CentOS Stream for Developers: A Practical Guide
- How to Replace a Game Controller Battery Safely
- Man-Sized 3D Printer? Why You Probably Don’t Need One100.0 cm³/h
- Scroll Horizontally with Mouse Wheel by Holding Shift5.00 min/km
- iPadOS 16 Supported Devices: Full List