From 340543dfc0f35c99afa734b62ac62c04abf95ed4 Mon Sep 17 00:00:00 2001 From: Cameron Katri Date: Sun, 3 Apr 2022 03:12:47 -0400 Subject: [PATCH] cli: add automatic manpage generation with -man (#13911) --- vlib/cli/command.v | 32 ++++ vlib/cli/man.v | 181 ++++++++++++++++++++ vlib/cli/man_test.v | 110 ++++++++++++ vlib/v/tests/inout/cli_root_default_help.vv | 1 + 4 files changed, 324 insertions(+) create mode 100644 vlib/cli/man.v create mode 100644 vlib/cli/man_test.v diff --git a/vlib/cli/command.v b/vlib/cli/command.v index 4401c5f45..c5d5d7d53 100644 --- a/vlib/cli/command.v +++ b/vlib/cli/command.v @@ -16,11 +16,13 @@ pub mut: name string usage string description string + man_description string version string pre_execute FnCommandCallback execute FnCommandCallback post_execute FnCommandCallback disable_help bool + disable_man bool disable_version bool disable_flags bool sort_flags bool @@ -41,7 +43,9 @@ pub fn (cmd Command) str() string { res << ' usage: "$cmd.usage"' res << ' version: "$cmd.version"' res << ' description: "$cmd.description"' + res << ' man_description: "$cmd.man_description"' res << ' disable_help: $cmd.disable_help' + res << ' disable_man: $cmd.disable_man' res << ' disable_flags: $cmd.disable_flags' res << ' disable_version: $cmd.disable_version' res << ' sort_flags: $cmd.sort_flags' @@ -155,6 +159,9 @@ fn (mut cmd Command) add_default_flags() { use_version_abbrev := !cmd.flags.contains('v') && cmd.posix_mode cmd.add_flag(version_flag(use_version_abbrev)) } + if !cmd.disable_man && !cmd.flags.contains('man') { + cmd.add_flag(man_flag()) + } } // add_default_commands adds the command functions of the @@ -166,6 +173,9 @@ fn (mut cmd Command) add_default_commands() { if !cmd.disable_version && cmd.version != '' && !cmd.commands.contains('version') { cmd.add_command(version_cmd()) } + if !cmd.disable_man && !cmd.commands.contains('man') && cmd.is_root() { + cmd.add_command(man_cmd()) + } } fn (mut cmd Command) parse_flags() { @@ -197,6 +207,7 @@ fn (mut cmd Command) parse_commands() { global_flags := cmd.flags.filter(it.global) cmd.check_help_flag() cmd.check_version_flag() + cmd.check_man_flag() for i in 0 .. cmd.args.len { arg := cmd.args[i] for j in 0 .. cmd.commands.len { @@ -248,6 +259,16 @@ fn (cmd Command) check_help_flag() { } } +fn (cmd Command) check_man_flag() { + if !cmd.disable_man && cmd.flags.contains('man') { + man_flag := cmd.flags.get_bool('man') or { return } // ignore error and handle command normally + if man_flag { + cmd.execute_man() + exit(0) + } + } +} + fn (cmd Command) check_version_flag() { if !cmd.disable_version && cmd.version != '' && cmd.flags.contains('version') { version_flag := cmd.flags.get_bool('version') or { return } // ignore error and handle command normally @@ -279,6 +300,17 @@ pub fn (cmd Command) execute_help() { } } +// execute_help executes the callback registered +// for the `-man` flag option. +pub fn (cmd Command) execute_man() { + if cmd.commands.contains('man') { + man_cmd := cmd.commands.get('man') or { return } + man_cmd.execute(man_cmd) or { panic(err) } + } else { + print(cmd.manpage()) + } +} + fn (cmds []Command) get(name string) ?Command { for cmd in cmds { if cmd.name == name { diff --git a/vlib/cli/man.v b/vlib/cli/man.v new file mode 100644 index 000000000..c27f23307 --- /dev/null +++ b/vlib/cli/man.v @@ -0,0 +1,181 @@ +module cli + +import time + +fn man_flag() Flag { + return Flag{ + flag: .bool + name: 'man' + description: 'Prints the auto-generated manpage.' + } +} + +fn man_cmd() Command { + return Command{ + name: 'man' + usage: '' + description: 'Prints the auto-generated manpage.' + execute: print_manpage_for_command + } +} + +// print_manpage_for_command prints the manpage for the +// command or subcommand in `man_cmd` to stdout +pub fn print_manpage_for_command(man_cmd Command) ? { + if man_cmd.args.len > 0 { + mut cmd := man_cmd.parent + for arg in man_cmd.args { + mut found := false + for sub_cmd in cmd.commands { + if sub_cmd.name == arg { + cmd = unsafe { &sub_cmd } + found = true + break + } + } + if !found { + args := man_cmd.args.join(' ') + println('Invalid command: $args') + return + } + } + print(cmd.manpage()) + } else { + if man_cmd.parent != 0 { + print(man_cmd.parent.manpage()) + } + } +} + +// manpage returns a `string` containing the mdoc(7) manpage for +// this `Command` +pub fn (cmd Command) manpage() string { + mut mdoc := '.Dd ${time.now().strftime('%B %d, %Y')}\n' + mdoc += '.Dt ${cmd.full_name().replace(' ', '-').to_upper()} 1\n' + mdoc += '.Os\n.Sh NAME\n.Nm ${cmd.full_name().replace(' ', '-')}\n.Nd $cmd.description\n' + mdoc += '.Sh SYNOPSIS\n' + mdoc += '.Nm $cmd.root().name\n' + if cmd.parent != 0 { + mut parents := []Command{} + if !cmd.parent.is_root() { + parents.prepend(cmd.parent) + for { + p := parents[0] + if p.parent.is_root() { + break + } else { + parents.prepend(p.parent) + } + } + for c in parents { + mdoc += '.Ar $c.name\n' + } + } + mdoc += '.Ar $cmd.name\n' + } + for flag in cmd.flags { + mdoc += '.Op' + if flag.abbrev != '' { + mdoc += ' Fl $flag.abbrev' + } else { + if cmd.posix_mode { + mdoc += ' Fl -$flag.name' + } else { + mdoc += ' Fl $flag.name' + } + } + match flag.flag { + .int, .float, .int_array, .float_array { mdoc += ' Ar num' } + .string, .string_array { mdoc += ' Ar string' } + else {} + } + mdoc += '\n' + } + for i in 0 .. cmd.required_args { + mdoc += '.Ar arg$i\n' + } + if cmd.commands.len > 0 { + mdoc += '.Nm $cmd.root().name\n' + if cmd.parent != 0 { + mut parents := []Command{} + if !cmd.parent.is_root() { + parents.prepend(cmd.parent) + for { + p := parents[0] + if p.parent.is_root() { + break + } else { + parents.prepend(p.parent) + } + } + for c in parents { + mdoc += '.Ar $c.name\n' + } + } + mdoc += '.Ar $cmd.name\n' + } + mdoc += '.Ar subcommand\n' + } + + mdoc += '.Sh DESCRIPTION\n' + if cmd.man_description != '' { + mdoc += '$cmd.man_description\n' + } else if cmd.description != '' { + mdoc += '$cmd.description\n' + } + if cmd.flags.len > 0 { + mdoc += '.Pp\nThe options are as follows:\n' + mdoc += '.Bl -tag -width indent\n' + for flag in cmd.flags { + mdoc += '.It' + if flag.abbrev != '' { + mdoc += ' Fl $flag.abbrev' + } + if cmd.posix_mode { + mdoc += ' Fl -$flag.name' + } else { + mdoc += ' Fl $flag.name' + } + mdoc += '\n' + if flag.description != '' { + mdoc += '$flag.description\n' + } + } + mdoc += '.El\n' + } + if cmd.commands.len > 0 { + 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' + if c.description != '' { + mdoc += '$c.description\n' + } + } + mdoc += '.El\n' + } + + if cmd.commands.len > 0 { + mdoc += '.Sh SEE ALSO\n' + mut cmds := []string{} + if cmd.parent != 0 { + cmds << cmd.parent.full_name().replace(' ', '-') + } + for c in cmd.commands { + cmds << c.full_name().replace(' ', '-') + } + cmds.sort() + mut i := 1 + for c in cmds { + mdoc += '.Xr $c 1' + if i == cmds.len { + mdoc += '\n' + } else { + mdoc += ' ,\n' + } + i++ + } + } + + return mdoc +} diff --git a/vlib/cli/man_test.v b/vlib/cli/man_test.v new file mode 100644 index 000000000..461a7b505 --- /dev/null +++ b/vlib/cli/man_test.v @@ -0,0 +1,110 @@ +module cli + +fn test_manpage() { + mut cmd := Command{ + name: 'command' + description: 'description' + commands: [ + Command{ + name: 'sub' + description: 'subcommand' + }, + Command{ + name: 'sub2' + description: 'another subcommand' + }, + ] + flags: [ + Flag{ + flag: .string + name: 'str' + description: 'str flag' + }, + Flag{ + flag: .bool + name: 'bool' + description: 'bool flag' + abbrev: 'b' + }, + Flag{ + flag: .string + name: 'required' + abbrev: 'r' + required: true + }, + ] + } + cmd.setup() + assert cmd.manpage().after_char(`\n`) == r'.Dt COMMAND 1 +.Os +.Sh NAME +.Nm command +.Nd description +.Sh SYNOPSIS +.Nm command +.Op Fl str Ar string +.Op Fl b +.Op Fl r Ar string +.Nm command +.Ar subcommand +.Sh DESCRIPTION +description +.Pp +The options are as follows: +.Bl -tag -width indent +.It Fl str +str flag +.It Fl b Fl bool +bool flag +.It Fl r Fl required +.El +.Pp +The subcommands are as follows: +.Bl -tag -width indent +.It Cm sub +subcommand +.It Cm sub2 +another subcommand +.El +.Sh SEE ALSO +.Xr command-sub 1 , +.Xr command-sub2 1 +' + + cmd.posix_mode = true + assert cmd.manpage().after_char(`\n`) == r'.Dt COMMAND 1 +.Os +.Sh NAME +.Nm command +.Nd description +.Sh SYNOPSIS +.Nm command +.Op Fl -str Ar string +.Op Fl b +.Op Fl r Ar string +.Nm command +.Ar subcommand +.Sh DESCRIPTION +description +.Pp +The options are as follows: +.Bl -tag -width indent +.It Fl -str +str flag +.It Fl b Fl -bool +bool flag +.It Fl r Fl -required +.El +.Pp +The subcommands are as follows: +.Bl -tag -width indent +.It Cm sub +subcommand +.It Cm sub2 +another subcommand +.El +.Sh SEE ALSO +.Xr command-sub 1 , +.Xr command-sub2 1 +' +} diff --git a/vlib/v/tests/inout/cli_root_default_help.vv b/vlib/v/tests/inout/cli_root_default_help.vv index 384624ac7..01da855d7 100644 --- a/vlib/v/tests/inout/cli_root_default_help.vv +++ b/vlib/v/tests/inout/cli_root_default_help.vv @@ -3,5 +3,6 @@ import os fn main() { mut cmd := Command {} + cmd.disable_man = true cmd.parse(os.args) } -- 2.30.2