module flag struct FlagData { raw string @[required] field_name string @[required] delimiter string name string arg ?string pos int repeats int = 1 } struct FlagContext { raw string @[required] // raw arg array entry. E.g.: `--id=val` delimiter string // usually `'-'` name string // either struct field name or what is defined in `@[long: ]` next string // peek at what is the next flag/arg pos int // position in arg array } pub enum ParseMode { strict // return errors for unknown or malformed flags per default relaxed // relax flag match errors and add them to `no_match` list instead } pub enum Style { short // Posix short only, allows multiple shorts -def is `-d -e -f` and "sticky" arguments e.g.: `-ofoo` = `-o foo` long // GNU style long option *only*. E.g.: `--name` or `--name=value` short_long // extends `posix` style shorts with GNU style long options: `--flag` or `--name=value` 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` 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`. 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` cmd_exe // `cmd.exe` style flags. Single flag denote `/` followed by lower- or upper-case character } struct StructInfo { name string // name of the struct itself attrs map[string]string // collection of `@[x: y]` sat on the struct, read via reflection fields map[string]StructField } @[flag] pub enum FieldHints { is_bool is_array is_ignore is_int_type has_tail short_only can_repeat } // StructField is a representation of the data collected via reflection on the input `T` struct. // `match_name` is resolved upon reflection and represents the longest flag name value the user wants // to be mapped to this field. If users has indicated that they want to match a field as a short/abbrevation flag // `short` is accounted instead of `match_name` struct StructField { name string // name of the field on the struct, *not used* for resolving mappings, use `match_name` match_name string // match_name is either `name` or the value of `@[long: x]` or `@[only: x]` short string // single char short alias of field, sat via `@[short: x]` or `@[only: x]` hints FieldHints = FieldHints.zero() attrs map[string]string // collection of `@[x: y]` sat on the field, read via reflection type_name string doc string // documentation string sat via `@[xdoc: x y z]` } fn (sf StructField) shortest_match_name() ?string { mut name := sf.short // if `short` is sat it takes precedence over `match_name` if name == '' && sf.match_name.len == 1 { name = sf.match_name } if name != '' { return name } return none } @[params] pub struct ParseConfig { pub: delimiter string = '-' // delimiter used for flags mode ParseMode = .strict // return errors for unknown or malformed flags per default style Style = .short_long // expected flag style stop ?string // single, usually '--', string that stops parsing flags/options skip u16 // skip this amount in the input argument array, usually `1` for skipping executable or subcmd entry } @[params] pub struct DocConfig { pub: delimiter string = '-' // delimiter used for flags style Style = .short_long // expected flag style pub mut: name string // application name version string // application version description string // application description footer string // application description footer written after auto-generated flags list/ field descriptions layout DocLayout // documentation layout options DocOptions // documentation options fields map[string]string // doc strings for each field (overwrites @[doc: xxx] attributes) } @[flag] pub enum Show { name version flags flag_type flag_hint description flags_header footer } pub struct DocLayout { pub mut: description_padding int = 28 description_width int = 50 flag_indent int = 2 } pub struct DocOptions { pub mut: flag_header string = '\nOptions:' compact bool show Show = ~Show.zero() } // max_width returns the total width of the `DocLayout`. pub fn (dl DocLayout) max_width() int { return dl.flag_indent + dl.description_padding + dl.description_width } pub struct FlagMapper { pub: config ParseConfig @[required] input []string @[required] mut: si StructInfo handled_pos []int // tracks handled positions in the `input` args array. NOTE: can contain duplicates field_map_flag map[string]FlagData array_field_map_flag map[string][]FlagData no_match []int // indicies of unmatched flags in the `input` array } @[if trace_flag_mapper ?] fn trace_println(str string) { println(str) } @[if trace_flag_mapper ? && debug] fn trace_dbg_println(str string) { println(str) } // dbg_match returns a debug string with data about the mapping of a flag to a field fn (fm FlagMapper) dbg_match(flag_ctx FlagContext, field StructField, arg string, field_extra string) string { struct_name := fm.si.name extra := if field_extra != '' { '/' + field_extra } else { '' } return '${struct_name}.${field.name}/${field.short}${extra} in ${flag_ctx.raw}/${flag_ctx.name} = `${arg}`' } fn (fm FlagMapper) get_struct_info[T]() !StructInfo { mut struct_fields := map[string]StructField{} mut struct_attrs := map[string]string{} mut struct_name := '' mut used_names := []string{} $if T is $struct { struct_name = T.name trace_println('${@FN}: mapping struct "${struct_name}"...') $for st_attr in T.attributes { if st_attr.has_arg && st_attr.kind == .string { struct_attrs[st_attr.name.trim_space()] = st_attr.arg.trim(' ') } } // Handle positional first so they can be marked as handled $for field in T.fields { mut hints := FieldHints.zero() mut match_name := field.name.replace('_', '-') trace_println('${@FN}: field "${field.name}":') mut attrs := map[string]string{} for attr in field.attrs { trace_println('\tattribute: "${attr}"') if attr.contains(':') { split := attr.split(':') attrs[split[0].trim_space()] = split[1].trim(' ') } else { attrs[attr.trim(' ')] = 'true' } } if long_alias := attrs['long'] { match_name = long_alias.replace('_', '-') } if only := attrs['only'] { if only.len == 0 { return error('attribute @[only] on ${struct_name}.${match_name} can not be empty, use @[only: x]') } else if only.len == 1 { hints.set(.short_only) attrs['short'] = only if only in used_names { return error('attribute @[only: ${only}] on ${struct_name}.${field.name}, "${only}" is already in use') } } else if only.len > 1 { match_name = only } } mut short := '' if short_alias := attrs['short'] { if short_alias.len != 1 { return error('attribute @[short: ${short}] on ${struct_name}.${field.name} can only be a single character') } short = short_alias if short in used_names { return error('attribute @[short: ${short_alias}] on ${struct_name}.${field.name}, "${short}" is already in use') } used_names << short } trace_println('\tmatch name: "${match_name}"') used_names << match_name $if field.typ is int { hints.set(.is_int_type) } $else $if field.typ is i64 { hints.set(.is_int_type) } $else $if field.typ is u64 { hints.set(.is_int_type) } $else $if field.typ is i32 { hints.set(.is_int_type) } $else $if field.typ is u32 { hints.set(.is_int_type) } $else $if field.typ is i16 { hints.set(.is_int_type) } $else $if field.typ is u16 { hints.set(.is_int_type) } $else $if field.typ is i8 { hints.set(.is_int_type) } $else $if field.typ is u8 { hints.set(.is_int_type) } if _ := attrs['repeats'] { hints.set(.can_repeat) } if _ := attrs['tail'] { hints.set(.has_tail) } if _ := attrs['ignore'] { hints.set(.is_ignore) } $if field.typ is bool { trace_println('\tfield "${field.name}" is a bool') hints.set(.is_bool) } $if field.is_array { trace_println('\tfield "${field.name}" is array') hints.set(.is_array) } if !hints.has(.is_int_type) && hints.has(.can_repeat) { return error('`@[repeats] can only be used on integer type fields like `int`, `u32` etc.') } mut doc := '' // `xdoc` was chosen because of `vfmt` sorting attributes alphabetically - so to avoid cluttering attributes // we want the doc strings to be among the last attributes. To make it flexible to users we provide // `$d('v:flag:doc_attr','xdoc')` to let users tweak it. if docs := attrs[$d('v:flag:doc_attr', 'xdoc')] { trace_println('\tdoc for field "${field.name}": ${docs}') doc = docs } struct_fields[field.name] = StructField{ name: field.name match_name: match_name hints: hints short: short attrs: attrs type_name: typeof(field).name doc: doc } } } $else { return error('the type `${T.name}` can not be decoded.') } return StructInfo{ name: struct_name attrs: struct_attrs fields: struct_fields } } fn (m map[string]FlagData) query_flag_with_name(name string) ?FlagData { for _, flag_data in m { if flag_data.name == name { return flag_data } } return none } // to_struct returns `T` with field values sat to any matching flags in `input`. // to_struct also returns any flags from `input`, in order of appearance, that could *not* be matched // with any field on `T`. pub fn to_struct[T](input []string, config ParseConfig) !(T, []string) { mut fm := FlagMapper{ config: config input: input } fm.parse[T]()! st := fm.to_struct[T](none)! return st, fm.no_matches() } // using returns `defaults` with field values overwritten with any matching flags in `input`. // Any field that could *not* be matched with a flag will have the same value as the // field(s) passed as `defaults`. // using also returns any flags from `input`, in order of appearance, that could *not* be matched // with any field on `T`. pub fn using[T](defaults T, input []string, config ParseConfig) !(T, []string) { mut fm := FlagMapper{ config: config input: input } fm.parse[T]()! st := fm.to_struct[T](defaults)! return st, fm.no_matches() } // to_doc returns a "usage" style documentation `string` generated from // attributes on `T` or via the `dc` argument. pub fn to_doc[T](dc DocConfig) !string { mut fm := FlagMapper{ config: ParseConfig{ delimiter: dc.delimiter style: dc.style } input: [] } fm.si = fm.get_struct_info[T]()! return fm.to_doc(dc)! } // no_matches returns any flags from the `input` array, in order of appearance, // that could *not* be matched against any fields. // no_matches should be called *after* `to_struct[T]()`. pub fn (fm FlagMapper) no_matches() []string { mut non_matching := []string{} for i in fm.no_match { non_matching << fm.input[i] } return non_matching } // parse parses `T` to an internal data representation. pub fn (mut fm FlagMapper) parse[T]() ! { config := fm.config style := config.style delimiter := config.delimiter args := fm.input trace_println('${@FN}: parsing ${args}') if config.skip > 0 { // skip X entries. Could be the "exe", "executable" or "subcmd" - or more if the user wishes for i in 0 .. int(config.skip) { fm.handled_pos << i } } // Gather runtime information about T fm.si = fm.get_struct_info[T]()! struct_name := fm.si.name // Find the position of the last flag in `input` mut pos_last_flag := -1 for pos, arg in args { if arg.starts_with(delimiter) { pos_last_flag = pos } } for pos, arg in args { if arg == '' { fm.no_match << pos continue } mut pos_is_handled := pos in fm.handled_pos if !pos_is_handled { // Stop parsing as soon as possible if `--` (or user defined) stop option is sat and encountered if arg == config.stop or { '' } { trace_println('${@FN}: reached option stop (${config.stop}) at index ${pos}') // record all positions after this as not matching, unless pos is the last entry if pos < args.len - 1 { for unused_pos in pos + 1 .. args.len { fm.no_match << unused_pos } } break } } // peek next arg mut next := if pos + 1 < args.len { args[pos + 1] } else { '' } // Parse arg entry (potential flag) mut is_flag := false // `flag` starts with `-` (default value) or user defined mut flag_name := '' if arg.starts_with(delimiter) { is_flag = true flag_name = arg.trim_left(delimiter) // Parse GNU (GO `flag`) `--name=value` if style in [.long, .short_long, .go_flag] { flag_name = flag_name.all_before('=') } } // A flag, find best matching field in struct, if any if is_flag { // Figure out and validate used delimiter used_delimiter := arg.all_before(flag_name) is_long_delimiter := used_delimiter.count(delimiter) == 2 is_short_delimiter := used_delimiter.count(delimiter) == 1 is_invalid_delimiter := !is_long_delimiter && !is_short_delimiter if is_invalid_delimiter { if config.mode == .relaxed { fm.no_match << pos continue } return error('invalid delimiter `${used_delimiter}` for flag `${arg}`') } if is_long_delimiter { if style == .v { if config.mode == .relaxed { fm.no_match << pos continue } return error('long delimiter `${used_delimiter}` encountered in flag `${arg}` in ${style} (V) style parsing mode. Maybe you meant `.v_flag_parser`?') } if style == .short { if config.mode == .relaxed { fm.no_match << pos continue } return error('long delimiter `${used_delimiter}` encountered in flag `${arg}` in ${style} (POSIX) style parsing mode') } } if is_short_delimiter { if style == .long { if config.mode == .relaxed { fm.no_match << pos continue } return error('short delimiter `${used_delimiter}` encountered in flag `${arg}` in ${style} (GNU) style parsing mode') } if style == .short_long && flag_name.len > 1 && flag_name.contains('-') { if config.mode == .relaxed { fm.no_match << pos continue } return error('long name `${flag_name}` used with short delimiter `${used_delimiter}` in flag `${arg}` in ${style} (POSIX/GNU) style parsing mode') } } if flag_name == '' { if config.mode == .relaxed { fm.no_match << pos continue } return error('invalid delimiter-only flag `${arg}`') } flag_ctx := FlagContext{ raw: arg delimiter: used_delimiter name: flag_name next: next pos: pos } // Identify and match short clusters first. Example: `-yxz( arg)` = `-y -x -z( arg)` if is_short_delimiter && style in [.short, .short_long] { fm.map_posix_short_cluster(flag_ctx)! } pos_is_handled = pos in fm.handled_pos if pos_is_handled { trace_dbg_println('${@FN}: skipping position "${pos}". Already handled') continue } for _, field in fm.si.fields { if field.hints.has(.is_ignore) { trace_dbg_println('${@FN}: skipping field "${field.name}" has an @[ignore] attribute') continue } // Field already identified, skip if _ := fm.field_map_flag[field.name] { trace_dbg_println('${@FN}: skipping field "${field.name}" already identified') continue } trace_println('${@FN}: matching `${used_delimiter}` ${if is_long_delimiter { '(long)' } else { '(short)' }} flag "${arg}/${flag_name}" is it matching "${field.name}${if field.short != '' { '/' + field.short } else { '' }}"?') if field.hints.has(.short_only) { trace_println('${@FN}: skipping long delimiter `${used_delimiter}` match for ${struct_name}.${field.name} since it has [only: ${field.short}]') } if is_short_delimiter { if style in [.short, .short_long] { if fm.map_posix_short(flag_ctx, field)! { continue } } else if style == .v { if fm.map_v(flag_ctx, field)! { continue } } else if style == .v_flag_parser { if fm.map_v_flag_parser_short(flag_ctx, field)! { continue } } else if style == .cmd_exe { if fm.map_cmd_exe(flag_ctx, field)! { continue } } else if style == .go_flag { if fm.map_go_flag_short(flag_ctx, field)! { continue } } } if is_long_delimiter { // Parse GNU `--name=value` if style in [.long, .short_long] { if fm.map_gnu_long(flag_ctx, field)! { continue } } else if style == .v_flag_parser { if fm.map_v_flag_parser_long(flag_ctx, field)! { continue } } else if style == .go_flag { if fm.map_go_flag_long(flag_ctx, field)! { continue } } } } } // Extract any tail(s) according to config if pos >= pos_last_flag + 1 { trace_dbg_println('${@FN}: (tail) looking for tail match for position "${pos}"...') pos_is_handled = pos in fm.handled_pos if pos_is_handled { trace_dbg_println('${@FN}: (tail) skipping position "${pos}". Already handled') continue } for _, field in fm.si.fields { if field.hints.has(.is_ignore) { trace_dbg_println('${@FN}: (tail) skipping field "${field.name}" has an @[ignore] attribute') continue } // Field already mapped, skip if _ := fm.field_map_flag[field.name] { trace_dbg_println('${@FN}: (tail) skipping field "${field.name}" already identified') continue } if field.hints.has(.has_tail) { trace_dbg_println('${@FN}: (tail) field "${field.name}" has a tail attribute. fm.handled_pos.len: ${fm.handled_pos.len}') last_handled_pos := fm.handled_pos[fm.handled_pos.len - 1] or { 0 } trace_println('${@FN}: (tail) flag `${arg}` last_handled_pos: ${last_handled_pos} pos: ${pos}') if pos == last_handled_pos + 1 || pos == pos_last_flag + 1 { if field.hints.has(.is_array) { fm.array_field_map_flag[field.name] << FlagData{ raw: arg field_name: field.name arg: ?string(arg) // .arg is used when assigning at comptime to []XYZ pos: pos } } else { fm.field_map_flag[field.name] = FlagData{ raw: arg field_name: field.name arg: ?string(arg) pos: pos } } fm.handled_pos << pos continue } } } } if pos !in fm.handled_pos && pos !in fm.no_match { fm.no_match << pos // TODO: flag_name := arg.trim_left(delimiter) // WHY? if already_flag := fm.field_map_flag.query_flag_with_name(arg.trim_left(delimiter)) { 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 { '' }}`') } } } } // to_doc returns a "usage" style documentation `string` generated from // the internal data structures generated via the `parse()` function. pub fn (fm FlagMapper) to_doc(dc DocConfig) !string { mut docs := []string{} mut name_and_version := '' // resolve name if dc.options.show.has(.name) { mut app_name := '' // struct `name: x` attribute, if defined if attr_name := fm.si.attrs['name'] { app_name = attr_name } // user passed `name` overrides the attribute and default name if dc.name != '' { app_name = dc.name } if app_name != '' { name_and_version = '${app_name}' } } // resolve version if dc.options.show.has(.version) { mut app_version := '' // struct `version` attribute, if defined if attr_version := fm.si.attrs['version'] { app_version = attr_version } // user passed `version` overrides the attribute if dc.version != '' { app_version = dc.version } if app_version != '' { if name_and_version != '' { name_and_version = '${name_and_version} ${app_version}' } else { name_and_version = '${app_version}' } } } if name_and_version != '' { docs << '${name_and_version}' } // Resolve the desciption if visible if dc.options.show.has(.description) { mut description := '' // Set the description from any `xdoc` (or user defined) from *struct* if attr_desc := fm.si.attrs[$d('v:flag:doc_attr', 'xdoc')] { description = attr_desc } // user passed description overrides the attribute if dc.description != '' { description = dc.description } if description != '' { docs << keep_at_max(description, dc.layout.max_width()) } } if dc.options.show.has(.flags) { fields_docs := fm.fields_docs(dc)! if fields_docs.len > 0 { if dc.options.show.has(.flags_header) { docs << dc.options.flag_header } docs << fields_docs } } if dc.options.show.has(.footer) { mut footer := '' // struct `footer` attribute, if defined if attr_footer := fm.si.attrs['footer'] { footer = attr_footer } // user passed `footer` overrides the attribute if dc.footer != '' { footer = dc.footer } if footer != '' { docs << keep_at_max(footer, dc.layout.max_width()) } } if name_and_version != '' { mut longest_line := 0 for doc_line in docs { lines := doc_line.split('\n') for line in lines { if line.len > longest_line { longest_line = line.len } } } docs.insert(1, '-'.repeat(longest_line)) } return docs.join('\n') } // fields_docs returns every line of the combined field documentation. pub fn (fm FlagMapper) fields_docs(dc DocConfig) ![]string { short_delimiter := match dc.style { .short, .short_long, .v, .v_flag_parser, .go_flag, .cmd_exe { dc.delimiter } .long { dc.delimiter.repeat(2) } } long_delimiter := match dc.style { .short, .v, .go_flag, .cmd_exe { dc.delimiter } .long, .v_flag_parser, .short_long { dc.delimiter.repeat(2) } } pad_desc := if dc.layout.description_padding < 0 { 0 } else { dc.layout.description_padding } empty_padding := ' '.repeat(pad_desc) indent_flags := if dc.layout.flag_indent < 0 { 0 } else { dc.layout.flag_indent } indent_flags_padding := ' '.repeat(indent_flags) desc_max := if dc.layout.description_width < 1 { 1 } else { dc.layout.description_width } mut docs := []string{} for _, field in fm.si.fields { if field.hints.has(.is_ignore) { trace_println('${@FN}: skipping field "${field.name}" has an @[ignore] attribute') continue } doc := dc.fields[field.name] or { field.doc } short := field.shortest_match_name() or { '' } long := field.match_name // -f, --flag mut flag_line := indent_flags_padding flag_line += if short != '' { '${short_delimiter}${short}' } else { '' } if !field.hints.has(.short_only) && long != '' { if short != '' { flag_line += ', ' } flag_line += '${long_delimiter}${long}' } if dc.options.show.has(.flag_type) && field.type_name != 'bool' { if !field.hints.has(.can_repeat) { flag_line += ' <${field.type_name.replace('[]', '')}>' } } if dc.options.show.has(.flag_hint) { if field.hints.has(.is_array) { flag_line += ' (allowed multiple times)' } if field.hints.has(.can_repeat) { flag_line += ', ${short_delimiter}${short}${short}${short}... (can repeat)' } } flag_line_diff := flag_line.len - pad_desc if flag_line_diff < 0 { // This makes sure the description is put on a new line if the flag line is // longer than the padding. diff := -flag_line_diff line := flag_line + ' '.repeat(diff) + keep_at_max(doc, desc_max).replace('\n', '\n${empty_padding}') docs << line.trim_space_right() } else { docs << flag_line.trim_space_right() if doc != '' { line := empty_padding + keep_at_max(doc, desc_max).replace('\n', '\n${empty_padding}') docs << line.trim_space_right() } } if !dc.options.compact { docs << '' } } // Look for custom flag entries starting with delimiter for entry, doc in dc.fields { if entry.starts_with(dc.delimiter) { flag_line_diff := entry.len - pad_desc + indent_flags if flag_line_diff < 0 { // This makes sure the description is put on a new line if the flag line is // longer than the padding. diff := -flag_line_diff line := indent_flags_padding + entry.trim(' ') + ' '.repeat(diff) + keep_at_max(doc, desc_max).replace('\n', '\n${empty_padding}') docs << line.trim_space_right() } else { docs << indent_flags_padding + entry.trim(' ') line := empty_padding + keep_at_max(doc, desc_max).replace('\n', '\n${empty_padding}') docs << line.trim_space_right() } if !dc.options.compact { docs << '' } } } if docs.len > 0 { if !dc.options.compact { // In non-compact mode the last item will be an empty line, delete it docs.delete_last() } } return docs } // keep_at_max returns `str` that is kept at a line width of `max` characters. // User provided newlines is ignored in case the `str` has to be corrected to // fit the provided `max` width value. Lines longer than `max` are not dealt with. fn keep_at_max(str string, max int) string { safe_max := if max <= 0 { 1 } else { max } if str.len <= safe_max || str.count(' ') == 0 { return str } mut fitted := '' mut width := 0 mut last_possible_break := str.index(' ') or { 0 } mut never_touched := true s := str.trim_space() for i, c in s { width++ if c == ` ` { last_possible_break = i } else if c == `\n` { width = 0 } if width == safe_max { never_touched = false fitted = s[..last_possible_break] + '\n' // At this point we are refitting the doc string so user provided newlines can not be kept // ... at least not without a tremendous increase in code complexity, that highly likely // will not suit all use-cases anyway. fitted += keep_at_max(s[last_possible_break..].replace('\n', ' ').trim(' '), safe_max).trim(' ') } else if width > safe_max { break } } if never_touched { return str } return fitted } // to_struct returns `defaults` or a new instance of `T` that has the parsed flags from `input` mapped to the fields of struct `T`. pub fn (fm FlagMapper) to_struct[T](defaults ?T) !T { // Generate T result mut result := defaults or { T{} } the_default := defaults or { T{} } $if T is $struct { struct_name := T.name $for field in T.fields { if f := fm.field_map_flag[field.name] { a_or_r := f.arg or { '${f.repeats}' } $if field.typ is int { // TODO: find a way to move this kind of duplicate code out if !a_or_r.is_int() { return error('can not assign non-integer value `${a_or_r}` from flag `${f.raw}` to `${struct_name}.${field.name}`') } trace_dbg_println('${@FN}: assigning (int) ${struct_name}.${field.name} = ${a_or_r}') result.$(field.name) = a_or_r.int() } $else $if field.typ is i64 { trace_dbg_println('${@FN}: assigning (i64) ${struct_name}.${field.name} = ${a_or_r}') if !a_or_r.is_int() { return error('can not assign non-integer value `${a_or_r}` from flag `${f.raw}` to `${struct_name}.${field.name}`') } result.$(field.name) = a_or_r.i64() } $else $if field.typ is u64 { trace_dbg_println('${@FN}: assigning (u64) ${struct_name}.${field.name} = ${a_or_r}') if !a_or_r.is_int() { return error('can not assign non-integer value `${a_or_r}` from flag `${f.raw}` to `${struct_name}.${field.name}`') } result.$(field.name) = a_or_r.u64() } $else $if field.typ is i32 { trace_dbg_println('${@FN}: assigning (i32) ${struct_name}.${field.name} = ${a_or_r}') if !a_or_r.is_int() { return error('can not assign non-integer value `${a_or_r}` from flag `${f.raw}` to `${struct_name}.${field.name}`') } result.$(field.name) = a_or_r.i32() } $else $if field.typ is u32 { trace_dbg_println('${@FN}: assigning (u32) ${struct_name}.${field.name} = ${a_or_r}') if !a_or_r.is_int() { return error('can not assign non-integer value `${a_or_r}` from flag `${f.raw}` to `${struct_name}.${field.name}`') } result.$(field.name) = a_or_r.u32() } $else $if field.typ is i16 { trace_dbg_println('${@FN}: assigning (i16) ${struct_name}.${field.name} = ${a_or_r}') if !a_or_r.is_int() { return error('can not assign non-integer value `${a_or_r}` from flag `${f.raw}` to `${struct_name}.${field.name}`') } result.$(field.name) = a_or_r.i16() } $else $if field.typ is u16 { trace_dbg_println('${@FN}: assigning (u16) ${struct_name}.${field.name} = ${a_or_r}') if !a_or_r.is_int() { return error('can not assign non-integer value `${a_or_r}` from flag `${f.raw}` to `${struct_name}.${field.name}`') } result.$(field.name) = a_or_r.u16() } $else $if field.typ is i8 { trace_dbg_println('${@FN}: assigning (i8) ${struct_name}.${field.name} = ${a_or_r}') if !a_or_r.is_int() { return error('can not assign non-integer value `${a_or_r}` from flag `${f.raw}` to `${struct_name}.${field.name}`') } result.$(field.name) = a_or_r.i8() } $else $if field.typ is u8 { trace_dbg_println('${@FN}: assigning (u8) ${struct_name}.${field.name} = ${a_or_r}') if !a_or_r.is_int() { return error('can not assign non-integer value `${a_or_r}` from flag `${f.raw}` to `${struct_name}.${field.name}`') } result.$(field.name) = a_or_r.u8() } $else $if field.typ is f32 { result.$(field.name) = f.arg or { return error('failed appending ${f.raw} to ${field.name}') } .f32() } $else $if field.typ is f64 { result.$(field.name) = f.arg or { return error('failed appending ${f.raw} to ${field.name}') } .f64() } $else $if field.typ is bool { if arg := f.arg { return error('can not assign `${arg}` to bool field `${field.name}`') } result.$(field.name) = !the_default.$(field.name) } $else $if field.typ is string { trace_dbg_println('${@FN}: assigning (string) ${struct_name}.${field.name} = ${f.arg or { 'ERROR' } .str()}') result.$(field.name) = f.arg or { return error('failed appending ${f.raw} to ${field.name}') } .str() } $else { return error('field type: ${field.typ} for ${field.name} is not supported') } } for f in fm.array_field_map_flag[field.name] { // trace_println('${@FN}: appending ${field.name} << ${f.arg}') $if field.typ is []string { // TODO: find a way to move this kind of duplicate code out result.$(field.name) << f.arg or { return error('failed appending ${f.raw} to ${field.name}') } .str() } $else $if field.typ is []int { result.$(field.name) << f.arg or { return error('failed appending ${f.raw} to ${field.name}') } .int() } $else $if field.typ is []i64 { result.$(field.name) << f.arg or { return error('failed appending ${f.raw} to ${field.name}') } .i64() } $else $if field.typ is []u64 { result.$(field.name) << f.arg or { return error('failed appending ${f.raw} to ${field.name}') } .u64() } $else $if field.typ is []i32 { result.$(field.name) << f.arg or { return error('failed appending ${f.raw} to ${field.name}') } .i32() } $else $if field.typ is []u32 { result.$(field.name) << f.arg or { return error('failed appending ${f.raw} to ${field.name}') } .u32() } $else $if field.typ is []i16 { result.$(field.name) << f.arg or { return error('failed appending ${f.raw} to ${field.name}') } .i16() } $else $if field.typ is []u16 { result.$(field.name) << f.arg or { return error('failed appending ${f.raw} to ${field.name}') } .u16() } $else $if field.typ is []i8 { result.$(field.name) << f.arg or { return error('failed appending ${f.raw} to ${field.name}') } .i8() } $else $if field.typ is []u8 { result.$(field.name) << f.arg or { return error('failed appending ${f.raw} to ${field.name}') } .u8() } $else $if field.typ is []f32 { result.$(field.name) << f.arg or { return error('failed appending ${f.raw} to ${field.name}') } .f32() } $else $if field.typ is []f64 { result.$(field.name) << f.arg or { return error('failed appending ${f.raw} to ${field.name}') } .f64() } $else { return error('field type: ${field.typ} for multi value ${field.name} is not supported') } } } } $else { return error('the type `${T.name}` can not be decoded.') } return result } // map_v returns `true` if the V style flag in `flag_ctx` can be mapped to `field`. // map_v adds data of the match in the internal structures for further processing if applicable fn (mut fm FlagMapper) map_v(flag_ctx FlagContext, field StructField) !bool { flag_raw := flag_ctx.raw flag_name := flag_ctx.name pos := flag_ctx.pos used_delimiter := flag_ctx.delimiter next := flag_ctx.next if field.hints.has(.is_bool) { if flag_name == field.match_name { arg := if flag_raw.contains('=') { flag_raw.all_after('=') } else { '' } if arg != '' { return error('flag `${flag_raw}` can not be assigned to bool field "${field.name}"') } trace_println('${@FN}: found match for (bool) ${fm.dbg_match(flag_ctx, field, 'true', '')}') fm.field_map_flag[field.name] = FlagData{ raw: flag_raw field_name: field.name delimiter: used_delimiter name: flag_name pos: pos } fm.handled_pos << pos return true } } if flag_name == field.match_name || flag_name == field.short { if field.hints.has(.is_array) { trace_println('${@FN}: found match for (V style multiple occurences) ${fm.dbg_match(flag_ctx, field, next, '')}') fm.array_field_map_flag[field.name] << FlagData{ raw: flag_raw field_name: field.name delimiter: used_delimiter name: flag_name arg: ?string(next) pos: pos } } else { trace_println('${@FN}: found match for (V style) ${fm.dbg_match(flag_ctx, field, next, '')}') fm.field_map_flag[field.name] = FlagData{ raw: flag_raw field_name: field.name delimiter: used_delimiter name: flag_name arg: ?string(next) pos: pos } } fm.handled_pos << pos fm.handled_pos << pos + 1 // arg return true } return false } // map_v_flag_parser_short returns `true` if the V `flag.FlagParser` short (-) flag in `flag_ctx` can be mapped to `field`. // map_v_flag_parser_short adds data of the match in the internal structures for further processing if applicable fn (mut fm FlagMapper) map_v_flag_parser_short(flag_ctx FlagContext, field StructField) !bool { flag_raw := flag_ctx.raw flag_name := flag_ctx.name pos := flag_ctx.pos used_delimiter := flag_ctx.delimiter next := flag_ctx.next if flag_name.len != 1 { 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') } if flag_raw.contains('=') { return error('`=` in flag `${flag_raw}` is not supported in V `flag.FlagParser` (short) style parsing mode. Use `-f value` instead') } if field.hints.has(.is_bool) { if flag_name == field.match_name || flag_name == field.short { trace_println('${@FN}: found match for (bool) ${fm.dbg_match(flag_ctx, field, 'true', '')}') fm.field_map_flag[field.name] = FlagData{ raw: flag_raw field_name: field.name delimiter: used_delimiter name: flag_name pos: pos } fm.handled_pos << pos return true } } if flag_name == field.match_name || flag_name == field.short { if field.hints.has(.is_array) { trace_println('${@FN}: found match for V (`flag.FlagParser` (short) style multiple occurences) ${fm.dbg_match(flag_ctx, field, next, '')}') fm.array_field_map_flag[field.name] << FlagData{ raw: flag_raw field_name: field.name delimiter: used_delimiter name: flag_name arg: ?string(next) pos: pos } } else { trace_println('${@FN}: found match for V (`flag.FlagParser` (short) style) ${fm.dbg_match(flag_ctx, field, next, '')}') fm.field_map_flag[field.name] = FlagData{ raw: flag_raw field_name: field.name delimiter: used_delimiter name: flag_name arg: ?string(next) pos: pos } } fm.handled_pos << pos fm.handled_pos << pos + 1 // arg return true } return false } // map_v_flag_parser_long returns `true` if the V `flag.FlagParser` long (--) style flag in `flag_ctx` can be mapped to `field`. // map_v_flag_parser_long adds data of the match in the internal structures for further processing if applicable fn (mut fm FlagMapper) map_v_flag_parser_long(flag_ctx FlagContext, field StructField) !bool { flag_raw := flag_ctx.raw flag_name := flag_ctx.name pos := flag_ctx.pos used_delimiter := flag_ctx.delimiter next := flag_ctx.next if flag_raw.contains('=') { return error('`=` in flag `${flag_raw}` is not supported in V `flag.FlagParser` (long) style parsing mode. Use `--flag value` instead') } if field.hints.has(.is_bool) { if flag_name == field.match_name { trace_println('${@FN}: found match for (bool) ${fm.dbg_match(flag_ctx, field, 'true', '')}') fm.field_map_flag[field.name] = FlagData{ raw: flag_raw field_name: field.name delimiter: used_delimiter name: flag_name pos: pos } fm.handled_pos << pos return true } } if flag_name == field.match_name || flag_name == field.short { if field.hints.has(.is_array) { trace_println('${@FN}: found match for (V `flag.FlagParser` (long) style multiple occurences) ${fm.dbg_match(flag_ctx, field, next, '')}') fm.array_field_map_flag[field.name] << FlagData{ raw: flag_raw field_name: field.name delimiter: used_delimiter name: flag_name arg: ?string(next) pos: pos } } else { trace_println('${@FN}: found match for V (`flag.FlagParser` (long) style) ${fm.dbg_match(flag_ctx, field, next, '')}') fm.field_map_flag[field.name] = FlagData{ raw: flag_raw field_name: field.name delimiter: used_delimiter name: flag_name arg: ?string(next) pos: pos } } fm.handled_pos << pos fm.handled_pos << pos + 1 // arg return true } return false } // map_go_flag_short returns `true` if the GO short style flag in `flag_ctx` can be mapped to `field`. // map_go_flag_short adds data of the match in the internal structures for further processing if applicable fn (mut fm FlagMapper) map_go_flag_short(flag_ctx FlagContext, field StructField) !bool { flag_raw := flag_ctx.raw flag_name := flag_ctx.name pos := flag_ctx.pos used_delimiter := flag_ctx.delimiter next := flag_ctx.next if field.hints.has(.is_bool) { if flag_name == field.match_name { arg := if flag_raw.contains('=') { flag_raw.all_after('=') } else { '' } if arg != '' { return error('flag `${flag_raw}` can not be assigned to bool field "${field.name}"') } trace_println('${@FN}: found match for (bool) ${fm.dbg_match(flag_ctx, field, 'true', '')}') fm.field_map_flag[field.name] = FlagData{ raw: flag_raw field_name: field.name delimiter: used_delimiter name: flag_name pos: pos } fm.handled_pos << pos return true } } if flag_name == field.match_name || flag_name == field.short { if field.hints.has(.is_array) { trace_println('${@FN}: found match for (GO short style multiple occurences) ${fm.dbg_match(flag_ctx, field, next, '')}') fm.array_field_map_flag[field.name] << FlagData{ raw: flag_raw field_name: field.name delimiter: used_delimiter name: flag_name arg: ?string(next) pos: pos } } else { trace_println('${@FN}: found match for (GO short style) ${fm.dbg_match(flag_ctx, field, next, '')}') fm.field_map_flag[field.name] = FlagData{ raw: flag_raw field_name: field.name delimiter: used_delimiter name: flag_name arg: ?string(next) pos: pos } } fm.handled_pos << pos fm.handled_pos << pos + 1 // arg return true } return false } // map_go_flag_long returns `true` if the GO long style flag in `flag_ctx` can be mapped to `field`. // map_go_flag_long adds data of the match in the internal structures for further processing if applicable fn (mut fm FlagMapper) map_go_flag_long(flag_ctx FlagContext, field StructField) !bool { flag_raw := flag_ctx.raw flag_name := flag_ctx.name pos := flag_ctx.pos used_delimiter := flag_ctx.delimiter if flag_name == field.match_name { if field.hints.has(.is_bool) { arg := if flag_raw.contains('=') { flag_raw.all_after('=') } else { '' } if arg != '' { return error('flag `${flag_raw}` can not be assigned to bool field "${field.name}"') } trace_println('${@FN}: found match for (bool) (GO `flag` style) ${fm.dbg_match(flag_ctx, field, 'true', '')}') fm.field_map_flag[field.name] = FlagData{ raw: flag_raw field_name: field.name delimiter: used_delimiter name: flag_name pos: pos } fm.handled_pos << pos return true } if !flag_raw.contains('=') { if field.hints.has(.is_int_type) && field.hints.has(.can_repeat) { return error('field `${field.name}` has @[repeats], only POSIX short style allows repeating') } 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') } arg := if flag_raw.contains('=') { flag_raw.all_after('=') } else { '' } if field.hints.has(.is_array) { trace_println('${@FN}: found match for (GO `flag` style multiple occurences) ${fm.dbg_match(flag_ctx, field, arg, '')}') fm.array_field_map_flag[field.name] << FlagData{ raw: flag_raw field_name: field.name delimiter: used_delimiter name: flag_name arg: ?string(arg) pos: pos } } else { trace_println('${@FN}: found match for (GO `flag` style) ${fm.dbg_match(flag_ctx, field, arg, '')}') fm.field_map_flag[field.name] = FlagData{ raw: flag_raw field_name: field.name delimiter: used_delimiter name: flag_name arg: ?string(arg) pos: pos } } fm.handled_pos << pos // NOTE: arg is part of the flag in GO (GNU) long style args return true } return false } // map_gnu_long returns `true` if the GNU (long) style flag in `flag_ctx` can be mapped to `field`. // map_gnu_long adds data of the match in the internal structures for further processing if applicable fn (mut fm FlagMapper) map_gnu_long(flag_ctx FlagContext, field StructField) !bool { flag_raw := flag_ctx.raw flag_name := flag_ctx.name pos := flag_ctx.pos used_delimiter := flag_ctx.delimiter if flag_name == field.match_name { if field.hints.has(.is_bool) { arg := if flag_raw.contains('=') { flag_raw.all_after('=') } else { '' } if arg != '' { return error('flag `${flag_raw}` can not be assigned to bool field "${field.name}"') } trace_println('${@FN}: found match for (bool) ${fm.dbg_match(flag_ctx, field, 'true', '')}') fm.field_map_flag[field.name] = FlagData{ raw: flag_raw field_name: field.name delimiter: used_delimiter name: flag_name pos: pos } fm.handled_pos << pos return true } else if fm.config.style in [.long, .short_long] && !flag_raw.contains('=') { if field.hints.has(.is_int_type) && field.hints.has(.can_repeat) { return error('field `${field.name}` has @[repeats], only POSIX short style allows repeating') } 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') } arg := if flag_raw.contains('=') { flag_raw.all_after('=') } else { '' } if field.hints.has(.is_array) { trace_println('${@FN}: found match for (GNU style multiple occurences) ${fm.dbg_match(flag_ctx, field, arg, '')}') fm.array_field_map_flag[field.name] << FlagData{ raw: flag_raw field_name: field.name delimiter: used_delimiter name: flag_name arg: ?string(arg) pos: pos } } else { trace_println('${@FN}: found match for (GNU style) ${fm.dbg_match(flag_ctx, field, arg, '')}') fm.field_map_flag[field.name] = FlagData{ raw: flag_raw field_name: field.name delimiter: used_delimiter name: flag_name arg: ?string(arg) pos: pos } } fm.handled_pos << pos // NOTE: arg is part of the flag in GNU long style args return true } return false } // map_posix_short_cluster looks for multiple combined short flags and optional // tail arguments e.g. `-yxz( a)` = `-y -x -z( a)` and maps them to the respective struct fields. // map_posix_short_cluster adds data of the match in the internal structures for further processing if applicable. fn (mut fm FlagMapper) map_posix_short_cluster(flag_ctx FlagContext) ! { flag_name := flag_ctx.name if flag_name.len <= 1 { return } // Do not handle multiple `-vv`, `map_posix_short` does that if flag_name[0] == flag_name[1] { return } if flag_name.len > 1 { mut split := flag_name.split('') mut matched_fields := map[string]StructField{} for mflag in split { mut matched := false for _, field in fm.si.fields { if smatch_name := field.shortest_match_name() { if mflag == smatch_name { trace_println('${@FN}: cluster flag `${mflag}` matches field `${smatch_name}`') matched_fields[mflag] = field matched = true } } } if !matched { break } } if matched_fields.len == 0 { return } // Iterate `split` instead of `matched_fields` since the order of appearance has significance for i := 0; i < split.len; i++ { mflag := split[i] if field := matched_fields[mflag] { // trace_println('${@FN}: ${mflag} matches ${field.name}') mf := FlagData{ raw: flag_ctx.raw field_name: field.name delimiter: flag_ctx.delimiter name: mflag pos: flag_ctx.pos } if field.hints.has(.is_bool) { trace_println('${@FN}: found match for (bool) ${fm.dbg_match(flag_ctx, field, 'true', '')}') fm.field_map_flag[mf.field_name] = mf fm.handled_pos << flag_ctx.pos } else { mut arg := split[i + 1..].clone().join('') mut next_is_used := false if arg == '' { arg = flag_ctx.next if arg != '' { next_is_used = true } } if field.hints.has(.is_array) { fm.array_field_map_flag[mf.field_name] << FlagData{ ...mf arg: ?string(arg) } trace_println('${@FN}: found match for (array) ${fm.dbg_match(flag_ctx, field, arg, '')}') } else { trace_println('${@FN}: found match for (other) ${fm.dbg_match(flag_ctx, field, arg, '')}') fm.field_map_flag[mf.field_name] = FlagData{ ...mf arg: ?string(arg) } } fm.handled_pos << flag_ctx.pos if next_is_used { fm.handled_pos << flag_ctx.pos + 1 // next } break } } } } } // map_posix_short returns `true` if the POSIX (short) style flag in `flag_ctx` can be mapped to `field`. // map_posix_short adds data of the match in the internal structures for further processing if applicable // map_posix_short handles, amoung other things the mapping of repeatable short flags. E.g.: `-vvv vvv` fn (mut fm FlagMapper) map_posix_short(flag_ctx FlagContext, field StructField) !bool { flag_raw := flag_ctx.raw mut flag_name := flag_ctx.name pos := flag_ctx.pos used_delimiter := flag_ctx.delimiter mut next := flag_ctx.next struct_name := fm.si.name first_letter := flag_name.split('')[0] next_first_letter := if next != '' { next.split('')[0] } else { '' } count_of_first_letter_repeats := flag_name.count(first_letter) count_of_next_first_letter_repeats := next.count(next_first_letter) if field.hints.has(.is_bool) { if flag_name == field.match_name { arg := if flag_raw.contains('=') { flag_raw.all_after('=') } else { '' } if arg != '' { return error('flag `${flag_raw}` can not be assigned to bool field "${field.name}"') } trace_println('${@FN}: found match for (bool) ${fm.dbg_match(flag_ctx, field, 'true', '')}') fm.field_map_flag[field.name] = FlagData{ raw: flag_raw field_name: field.name delimiter: used_delimiter name: flag_name pos: pos } fm.handled_pos << pos return true } if field.short == flag_name { trace_println('${@FN}: found match for (bool) ${fm.dbg_match(flag_ctx, field, 'true', '')}') fm.field_map_flag[field.name] = FlagData{ raw: flag_raw field_name: field.name delimiter: used_delimiter name: flag_name pos: pos } fm.handled_pos << pos return true } } if first_letter == field.short { // `-vvvvv`, `-vv vvv` or `-v vvvv` if field.hints.has(.can_repeat) { mut do_continue := false if count_of_first_letter_repeats == flag_name.len { trace_println('${@FN}: found match for (repeatable) ${fm.dbg_match(flag_ctx, field, 'true', '')}') fm.field_map_flag[field.name] = FlagData{ raw: flag_raw field_name: field.name delimiter: used_delimiter name: flag_name pos: pos repeats: count_of_first_letter_repeats } fm.handled_pos << pos do_continue = true if next_first_letter == first_letter && count_of_next_first_letter_repeats == next.len { trace_println('${@FN}: field "${field.name}" allow repeats and ${flag_raw} ${next} repeats ${ count_of_next_first_letter_repeats + count_of_first_letter_repeats} times (via argument)') fm.field_map_flag[field.name] = FlagData{ raw: flag_raw field_name: field.name delimiter: used_delimiter name: flag_name pos: pos repeats: count_of_next_first_letter_repeats + count_of_first_letter_repeats } fm.handled_pos << pos fm.handled_pos << pos + 1 // next do_continue = true } else { trace_println('${@FN}: field "${field.name}" allow repeats and ${flag_raw} repeats ${count_of_first_letter_repeats} times') } if do_continue { return true } } } else if field.hints.has(.is_array) { split := flag_name.trim_string_left(field.short) mut next_is_handled := true if split != '' { next = split flag_name = flag_name.trim_string_right(split) next_is_handled = false } if next == '' { return error('flag "${flag_raw}" expects an argument') } trace_println('${@FN}: found match for (multiple occurences) ${fm.dbg_match(flag_ctx, field, next, '')}') fm.array_field_map_flag[field.name] << FlagData{ raw: flag_raw field_name: field.name delimiter: used_delimiter name: flag_name arg: ?string(next) pos: pos } fm.handled_pos << pos if next_is_handled { fm.handled_pos << pos + 1 // next } return true } } if (fm.config.style == .short || field.hints.has(.short_only)) && first_letter == field.short { split := flag_name.trim_string_left(field.short) mut next_is_handled := true if split != '' { next = split flag_name = flag_name.trim_string_right(split) next_is_handled = false } if next == '' { return error('flag "${flag_raw}" expects an argument') } trace_println('${@FN}: found match for (short only) ${struct_name}.${field.name} (${field.match_name}) = ${field.short} = ${next}') fm.field_map_flag[field.name] = FlagData{ raw: flag_raw field_name: field.name delimiter: used_delimiter name: flag_name arg: ?string(next) pos: pos repeats: count_of_first_letter_repeats } fm.handled_pos << pos if next_is_handled { fm.handled_pos << pos + 1 // next } return true } else if flag_name == field.match_name && !(field.hints.has(.short_only) && flag_name == field.short) { trace_println('${@FN}: found match for (repeats) ${fm.dbg_match(flag_ctx, field, next, '')}') if next == '' { return error('flag "${flag_raw}" expects an argument') } fm.field_map_flag[field.name] = FlagData{ raw: flag_raw field_name: field.name delimiter: used_delimiter name: flag_name arg: ?string(next) pos: pos repeats: count_of_first_letter_repeats } fm.handled_pos << pos fm.handled_pos << pos + 1 // next return true } return false } // map_cmd_exe returns `true` if the CMD.EXE style flag in `flag_ctx` can be mapped to `field`. // map_cmd_exe adds data of the match in the internal structures for further processing if applicable fn (mut fm FlagMapper) map_cmd_exe(flag_ctx FlagContext, field StructField) !bool { flag_raw := flag_ctx.raw flag_name := flag_ctx.name pos := flag_ctx.pos used_delimiter := flag_ctx.delimiter next := flag_ctx.next if flag_name == field.match_name { if field.hints.has(.is_bool) { trace_println('${@FN}: found (long) match for (bool) (CMD.EXE style) ${fm.dbg_match(flag_ctx, field, 'true', '')}') fm.field_map_flag[field.name] = FlagData{ raw: flag_raw field_name: field.name delimiter: used_delimiter name: flag_name pos: pos } fm.handled_pos << pos return true } // Not sure original CMD.EXE flags supported multiple flags with same name?? if field.hints.has(.is_array) { trace_println('${@FN}: found match for (CMD.EXE style multiple occurences) ${fm.dbg_match(flag_ctx, field, next, '')}') fm.array_field_map_flag[field.name] << FlagData{ raw: flag_raw field_name: field.name delimiter: used_delimiter name: flag_name arg: ?string(next) pos: pos } } else { trace_println('${@FN}: found match for (CMD.EXE style) ${fm.dbg_match(flag_ctx, field, next, '')}') fm.field_map_flag[field.name] = FlagData{ raw: flag_raw field_name: field.name delimiter: used_delimiter name: flag_name arg: ?string(next) pos: pos } } fm.handled_pos << pos fm.handled_pos << pos + 1 // arg return true } // Not aware of CMD.EXE flags longer than one (ASCII??) character if shortest_match_name := field.shortest_match_name() { if flag_name == shortest_match_name { if field.hints.has(.is_bool) { trace_println('${@FN}: found match for (bool) (CMD.EXE style) ${fm.dbg_match(flag_ctx, field, 'true', '')}') fm.field_map_flag[field.name] = FlagData{ raw: flag_raw field_name: field.name delimiter: used_delimiter name: flag_name pos: pos } fm.handled_pos << pos return true } // Not sure original CMD.EXE flags supported multiple flags with same name?? if field.hints.has(.is_array) { trace_println('${@FN}: found match for (CMD.EXE style multiple occurences) ${fm.dbg_match(flag_ctx, field, next, '')}') fm.array_field_map_flag[field.name] << FlagData{ raw: flag_raw field_name: field.name delimiter: used_delimiter name: flag_name arg: ?string(next) pos: pos } } else { trace_println('${@FN}: found match for (CMD.EXE style) ${fm.dbg_match(flag_ctx, field, next, '')}') fm.field_map_flag[field.name] = FlagData{ raw: flag_raw field_name: field.name delimiter: used_delimiter name: flag_name arg: ?string(next) pos: pos } } fm.handled_pos << pos fm.handled_pos << pos + 1 // arg return true } } return false }