From 3a9f50c6107797ecc787de4a0a870d79f1a3fa27 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Tue, 14 Apr 2026 22:31:09 +0300 Subject: [PATCH] cli: add command alias support (fixes #19136) --- vlib/cli/README.md | 6 +++- vlib/cli/command.v | 27 ++++++++++++++-- vlib/cli/command_test.v | 69 +++++++++++++++++++++++++++++++++++++++++ vlib/cli/help.v | 11 ++++--- vlib/cli/man.v | 8 +++-- 5 files changed, 110 insertions(+), 11 deletions(-) diff --git a/vlib/cli/README.md b/vlib/cli/README.md index c865e25ea..432560d0b 100644 --- a/vlib/cli/README.md +++ b/vlib/cli/README.md @@ -25,6 +25,7 @@ fn main() { commands: [ cli.Command{ name: 'sub' + alias: 's' execute: fn (cmd cli.Command) ! { println('hello subcommand') return @@ -35,4 +36,7 @@ fn main() { app.setup() app.parse(os.args) } -``` \ No newline at end of file +``` + +Subcommands can set `alias` to accept a shorter invocation token, for example +`example-app s`. diff --git a/vlib/cli/command.v b/vlib/cli/command.v index 10c94a719..97f037573 100644 --- a/vlib/cli/command.v +++ b/vlib/cli/command.v @@ -13,6 +13,7 @@ pub fn (f FnCommandCallback) str() string { pub struct Command { pub mut: name string + alias string usage string description string man_description string @@ -57,6 +58,7 @@ pub fn (cmd &Command) str() string { mut res := []string{} res << 'Command{' res << ' name: "${cmd.name}"' + res << ' alias: "${cmd.alias}"' res << ' usage: "${cmd.usage}"' res << ' version: "${cmd.version}"' res << ' description: "${cmd.description}"' @@ -139,8 +141,16 @@ pub fn (mut cmd Command) add_commands(commands []Command) { // add_command adds `command` as a sub-command of this `Command`. pub fn (mut cmd Command) add_command(command Command) { mut subcmd := command - if cmd.commands.contains(subcmd.name) { - eprintln_exit('Command with the name `${subcmd.name}` already exists') + for existing in cmd.commands { + if existing.name == subcmd.name { + eprintln_exit('Command with the name `${subcmd.name}` already exists') + } + if existing.alias != '' && existing.alias == subcmd.name { + eprintln_exit('Command with the name `${subcmd.name}` already exists as an alias') + } + if subcmd.alias != '' && existing.matches(subcmd.alias) { + eprintln_exit('Command alias `${subcmd.alias}` already exists') + } } subcmd.parent = unsafe { cmd } cmd.commands << subcmd @@ -282,7 +292,7 @@ fn (mut cmd Command) parse_commands() { arg := cmd.args[i] for j in 0 .. cmd.commands.len { mut command := cmd.commands[j] - if command.name == arg { + if command.matches(arg) { for flag in global_flags { command.add_flag(flag) } @@ -311,6 +321,17 @@ fn (mut cmd Command) parse_commands() { cmd.handle_cb(cmd.post_execute, 'postexecution') } +fn (cmd &Command) display_name() string { + if cmd.alias == '' { + return cmd.name + } + return '${cmd.name} (${cmd.alias})' +} + +fn (cmd &Command) matches(token string) bool { + return cmd.name == token || (cmd.alias != '' && cmd.alias == token) +} + fn (mut cmd Command) handle_cb(cb FnCommandCallback, label string) { if !isnil(cb) { cb(*cmd) or { diff --git a/vlib/cli/command_test.v b/vlib/cli/command_test.v index 471ff0ecd..4e11a7d69 100644 --- a/vlib/cli/command_test.v +++ b/vlib/cli/command_test.v @@ -30,6 +30,19 @@ fn test_if_subcommands_parse_args() { cmd.parse(['command', 'subcommand', 'arg0', 'arg1']) } +fn test_if_subcommand_alias_parses_args() { + mut cmd := cli.Command{ + name: 'command' + } + subcmd := cli.Command{ + name: 'subcommand' + alias: 'sc' + execute: if_subcommands_parse_args_func + } + cmd.add_command(subcmd) + cmd.parse(['command', 'sc', 'arg0', 'arg1']) +} + fn if_subcommands_parse_args_func(cmd cli.Command) ! { assert cmd.name == 'subcommand' && cmd.args == ['arg0', 'arg1'] } @@ -205,6 +218,62 @@ fn test_command_setup() { assert cmd.commands[0].commands[0].parent.name == 'child' } +fn test_help_message_lists_command_aliases() { + cmd := cli.Command{ + name: 'command' + description: 'description' + commands: [ + cli.Command{ + name: 'sub' + alias: 's' + description: 'subcommand' + }, + ] + } + assert cmd.help_message() == r'Usage: command [commands] + +description + +Commands: + sub (s) subcommand +' +} + +fn test_manpage_lists_command_aliases() { + mut cmd := cli.Command{ + name: 'command' + description: 'description' + commands: [ + cli.Command{ + name: 'sub' + alias: 's' + description: 'subcommand' + }, + ] + } + cmd.setup() + assert cmd.manpage().after_char(`\n`) == r'.Dt COMMAND 1 +.Os +.Sh NAME +.Nm command +.Nd description +.Sh SYNOPSIS +.Nm command +.Nm command +.Ar subcommand +.Sh DESCRIPTION +description +.Pp +The subcommands are as follows: +.Bl -tag -width indent +.It Cm sub Pq Cm s +subcommand +.El +.Sh SEE ALSO +.Xr command-sub 1 +' +} + // helper functions fn empty_func(cmd cli.Command) ! { } diff --git a/vlib/cli/help.v b/vlib/cli/help.v index 734193306..9a1baa5ef 100644 --- a/vlib/cli/help.v +++ b/vlib/cli/help.v @@ -30,7 +30,7 @@ fn help_cmd() Command { pub fn print_help_for_command(cmd Command) ! { if cmd.args.len > 0 { for sub_cmd in cmd.commands { - if sub_cmd.name == cmd.args[0] { + if sub_cmd.matches(cmd.args[0]) { cmd_ := unsafe { &sub_cmd } print(cmd_.help_message()) return @@ -74,7 +74,7 @@ pub fn (cmd &Command) help_message() string { max(name_len, abbrev_len + flag.name.len + spacing + 2) // + 2 for '--' in front } for command in cmd.commands { - name_len = max(name_len, command.name.len + spacing) + name_len = max(name_len, command.display_name().len + spacing) } } else { for flag in cmd.flags { @@ -85,7 +85,7 @@ pub fn (cmd &Command) help_message() string { max(name_len, abbrev_len + flag.name.len + spacing + 1) // + 1 for '-' in front } for command in cmd.commands { - name_len = max(name_len, command.name.len + spacing) + name_len = max(name_len, command.display_name().len + spacing) } } if cmd.flags.len > 0 { @@ -114,9 +114,10 @@ pub fn (cmd &Command) help_message() string { if cmd.commands.len > 0 { help += '\nCommands:\n' for command in cmd.commands { + command_name := command.display_name() base_indent := ' '.repeat(base_indent_len) - description_indent := ' '.repeat(name_len - command.name.len) - help += '${base_indent}${command.name}${description_indent}' + + description_indent := ' '.repeat(name_len - command_name.len) + help += '${base_indent}${command_name}${description_indent}' + pretty_description(command.description, base_indent_len + name_len) + '\n' } } diff --git a/vlib/cli/man.v b/vlib/cli/man.v index 1469da4d4..737a3a83e 100644 --- a/vlib/cli/man.v +++ b/vlib/cli/man.v @@ -23,7 +23,7 @@ fn man_cmd() Command { pub fn print_manpage_for_command(cmd Command) ! { if cmd.args.len > 0 { for sub_cmd in cmd.commands { - if sub_cmd.name == cmd.args[0] { + if sub_cmd.matches(cmd.args[0]) { man_cmd := unsafe { &sub_cmd } print(man_cmd.manpage()) return @@ -134,7 +134,11 @@ pub fn (cmd &Command) manpage() string { mdoc += '.Pp\nThe subcommands are as follows:\n' mdoc += '.Bl -tag -width indent\n' for c in cmd.commands { - mdoc += '.It Cm ${c.name}\n' + mdoc += '.It Cm ${c.name}' + if c.alias != '' { + mdoc += ' Pq Cm ${c.alias}' + } + mdoc += '\n' if c.description != '' { mdoc += '${c.description}\n' } -- 2.39.5