v / vlib / flag / flag_to.v
1816 lines · 1692 sloc · 56.17 KB · 22e8b0fe27c2b6f1fb9b76cf4cd094980ae5e77f
Raw
1module flag
2
3import v.ast
4
5struct FlagData {
6 raw string @[required]
7 field_name string @[required]
8 delimiter string
9 name string
10 arg ?string
11 pos int
12 repeats int = 1
13}
14
15struct FlagContext {
16 raw string @[required] // raw arg array entry. E.g.: `--id=val`
17 delimiter string // usually `'-'`
18 name string // either struct field name or what is defined in `@[long: <name>]`
19 next string // peek at what is the next flag/arg
20 pos int // position in arg array
21}
22
23pub enum ParseMode {
24 strict // return errors for unknown or malformed flags per default
25 relaxed // relax flag match errors and add them to `no_match` list instead
26}
27
28pub enum Style {
29 short // Posix short only, allows multiple shorts -def is `-d -e -f` and "sticky" arguments e.g.: `-ofoo` = `-o foo`
30 long // GNU style long option *only*. E.g.: `--name` or `--name=value`
31 short_long // extends `posix` style shorts with GNU style long options: `--flag` or `--name=value`
32 v // V style flags as found in flags for the `v` compiler. Single flag denote `-` followed by string identifier e.g.: `-verbose`, `-name value`, `-v`, `-n value` or `-d ident=value`
33 v_flag_parser // V `flag.FlagParser` style flags as supported by `flag.FlagParser`. Long flag denote `--` followed by string identifier e.g.: `--verbose`, `--name value`, `-v` or `-n value`.
34 go_flag // GO `flag` module style. Single flag denote `-` followed by string identifier e.g.: `-verbose`, `-name value`, `-v` or `-n value` and both long `--name value` and GNU long `--name=value`
35 cmd_exe // `cmd.exe` style flags. Single flag denote `/` followed by lower- or upper-case character
36}
37
38struct StructInfo {
39 name string // name of the struct itself
40 attrs map[string]string // collection of `@[x: y]` sat on the struct, read via reflection
41 fields map[string]StructField
42}
43
44@[flag]
45pub enum FieldHints {
46 is_bool
47 is_array
48 is_ignore
49 is_int_type
50 has_tail
51 short_only
52 can_repeat
53}
54
55// StructField is a representation of the data collected via reflection on the input `T` struct.
56// `match_name` is resolved upon reflection and represents the longest flag name value the user wants
57// to be mapped to this field. If users has indicated that they want to match a field as a short/abbrevation flag
58// `short` is accounted instead of `match_name`
59struct StructField {
60 name string // name of the field on the struct, *not used* for resolving mappings, use `match_name`
61 match_name string // match_name is either `name` or the value of `@[long: x]` or `@[only: x]`
62 short string // single char short alias of field, sat via `@[short: x]` or `@[only: x]`
63 hints FieldHints = FieldHints.zero()
64 attrs map[string]string // collection of `@[x: y]` sat on the field, read via reflection
65 type_name string
66 doc string // documentation string sat via `@[xdoc: x y z]`
67}
68
69fn assign_single_flag_value[T](mut target T, default_value T, f FlagData, struct_name string, field_name string) ! {
70 _ = target
71 a_or_r := f.arg or { '${f.repeats}' }
72 $if T is int {
73 if !a_or_r.is_int() {
74 return error('can not assign non-integer value `${a_or_r}` from flag `${f.raw}` to `${struct_name}.${field_name}`')
75 }
76 target = a_or_r.int()
77 } $else $if T is i64 {
78 if !a_or_r.is_int() {
79 return error('can not assign non-integer value `${a_or_r}` from flag `${f.raw}` to `${struct_name}.${field_name}`')
80 }
81 target = a_or_r.i64()
82 } $else $if T is u64 {
83 if !a_or_r.is_int() {
84 return error('can not assign non-integer value `${a_or_r}` from flag `${f.raw}` to `${struct_name}.${field_name}`')
85 }
86 target = a_or_r.u64()
87 } $else $if T is i32 {
88 if !a_or_r.is_int() {
89 return error('can not assign non-integer value `${a_or_r}` from flag `${f.raw}` to `${struct_name}.${field_name}`')
90 }
91 target = a_or_r.i32()
92 } $else $if T is u32 {
93 if !a_or_r.is_int() {
94 return error('can not assign non-integer value `${a_or_r}` from flag `${f.raw}` to `${struct_name}.${field_name}`')
95 }
96 target = a_or_r.u32()
97 } $else $if T is i16 {
98 if !a_or_r.is_int() {
99 return error('can not assign non-integer value `${a_or_r}` from flag `${f.raw}` to `${struct_name}.${field_name}`')
100 }
101 target = a_or_r.i16()
102 } $else $if T is u16 {
103 if !a_or_r.is_int() {
104 return error('can not assign non-integer value `${a_or_r}` from flag `${f.raw}` to `${struct_name}.${field_name}`')
105 }
106 target = a_or_r.u16()
107 } $else $if T is i8 {
108 if !a_or_r.is_int() {
109 return error('can not assign non-integer value `${a_or_r}` from flag `${f.raw}` to `${struct_name}.${field_name}`')
110 }
111 target = a_or_r.i8()
112 } $else $if T is u8 {
113 if !a_or_r.is_int() {
114 return error('can not assign non-integer value `${a_or_r}` from flag `${f.raw}` to `${struct_name}.${field_name}`')
115 }
116 target = a_or_r.u8()
117 } $else $if T is f32 {
118 target = f.arg or { return error('failed appending ${f.raw} to ${field_name}') }
119 .f32()
120 } $else $if T is f64 {
121 target = f.arg or { return error('failed appending ${f.raw} to ${field_name}') }
122 .f64()
123 } $else $if T is bool {
124 if arg := f.arg {
125 return error('can not assign `${arg}` to bool field `${field_name}`')
126 }
127 target = !default_value
128 } $else $if T is string {
129 target = f.arg or { return error('failed appending ${f.raw} to ${field_name}') }
130 .str()
131 } $else {
132 return error('field type: ${T.name} for ${field_name} is not supported')
133 }
134}
135
136fn append_multi_flag_value[T](mut target T, f FlagData, field_name string) ! {
137 $if T is []string {
138 target << f.arg or { return error('failed appending ${f.raw} to ${field_name}') }
139 .str()
140 } $else $if T is []int {
141 target << f.arg or { return error('failed appending ${f.raw} to ${field_name}') }
142 .int()
143 } $else $if T is []i64 {
144 target << f.arg or { return error('failed appending ${f.raw} to ${field_name}') }
145 .i64()
146 } $else $if T is []u64 {
147 target << f.arg or { return error('failed appending ${f.raw} to ${field_name}') }
148 .u64()
149 } $else $if T is []i32 {
150 target << f.arg or { return error('failed appending ${f.raw} to ${field_name}') }
151 .i32()
152 } $else $if T is []u32 {
153 target << f.arg or { return error('failed appending ${f.raw} to ${field_name}') }
154 .u32()
155 } $else $if T is []i16 {
156 target << f.arg or { return error('failed appending ${f.raw} to ${field_name}') }
157 .i16()
158 } $else $if T is []u16 {
159 target << f.arg or { return error('failed appending ${f.raw} to ${field_name}') }
160 .u16()
161 } $else $if T is []i8 {
162 target << f.arg or { return error('failed appending ${f.raw} to ${field_name}') }
163 .i8()
164 } $else $if T is []u8 {
165 target << f.arg or { return error('failed appending ${f.raw} to ${field_name}') }
166 .u8()
167 } $else $if T is []f32 {
168 target << f.arg or { return error('failed appending ${f.raw} to ${field_name}') }
169 .f32()
170 } $else $if T is []f64 {
171 target << f.arg or { return error('failed appending ${f.raw} to ${field_name}') }
172 .f64()
173 } $else {
174 return error('field type: ${T.name} for multi value ${field_name} is not supported')
175 }
176}
177
178fn (sf StructField) shortest_match_name() ?string {
179 mut name := sf.short // if `short` is sat it takes precedence over `match_name`
180 if name == '' && sf.match_name.len == 1 {
181 name = sf.match_name
182 }
183 if name != '' {
184 return name
185 }
186 return none
187}
188
189@[params]
190pub struct ParseConfig {
191pub:
192 delimiter string = '-' // delimiter used for flags
193 mode ParseMode = .strict // return errors for unknown or malformed flags per default
194 style Style = .short_long // expected flag style
195 stop ?string // single, usually '--', string that stops parsing flags/options
196 skip u16 // skip this amount in the input argument array, usually `1` for skipping executable or subcmd entry
197}
198
199@[params]
200pub struct DocConfig {
201pub:
202 delimiter string = '-' // delimiter used for flags
203 style Style = .short_long // expected flag style
204pub mut:
205 name string // application name
206 version string // application version
207 description string // application description
208 footer string // application description footer written after auto-generated flags list/ field descriptions
209 layout DocLayout // documentation layout
210 options DocOptions // documentation options
211 fields map[string]string // doc strings for each field (overwrites @[doc: xxx] attributes)
212}
213
214@[flag]
215pub enum Show {
216 name
217 version
218 flags
219 flag_type
220 flag_hint
221 description
222 flags_header
223 footer
224}
225
226pub struct DocLayout {
227pub mut:
228 description_padding int = 28
229 description_width int = 50
230 flag_indent int = 2
231}
232
233pub struct DocOptions {
234pub mut:
235 flag_header string = '\nOptions:'
236 compact bool
237 show Show = ~Show.zero()
238}
239
240// max_width returns the total width of the `DocLayout`.
241pub fn (dl DocLayout) max_width() int {
242 return dl.flag_indent + dl.description_padding + dl.description_width
243}
244
245pub struct FlagMapper {
246pub:
247 config ParseConfig @[required]
248 input []string @[required]
249mut:
250 si StructInfo
251 handled_pos []int // tracks handled positions in the `input` args array. NOTE: can contain duplicates
252 field_map_flag map[string]FlagData
253 array_field_map_flag map[string][]FlagData
254 no_match []int // indicies of unmatched flags in the `input` array
255}
256
257@[if trace_flag_mapper ?]
258fn trace_println(str string) {
259 println(str)
260}
261
262@[if trace_flag_mapper ? && debug]
263fn trace_dbg_println(str string) {
264 println(str)
265}
266
267// dbg_match returns a debug string with data about the mapping of a flag to a field
268fn (fm FlagMapper) dbg_match(flag_ctx FlagContext, field StructField, arg string, field_extra string) string {
269 struct_name := fm.si.name
270 extra := if field_extra != '' { '/' + field_extra } else { '' }
271 return '${struct_name}.${field.name}/${field.short}${extra} in ${flag_ctx.raw}/${flag_ctx.name} = `${arg}`'
272}
273
274fn normalize_attr_value(value string) string {
275 trimmed := value.trim_space()
276 if trimmed.len > 1 {
277 if (trimmed[0] == `'` && trimmed[trimmed.len - 1] == `'`)
278 || (trimmed[0] == `"` && trimmed[trimmed.len - 1] == `"`) {
279 return trimmed[1..trimmed.len - 1]
280 }
281 }
282 return trimmed
283}
284
285fn (mut fm FlagMapper) add_array_flag(field_name string, flag_data FlagData) {
286 if field_name !in fm.array_field_map_flag {
287 fm.array_field_map_flag[field_name] = []FlagData{}
288 }
289 fm.array_field_map_flag[field_name] << flag_data
290}
291
292fn (fm FlagMapper) get_struct_info[T]() !StructInfo {
293 mut struct_fields := map[string]StructField{}
294 mut struct_attrs := map[string]string{}
295 mut struct_name := ''
296 mut used_names := []string{}
297 $if T is $struct {
298 struct_name = T.name
299 trace_println('${@FN}: mapping struct "${struct_name}"...')
300
301 $for st_attr in T.attributes {
302 if st_attr.has_arg && st_attr.kind == .string {
303 struct_attrs[st_attr.name.trim_space()] = st_attr.arg.trim(' ')
304 }
305 }
306 // Handle positional first so they can be marked as handled
307 $for field in T.fields {
308 mut hints := FieldHints.zero()
309 mut match_name := field.name.replace('_', '-')
310 trace_println('${@FN}: field "${field.name}":')
311 mut attrs := map[string]string{}
312 for attr in field.attrs {
313 trace_println('\tattribute: "${attr}"')
314 if attr.contains(':') {
315 attr_name, attr_value := attr.split_once(':') or { '', '' }
316 attrs[attr_name.trim_space()] = normalize_attr_value(attr_value)
317 } else {
318 attrs[attr.trim(' ')] = 'true'
319 }
320 }
321 if long_alias := attrs['long'] {
322 match_name = long_alias.replace('_', '-')
323 }
324 if only := attrs['only'] {
325 if only.len == 0 {
326 return error('attribute @[only] on ${struct_name}.${match_name} can not be empty, use @[only: x]')
327 } else if only.len == 1 {
328 hints.set(.short_only)
329 attrs['short'] = only
330 if only in used_names {
331 return error('attribute @[only: ${only}] on ${struct_name}.${field.name}, "${only}" is already in use')
332 }
333 } else if only.len > 1 {
334 match_name = only
335 }
336 }
337
338 mut short := ''
339 if short_alias := attrs['short'] {
340 if short_alias.len != 1 {
341 return error('attribute @[short: ${short}] on ${struct_name}.${field.name} can only be a single character')
342 }
343 short = short_alias
344
345 if short in used_names {
346 return error('attribute @[short: ${short_alias}] on ${struct_name}.${field.name}, "${short}" is already in use')
347 }
348 used_names << short
349 }
350
351 trace_println('\tmatch name: "${match_name}"')
352 used_names << match_name
353
354 if field.typ in [
355 int(ast.int_type),
356 int(ast.i64_type),
357 int(ast.u64_type),
358 int(ast.i32_type),
359 int(ast.u32_type),
360 int(ast.i16_type),
361 int(ast.u16_type),
362 int(ast.i8_type),
363 int(ast.u8_type),
364 ] {
365 hints.set(.is_int_type)
366 }
367
368 if _ := attrs['repeats'] {
369 hints.set(.can_repeat)
370 }
371 if _ := attrs['tail'] {
372 hints.set(.has_tail)
373 }
374 if _ := attrs['ignore'] {
375 hints.set(.is_ignore)
376 }
377
378 if field.typ == int(ast.bool_type) {
379 trace_println('\tfield "${field.name}" is a bool')
380 hints.set(.is_bool)
381 }
382 if field.is_array {
383 trace_println('\tfield "${field.name}" is array')
384 hints.set(.is_array)
385 }
386
387 if !hints.has(.is_int_type) && hints.has(.can_repeat) {
388 return error('`@[repeats] can only be used on integer type fields like `int`, `u32` etc.')
389 }
390
391 mut doc := ''
392 // `xdoc` was chosen because of `vfmt` sorting attributes alphabetically - so to avoid cluttering attributes
393 // we want the doc strings to be among the last attributes. To make it flexible to users we provide
394 // `$d('v:flag:doc_attr','xdoc')` to let users tweak it.
395 if docs := attrs[$d('v:flag:doc_attr', 'xdoc')] {
396 trace_println('\tdoc for field "${field.name}": ${docs}')
397 doc = docs
398 }
399
400 struct_fields[field.name] = StructField{
401 name: field.name
402 match_name: match_name
403 hints: hints
404 short: short
405 attrs: attrs
406 type_name: typeof(field).name
407 doc: doc
408 }
409 }
410 } $else {
411 return error('the type `${T.name}` can not be decoded.')
412 }
413
414 return StructInfo{
415 name: struct_name
416 attrs: struct_attrs
417 fields: struct_fields
418 }
419}
420
421fn (m map[string]FlagData) query_flag_with_name(name string) ?FlagData {
422 for _, flag_data in m {
423 if flag_data.name == name {
424 return flag_data
425 }
426 }
427 return none
428}
429
430// to_struct returns `T` with field values sat to any matching flags in `input`.
431// to_struct also returns any flags from `input`, in order of appearance, that could *not* be matched
432// with any field on `T`.
433pub fn to_struct[T](input []string, config ParseConfig) !(T, []string) {
434 mut fm := FlagMapper{
435 config: config
436 input: input
437 }
438 fm.parse[T]()!
439 st := fm.to_struct[T](none)!
440 return st, fm.no_matches()
441}
442
443// using returns `defaults` with field values overwritten with any matching flags in `input`.
444// Any field that could *not* be matched with a flag will have the same value as the
445// field(s) passed as `defaults`.
446// using also returns any flags from `input`, in order of appearance, that could *not* be matched
447// with any field on `T`.
448pub fn using[T](defaults T, input []string, config ParseConfig) !(T, []string) {
449 mut fm := FlagMapper{
450 config: config
451 input: input
452 }
453 fm.parse[T]()!
454 st := fm.to_struct[T](defaults)!
455 return st, fm.no_matches()
456}
457
458// to_doc returns a "usage" style documentation `string` generated from attributes on `T` or via the `dc` argument.
459pub fn to_doc[T](dc DocConfig) !string {
460 mut fm := FlagMapper{
461 config: ParseConfig{
462 delimiter: dc.delimiter
463 style: dc.style
464 }
465 input: []
466 }
467 fm.si = fm.get_struct_info[T]()!
468 return fm.to_doc(dc)!
469}
470
471// no_matches returns any flags from the `input` array, in order of appearance, that could *not* be matched against any fields.
472// This method should be called *after* `to_struct[T]()`.
473pub fn (fm FlagMapper) no_matches() []string {
474 mut non_matching := []string{}
475 for i in fm.no_match {
476 non_matching << fm.input[i]
477 }
478 return non_matching
479}
480
481// parse parses `T` to an internal data representation.
482pub fn (mut fm FlagMapper) parse[T]() ! {
483 config := fm.config
484 style := config.style
485 delimiter := config.delimiter
486 args := fm.input
487
488 trace_println('${@FN}: parsing ${args}')
489 if config.skip > 0 {
490 // skip X entries. Could be the "exe", "executable" or "subcmd" - or more if the user wishes
491 for i in 0 .. int(config.skip) {
492 fm.handled_pos << i
493 }
494 }
495
496 // Gather runtime information about T
497 fm.si = fm.get_struct_info[T]()!
498 struct_name := fm.si.name
499
500 // Find the position of the last flag in `input`
501 mut pos_last_flag := -1
502 for pos, arg in args {
503 if arg.starts_with(delimiter) {
504 pos_last_flag = pos
505 }
506 }
507
508 for pos, arg in args {
509 if arg == '' {
510 fm.no_match << pos
511 continue
512 }
513 mut pos_is_handled := pos in fm.handled_pos
514
515 if !pos_is_handled {
516 // Stop parsing as soon as possible if `--` (or user defined) stop option is sat and encountered
517 if arg == config.stop or { '' } {
518 trace_println('${@FN}: reached option stop (${config.stop}) at index ${pos}')
519 // record all positions after this as not matching, unless pos is the last entry
520 if pos < args.len - 1 {
521 for unused_pos in pos + 1 .. args.len {
522 fm.no_match << unused_pos
523 }
524 }
525 break
526 }
527 }
528
529 // peek next arg
530 mut next := if pos + 1 < args.len { args[pos + 1] } else { '' }
531
532 // Parse arg entry (potential flag)
533 mut is_flag := false // `flag` starts with `-` (default value) or user defined
534 mut flag_name := ''
535 if arg.starts_with(delimiter) {
536 is_flag = true
537 flag_name = arg.trim_left(delimiter)
538 // Parse GNU (GO `flag`) `--name=value`
539 if style in [.long, .short_long, .go_flag] {
540 flag_name = flag_name.all_before('=')
541 }
542 }
543
544 // A flag, find best matching field in struct, if any
545 if is_flag {
546 // Figure out and validate used delimiter
547 used_delimiter := arg.all_before(flag_name)
548 is_long_delimiter := used_delimiter.count(delimiter) == 2
549 is_short_delimiter := used_delimiter.count(delimiter) == 1
550 is_invalid_delimiter := !is_long_delimiter && !is_short_delimiter
551 if is_invalid_delimiter {
552 if config.mode == .relaxed {
553 fm.no_match << pos
554 continue
555 }
556 return error('invalid delimiter `${used_delimiter}` for flag `${arg}`')
557 }
558 if is_long_delimiter {
559 if style == .v {
560 if config.mode == .relaxed {
561 fm.no_match << pos
562 continue
563 }
564 return error('long delimiter `${used_delimiter}` encountered in flag `${arg}` in ${style} (V) style parsing mode. Maybe you meant `.v_flag_parser`?')
565 }
566 if style == .short {
567 if config.mode == .relaxed {
568 fm.no_match << pos
569 continue
570 }
571 return error('long delimiter `${used_delimiter}` encountered in flag `${arg}` in ${style} (POSIX) style parsing mode')
572 }
573 }
574
575 if is_short_delimiter {
576 if style == .long {
577 if config.mode == .relaxed {
578 fm.no_match << pos
579 continue
580 }
581 return error('short delimiter `${used_delimiter}` encountered in flag `${arg}` in ${style} (GNU) style parsing mode')
582 }
583 if style == .short_long && flag_name.len > 1 && flag_name.contains('-') {
584 if config.mode == .relaxed {
585 fm.no_match << pos
586 continue
587 }
588 return error('long name `${flag_name}` used with short delimiter `${used_delimiter}` in flag `${arg}` in ${style} (POSIX/GNU) style parsing mode')
589 }
590 }
591
592 if flag_name == '' {
593 if config.mode == .relaxed {
594 fm.no_match << pos
595 continue
596 }
597 return error('invalid delimiter-only flag `${arg}`')
598 }
599
600 flag_ctx := FlagContext{
601 raw: arg
602 delimiter: used_delimiter
603 name: flag_name
604 next: next
605 pos: pos
606 }
607
608 // Identify and match short clusters first. Example: `-yxz( arg)` = `-y -x -z( arg)`
609 if is_short_delimiter && style in [.short, .short_long] {
610 fm.map_posix_short_cluster(flag_ctx)!
611 }
612
613 pos_is_handled = pos in fm.handled_pos
614 if pos_is_handled {
615 trace_dbg_println('${@FN}: skipping position "${pos}". Already handled')
616 continue
617 }
618
619 for _, field in fm.si.fields {
620 if field.hints.has(.is_ignore) {
621 trace_dbg_println('${@FN}: skipping field "${field.name}" has an @[ignore] attribute')
622 continue
623 }
624 // Field already identified, skip
625 if _ := fm.field_map_flag[field.name] {
626 trace_dbg_println('${@FN}: skipping field "${field.name}" already identified')
627 continue
628 }
629
630 trace_println('${@FN}: matching `${used_delimiter}` ${if is_long_delimiter {
631 '(long)'
632 } else {
633 '(short)'
634 }} flag "${arg}/${flag_name}" is it matching "${field.name}${if field.short != '' {
635 '/' + field.short
636 } else {
637 ''
638 }}"?')
639
640 if field.hints.has(.short_only) {
641 trace_println('${@FN}: skipping long delimiter `${used_delimiter}` match for ${struct_name}.${field.name} since it has [only: ${field.short}]')
642 }
643
644 if is_short_delimiter {
645 if style in [.short, .short_long] {
646 if fm.map_posix_short(flag_ctx, field)! {
647 continue
648 }
649 } else if style == .v {
650 if fm.map_v(flag_ctx, field)! {
651 continue
652 }
653 } else if style == .v_flag_parser {
654 if fm.map_v_flag_parser_short(flag_ctx, field)! {
655 continue
656 }
657 } else if style == .cmd_exe {
658 if fm.map_cmd_exe(flag_ctx, field)! {
659 continue
660 }
661 } else if style == .go_flag {
662 if fm.map_go_flag_short(flag_ctx, field)! {
663 continue
664 }
665 }
666 }
667
668 if is_long_delimiter {
669 // Parse GNU `--name=value`
670 if style in [.long, .short_long] {
671 if fm.map_gnu_long(flag_ctx, field)! {
672 continue
673 }
674 } else if style == .v_flag_parser {
675 if fm.map_v_flag_parser_long(flag_ctx, field)! {
676 continue
677 }
678 } else if style == .go_flag {
679 if fm.map_go_flag_long(flag_ctx, field)! {
680 continue
681 }
682 }
683 }
684 }
685 }
686
687 // Extract any tail(s) according to config
688 if pos >= pos_last_flag + 1 {
689 trace_dbg_println('${@FN}: (tail) looking for tail match for position "${pos}"...')
690 pos_is_handled = pos in fm.handled_pos
691 if pos_is_handled {
692 trace_dbg_println('${@FN}: (tail) skipping position "${pos}". Already handled')
693 continue
694 }
695 for _, field in fm.si.fields {
696 if field.hints.has(.is_ignore) {
697 trace_dbg_println('${@FN}: (tail) skipping field "${field.name}" has an @[ignore] attribute')
698 continue
699 }
700 // Field already mapped, skip
701 if _ := fm.field_map_flag[field.name] {
702 trace_dbg_println('${@FN}: (tail) skipping field "${field.name}" already identified')
703 continue
704 }
705 if field.hints.has(.has_tail) {
706 trace_dbg_println('${@FN}: (tail) field "${field.name}" has a tail attribute. fm.handled_pos.len: ${fm.handled_pos.len}')
707 last_handled_pos := fm.handled_pos[fm.handled_pos.len - 1] or { 0 }
708 trace_println('${@FN}: (tail) flag `${arg}` last_handled_pos: ${last_handled_pos} pos: ${pos}')
709 if pos == last_handled_pos + 1 || pos == pos_last_flag + 1 {
710 if field.hints.has(.is_array) {
711 fm.add_array_flag(field.name, FlagData{
712 raw: arg
713 field_name: field.name
714 arg: ?string(arg) // .arg is used when assigning at comptime to []XYZ
715 pos: pos
716 })
717 } else {
718 fm.field_map_flag[field.name] = FlagData{
719 raw: arg
720 field_name: field.name
721 arg: ?string(arg)
722 pos: pos
723 }
724 }
725 fm.handled_pos << pos
726 continue
727 }
728 }
729 }
730 }
731 if pos !in fm.handled_pos && pos !in fm.no_match {
732 fm.no_match << pos
733 // TODO: flag_name := arg.trim_left(delimiter) // WHY?
734 if already_flag := fm.field_map_flag.query_flag_with_name(arg.trim_left(delimiter)) {
735 return error('flag `${arg} ${next}` is already mapped to field `${already_flag.field_name}` via `${already_flag.delimiter}${already_flag.name} ${already_flag.arg or {
736 ''
737 }}`')
738 }
739 }
740 }
741}
742
743// add name and/or version to doc. Returns true if name or version has been added.
744fn doc_add_name_and_version(app_name string, app_version string, options DocOptions, mut docs []string) bool {
745 mut name_and_version := ''
746
747 if options.show.has(.name) && app_name != '' {
748 name_and_version = app_name
749 }
750
751 if options.show.has(.version) && app_version != '' {
752 if app_name != '' {
753 name_and_version = '${app_name} ${app_version}' // Version string is always name + version
754 } else {
755 name_and_version = app_version
756 }
757 }
758
759 if name_and_version != '' {
760 docs << '${name_and_version}'
761
762 return true
763 }
764
765 return false
766}
767
768// to_doc returns a "usage" style documentation `string` generated from the internal data structures generated via the `parse()` function.
769pub fn (fm FlagMapper) to_doc(dc DocConfig) !string {
770 mut docs := []string{}
771
772 mut app_name := ''
773 // resolve name
774 if dc.options.show.has(.name) {
775 // struct `name: x` attribute, if defined
776 if attr_name := fm.si.attrs['name'] {
777 app_name = attr_name
778 }
779 // user passed `name` overrides the attribute and default name
780 if dc.name != '' {
781 app_name = dc.name
782 }
783 }
784
785 mut app_version := ''
786 // resolve version
787 if dc.options.show.has(.version) {
788 // struct `version` attribute, if defined
789 if attr_version := fm.si.attrs['version'] {
790 app_version = attr_version
791 }
792 // user passed `version` overrides the attribute
793 if dc.version != '' {
794 app_version = dc.version
795 }
796 }
797
798 name_and_version := doc_add_name_and_version(app_name, app_version, dc.options, mut docs)
799
800 // Resolve the description if visible
801 if dc.options.show.has(.description) {
802 mut description := ''
803 // Set the description from any `xdoc` (or user defined) from *struct*
804 if attr_desc := fm.si.attrs[$d('v:flag:doc_attr', 'xdoc')] {
805 description = attr_desc
806 }
807 // user passed description overrides the attribute
808 if dc.description != '' {
809 description = dc.description
810 }
811 if description != '' {
812 docs << keep_at_max(description, dc.layout.max_width())
813 }
814 }
815
816 if dc.options.show.has(.flags) {
817 fields_docs := fm.fields_docs(dc)!
818 if fields_docs.len > 0 {
819 if dc.options.show.has(.flags_header) {
820 docs << dc.options.flag_header
821 }
822 docs << fields_docs
823 }
824 }
825
826 if dc.options.show.has(.footer) {
827 mut footer := ''
828 // struct `footer` attribute, if defined
829 if attr_footer := fm.si.attrs['footer'] {
830 footer = attr_footer
831 }
832 // user passed `footer` overrides the attribute
833 if dc.footer != '' {
834 footer = dc.footer
835 }
836 if footer != '' {
837 docs << keep_at_max(footer, dc.layout.max_width())
838 }
839 }
840
841 if name_and_version {
842 mut longest_line := 0
843 for doc_line in docs {
844 lines := doc_line.split('\n')
845 for line in lines {
846 if line.len > longest_line {
847 longest_line = line.len
848 }
849 }
850 }
851 docs.insert(1, '-'.repeat(longest_line))
852 }
853 return docs.join('\n')
854}
855
856// fields_docs returns every line of the combined field documentation.
857pub fn (fm FlagMapper) fields_docs(dc DocConfig) ![]string {
858 short_delimiter := match dc.style {
859 .short, .short_long, .v, .v_flag_parser, .go_flag, .cmd_exe { dc.delimiter }
860 .long { dc.delimiter.repeat(2) }
861 }
862
863 long_delimiter := match dc.style {
864 .short, .v, .go_flag, .cmd_exe { dc.delimiter }
865 .long, .v_flag_parser, .short_long { dc.delimiter.repeat(2) }
866 }
867
868 pad_desc := if dc.layout.description_padding < 0 { 0 } else { dc.layout.description_padding }
869 empty_padding := ' '.repeat(pad_desc)
870 indent_flags := if dc.layout.flag_indent < 0 { 0 } else { dc.layout.flag_indent }
871 indent_flags_padding := ' '.repeat(indent_flags)
872 desc_max := if dc.layout.description_width < 1 { 1 } else { dc.layout.description_width }
873
874 mut docs := []string{}
875 for _, field in fm.si.fields {
876 if field.hints.has(.is_ignore) {
877 trace_println('${@FN}: skipping field "${field.name}" has an @[ignore] attribute')
878 continue
879 }
880 doc := dc.fields[field.name] or { field.doc }
881 short := field.shortest_match_name() or { '' }
882 long := field.match_name
883
884 // -f, --flag <type>
885 mut flag_line := indent_flags_padding
886 flag_line += if short != '' { '${short_delimiter}${short}' } else { '' }
887 if !field.hints.has(.short_only) && long != '' {
888 if short != '' {
889 flag_line += ', '
890 }
891 flag_line += '${long_delimiter}${long}'
892 }
893 if dc.options.show.has(.flag_type) && field.type_name != 'bool' {
894 if !field.hints.has(.can_repeat) {
895 flag_line += ' <${field.type_name.replace('[]', '')}>'
896 }
897 }
898 if dc.options.show.has(.flag_hint) {
899 if field.hints.has(.is_array) {
900 flag_line += ' (allowed multiple times)'
901 }
902 if field.hints.has(.can_repeat) {
903 flag_line += ', ${short_delimiter}${short}${short}${short}... (can repeat)'
904 }
905 }
906 flag_line_diff := flag_line.len - pad_desc
907 if flag_line_diff < 0 {
908 // This makes sure the description is put on a new line if the flag line is
909 // longer than the padding.
910 diff := -flag_line_diff
911 line := flag_line + ' '.repeat(diff) +
912 keep_at_max(doc, desc_max).replace('\n', '\n${empty_padding}')
913 docs << line.trim_space_right()
914 } else {
915 docs << flag_line.trim_space_right()
916 if doc != '' {
917 line := empty_padding +
918 keep_at_max(doc, desc_max).replace('\n', '\n${empty_padding}')
919 docs << line.trim_space_right()
920 }
921 }
922 if !dc.options.compact {
923 docs << ''
924 }
925 }
926 // Look for custom flag entries starting with delimiter
927 for entry, doc in dc.fields {
928 if entry.starts_with(dc.delimiter) {
929 flag_line_diff := entry.len - pad_desc + indent_flags
930 if flag_line_diff < 0 {
931 // This makes sure the description is put on a new line if the flag line is
932 // longer than the padding.
933 diff := -flag_line_diff
934 line := indent_flags_padding + entry.trim(' ') + ' '.repeat(diff) +
935 keep_at_max(doc, desc_max).replace('\n', '\n${empty_padding}')
936 docs << line.trim_space_right()
937 } else {
938 docs << indent_flags_padding + entry.trim(' ')
939 line := empty_padding +
940 keep_at_max(doc, desc_max).replace('\n', '\n${empty_padding}')
941 docs << line.trim_space_right()
942 }
943 if !dc.options.compact {
944 docs << ''
945 }
946 }
947 }
948 if docs.len > 0 {
949 if !dc.options.compact {
950 // In non-compact mode the last item will be an empty line, delete it
951 docs.delete_last()
952 }
953 }
954 return docs
955}
956
957// keep_at_max returns `str` that is kept at a line width of `max` characters.
958// User provided newlines is ignored in case the `str` has to be corrected to
959// fit the provided `max` width value. Lines longer than `max` are not dealt with.
960fn keep_at_max(str string, max int) string {
961 safe_max := if max <= 0 { 1 } else { max }
962 if str.len <= safe_max || str.count(' ') == 0 {
963 return str
964 }
965 mut fitted := ''
966 mut width := 0
967 mut last_possible_break := str.index(' ') or { 0 }
968 mut never_touched := true
969 s := str.trim_space()
970 for i, c in s {
971 width++
972 if c == ` ` {
973 last_possible_break = i
974 } else if c == `\n` {
975 width = 0
976 }
977 if width == safe_max {
978 never_touched = false
979 fitted = s[..last_possible_break] + '\n'
980 // At this point we are refitting the doc string so user provided newlines can not be kept
981 // ... at least not without a tremendous increase in code complexity, that highly likely
982 // will not suit all use-cases anyway.
983 fitted +=
984 keep_at_max(s[last_possible_break..].replace('\n', ' ').trim(' '), safe_max).trim(' ')
985 } else if width > safe_max {
986 break
987 }
988 }
989 if never_touched {
990 return str
991 }
992 return fitted
993}
994
995// to_struct returns `defaults` or a new instance of `T` that has the parsed flags from `input` mapped to the fields of struct `T`.
996pub fn (fm FlagMapper) to_struct[T](defaults ?T) !T {
997 // Generate T result
998 mut result := defaults or { T{} }
999 the_default := defaults or { T{} }
1000
1001 $if T is $struct {
1002 struct_name := T.name.all_after_last('.')
1003 $for field in T.fields {
1004 if f := fm.field_map_flag[field.name] {
1005 assign_single_flag_value(mut result.$(field.name), the_default.$(field.name), f,
1006 struct_name, field.name)!
1007 }
1008 for f in fm.array_field_map_flag[field.name] {
1009 append_multi_flag_value(mut result.$(field.name), f, field.name)!
1010 }
1011 }
1012 } $else {
1013 return error('the type `${T.name}` can not be decoded.')
1014 }
1015
1016 return result
1017}
1018
1019// map_v returns `true` if the V style flag in `flag_ctx` can be mapped to `field`.
1020// map_v adds data of the match in the internal structures for further processing if applicable
1021fn (mut fm FlagMapper) map_v(flag_ctx FlagContext, field StructField) !bool {
1022 flag_raw := flag_ctx.raw
1023 flag_name := flag_ctx.name
1024 pos := flag_ctx.pos
1025 used_delimiter := flag_ctx.delimiter
1026 next := flag_ctx.next
1027
1028 if field.hints.has(.is_bool) {
1029 if flag_name == field.match_name || flag_name == field.short {
1030 arg := if flag_raw.contains('=') { flag_raw.all_after('=') } else { '' }
1031 if arg != '' {
1032 return error('flag `${flag_raw}` can not be assigned to bool field "${field.name}"')
1033 }
1034 trace_println('${@FN}: found match for (bool) ${fm.dbg_match(flag_ctx, field, 'true',
1035 '')}')
1036 fm.field_map_flag[field.name] = FlagData{
1037 raw: flag_raw
1038 field_name: field.name
1039 delimiter: used_delimiter
1040 name: flag_name
1041 pos: pos
1042 }
1043 fm.handled_pos << pos
1044 return true
1045 }
1046 }
1047
1048 if flag_name == field.match_name || flag_name == field.short {
1049 if field.hints.has(.is_array) {
1050 trace_println('${@FN}: found match for (V style multiple occurrences) ${fm.dbg_match(flag_ctx,
1051 field, next, '')}')
1052 fm.add_array_flag(field.name, FlagData{
1053 raw: flag_raw
1054 field_name: field.name
1055 delimiter: used_delimiter
1056 name: flag_name
1057 arg: ?string(next)
1058 pos: pos
1059 })
1060 } else {
1061 trace_println('${@FN}: found match for (V style) ${fm.dbg_match(flag_ctx, field, next,
1062 '')}')
1063 fm.field_map_flag[field.name] = FlagData{
1064 raw: flag_raw
1065 field_name: field.name
1066 delimiter: used_delimiter
1067 name: flag_name
1068 arg: ?string(next)
1069 pos: pos
1070 }
1071 }
1072 fm.handled_pos << pos
1073 fm.handled_pos << pos + 1 // arg
1074 return true
1075 }
1076 return false
1077}
1078
1079// map_v_flag_parser_short returns `true` if the V `flag.FlagParser` short (-) flag in `flag_ctx` can be mapped to `field`.
1080// map_v_flag_parser_short adds data of the match in the internal structures for further processing if applicable
1081fn (mut fm FlagMapper) map_v_flag_parser_short(flag_ctx FlagContext, field StructField) !bool {
1082 flag_raw := flag_ctx.raw
1083 flag_name := flag_ctx.name
1084 pos := flag_ctx.pos
1085 used_delimiter := flag_ctx.delimiter
1086 next := flag_ctx.next
1087
1088 if flag_name.len != 1 {
1089 return error('`${flag_raw}` is not supported in V `flag.FlagParser` (short) style parsing mode. Only single character flag names are supported. Use `-f value` instead')
1090 }
1091
1092 if flag_raw.contains('=') {
1093 return error('`=` in flag `${flag_raw}` is not supported in V `flag.FlagParser` (short) style parsing mode. Use `-f value` instead')
1094 }
1095
1096 if field.hints.has(.is_bool) {
1097 if flag_name == field.match_name || flag_name == field.short {
1098 trace_println('${@FN}: found match for (bool) ${fm.dbg_match(flag_ctx, field, 'true',
1099 '')}')
1100 fm.field_map_flag[field.name] = FlagData{
1101 raw: flag_raw
1102 field_name: field.name
1103 delimiter: used_delimiter
1104 name: flag_name
1105 pos: pos
1106 }
1107 fm.handled_pos << pos
1108 return true
1109 }
1110 }
1111
1112 if flag_name == field.match_name || flag_name == field.short {
1113 if field.hints.has(.is_array) {
1114 trace_println('${@FN}: found match for V (`flag.FlagParser` (short) style multiple occurrences) ${fm.dbg_match(flag_ctx,
1115 field, next, '')}')
1116 fm.add_array_flag(field.name, FlagData{
1117 raw: flag_raw
1118 field_name: field.name
1119 delimiter: used_delimiter
1120 name: flag_name
1121 arg: ?string(next)
1122 pos: pos
1123 })
1124 } else {
1125 trace_println('${@FN}: found match for V (`flag.FlagParser` (short) style) ${fm.dbg_match(flag_ctx,
1126 field, next, '')}')
1127 fm.field_map_flag[field.name] = FlagData{
1128 raw: flag_raw
1129 field_name: field.name
1130 delimiter: used_delimiter
1131 name: flag_name
1132 arg: ?string(next)
1133 pos: pos
1134 }
1135 }
1136 fm.handled_pos << pos
1137 fm.handled_pos << pos + 1 // arg
1138 return true
1139 }
1140 return false
1141}
1142
1143// map_v_flag_parser_long returns `true` if the V `flag.FlagParser` long (--) style flag in `flag_ctx` can be mapped to `field`.
1144// map_v_flag_parser_long adds data of the match in the internal structures for further processing if applicable
1145fn (mut fm FlagMapper) map_v_flag_parser_long(flag_ctx FlagContext, field StructField) !bool {
1146 flag_raw := flag_ctx.raw
1147 flag_name := flag_ctx.name
1148 pos := flag_ctx.pos
1149 used_delimiter := flag_ctx.delimiter
1150 next := flag_ctx.next
1151
1152 if flag_raw.contains('=') {
1153 return error('`=` in flag `${flag_raw}` is not supported in V `flag.FlagParser` (long) style parsing mode. Use `--flag value` instead')
1154 }
1155
1156 if field.hints.has(.is_bool) {
1157 if flag_name == field.match_name {
1158 trace_println('${@FN}: found match for (bool) ${fm.dbg_match(flag_ctx, field, 'true',
1159 '')}')
1160 fm.field_map_flag[field.name] = FlagData{
1161 raw: flag_raw
1162 field_name: field.name
1163 delimiter: used_delimiter
1164 name: flag_name
1165 pos: pos
1166 }
1167 fm.handled_pos << pos
1168 return true
1169 }
1170 }
1171
1172 if flag_name == field.match_name || flag_name == field.short {
1173 if field.hints.has(.is_array) {
1174 trace_println('${@FN}: found match for (V `flag.FlagParser` (long) style multiple occurrences) ${fm.dbg_match(flag_ctx,
1175 field, next, '')}')
1176 fm.add_array_flag(field.name, FlagData{
1177 raw: flag_raw
1178 field_name: field.name
1179 delimiter: used_delimiter
1180 name: flag_name
1181 arg: ?string(next)
1182 pos: pos
1183 })
1184 } else {
1185 trace_println('${@FN}: found match for V (`flag.FlagParser` (long) style) ${fm.dbg_match(flag_ctx,
1186 field, next, '')}')
1187 fm.field_map_flag[field.name] = FlagData{
1188 raw: flag_raw
1189 field_name: field.name
1190 delimiter: used_delimiter
1191 name: flag_name
1192 arg: ?string(next)
1193 pos: pos
1194 }
1195 }
1196 fm.handled_pos << pos
1197 fm.handled_pos << pos + 1 // arg
1198 return true
1199 }
1200 return false
1201}
1202
1203// map_go_flag_short returns `true` if the GO short style flag in `flag_ctx` can be mapped to `field`.
1204// map_go_flag_short adds data of the match in the internal structures for further processing if applicable
1205fn (mut fm FlagMapper) map_go_flag_short(flag_ctx FlagContext, field StructField) !bool {
1206 flag_raw := flag_ctx.raw
1207 flag_name := flag_ctx.name
1208 pos := flag_ctx.pos
1209 used_delimiter := flag_ctx.delimiter
1210 next := flag_ctx.next
1211
1212 if field.hints.has(.is_bool) {
1213 if flag_name == field.match_name {
1214 arg := if flag_raw.contains('=') { flag_raw.all_after('=') } else { '' }
1215 if arg != '' {
1216 return error('flag `${flag_raw}` can not be assigned to bool field "${field.name}"')
1217 }
1218 trace_println('${@FN}: found match for (bool) ${fm.dbg_match(flag_ctx, field, 'true',
1219 '')}')
1220 fm.field_map_flag[field.name] = FlagData{
1221 raw: flag_raw
1222 field_name: field.name
1223 delimiter: used_delimiter
1224 name: flag_name
1225 pos: pos
1226 }
1227 fm.handled_pos << pos
1228 return true
1229 }
1230 }
1231
1232 if flag_name == field.match_name || flag_name == field.short {
1233 if field.hints.has(.is_array) {
1234 trace_println('${@FN}: found match for (GO short style multiple occurrences) ${fm.dbg_match(flag_ctx,
1235 field, next, '')}')
1236 fm.add_array_flag(field.name, FlagData{
1237 raw: flag_raw
1238 field_name: field.name
1239 delimiter: used_delimiter
1240 name: flag_name
1241 arg: ?string(next)
1242 pos: pos
1243 })
1244 } else {
1245 trace_println('${@FN}: found match for (GO short style) ${fm.dbg_match(flag_ctx, field,
1246 next, '')}')
1247 fm.field_map_flag[field.name] = FlagData{
1248 raw: flag_raw
1249 field_name: field.name
1250 delimiter: used_delimiter
1251 name: flag_name
1252 arg: ?string(next)
1253 pos: pos
1254 }
1255 }
1256 fm.handled_pos << pos
1257 fm.handled_pos << pos + 1 // arg
1258 return true
1259 }
1260 return false
1261}
1262
1263// map_go_flag_long returns `true` if the GO long style flag in `flag_ctx` can be mapped to `field`.
1264// map_go_flag_long adds data of the match in the internal structures for further processing if applicable
1265fn (mut fm FlagMapper) map_go_flag_long(flag_ctx FlagContext, field StructField) !bool {
1266 flag_raw := flag_ctx.raw
1267 flag_name := flag_ctx.name
1268 pos := flag_ctx.pos
1269 used_delimiter := flag_ctx.delimiter
1270
1271 if flag_name == field.match_name {
1272 if field.hints.has(.is_bool) {
1273 arg := if flag_raw.contains('=') { flag_raw.all_after('=') } else { '' }
1274 if arg != '' {
1275 return error('flag `${flag_raw}` can not be assigned to bool field "${field.name}"')
1276 }
1277 trace_println('${@FN}: found match for (bool) (GO `flag` style) ${fm.dbg_match(flag_ctx,
1278 field, 'true', '')}')
1279 fm.field_map_flag[field.name] = FlagData{
1280 raw: flag_raw
1281 field_name: field.name
1282 delimiter: used_delimiter
1283 name: flag_name
1284 pos: pos
1285 }
1286 fm.handled_pos << pos
1287 return true
1288 }
1289
1290 if !flag_raw.contains('=') {
1291 if field.hints.has(.is_int_type) && field.hints.has(.can_repeat) {
1292 return error('field `${field.name}` has @[repeats], only POSIX short style allows repeating')
1293 }
1294 return error('long delimiter `${used_delimiter}` flag `${flag_raw}` mapping to `${field.name}` in ${fm.config.style} style parsing mode, expects GO (GNU) style assignment. E.g.: --name=value')
1295 }
1296
1297 arg := if flag_raw.contains('=') { flag_raw.all_after('=') } else { '' }
1298 if field.hints.has(.is_array) {
1299 trace_println('${@FN}: found match for (GO `flag` style multiple occurrences) ${fm.dbg_match(flag_ctx,
1300 field, arg, '')}')
1301 fm.add_array_flag(field.name, FlagData{
1302 raw: flag_raw
1303 field_name: field.name
1304 delimiter: used_delimiter
1305 name: flag_name
1306 arg: ?string(arg)
1307 pos: pos
1308 })
1309 } else {
1310 trace_println('${@FN}: found match for (GO `flag` style) ${fm.dbg_match(flag_ctx,
1311 field, arg, '')}')
1312 fm.field_map_flag[field.name] = FlagData{
1313 raw: flag_raw
1314 field_name: field.name
1315 delimiter: used_delimiter
1316 name: flag_name
1317 arg: ?string(arg)
1318 pos: pos
1319 }
1320 }
1321 fm.handled_pos << pos // NOTE: arg is part of the flag in GO (GNU) long style args
1322 return true
1323 }
1324 return false
1325}
1326
1327// map_gnu_long returns `true` if the GNU (long) style flag in `flag_ctx` can be mapped to `field`.
1328// map_gnu_long adds data of the match in the internal structures for further processing if applicable
1329fn (mut fm FlagMapper) map_gnu_long(flag_ctx FlagContext, field StructField) !bool {
1330 flag_raw := flag_ctx.raw
1331 flag_name := flag_ctx.name
1332 pos := flag_ctx.pos
1333 used_delimiter := flag_ctx.delimiter
1334
1335 if flag_name == field.match_name {
1336 if field.hints.has(.is_bool) {
1337 arg := if flag_raw.contains('=') { flag_raw.all_after('=') } else { '' }
1338 if arg != '' {
1339 return error('flag `${flag_raw}` can not be assigned to bool field "${field.name}"')
1340 }
1341 trace_println('${@FN}: found match for (bool) ${fm.dbg_match(flag_ctx, field, 'true',
1342 '')}')
1343 fm.field_map_flag[field.name] = FlagData{
1344 raw: flag_raw
1345 field_name: field.name
1346 delimiter: used_delimiter
1347 name: flag_name
1348 pos: pos
1349 }
1350 fm.handled_pos << pos
1351 return true
1352 } else if fm.config.style in [.long, .short_long] && !flag_raw.contains('=') {
1353 if field.hints.has(.is_int_type) && field.hints.has(.can_repeat) {
1354 return error('field `${field.name}` has @[repeats], only POSIX short style allows repeating')
1355 }
1356 return error('long delimiter `${used_delimiter}` flag `${flag_raw}` mapping to `${field.name}` in ${fm.config.style} style parsing mode, expects GNU style assignment. E.g.: --name=value')
1357 }
1358
1359 arg := if flag_raw.contains('=') { flag_raw.all_after('=') } else { '' }
1360 if field.hints.has(.is_array) {
1361 trace_println('${@FN}: found match for (GNU style multiple occurrences) ${fm.dbg_match(flag_ctx,
1362 field, arg, '')}')
1363 fm.add_array_flag(field.name, FlagData{
1364 raw: flag_raw
1365 field_name: field.name
1366 delimiter: used_delimiter
1367 name: flag_name
1368 arg: ?string(arg)
1369 pos: pos
1370 })
1371 } else {
1372 trace_println('${@FN}: found match for (GNU style) ${fm.dbg_match(flag_ctx, field, arg,
1373 '')}')
1374 fm.field_map_flag[field.name] = FlagData{
1375 raw: flag_raw
1376 field_name: field.name
1377 delimiter: used_delimiter
1378 name: flag_name
1379 arg: ?string(arg)
1380 pos: pos
1381 }
1382 }
1383 fm.handled_pos << pos // NOTE: arg is part of the flag in GNU long style args
1384 return true
1385 }
1386 return false
1387}
1388
1389// map_posix_short_cluster looks for multiple combined short flags and optional
1390// tail arguments e.g. `-yxz( a)` = `-y -x -z( a)` and maps them to the respective struct fields.
1391// map_posix_short_cluster adds data of the match in the internal structures for further processing if applicable.
1392fn (mut fm FlagMapper) map_posix_short_cluster(flag_ctx FlagContext) ! {
1393 flag_name := flag_ctx.name
1394 if flag_name.len <= 1 {
1395 return
1396 }
1397 first_letter := flag_name[0].ascii_str()
1398 // Do not handle repeated-only bundles like `-vv`; `map_posix_short` does that.
1399 if flag_name.count(first_letter) == flag_name.len {
1400 return
1401 }
1402
1403 if flag_name.len > 1 {
1404 mut split := flag_name.split('')
1405 mut matched_fields := map[string]StructField{}
1406 for mflag in split {
1407 mut matched := false
1408 for _, field in fm.si.fields {
1409 if smatch_name := field.shortest_match_name() {
1410 if mflag == smatch_name {
1411 trace_println('${@FN}: cluster flag `${mflag}` matches field `${smatch_name}`')
1412 matched_fields[mflag] = field
1413 matched = true
1414 }
1415 }
1416 }
1417 if !matched {
1418 break
1419 }
1420 }
1421
1422 if matched_fields.len == 0 {
1423 return
1424 }
1425
1426 // Iterate `split` instead of `matched_fields` since the order of appearance has significance
1427 for i := 0; i < split.len; i++ {
1428 mflag := split[i]
1429 if field := matched_fields[mflag] {
1430 // trace_println('${@FN}: ${mflag} matches ${field.name}')
1431 mf := FlagData{
1432 raw: flag_ctx.raw
1433 field_name: field.name
1434 delimiter: flag_ctx.delimiter
1435 name: mflag
1436 pos: flag_ctx.pos
1437 }
1438 if field.hints.has(.is_bool) {
1439 trace_println('${@FN}: found match for (bool) ${fm.dbg_match(flag_ctx, field,
1440 'true', '')}')
1441 fm.field_map_flag[mf.field_name] = mf
1442 fm.handled_pos << flag_ctx.pos
1443 } else if field.hints.has(.can_repeat) {
1444 repeats := if existing := fm.field_map_flag[mf.field_name] {
1445 existing.repeats + 1
1446 } else {
1447 1
1448 }
1449 trace_println('${@FN}: found match for (repeatable cluster) ${fm.dbg_match(flag_ctx,
1450 field, '${repeats}', '')}')
1451 fm.field_map_flag[mf.field_name] = FlagData{
1452 ...mf
1453 repeats: repeats
1454 }
1455 fm.handled_pos << flag_ctx.pos
1456 } else {
1457 mut arg := split[i + 1..].clone().join('')
1458 mut next_is_used := false
1459 if arg == '' {
1460 arg = flag_ctx.next
1461 if arg != '' {
1462 next_is_used = true
1463 }
1464 }
1465 if field.hints.has(.is_array) {
1466 fm.add_array_flag(mf.field_name, FlagData{
1467 ...mf
1468 arg: ?string(arg)
1469 })
1470 trace_println('${@FN}: found match for (array) ${fm.dbg_match(flag_ctx,
1471 field, arg, '')}')
1472 } else {
1473 trace_println('${@FN}: found match for (other) ${fm.dbg_match(flag_ctx,
1474 field, arg, '')}')
1475 fm.field_map_flag[mf.field_name] = FlagData{
1476 ...mf
1477 arg: ?string(arg)
1478 }
1479 }
1480 fm.handled_pos << flag_ctx.pos
1481 if next_is_used {
1482 fm.handled_pos << flag_ctx.pos + 1 // next
1483 }
1484 break
1485 }
1486 }
1487 }
1488 }
1489}
1490
1491// map_posix_short returns `true` if the POSIX (short) style flag in `flag_ctx` can be mapped to `field`.
1492// map_posix_short adds data of the match in the internal structures for further processing if applicable
1493// map_posix_short handles, amoung other things the mapping of repeatable short flags. E.g.: `-vvv vvv`
1494fn (mut fm FlagMapper) map_posix_short(flag_ctx FlagContext, field StructField) !bool {
1495 flag_raw := flag_ctx.raw
1496 mut flag_name := flag_ctx.name
1497 pos := flag_ctx.pos
1498 used_delimiter := flag_ctx.delimiter
1499 mut next := flag_ctx.next
1500 struct_name := fm.si.name
1501
1502 first_letter := flag_name.split('')[0]
1503 next_first_letter := if next != '' { next.split('')[0] } else { '' }
1504 count_of_first_letter_repeats := flag_name.count(first_letter)
1505 count_of_next_first_letter_repeats := next.count(next_first_letter)
1506
1507 if field.hints.has(.is_bool) {
1508 if flag_name == field.match_name {
1509 arg := if flag_raw.contains('=') { flag_raw.all_after('=') } else { '' }
1510 if arg != '' {
1511 return error('flag `${flag_raw}` can not be assigned to bool field "${field.name}"')
1512 }
1513 trace_println('${@FN}: found match for (bool) ${fm.dbg_match(flag_ctx, field, 'true',
1514 '')}')
1515 fm.field_map_flag[field.name] = FlagData{
1516 raw: flag_raw
1517 field_name: field.name
1518 delimiter: used_delimiter
1519 name: flag_name
1520 pos: pos
1521 }
1522 fm.handled_pos << pos
1523 return true
1524 }
1525
1526 if field.short == flag_name {
1527 trace_println('${@FN}: found match for (bool) ${fm.dbg_match(flag_ctx, field, 'true',
1528 '')}')
1529 fm.field_map_flag[field.name] = FlagData{
1530 raw: flag_raw
1531 field_name: field.name
1532 delimiter: used_delimiter
1533 name: flag_name
1534 pos: pos
1535 }
1536 fm.handled_pos << pos
1537 return true
1538 }
1539 }
1540
1541 if first_letter == field.short {
1542 // `-vvvvv`, `-vv vvv` or `-v vvvv`
1543 if field.hints.has(.can_repeat) {
1544 mut do_continue := false
1545 if count_of_first_letter_repeats == flag_name.len {
1546 trace_println('${@FN}: found match for (repeatable) ${fm.dbg_match(flag_ctx, field,
1547 'true', '')}')
1548 fm.field_map_flag[field.name] = FlagData{
1549 raw: flag_raw
1550 field_name: field.name
1551 delimiter: used_delimiter
1552 name: flag_name
1553 pos: pos
1554 repeats: count_of_first_letter_repeats
1555 }
1556 fm.handled_pos << pos
1557 do_continue = true
1558
1559 if next_first_letter == first_letter
1560 && count_of_next_first_letter_repeats == next.len {
1561 trace_println('${@FN}: field "${field.name}" allow repeats and ${flag_raw} ${next} repeats ${
1562 count_of_next_first_letter_repeats + count_of_first_letter_repeats} times (via argument)')
1563 fm.field_map_flag[field.name] = FlagData{
1564 raw: flag_raw
1565 field_name: field.name
1566 delimiter: used_delimiter
1567 name: flag_name
1568 pos: pos
1569 repeats: count_of_next_first_letter_repeats +
1570 count_of_first_letter_repeats
1571 }
1572 fm.handled_pos << pos
1573 fm.handled_pos << pos + 1 // next
1574 do_continue = true
1575 } else {
1576 trace_println('${@FN}: field "${field.name}" allow repeats and ${flag_raw} repeats ${count_of_first_letter_repeats} times')
1577 }
1578 if do_continue {
1579 return true
1580 }
1581 }
1582 } else if field.hints.has(.is_array) {
1583 split := flag_name.trim_string_left(field.short)
1584 mut next_is_handled := true
1585 if split != '' {
1586 next = split
1587 flag_name = flag_name.trim_string_right(split)
1588 next_is_handled = false
1589 }
1590
1591 if next == '' {
1592 return error('flag "${flag_raw}" expects an argument')
1593 }
1594 trace_println('${@FN}: found match for (multiple occurrences) ${fm.dbg_match(flag_ctx,
1595 field, next, '')}')
1596
1597 fm.add_array_flag(field.name, FlagData{
1598 raw: flag_raw
1599 field_name: field.name
1600 delimiter: used_delimiter
1601 name: flag_name
1602 arg: ?string(next)
1603 pos: pos
1604 })
1605 fm.handled_pos << pos
1606 if next_is_handled {
1607 fm.handled_pos << pos + 1 // next
1608 }
1609 return true
1610 } else if !flag_ctx.next.starts_with(used_delimiter) {
1611 if field.short == flag_name {
1612 trace_println('${@FN}: found match for (${field.type_name}) ${fm.dbg_match(flag_ctx,
1613 field, next, '')}')
1614 fm.field_map_flag[field.name] = FlagData{
1615 raw: flag_raw
1616 field_name: field.name
1617 delimiter: used_delimiter
1618 name: flag_name
1619 arg: ?string(next)
1620 pos: pos
1621 }
1622 fm.handled_pos << pos
1623 fm.handled_pos << pos + 1 // next
1624 return true
1625 }
1626 }
1627 }
1628 if (fm.config.style == .short || field.hints.has(.short_only)) && first_letter == field.short {
1629 split := flag_name.trim_string_left(field.short)
1630 mut next_is_handled := true
1631 if split != '' {
1632 next = split
1633 flag_name = flag_name.trim_string_right(split)
1634 next_is_handled = false
1635 }
1636
1637 if next == '' {
1638 return error('flag "${flag_raw}" expects an argument')
1639 }
1640 trace_println('${@FN}: found match for (short only) ${struct_name}.${field.name} (${field.match_name}) = ${field.short} = ${next}')
1641
1642 fm.field_map_flag[field.name] = FlagData{
1643 raw: flag_raw
1644 field_name: field.name
1645 delimiter: used_delimiter
1646 name: flag_name
1647 arg: ?string(next)
1648 pos: pos
1649 repeats: count_of_first_letter_repeats
1650 }
1651 fm.handled_pos << pos
1652 if next_is_handled {
1653 fm.handled_pos << pos + 1 // next
1654 }
1655 return true
1656 } else if flag_name == field.match_name && !(field.hints.has(.short_only)
1657 && flag_name == field.short) {
1658 trace_println('${@FN}: found match for (repeats) ${fm.dbg_match(flag_ctx, field, next, '')}')
1659 if next == '' {
1660 return error('flag "${flag_raw}" expects an argument')
1661 }
1662 fm.field_map_flag[field.name] = FlagData{
1663 raw: flag_raw
1664 field_name: field.name
1665 delimiter: used_delimiter
1666 name: flag_name
1667 arg: ?string(next)
1668 pos: pos
1669 repeats: count_of_first_letter_repeats
1670 }
1671 fm.handled_pos << pos
1672 fm.handled_pos << pos + 1 // next
1673 return true
1674 }
1675 return false
1676}
1677
1678// map_cmd_exe returns `true` if the CMD.EXE style flag in `flag_ctx` can be mapped to `field`.
1679// map_cmd_exe adds data of the match in the internal structures for further processing if applicable
1680fn (mut fm FlagMapper) map_cmd_exe(flag_ctx FlagContext, field StructField) !bool {
1681 flag_raw := flag_ctx.raw
1682 flag_name := flag_ctx.name
1683 pos := flag_ctx.pos
1684 used_delimiter := flag_ctx.delimiter
1685 next := flag_ctx.next
1686
1687 if flag_name == field.match_name {
1688 if field.hints.has(.is_bool) {
1689 trace_println('${@FN}: found (long) match for (bool) (CMD.EXE style) ${fm.dbg_match(flag_ctx,
1690 field, 'true', '')}')
1691 fm.field_map_flag[field.name] = FlagData{
1692 raw: flag_raw
1693 field_name: field.name
1694 delimiter: used_delimiter
1695 name: flag_name
1696 pos: pos
1697 }
1698 fm.handled_pos << pos
1699 return true
1700 }
1701 // Not sure original CMD.EXE flags supported multiple flags with same name??
1702 if field.hints.has(.is_array) {
1703 trace_println('${@FN}: found match for (CMD.EXE style multiple occurrences) ${fm.dbg_match(flag_ctx,
1704 field, next, '')}')
1705 fm.add_array_flag(field.name, FlagData{
1706 raw: flag_raw
1707 field_name: field.name
1708 delimiter: used_delimiter
1709 name: flag_name
1710 arg: ?string(next)
1711 pos: pos
1712 })
1713 } else {
1714 trace_println('${@FN}: found match for (CMD.EXE style) ${fm.dbg_match(flag_ctx, field,
1715 next, '')}')
1716 fm.field_map_flag[field.name] = FlagData{
1717 raw: flag_raw
1718 field_name: field.name
1719 delimiter: used_delimiter
1720 name: flag_name
1721 arg: ?string(next)
1722 pos: pos
1723 }
1724 }
1725 fm.handled_pos << pos
1726 fm.handled_pos << pos + 1 // arg
1727 return true
1728 }
1729 // Not aware of CMD.EXE flags longer than one (ASCII??) character
1730 if shortest_match_name := field.shortest_match_name() {
1731 if flag_name == shortest_match_name {
1732 if field.hints.has(.is_bool) {
1733 trace_println('${@FN}: found match for (bool) (CMD.EXE style) ${fm.dbg_match(flag_ctx,
1734 field, 'true', '')}')
1735 fm.field_map_flag[field.name] = FlagData{
1736 raw: flag_raw
1737 field_name: field.name
1738 delimiter: used_delimiter
1739 name: flag_name
1740 pos: pos
1741 }
1742 fm.handled_pos << pos
1743 return true
1744 }
1745
1746 // Not sure original CMD.EXE flags supported multiple flags with same name??
1747 if field.hints.has(.is_array) {
1748 trace_println('${@FN}: found match for (CMD.EXE style multiple occurrences) ${fm.dbg_match(flag_ctx,
1749 field, next, '')}')
1750 fm.add_array_flag(field.name, FlagData{
1751 raw: flag_raw
1752 field_name: field.name
1753 delimiter: used_delimiter
1754 name: flag_name
1755 arg: ?string(next)
1756 pos: pos
1757 })
1758 } else {
1759 trace_println('${@FN}: found match for (CMD.EXE style) ${fm.dbg_match(flag_ctx,
1760 field, next, '')}')
1761 fm.field_map_flag[field.name] = FlagData{
1762 raw: flag_raw
1763 field_name: field.name
1764 delimiter: used_delimiter
1765 name: flag_name
1766 arg: ?string(next)
1767 pos: pos
1768 }
1769 }
1770 fm.handled_pos << pos
1771 fm.handled_pos << pos + 1 // arg
1772 return true
1773 }
1774 }
1775 return false
1776}
1777
1778// parsed_flags returns all parsed flags in order of position.
1779pub fn (fm FlagMapper) parsed_flags() []FlagData {
1780 mut flags := []FlagData{}
1781 for _, f in fm.field_map_flag {
1782 flags << f
1783 }
1784 for _, arr in fm.array_field_map_flag {
1785 for f in arr {
1786 flags << f
1787 }
1788 }
1789 flags.sort_with_compare(fn (a &FlagData, b &FlagData) int {
1790 if a.pos != b.pos {
1791 return if a.pos < b.pos { -1 } else { 1 }
1792 }
1793 return if a.name < b.name {
1794 -1
1795 } else if a.name > b.name {
1796 1
1797 } else {
1798 0
1799 }
1800 })
1801 return flags
1802}
1803
1804// `handled_positions` returns unique, sorted position indices from the input args that were consumed during parsing.
1805pub fn (fm FlagMapper) handled_positions() []int {
1806 mut seen := map[int]bool{}
1807 mut result := []int{}
1808 for p in fm.handled_pos {
1809 if p !in seen {
1810 seen[p] = true
1811 result << p
1812 }
1813 }
1814 result.sort(a < b)
1815 return result
1816}
1817