v / vlib / cli / help.v
269 lines · 255 sloc · 7.63 KB · ef702a6b3b841a6ea311bc9c8cf89f40daf7267f
Raw
1module cli
2
3import term
4import strings
5
6const base_indent_len = 2
7const min_description_indent_len = 20
8const spacing = 2
9
10fn help_flag(with_abbrev bool) Flag {
11 sabbrev := if with_abbrev { 'h' } else { '' }
12 return Flag{
13 flag: .bool
14 name: 'help'
15 abbrev: sabbrev
16 description: 'Print help information'
17 }
18}
19
20fn help_cmd() Command {
21 return Command{
22 name: 'help'
23 usage: '<command>'
24 description: 'Print help information'
25 execute: print_help_for_command
26 }
27}
28
29// print_help_for_command outputs the help message of `help_cmd`.
30pub fn print_help_for_command(cmd Command) ! {
31 if cmd.args.len > 0 {
32 for sub_cmd in cmd.commands {
33 if sub_cmd.matches(cmd.args[0]) {
34 cmd_ := unsafe { &sub_cmd }
35 print(cmd_.help_message())
36 return
37 }
38 }
39 print('Invalid command: ${cmd.args.join(' ')}')
40 } else if cmd.parent != unsafe { nil } {
41 print(cmd.parent.help_message())
42 }
43}
44
45// help_message returns a generated help message as a `string` for the `Command`.
46//
47// The output is composed of (each section is omitted when empty):
48//
49// 1. `Usage:` line
50// 2. The description, if any
51// 3. `Flags:` — flags defined on this command
52// 4. `Inherited flags:` — flags propagated from any ancestor via `Flag.global`
53// 5. One section per sub-command group; the group name is rendered as the
54// user wrote it followed by `:`. Sub-commands without a group go under
55// the default `Commands:` section.
56// 6. `Examples:` — `Command.examples` entries, one per line
57// 7. `Learn more:` — the multi-line `Command.learn_more` field
58pub fn (cmd &Command) help_message() string {
59 mut help := ''
60 help += 'Usage: ${cmd.full_name()}'
61 if cmd.flags.len > 0 {
62 help += ' [flags]'
63 }
64 if cmd.commands.len > 0 {
65 help += ' [commands]'
66 }
67 if cmd.usage.len > 0 {
68 help += ' ${cmd.usage}'
69 } else {
70 for i in 0 .. cmd.required_args {
71 help += ' <arg${i}>'
72 }
73 }
74 help += '\n'
75 if cmd.description != '' {
76 help += '\n${cmd.description}\n'
77 }
78 abbrev_len, name_len := cmd.help_columns()
79 local_flags, inherited_flags := cmd.partition_flags()
80 if local_flags.len > 0 {
81 help += render_flag_section('Flags', local_flags, abbrev_len, name_len, cmd.posix_mode)
82 }
83 if inherited_flags.len > 0 {
84 help += render_flag_section('Inherited flags', inherited_flags, abbrev_len, name_len,
85 cmd.posix_mode)
86 }
87 if cmd.commands.len > 0 {
88 groups, order := group_commands(cmd.commands)
89 for idx, key in order {
90 title := if key == '' { 'Commands' } else { key }
91 help += render_command_section(title, groups[idx], name_len)
92 }
93 }
94 if cmd.examples.len > 0 {
95 help += '\nExamples:\n'
96 base_indent := ' '.repeat(base_indent_len)
97 for line in cmd.examples {
98 help += '${base_indent}${line}\n'
99 }
100 }
101 if cmd.learn_more != '' {
102 help += '\nLearn more:\n'
103 base_indent := ' '.repeat(base_indent_len)
104 for line in cmd.learn_more.split_into_lines() {
105 help += '${base_indent}${line}\n'
106 }
107 }
108 return help
109}
110
111// help_columns computes the abbreviation column width and the name column
112// width used by `Flags:`, `Inherited Flags:` and command sections so all
113// rows align vertically.
114fn (cmd &Command) help_columns() (int, int) {
115 mut abbrev_len := 0
116 mut name_len := min_description_indent_len
117 if cmd.posix_mode {
118 for flag in cmd.flags {
119 if flag.abbrev != '' {
120 abbrev_len = max(abbrev_len, flag.abbrev.len + spacing + 1) // + 1 for '-' in front
121 }
122 name_len =
123 max(name_len, abbrev_len + flag.name.len + spacing + 2) // + 2 for '--' in front
124 }
125 } else {
126 for flag in cmd.flags {
127 if flag.abbrev != '' {
128 abbrev_len = max(abbrev_len, flag.abbrev.len + spacing + 1) // + 1 for '-' in front
129 }
130 name_len =
131 max(name_len, abbrev_len + flag.name.len + spacing + 1) // + 1 for '-' in front
132 }
133 }
134 for command in cmd.commands {
135 name_len = max(name_len, command.display_name().len + spacing)
136 }
137 return abbrev_len, name_len
138}
139
140// partition_flags splits this command's `flags` into two arrays preserving
141// declaration order: locally-defined flags first, then flags inherited from
142// any ancestor via `Flag.global`.
143fn (cmd &Command) partition_flags() ([]Flag, []Flag) {
144 inherited_names := cmd.inherited_global_flag_names()
145 mut local := []Flag{}
146 mut inherited := []Flag{}
147 for flag in cmd.flags {
148 if flag.name in inherited_names {
149 inherited << flag
150 } else {
151 local << flag
152 }
153 }
154 return local, inherited
155}
156
157// inherited_global_flag_names returns the names of flags that any ancestor
158// declared with `global = true`. A flag is "inherited" only when an ancestor
159// declared it global, never when this command itself defines it.
160fn (cmd &Command) inherited_global_flag_names() []string {
161 mut names := []string{}
162 mut walker := cmd.parent
163 for unsafe { walker != nil } {
164 for flag in walker.flags {
165 if flag.global && flag.name !in names {
166 names << flag.name
167 }
168 }
169 walker = walker.parent
170 }
171 return names
172}
173
174// group_commands buckets `commands` by their `group` field, preserving the
175// declaration order of both groups and members.
176fn group_commands(commands []Command) ([][]Command, []string) {
177 mut order := []string{}
178 mut groups := [][]Command{}
179 for sub in commands {
180 key := sub.group
181 idx := order.index(key)
182 if idx == -1 {
183 order << key
184 groups << [sub]
185 } else {
186 groups[idx] << sub
187 }
188 }
189 return groups, order
190}
191
192fn render_flag_section(title string, flags []Flag, abbrev_len int, name_len int, posix_mode bool) string {
193 mut out := '\n${title}:\n'
194 prefix := if posix_mode { '--' } else { '-' }
195 base_indent := ' '.repeat(base_indent_len)
196 for flag in flags {
197 mut flag_name := ''
198 if flag.abbrev != '' {
199 abbrev_indent := ' '.repeat(abbrev_len - flag.abbrev.len - 1) // - 1 for '-' in front
200 flag_name = '-${flag.abbrev}${abbrev_indent}${prefix}${flag.name}'
201 } else {
202 abbrev_indent := ' '.repeat(abbrev_len)
203 flag_name = '${abbrev_indent}${prefix}${flag.name}'
204 }
205 mut required := ''
206 if flag.required {
207 required = ' (required)'
208 }
209 description_indent := ' '.repeat(name_len - flag_name.len)
210 out += '${base_indent}${flag_name}${description_indent}' +
211 pretty_description(flag.description + required, base_indent_len + name_len) + '\n'
212 }
213 return out
214}
215
216fn render_command_section(title string, commands []Command, name_len int) string {
217 mut out := '\n${title}:\n'
218 base_indent := ' '.repeat(base_indent_len)
219 for command in commands {
220 command_name := command.display_name()
221 description_indent := ' '.repeat(name_len - command_name.len)
222 out += '${base_indent}${command_name}${description_indent}' +
223 pretty_description(command.description, base_indent_len + name_len) + '\n'
224 }
225 return out
226}
227
228// pretty_description resizes description text depending on terminal width.
229// Essentially, smart wrap-around
230fn pretty_description(s string, indent_len int) string {
231 width, _ := term.get_terminal_size()
232 // Don't prettify if the terminal is that small, it won't be pretty anyway.
233 if indent_len > width {
234 return s
235 }
236 indent := ' '.repeat(indent_len)
237 chars_per_line := width - indent_len
238 // Give us enough room, better a little bigger than smaller
239 mut acc := strings.new_builder(((s.len / chars_per_line) + 1) * (width + 1))
240 for k, line in s.split('\n') {
241 if k != 0 {
242 acc.write_string('\n${indent}')
243 }
244 mut i := chars_per_line - 2
245 mut j := 0
246 for ; i < line.len; i += chars_per_line - 2 {
247 for j > 0 && line[j] != ` ` {
248 j--
249 }
250 // indent was already done the first iteration
251 if j != 0 {
252 acc.write_string(indent)
253 }
254 acc.writeln(line[j..i].trim_space())
255 j = i
256 }
257 // We need this even though it should never happen
258 if j != 0 {
259 acc.write_string(indent)
260 }
261 acc.write_string(line[j..].trim_space())
262 }
263 return acc.str()
264}
265
266fn max(a int, b int) int {
267 res := if a > b { a } else { b }
268 return res
269}
270