Description
I have a script I maintain that either predated nested commands, or where I missed the feature when creating it. As such, I used namespace-
prefixes:
- foo-list
- foo-install
- foo-uninstall
Now that I know about nested commands, I'd like to refactor the script to use those. However, because this script is already shipped to end users, I cannot break backwards compatibility.
This is where things get interesting.
Let's say I had this:
commands:
- name: foo-list
args:
- name: alpha
required: false
flags:
- long: --beta
examples:
- cli foo-list bar
- cli foo-list bar --beta
If I then add the new command and nested command alongside this:
commands:
- name: foo-list
help: "DEPRECATED use cli foo list"
args:
- name: alpha
required: false
flags:
- long: --beta
examples:
- cli foo-list bar
- cli foo-list bar --beta
- name: foo
commands:
- name: list
args:
- name: alpha
required: false
flags:
- long: --beta
examples:
- cli foo list bar
- cli foo list bar --beta
I then run into an interesting phenomenon: If I call cli foo-list --help
, I do not get the "DEPRECATED use cli foo list" help message. If I reverse the order, so that the deprecated command comes last in the file, I do, but then I also get it for cli foo list --help
.
(Helpfully, these both resolve to the same command script, so nothing changes in terms of actual implementation.)
I understand why this happens. Clearly, internally, bashly is normalizing the names to convert -
to an underscore, and concatenates command + subcommand using an underscore, so the last definition "wins".
What would be great is if we could alias a command or subcommand to another command or subcommand.
This would not work like the current aliasing which allows providing an alternate name, usually a shorter one, for a given command. Instead, it would mean that when an alias is invoked for any reason, it would act exactly like the other command: the help/usage text would come from that command, and it would call its associated command script. The "alias" would be a name and a target only.
Such an approach would mean that my previous commands would continue to work, but users could start adopting the new syntax.
Proposal
In these examples, I'm choosing the term "target" to indicate that the given target will be executed/merged for the given command, and that no command script should be directly associated.
Behavior:
- The "target" is specified as the action that should be invoked. These are always resolved as if they were global (not from the parent command).
- If a command has a "target" element, bashly should consider any other metadata beyond the name and target to be invalid. If encountered, they should cause
bashly generate
to fail and bashly validate
to raise an error.
- If a command has a "target" element, bashly would generate the "action" for the command such that it invokes the usage or command function associated with the target element instead, or intercept the command during parsing. (See examples below.)
Example 1: aliasing command to a subcommand
This example aliases the command "bar-list" to the nested command "foo list".
commands:
- name: foo
commands:
- name: list
args:
- name: alpha
required: false
flags:
- long: --beta
examples:
- cli foo list bar
- cli foo list bar --beta
- name: bar-list
target: "foo list"
Results:
- Script
src/foo_list_command.sh
created.
- Script
src/bar_list_command.sh
NOT created.
- Calling
cli bar-list --help
would output the same help as cli foo list --help
- Calling
cli bar-list
would call whatever command script is associated with cli foo list
.
In other words, when generating the production script, this could get generated:
elif [[ $action == "bar-list" ]]; then
if [[ ${args[--help]:-} ]]; then
long_usage=yes
cli_foo_list_usage
else
cli_foo_list_command
fi
Alternately, the generated parse_requirements()
function could match "bar-list" and set the action to "foo list" and call the "cli_foo_list_parse_requirements" function.
Example 2: aliasing command to a subcommand (normalized to same name)
This example aliases the command "foo-list" to the nested command "foo list"; doing so allows users to use "foo-list" and "foo list" interchangeably, and the usage text from "foo list" would be presented to users.
commands:
- name: foo
commands:
- name: list
args:
- name: alpha
required: false
flags:
- long: --beta
examples:
- cli foo list bar
- cli foo list bar --beta
- name: foo-list
target: "foo list"
Results:
- Script
src/foo_list_command.sh
created.
- Calling
cli foo-list --help
would output the same help as cli foo list --help
- Calling
cli foo-list
would call whatever command script is associated with cli foo list
.
This one is interesting as the normalized names for the usage and command functions would be the same.
However, the point is that the non-aliased version is what would be used (not whichever comes last per current versions).
Example 3: aliasing a subcommand to a command
This example aliases the nested command "foo list" to the command "bar-list"
commands:
- name: foo
commands:
- name: list
target: "bar-list"
- name: bar-list
args:
- name: alpha
required: false
flags:
- long: --beta
examples:
- cli bar-list bar
- cli bar-list bar --beta
Results:
- Script
src/bar_list_command.sh
created.
- Script
src/voo_list_command.sh
NOT created.
- Calling
cli foo list --help
would output the same help as cli bar-list --help
- Calling
cli foo list
would call whatever command script is associated with cli bar-list
.
In other words, when generating the production script, this would get generated:
elif [[ $action == "foo list" ]]; then
if [[ ${args[--help]:-} ]]; then
long_usage=yes
cli_bar_list_usage
else
cli_bar_list_command
fi
Alternately, the generated parse_requirements()
function could match "foo list" and set the action to "bar-list" and call the "cli_bar_list_parse_requirements" function.
Example 4: aliasing a subcommand to a command (normalized to same name)
commands:
- name: foo
commands:
- name: list
target: "foo-list"
- name: foo-list
args:
- name: alpha
required: false
flags:
- long: --beta
examples:
- cli foo-list bar
- cli foo-list bar --beta
Results:
- Script
src/foo_list_command.sh
created.
- Calling
cli foo list --help
would output the same help as cli foo-list --help
- Calling
cli foo list
would call whatever command script is associated with cli foo-list
.
Example 5: aliasing a command to another command
commands:
- name: bar-list
target: "foo-list"
- name: foo-list
args:
- name: alpha
required: false
flags:
- long: --beta
examples:
- cli foo-list bar
- cli foo-list bar --beta
Results:
- Script
src/foo_list_command.sh
created.
- Script
src/bar_list_command.sh
NOT created.
- Calling
cli bar-list --help
would output the same help as cli foo-list --help
- Calling
cli bar-list
would call whatever command script is associated with cli foo-list
.
In other words, when generating the production script, this would get generated:
elif [[ $action == "bar-list" ]]; then
if [[ ${args[--help]:-} ]]; then
long_usage=yes
cli_foo_list_usage
else
cli_foo_list_command
fi
enhancement