Summary
AutoCLI simplifies managing scripts by auto generating option menus, all in bash, without any dependencies. Sub menus, customization, and positional arguments are all supported. See autocli.sh on GitHub for the source and more examples.
The problem
One of the best parts of using the command line is that you can automate your workflow with scripts. Commonly repeated commands can be turned into aliases and commonly repeated logical steps can be turned into scripts. However, at some point, it becomes difficult to manage a lot of aliases and scripts.
-
How are they named?
-
What have you already made?
-
Where are they stored if you want to make changes?
Another problem, particularly with aliases, is portability. I prefer to write scripts in bash
, but use fish
as my primary shell, and zsh
fairly often at work. Aliases written in bash may work with zsh, but there are some gotchas, and they're completely incompatible with fish. If I'm on a system that doesn't have fish, and all my scripts are written in fish, I'm out of luck.
A partial solution
You can combine the logic and actions of several of your scripts together in to a "super script". Usually, this is wrapped with some kind of case statement that looks through the input arguments, determines what you're trying to do, then calls the appropriate sub script (function). A common, simple pattern:
case $1 in
s)
git status
;;
a)
git add .
;;
*)
git --help
;;
esac
A more advanced pattern, that passes arguments along to the sub scripts.
case $1 in
something)
shift # remove "something"
do_something "$@"
;;
another_thing)
shift # remove "another_thing"
do_another_thing "$@"
;;
*)
echo "Don't know what to do!"
exit 1
;;
esac
Neither of these examples is doing anything out of the ordinary. Unfortunately, managing these menus quickly becomes difficult:
- Are you passing all the arguments along correctly?
- Help text? How do you know what options are available?
- What if you want to use flags or positional arguments?
cook --with carrots cake
? - What about sub menus?
cook dinner spaghetti -h
AutoCLI
AutoCLI is a script that, when sourced and ran, reads in all the functions that have been defined and generates intermediary menus to connect them. Menus show where sub menus are and include help text by default. Functions are grouped into commands and library functions. All commands start with the name of the output script, followed by an underscore and a name. For example:
-
wizard_update
-
wizard_devbot_start
-
wizard_devbot_stop
These functions are defined in your source, they do the interesting thing you're trying to accomplish. AutoCLI will generate the following functions:
-
wizard
-
wizard_devbot
When we call the output script, we get a menu
leaf@home ~> ./wizard
wizard
update
devbot ...
And sub menu for the devbot
commands
leaf@home ~> ./wizard devbot
wizard devbot
start
stop
Library functions are denoted with ::
, for example devbot::setup
. This tells AutoCLI that the given function is for internal use only, and shouldn't be exposed as a command in a menu.
For very large scripts composed of multiple files, you can define the sources
array variable with the paths to the files containing your commands and library functions. See auto_wizard on GitHub for an example.
The output from AutoCLI is always a single Bash script; this makes it easy to copy to other machines and to install (one position independent file in your $PATH). To run, AutoCLI requires Bash 4.0, but the output should run on any version.
To make calling commands easier, AutoCLI does pattern matching on the input you provide to match it to a command. In many cases, this allows you to specify a long list of sub commands with a single letter. This are all equivalent:
-
wizard create file python
-
wizard crea fi py
-
wizard c f p
Customization
AutoCLI exposes a couple variables you can use to customize the generated functions.
meta_head
inject additional statements between the help text and the case statements. This is useful for defining variables used by sub commands.
meta_head[wizard_devbot]='
local where="$(hostname)"
local who="$(whoami)"
local force=0
'
meta_body
inject additional case statements into the menu. This is useful for flags or positional arguments.
meta_body[wizard_devbot]='
--force) force=1 ;;
'
Appendix: Full example
Input file
source autocli.sh
devbot::setup() {
echo "setup devbot"
}
wizard_update() {
sudo apt update
sudo apt upgrade
}
wizard_devbot_start() {
devbot::setup
echo "starting devbot"
}
wizard_devbot_stop() {
echo "stop devbot"
}
meta_head[wizard]='
local where="$(hostname)"
local force=0
'
meta_body[wizard]='
--force) force=1 ;;
'
# create the output script, named "wizard",
# in this directory
autocli::create wizard .
Output file
# this is an auto generated file. do not edit manually
{
devbot::setup ()
{
echo "setup devbot"
}
wizard ()
{
[[ -n $1 ]] || "${FUNCNAME[0]}" --help;
local __shifts=0 __usage="
devbot ...
update" __name="wizard";
local where="$(hostname)";
local force=0;
while [[ -n $1 ]]; do
case $1 in
--force)
force=1
;;
"d" | "de" | "dev" | "devb" | "devbo" | "devbot")
wizard_devbot "${@:2}"
;;
"u" | "up" | "upd" | "upda" | "updat" | "update")
wizard_update "${@:2}"
;;
__list)
echo devbot update
;;
*)
if [[ -n $usage ]]; then
echo "$usage";
exit 0;
else
printf '\n%s\n%s\n\n' "$__name" "$__usage";
exit 0;
fi
;;
esac;
local __ret=$?;
shift;
shift $__ret;
(( __shifts += __ret + 1 ));
done;
return $__shifts
return $#;
}
wizard_devbot ()
{
[[ -n $1 ]] || "${FUNCNAME[0]}" --help;
local __shifts=0 __usage="
start
stop" __name="wizard devbot";
while [[ -n $1 ]]; do
case $1 in
"s" | "st" | "sta" | "star" | "start")
wizard_devbot_start "${@:2}"
;;
"s" | "st" | "sto" | "stop")
wizard_devbot_stop "${@:2}"
;;
__list)
echo start stop
;;
*)
if [[ -n $usage ]]; then
echo "$usage";
exit 0;
else
printf '\n%s\n%s\n\n' "$__name" "$__usage";
exit 0;
fi
;;
esac;
local __ret=$?;
shift;
shift $__ret;
(( __shifts += __ret + 1 ));
done;
return $__shifts
return $#;
}
wizard_devbot_start ()
{
devbot::setup
echo "starting devbot"
return $#;
}
wizard_devbot_stop ()
{
echo "stop devbot"
return $#;
}
wizard_update ()
{
sudo apt update;
sudo apt upgrade
return $#;
}
[[ "${BASH_SOURCE[0]}" == "${0}" ]] && wizard "$@"
true
} || exit