error "punk::args::resolve - bad optionspecs line - unable to parse first word of record '$trimrec' id:$DEF_definition_id"
}
}
set record_values [lassign $trimrec firstword] ;#after first word, the remaining list elements up to the first newline that isn't inside a value, form a dict
#set record_values [lassign $trimrec firstword]
if {[llength $record_values] % 2 != 0} {
if {[llength $record_values] % 2 != 0} {
#todo - avoid raising an error - store invalid defs keyed on id
#todo - avoid raising an error - store invalid defs keyed on id
error "punk::args::resolve - bad optionspecs line for record '$firstword' Remaining items on line must be in paired option-value format - received '$record_values' id:$DEF_definition_id"
error "punk::args::resolve - bad optionspecs line for record '$firstword' Remaining items on line must be in paired option-value format - received '$record_values' id:$DEF_definition_id"
#OPTSPEC_DEFAULTS are the base defaults for options - these can be overridden by @opts lines
#we may still need to test some of these defaults for validity, e.g -mash true can only apply if the argname has at least one single-character alias (e.g -x or -x|--xxx)
set spec_merged [dict get $F $fid OPTSPEC_DEFAULTS]
set spec_merged [dict get $F $fid OPTSPEC_DEFAULTS]
#allow when any alt in argname is a single letter flag such s -a or -Z
#single letter flags do not have to be -type none to allow -mash to be set true.
#a mash can be supplied where the last flag in the mash is a value-taking flag.
if {$specval} {
set has_single_letter_flag 0
foreach alias $optaliases {
if {[string length $alias] == 2 && [string match -* $alias]} {
set has_single_letter_flag 1
break
}
}
if {!$has_single_letter_flag} {
error "punk::args::resolve - invalid use of -mash for argument '$argname'. -mash can only be true if at least one alias in the argname is a single-letter flag (e.g -a or -Z) @id:$DEF_definition_id"
#todo - we also have to set -mash false when processing defaults from @opts if the argname doesn't contain any single-letter flags
if {[tcl::dict::get $spec_merged -type] eq "none"} {
if {[tcl::dict::get $spec_merged -type] eq "none"} {
dict set F $fid OPT_SOLOS [list {*}[dict get $F $fid OPT_SOLOS] $argname]
dict set F $fid OPT_SOLOS [list {*}[dict get $F $fid OPT_SOLOS] $argname]
}
}
if {[tcl::dict::get $spec_merged -mash]} {
#The value for -mash might be true only due to a default from @opts - in which case we need to check the argname for validity of -mash as described above and if not valid, set -mash false in the ARG_INFO for this argname
if {$argname eq "--"} {
#force -mash false - in case no -mash was specified on the flag itself and @opts -mash is true
tcl::dict::set spec_merged -mash false
} else {
set has_single_letter_flag 0
foreach alias $optaliases {
if {[string length $alias] == 2 && [string match -* $alias]} {
set has_single_letter_flag 1
break
}
}
if {!$has_single_letter_flag} {
#force -mash false in ARG_INFO for this argname - in case no -mash was specified and @opts -mash is true by default but argname doesn't contain any single-letter flags
tcl::dict::set spec_merged -mash false
}
}
#re-test state of -mash after any adjustments based on argname validity and defaults
if {[tcl::dict::get $spec_merged -mash]} {
#we add the whole argname with all aliases to the OPT_MASHES list - this is used during parsing to check if any of the aliases for a given flag are mashable
dict set F $fid OPT_MASHES [list {*}[dict get $F $fid OPT_MASHES] $argname]
}
}
} else {
} else {
tcl::dict::set F $fid ARG_CHECKS $argname\
tcl::dict::set F $fid ARG_CHECKS $argname\
[tcl::dict::remove $spec_merged -form -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi -choicecolumns -group -typesynopsis -help -ARGTYPE] ;#leave things like -range -minsize
[tcl::dict::remove $spec_merged -form -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi -choicecolumns -group -typesynopsis -help -ARGTYPE] ;#leave things like -range -minsize
set trie [punk::trie::trieclass new {*}$all_opts --]
set trie [punk::trie::trieclass new {*}$all_opts --]
set idents [dict get [$trie shortest_idents ""] scanned]
set idents [dict get [$trie shortest_idents ""] scanned]
if {[llength [dict get $form_dict OPT_MASHES]]} {
set all_mash_letters [dict get $form_dict OPT_ALL_MASH_LETTERS]
#now extend idents to be at least as long as the number of mash/bundle flags that exist.
#(when the flag itself is longer than number of mash flags
# - e.g for flags -x -v -c -f -collection, the ident for -collection would be -co normally
# but if we have 4 mash flags, we want it to be -colle to satisfy the requirement that it is longer then the number of mash flags
# unless it is an exact match.)
#
#e.g if all the single letter flags are configured with -mash true:
#our prefix calculation might give us the following idents:
# idents: -cabinet -ca -a -a -b -b -c -c -- --
#we need only to extend -cabinet to -cabi to satisfy the requirement that it is longer than the number of mash flags (3 in this example because -- is never a mash flag)
#note all_opts will necessarily not include mashed flags (e.g -abc) when only -a -b -c are defined - but we will detect and break those down in the main loop below
#flagsupplied when --longopt=x is --longopt (may still be a prefix)
#flagsupplied when --longopt=x is --longopt (may still be a prefix)
#get full flagname from possible prefix $flagsupplied
#get full flagname from possible prefix $flagsupplied
set flagname [tcl::prefix match -error "" [list {*}$all_opts --] $flagsupplied]
set flagname [tcl::prefix match -error "" [list {*}$all_opts --] $flagsupplied]
if {$flagname eq "--"} {
#The prefix matching above doesn't consider that mashed flags can make shorter prefixes an invalid match for the whole flag.
#if the length of our matched flagname is less than the length of $OPT_ALL_MASH_LETTERS, then we may have a mash of other flags,
#not a valid match for some longer flag that just happens to share the same prefix as the start of the mash.
#we have defined valid prefix matches in the presence of mashed flags to be only those that are longer than any possible mash of flags
#(review - for small numbers of mashed flags we could be more precise, but the combinatoric explosion of longer mash lengths makes it
#simpler to just say any match that is shorter than the length of the longest possible mash is invalid
# we may need consider what common utilities do in practice regarding allowing prefixes in the presence of mashed flags
#- but it seems likely that they would either not allow prefixes at all, or only allow prefixes that are longer than any possible mash of flags)
#So if we have a match that isn't exact and is shorter than the length of the longest possible mash, we need to check if it's actually a mash of valid flags rather than a valid prefix match for a longer flag.
if {$flagname ne $flagsupplied && [llength $OPT_MASHES] && (([string length $flagsupplied] -1) <= [llength $OPT_ALL_MASH_LETTERS])} {
#invalidate the match
set flagname ""
}
switch -- $flagname {
-- {
set optionset ""
}
"" {
#no match for flagname - could be a mashed flag e.g -abc where only -a -b -c are defined
if {![llength $OPT_MASHES]} {
#no mashed flags defined - so this probably isn't a flag - could be a value
set optionset ""
} else {
#check if every letter after the first matches a defined opt - if so treat as mashed flags
set mashflags [string range $flagsupplied 1 end]
set mashletters [split $mashflags ""]
set all_mashable true
foreach mf $mashletters {
if {$mf ni $OPT_ALL_MASH_LETTERS} {
set all_mashable false
break
}
}
#todo - move block below up here.
if {!$all_mashable} {
puts stderr "Debug: flagsupplied '$flagsupplied' not a valid flagname and not a valid mash of flags - treating as value"
#- probably isn't a flag at all - could be a value
#treat as value
set optionset ""
set optionset ""
} else {
} else {
#puts stderr "Debug: flagsupplied '$flagsupplied' not a valid flagname but is a valid mash of flags - treating as mash of flags"
#treat as mashed flags - we will break down into individual flags and process each one in turn
set optionset $flagsupplied
#the -mash option means we may have to process multiple flags as received for one arg that looks like a flag
#we can still use the lookup_optset dict to get the optionset for each individual flag - as the keys of lookup_optset are all the individual flags (not mashed together)
#we need to update:
# vals_remaining_possible after processing all matchletters (by -1 or -2 depending on whether the mash includes a flag with an attached value (trailing=<val>) or accepts a value.)
# multisreceived
# soloreceived (if any of the flags in the mash are solo)
# flagsreceived (add the mash as received - but also add each individual flag in the mash as received for the purposes of checking for multiple and solo)
# opts (for each flag in the mash)
set posn 0
set consume_value 0 ;#if last mash flag accepts a value, we will consume the next arg as its value
foreach mf $mashletters {
set matchopt [dict get $lookup_optset -$mf]
if {$matchopt eq ""} {
#this should not happen as we have already checked all letters are mashable - but check just in case
puts stderr "Debug: mash letter '-$mf' not in lookup_optset - this should not happen"
} else {
#process each mashed flag as if it were received separately
#- we can reuse the same flagval for each as they won't be expected to have values (as they are single letter flags)
#we will still need to check for multiple and defaults for each individual flag
#we can also still use the same argstate entries for each individual flag as the optionset will be the same for each of the mashed flags (as they will all be defined in the same optionset e.g -a|-b|-c)
set mashflagname -$mf
set mashflagoptionset [dict get $lookup_optset $mashflagname]
set raw_optionset_members [split $mashflagoptionset |]
#set mashflagapiopt [dict get $argstate $mashflagoptionset -parsekey]
if {[tcl::dict::get $argstate $mashflagoptionset -parsekey] ne ""} {
set api_opt [dict get $argstate $mashflagoptionset -parsekey]
} else {
set api_opt [string trimright [lindex $raw_optionset_members end] =]
}
if {$api_opt eq $flagname} {
set flag_ident $api_opt
set flag_ident_is_parsekey 0
} else {
#initially key our opts on a long form allowing us to know which specific flag was used
#(for when multiple map to same parsekey e.g lsearch)
#e.g -increasing|-SORTOPTION
set flag_ident $flagname|$api_opt
set flag_ident_is_parsekey 1
}
set optionset_type [tcl::dict::get $argstate $mashflagoptionset -type]
#only the last flag in a mash can be allowed to have a value, and the other flags must be of type none.
#flags are by default optional.
if {$optionset_type ne "none"} {
#A flag with a value - only allowed for the last flag in a mash
if {$posn != [expr {[llength $mashletters] - 1}]} {
#not the last flag in the mash - can't have a value
set errmsg "bad options for %caller%. Flag \"$mashflagname\" in mash \"$flagsupplied\" cannot have a value as only the last flag in a mash can have a value. The flag \"$mashflagname\" must be of type none. (1)"
#flag with no value - check for -typedefaults for the flag
#none / solo
if {[tcl::dict::exists $argstate $mashflagoptionset -typedefaults]} {
set tdflt [tcl::dict::get $argstate $mashflagoptionset -typedefaults]
} else {
#normal default for a solo is 1 unless overridden by -typedefaults
set tdflt 1
}
if {[tcl::dict::get $argstate $mashflagoptionset -multiple]} {
#puts stderr "Debug: flag '$mashflagname' in mash '$flagsupplied' is a multiple with typedefaults $tdflt -- api_opt: $api_opt flag_ident: $flag_ident flagsreceived: $flagsreceived multisreceived: $multisreceived"
if {$api_opt ni $flagsreceived} {
#override any default - don't lappend to it
tcl::dict::set opts $flag_ident $tdflt
} else {
tcl::dict::lappend opts $flag_ident $tdflt
}
if {$api_opt ni $multisreceived} {
lappend multisreceived $api_opt
}
} else {
#test parse_withdef_parsekey_repeat_ordering {Ensure last flag has precedence}
#tcl::dict::set opts $flag_ident $tdflt
if {$flag_ident_is_parsekey} {
#(shimmer - but required for ordering correctness during override)
puts stderr "Debug: flag '$mashflagname' in mash '$flagsupplied' flag_ident '$flag_ident' is the same as parsekey '$api_opt' tdflt: $tdflt - using lappend to ensure it ends up after any previous flag in the mash that had the same parsekey"
lappend opts $flag_ident $tdflt
puts stderr "opts after lappend: $opts"
} else {
tcl::dict::set opts $flag_ident $tdflt
}
}
#incr vals_remaining_possible -1
lappend solosreceived $api_opt ;#dups ok
}
}
lappend flagsreceived $api_opt
incr posn
}
#update vals_remaining_possible by one or 2 if the last flag took a value.
incr vals_remaining_possible -1
if {$flagval_included || $consume_value} {
incr vals_remaining_possible -1
}
#after processing the mash, we will have updated opts for each individual flag in the mash,
#and updated multisreceived and solo_received as needed based on the optionset entries for each individual flag in the mash
#we possibly need to incr i to skip a received value for the mash if the last flag in the mash had a value.
#or break if we have reached the end of the args after processing the mash
if {$flagval_included || $consume_value} {
#the last flag in the mash had a value - we have already processed it for that flag - so we need to skip it for the next iteration of the loop
incr i
if {$i > $maxidx} {
#we have reached the end of the args after processing the mash and its value - so we can break out of the loop
break
}
} else {
#no value included for the last flag in the mash - so we just continue to the next iteration of the loop to process the next arg
}
continue
}
}
}
default {
if {[dict exists $lookup_optset $flagname]} {
if {[dict exists $lookup_optset $flagname]} {
set optionset [dict get $lookup_optset $flagname]
set optionset [dict get $lookup_optset $flagname]
} else {
} else {
#we matched a prefix of all_opts - but it's not in the lookup_optset?
#review - this should not happen as we only match prefixes from all_opts which is derived from the keys of lookup_optset
puts stderr "Debug: matched prefix '$flagname' not in lookup_optset - this should not happen"
#(at least some "unsupported" test- commands don't provide a Usage line at all - e.g fossil help test-http)
foreach ln $basic_opt_lines {
set ln [string trim $ln]
if {$ln eq ""} {
continue
}
#the truncated description lines aren't useful here - but are always separated from the option info by more than one space.
set colbreak [string first " " $ln] ;#first occurrence of 2 spaces in a row - which is the separator between option info and description in fossil help output
set optinfo [string range $ln 0 $colbreak-1]
#this isn't the full help info for the option - but it's what we have available in the output of 'fossil help subcmd -o' - which is more concise and easier to parse than the full help for each option.
#todo - call fossil help <subcmd> and retrieve full help for each option.
set temphelp [string range $ln $colbreak end]
set opthelp [string trim $temphelp]
#we expect either one or two parts.
lassign $optinfo namepart typepart
#e.g --case-sensitive BOOL
#e.g -v|--verbose
#e.g -ci|--checkin VERSION (convert to -ci|--checkin=|--checkin -type VERSION)
if {$typepart ne ""} {
set optnames [split $namepart "|"]
#rebuild optnames as punkoptiondef string retaining dashes and pipes but adding in additional forms for longopts - e.g -ci|--checkin becomes -ci|--checkin=|--checkin
set punknames [list]
foreach n $optnames {
if {[string match --* $n]} {
#set n [list $n [string range $n 2 end]= [string range $n 2 end]]
lappend punknames $n ${n}=
} elseif {[string match -* $n]} {
lappend punknames $n
} else {
error "Unexpected option format: $n"
}
}
set typepart "-type $typepart"
} else {
#use as is if the flag doesn't have an argument - e.g -v|--verbose
error "punk::args::resolve - bad optionspecs line - unable to parse first word of record '$trimrec' id:$DEF_definition_id"
}
}
set record_values [lassign $trimrec firstword] ;#after first word, the remaining list elements up to the first newline that isn't inside a value, form a dict
#set record_values [lassign $trimrec firstword]
if {[llength $record_values] % 2 != 0} {
if {[llength $record_values] % 2 != 0} {
#todo - avoid raising an error - store invalid defs keyed on id
#todo - avoid raising an error - store invalid defs keyed on id
error "punk::args::resolve - bad optionspecs line for record '$firstword' Remaining items on line must be in paired option-value format - received '$record_values' id:$DEF_definition_id"
error "punk::args::resolve - bad optionspecs line for record '$firstword' Remaining items on line must be in paired option-value format - received '$record_values' id:$DEF_definition_id"
#OPTSPEC_DEFAULTS are the base defaults for options - these can be overridden by @opts lines
#we may still need to test some of these defaults for validity, e.g -mash true can only apply if the argname has at least one single-character alias (e.g -x or -x|--xxx)
set spec_merged [dict get $F $fid OPTSPEC_DEFAULTS]
set spec_merged [dict get $F $fid OPTSPEC_DEFAULTS]
#allow when any alt in argname is a single letter flag such s -a or -Z
#single letter flags do not have to be -type none to allow -mash to be set true.
#a mash can be supplied where the last flag in the mash is a value-taking flag.
if {$specval} {
set has_single_letter_flag 0
foreach alias $optaliases {
if {[string length $alias] == 2 && [string match -* $alias]} {
set has_single_letter_flag 1
break
}
}
if {!$has_single_letter_flag} {
error "punk::args::resolve - invalid use of -mash for argument '$argname'. -mash can only be true if at least one alias in the argname is a single-letter flag (e.g -a or -Z) @id:$DEF_definition_id"
#todo - we also have to set -mash false when processing defaults from @opts if the argname doesn't contain any single-letter flags
if {[tcl::dict::get $spec_merged -type] eq "none"} {
if {[tcl::dict::get $spec_merged -type] eq "none"} {
dict set F $fid OPT_SOLOS [list {*}[dict get $F $fid OPT_SOLOS] $argname]
dict set F $fid OPT_SOLOS [list {*}[dict get $F $fid OPT_SOLOS] $argname]
}
}
if {[tcl::dict::get $spec_merged -mash]} {
#The value for -mash might be true only due to a default from @opts - in which case we need to check the argname for validity of -mash as described above and if not valid, set -mash false in the ARG_INFO for this argname
if {$argname eq "--"} {
#force -mash false - in case no -mash was specified on the flag itself and @opts -mash is true
tcl::dict::set spec_merged -mash false
} else {
set has_single_letter_flag 0
foreach alias $optaliases {
if {[string length $alias] == 2 && [string match -* $alias]} {
set has_single_letter_flag 1
break
}
}
if {!$has_single_letter_flag} {
#force -mash false in ARG_INFO for this argname - in case no -mash was specified and @opts -mash is true by default but argname doesn't contain any single-letter flags
tcl::dict::set spec_merged -mash false
}
}
#re-test state of -mash after any adjustments based on argname validity and defaults
if {[tcl::dict::get $spec_merged -mash]} {
#we add the whole argname with all aliases to the OPT_MASHES list - this is used during parsing to check if any of the aliases for a given flag are mashable
dict set F $fid OPT_MASHES [list {*}[dict get $F $fid OPT_MASHES] $argname]
}
}
} else {
} else {
tcl::dict::set F $fid ARG_CHECKS $argname\
tcl::dict::set F $fid ARG_CHECKS $argname\
[tcl::dict::remove $spec_merged -form -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi -choicecolumns -group -typesynopsis -help -ARGTYPE] ;#leave things like -range -minsize
[tcl::dict::remove $spec_merged -form -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi -choicecolumns -group -typesynopsis -help -ARGTYPE] ;#leave things like -range -minsize
set trie [punk::trie::trieclass new {*}$all_opts --]
set trie [punk::trie::trieclass new {*}$all_opts --]
set idents [dict get [$trie shortest_idents ""] scanned]
set idents [dict get [$trie shortest_idents ""] scanned]
if {[llength [dict get $form_dict OPT_MASHES]]} {
set all_mash_letters [dict get $form_dict OPT_ALL_MASH_LETTERS]
#now extend idents to be at least as long as the number of mash/bundle flags that exist.
#(when the flag itself is longer than number of mash flags
# - e.g for flags -x -v -c -f -collection, the ident for -collection would be -co normally
# but if we have 4 mash flags, we want it to be -colle to satisfy the requirement that it is longer then the number of mash flags
# unless it is an exact match.)
#
#e.g if all the single letter flags are configured with -mash true:
#our prefix calculation might give us the following idents:
# idents: -cabinet -ca -a -a -b -b -c -c -- --
#we need only to extend -cabinet to -cabi to satisfy the requirement that it is longer than the number of mash flags (3 in this example because -- is never a mash flag)
#note all_opts will necessarily not include mashed flags (e.g -abc) when only -a -b -c are defined - but we will detect and break those down in the main loop below
#flagsupplied when --longopt=x is --longopt (may still be a prefix)
#flagsupplied when --longopt=x is --longopt (may still be a prefix)
#get full flagname from possible prefix $flagsupplied
#get full flagname from possible prefix $flagsupplied
set flagname [tcl::prefix match -error "" [list {*}$all_opts --] $flagsupplied]
set flagname [tcl::prefix match -error "" [list {*}$all_opts --] $flagsupplied]
if {$flagname eq "--"} {
#The prefix matching above doesn't consider that mashed flags can make shorter prefixes an invalid match for the whole flag.
#if the length of our matched flagname is less than the length of $OPT_ALL_MASH_LETTERS, then we may have a mash of other flags,
#not a valid match for some longer flag that just happens to share the same prefix as the start of the mash.
#we have defined valid prefix matches in the presence of mashed flags to be only those that are longer than any possible mash of flags
#(review - for small numbers of mashed flags we could be more precise, but the combinatoric explosion of longer mash lengths makes it
#simpler to just say any match that is shorter than the length of the longest possible mash is invalid
# we may need consider what common utilities do in practice regarding allowing prefixes in the presence of mashed flags
#- but it seems likely that they would either not allow prefixes at all, or only allow prefixes that are longer than any possible mash of flags)
#So if we have a match that isn't exact and is shorter than the length of the longest possible mash, we need to check if it's actually a mash of valid flags rather than a valid prefix match for a longer flag.
if {$flagname ne $flagsupplied && [llength $OPT_MASHES] && (([string length $flagsupplied] -1) <= [llength $OPT_ALL_MASH_LETTERS])} {
#invalidate the match
set flagname ""
}
switch -- $flagname {
-- {
set optionset ""
}
"" {
#no match for flagname - could be a mashed flag e.g -abc where only -a -b -c are defined
if {![llength $OPT_MASHES]} {
#no mashed flags defined - so this probably isn't a flag - could be a value
set optionset ""
} else {
#check if every letter after the first matches a defined opt - if so treat as mashed flags
set mashflags [string range $flagsupplied 1 end]
set mashletters [split $mashflags ""]
set all_mashable true
foreach mf $mashletters {
if {$mf ni $OPT_ALL_MASH_LETTERS} {
set all_mashable false
break
}
}
#todo - move block below up here.
if {!$all_mashable} {
puts stderr "Debug: flagsupplied '$flagsupplied' not a valid flagname and not a valid mash of flags - treating as value"
#- probably isn't a flag at all - could be a value
#treat as value
set optionset ""
set optionset ""
} else {
} else {
#puts stderr "Debug: flagsupplied '$flagsupplied' not a valid flagname but is a valid mash of flags - treating as mash of flags"
#treat as mashed flags - we will break down into individual flags and process each one in turn
set optionset $flagsupplied
#the -mash option means we may have to process multiple flags as received for one arg that looks like a flag
#we can still use the lookup_optset dict to get the optionset for each individual flag - as the keys of lookup_optset are all the individual flags (not mashed together)
#we need to update:
# vals_remaining_possible after processing all matchletters (by -1 or -2 depending on whether the mash includes a flag with an attached value (trailing=<val>) or accepts a value.)
# multisreceived
# soloreceived (if any of the flags in the mash are solo)
# flagsreceived (add the mash as received - but also add each individual flag in the mash as received for the purposes of checking for multiple and solo)
# opts (for each flag in the mash)
set posn 0
set consume_value 0 ;#if last mash flag accepts a value, we will consume the next arg as its value
foreach mf $mashletters {
set matchopt [dict get $lookup_optset -$mf]
if {$matchopt eq ""} {
#this should not happen as we have already checked all letters are mashable - but check just in case
puts stderr "Debug: mash letter '-$mf' not in lookup_optset - this should not happen"
} else {
#process each mashed flag as if it were received separately
#- we can reuse the same flagval for each as they won't be expected to have values (as they are single letter flags)
#we will still need to check for multiple and defaults for each individual flag
#we can also still use the same argstate entries for each individual flag as the optionset will be the same for each of the mashed flags (as they will all be defined in the same optionset e.g -a|-b|-c)
set mashflagname -$mf
set mashflagoptionset [dict get $lookup_optset $mashflagname]
set raw_optionset_members [split $mashflagoptionset |]
#set mashflagapiopt [dict get $argstate $mashflagoptionset -parsekey]
if {[tcl::dict::get $argstate $mashflagoptionset -parsekey] ne ""} {
set api_opt [dict get $argstate $mashflagoptionset -parsekey]
} else {
set api_opt [string trimright [lindex $raw_optionset_members end] =]
}
if {$api_opt eq $flagname} {
set flag_ident $api_opt
set flag_ident_is_parsekey 0
} else {
#initially key our opts on a long form allowing us to know which specific flag was used
#(for when multiple map to same parsekey e.g lsearch)
#e.g -increasing|-SORTOPTION
set flag_ident $flagname|$api_opt
set flag_ident_is_parsekey 1
}
set optionset_type [tcl::dict::get $argstate $mashflagoptionset -type]
#only the last flag in a mash can be allowed to have a value, and the other flags must be of type none.
#flags are by default optional.
if {$optionset_type ne "none"} {
#A flag with a value - only allowed for the last flag in a mash
if {$posn != [expr {[llength $mashletters] - 1}]} {
#not the last flag in the mash - can't have a value
set errmsg "bad options for %caller%. Flag \"$mashflagname\" in mash \"$flagsupplied\" cannot have a value as only the last flag in a mash can have a value. The flag \"$mashflagname\" must be of type none. (1)"
#flag with no value - check for -typedefaults for the flag
#none / solo
if {[tcl::dict::exists $argstate $mashflagoptionset -typedefaults]} {
set tdflt [tcl::dict::get $argstate $mashflagoptionset -typedefaults]
} else {
#normal default for a solo is 1 unless overridden by -typedefaults
set tdflt 1
}
if {[tcl::dict::get $argstate $mashflagoptionset -multiple]} {
#puts stderr "Debug: flag '$mashflagname' in mash '$flagsupplied' is a multiple with typedefaults $tdflt -- api_opt: $api_opt flag_ident: $flag_ident flagsreceived: $flagsreceived multisreceived: $multisreceived"
if {$api_opt ni $flagsreceived} {
#override any default - don't lappend to it
tcl::dict::set opts $flag_ident $tdflt
} else {
tcl::dict::lappend opts $flag_ident $tdflt
}
if {$api_opt ni $multisreceived} {
lappend multisreceived $api_opt
}
} else {
#test parse_withdef_parsekey_repeat_ordering {Ensure last flag has precedence}
#tcl::dict::set opts $flag_ident $tdflt
if {$flag_ident_is_parsekey} {
#(shimmer - but required for ordering correctness during override)
puts stderr "Debug: flag '$mashflagname' in mash '$flagsupplied' flag_ident '$flag_ident' is the same as parsekey '$api_opt' tdflt: $tdflt - using lappend to ensure it ends up after any previous flag in the mash that had the same parsekey"
lappend opts $flag_ident $tdflt
puts stderr "opts after lappend: $opts"
} else {
tcl::dict::set opts $flag_ident $tdflt
}
}
#incr vals_remaining_possible -1
lappend solosreceived $api_opt ;#dups ok
}
}
lappend flagsreceived $api_opt
incr posn
}
#update vals_remaining_possible by one or 2 if the last flag took a value.
incr vals_remaining_possible -1
if {$flagval_included || $consume_value} {
incr vals_remaining_possible -1
}
#after processing the mash, we will have updated opts for each individual flag in the mash,
#and updated multisreceived and solo_received as needed based on the optionset entries for each individual flag in the mash
#we possibly need to incr i to skip a received value for the mash if the last flag in the mash had a value.
#or break if we have reached the end of the args after processing the mash
if {$flagval_included || $consume_value} {
#the last flag in the mash had a value - we have already processed it for that flag - so we need to skip it for the next iteration of the loop
incr i
if {$i > $maxidx} {
#we have reached the end of the args after processing the mash and its value - so we can break out of the loop
break
}
} else {
#no value included for the last flag in the mash - so we just continue to the next iteration of the loop to process the next arg
}
continue
}
}
}
default {
if {[dict exists $lookup_optset $flagname]} {
if {[dict exists $lookup_optset $flagname]} {
set optionset [dict get $lookup_optset $flagname]
set optionset [dict get $lookup_optset $flagname]
} else {
} else {
#we matched a prefix of all_opts - but it's not in the lookup_optset?
#review - this should not happen as we only match prefixes from all_opts which is derived from the keys of lookup_optset
puts stderr "Debug: matched prefix '$flagname' not in lookup_optset - this should not happen"
#(at least some "unsupported" test- commands don't provide a Usage line at all - e.g fossil help test-http)
foreach ln $basic_opt_lines {
set ln [string trim $ln]
if {$ln eq ""} {
continue
}
#the truncated description lines aren't useful here - but are always separated from the option info by more than one space.
set colbreak [string first " " $ln] ;#first occurrence of 2 spaces in a row - which is the separator between option info and description in fossil help output
set optinfo [string range $ln 0 $colbreak-1]
#this isn't the full help info for the option - but it's what we have available in the output of 'fossil help subcmd -o' - which is more concise and easier to parse than the full help for each option.
#todo - call fossil help <subcmd> and retrieve full help for each option.
set temphelp [string range $ln $colbreak end]
set opthelp [string trim $temphelp]
#we expect either one or two parts.
lassign $optinfo namepart typepart
#e.g --case-sensitive BOOL
#e.g -v|--verbose
#e.g -ci|--checkin VERSION (convert to -ci|--checkin=|--checkin -type VERSION)
if {$typepart ne ""} {
set optnames [split $namepart "|"]
#rebuild optnames as punkoptiondef string retaining dashes and pipes but adding in additional forms for longopts - e.g -ci|--checkin becomes -ci|--checkin=|--checkin
set punknames [list]
foreach n $optnames {
if {[string match --* $n]} {
#set n [list $n [string range $n 2 end]= [string range $n 2 end]]
lappend punknames $n ${n}=
} elseif {[string match -* $n]} {
lappend punknames $n
} else {
error "Unexpected option format: $n"
}
}
set typepart "-type $typepart"
} else {
#use as is if the flag doesn't have an argument - e.g -v|--verbose
#repeating flags in mash should still work and be treated as if they were repeated separately (ie -aa should be treated as if it were -a -a)
#in this case we have not configured any of the flags to be multiple, so the second occurrence of each flag should just override the first occurrence and have no effect
#order of flags in the result should be the same as the order of flags in the definition of the optionset,
#not the order in which they were supplied in the mash - this is because we want the result to be deterministic and not depend on the order in which the user happened to combine the flags in the mash
#the actual order should be reflected in the received list.
#the received list should show the repeated -a even though it's not set for multiple.
lappend result [dict get $argd received]
}\
-cleanup {
}\
-result [list\
{-a 1 -b 1 -c 1}\
{-a 1 -b 1 -c 1}\
{-a 1 -b 1 -c 1}\
{-a 1 -b 1 -c 1}\
{-c 1 -a 1 -b 1}\
{-c 0 -a 1 -b 2 -a 3}\
]
test mashopts_default_with_multiple {Test combining of short options when -mash set as default for short flags on @opts directive and a flag is set to -multiple}\
test mashopts_default_with_typed_shortflag {Test combining of short options when -mash set as default for short flags on @opts directive and a shortopt accepts a value}\
#should error if the flag that accepts a value is not at the end of the mash, because that would be ambiguous - we would not know which flag the value belongs to
test mashopts_default_with_other_flags {Test combining of short options when -mash set as default for short flags on @opts directive plus a longer value-accepting flag and a value}\
#should error if the non-mash flag that accepts a value is supplied with a prefix shorter than the number of mash flags.
#(we don't calculate prefixes based on a possibly huge combination of mash flags, so we simply require prefixes for non-mash flags to be at least as long as the number of mash flags)
#attempting to explicitly apply -mash 1 to -cabinet should raise an error because -cabinet is not a short flag and we only allow -mash 1 to be applied to short flags.
#(default -mash 1 on @opts is different as it is automatically only propagated to short flags.)
error "punk::args::resolve - bad optionspecs line - unable to parse first word of record '$trimrec' id:$DEF_definition_id"
}
}
set record_values [lassign $trimrec firstword] ;#after first word, the remaining list elements up to the first newline that isn't inside a value, form a dict
#set record_values [lassign $trimrec firstword]
if {[llength $record_values] % 2 != 0} {
if {[llength $record_values] % 2 != 0} {
#todo - avoid raising an error - store invalid defs keyed on id
#todo - avoid raising an error - store invalid defs keyed on id
error "punk::args::resolve - bad optionspecs line for record '$firstword' Remaining items on line must be in paired option-value format - received '$record_values' id:$DEF_definition_id"
error "punk::args::resolve - bad optionspecs line for record '$firstword' Remaining items on line must be in paired option-value format - received '$record_values' id:$DEF_definition_id"
#OPTSPEC_DEFAULTS are the base defaults for options - these can be overridden by @opts lines
#we may still need to test some of these defaults for validity, e.g -mash true can only apply if the argname has at least one single-character alias (e.g -x or -x|--xxx)
set spec_merged [dict get $F $fid OPTSPEC_DEFAULTS]
set spec_merged [dict get $F $fid OPTSPEC_DEFAULTS]
#allow when any alt in argname is a single letter flag such s -a or -Z
#single letter flags do not have to be -type none to allow -mash to be set true.
#a mash can be supplied where the last flag in the mash is a value-taking flag.
if {$specval} {
set has_single_letter_flag 0
foreach alias $optaliases {
if {[string length $alias] == 2 && [string match -* $alias]} {
set has_single_letter_flag 1
break
}
}
if {!$has_single_letter_flag} {
error "punk::args::resolve - invalid use of -mash for argument '$argname'. -mash can only be true if at least one alias in the argname is a single-letter flag (e.g -a or -Z) @id:$DEF_definition_id"
#todo - we also have to set -mash false when processing defaults from @opts if the argname doesn't contain any single-letter flags
if {[tcl::dict::get $spec_merged -type] eq "none"} {
if {[tcl::dict::get $spec_merged -type] eq "none"} {
dict set F $fid OPT_SOLOS [list {*}[dict get $F $fid OPT_SOLOS] $argname]
dict set F $fid OPT_SOLOS [list {*}[dict get $F $fid OPT_SOLOS] $argname]
}
}
if {[tcl::dict::get $spec_merged -mash]} {
#The value for -mash might be true only due to a default from @opts - in which case we need to check the argname for validity of -mash as described above and if not valid, set -mash false in the ARG_INFO for this argname
if {$argname eq "--"} {
#force -mash false - in case no -mash was specified on the flag itself and @opts -mash is true
tcl::dict::set spec_merged -mash false
} else {
set has_single_letter_flag 0
foreach alias $optaliases {
if {[string length $alias] == 2 && [string match -* $alias]} {
set has_single_letter_flag 1
break
}
}
if {!$has_single_letter_flag} {
#force -mash false in ARG_INFO for this argname - in case no -mash was specified and @opts -mash is true by default but argname doesn't contain any single-letter flags
tcl::dict::set spec_merged -mash false
}
}
#re-test state of -mash after any adjustments based on argname validity and defaults
if {[tcl::dict::get $spec_merged -mash]} {
#we add the whole argname with all aliases to the OPT_MASHES list - this is used during parsing to check if any of the aliases for a given flag are mashable
dict set F $fid OPT_MASHES [list {*}[dict get $F $fid OPT_MASHES] $argname]
}
}
} else {
} else {
tcl::dict::set F $fid ARG_CHECKS $argname\
tcl::dict::set F $fid ARG_CHECKS $argname\
[tcl::dict::remove $spec_merged -form -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi -choicecolumns -group -typesynopsis -help -ARGTYPE] ;#leave things like -range -minsize
[tcl::dict::remove $spec_merged -form -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi -choicecolumns -group -typesynopsis -help -ARGTYPE] ;#leave things like -range -minsize
set trie [punk::trie::trieclass new {*}$all_opts --]
set trie [punk::trie::trieclass new {*}$all_opts --]
set idents [dict get [$trie shortest_idents ""] scanned]
set idents [dict get [$trie shortest_idents ""] scanned]
if {[llength [dict get $form_dict OPT_MASHES]]} {
set all_mash_letters [dict get $form_dict OPT_ALL_MASH_LETTERS]
#now extend idents to be at least as long as the number of mash/bundle flags that exist.
#(when the flag itself is longer than number of mash flags
# - e.g for flags -x -v -c -f -collection, the ident for -collection would be -co normally
# but if we have 4 mash flags, we want it to be -colle to satisfy the requirement that it is longer then the number of mash flags
# unless it is an exact match.)
#
#e.g if all the single letter flags are configured with -mash true:
#our prefix calculation might give us the following idents:
# idents: -cabinet -ca -a -a -b -b -c -c -- --
#we need only to extend -cabinet to -cabi to satisfy the requirement that it is longer than the number of mash flags (3 in this example because -- is never a mash flag)
#note all_opts will necessarily not include mashed flags (e.g -abc) when only -a -b -c are defined - but we will detect and break those down in the main loop below
#flagsupplied when --longopt=x is --longopt (may still be a prefix)
#flagsupplied when --longopt=x is --longopt (may still be a prefix)
#get full flagname from possible prefix $flagsupplied
#get full flagname from possible prefix $flagsupplied
set flagname [tcl::prefix match -error "" [list {*}$all_opts --] $flagsupplied]
set flagname [tcl::prefix match -error "" [list {*}$all_opts --] $flagsupplied]
if {$flagname eq "--"} {
#The prefix matching above doesn't consider that mashed flags can make shorter prefixes an invalid match for the whole flag.
#if the length of our matched flagname is less than the length of $OPT_ALL_MASH_LETTERS, then we may have a mash of other flags,
#not a valid match for some longer flag that just happens to share the same prefix as the start of the mash.
#we have defined valid prefix matches in the presence of mashed flags to be only those that are longer than any possible mash of flags
#(review - for small numbers of mashed flags we could be more precise, but the combinatoric explosion of longer mash lengths makes it
#simpler to just say any match that is shorter than the length of the longest possible mash is invalid
# we may need consider what common utilities do in practice regarding allowing prefixes in the presence of mashed flags
#- but it seems likely that they would either not allow prefixes at all, or only allow prefixes that are longer than any possible mash of flags)
#So if we have a match that isn't exact and is shorter than the length of the longest possible mash, we need to check if it's actually a mash of valid flags rather than a valid prefix match for a longer flag.
if {$flagname ne $flagsupplied && [llength $OPT_MASHES] && (([string length $flagsupplied] -1) <= [llength $OPT_ALL_MASH_LETTERS])} {
#invalidate the match
set flagname ""
}
switch -- $flagname {
-- {
set optionset ""
}
"" {
#no match for flagname - could be a mashed flag e.g -abc where only -a -b -c are defined
if {![llength $OPT_MASHES]} {
#no mashed flags defined - so this probably isn't a flag - could be a value
set optionset ""
} else {
#check if every letter after the first matches a defined opt - if so treat as mashed flags
set mashflags [string range $flagsupplied 1 end]
set mashletters [split $mashflags ""]
set all_mashable true
foreach mf $mashletters {
if {$mf ni $OPT_ALL_MASH_LETTERS} {
set all_mashable false
break
}
}
#todo - move block below up here.
if {!$all_mashable} {
puts stderr "Debug: flagsupplied '$flagsupplied' not a valid flagname and not a valid mash of flags - treating as value"
#- probably isn't a flag at all - could be a value
#treat as value
set optionset ""
set optionset ""
} else {
} else {
#puts stderr "Debug: flagsupplied '$flagsupplied' not a valid flagname but is a valid mash of flags - treating as mash of flags"
#treat as mashed flags - we will break down into individual flags and process each one in turn
set optionset $flagsupplied
#the -mash option means we may have to process multiple flags as received for one arg that looks like a flag
#we can still use the lookup_optset dict to get the optionset for each individual flag - as the keys of lookup_optset are all the individual flags (not mashed together)
#we need to update:
# vals_remaining_possible after processing all matchletters (by -1 or -2 depending on whether the mash includes a flag with an attached value (trailing=<val>) or accepts a value.)
# multisreceived
# soloreceived (if any of the flags in the mash are solo)
# flagsreceived (add the mash as received - but also add each individual flag in the mash as received for the purposes of checking for multiple and solo)
# opts (for each flag in the mash)
set posn 0
set consume_value 0 ;#if last mash flag accepts a value, we will consume the next arg as its value
foreach mf $mashletters {
set matchopt [dict get $lookup_optset -$mf]
if {$matchopt eq ""} {
#this should not happen as we have already checked all letters are mashable - but check just in case
puts stderr "Debug: mash letter '-$mf' not in lookup_optset - this should not happen"
} else {
#process each mashed flag as if it were received separately
#- we can reuse the same flagval for each as they won't be expected to have values (as they are single letter flags)
#we will still need to check for multiple and defaults for each individual flag
#we can also still use the same argstate entries for each individual flag as the optionset will be the same for each of the mashed flags (as they will all be defined in the same optionset e.g -a|-b|-c)
set mashflagname -$mf
set mashflagoptionset [dict get $lookup_optset $mashflagname]
set raw_optionset_members [split $mashflagoptionset |]
#set mashflagapiopt [dict get $argstate $mashflagoptionset -parsekey]
if {[tcl::dict::get $argstate $mashflagoptionset -parsekey] ne ""} {
set api_opt [dict get $argstate $mashflagoptionset -parsekey]
} else {
set api_opt [string trimright [lindex $raw_optionset_members end] =]
}
if {$api_opt eq $flagname} {
set flag_ident $api_opt
set flag_ident_is_parsekey 0
} else {
#initially key our opts on a long form allowing us to know which specific flag was used
#(for when multiple map to same parsekey e.g lsearch)
#e.g -increasing|-SORTOPTION
set flag_ident $flagname|$api_opt
set flag_ident_is_parsekey 1
}
set optionset_type [tcl::dict::get $argstate $mashflagoptionset -type]
#only the last flag in a mash can be allowed to have a value, and the other flags must be of type none.
#flags are by default optional.
if {$optionset_type ne "none"} {
#A flag with a value - only allowed for the last flag in a mash
if {$posn != [expr {[llength $mashletters] - 1}]} {
#not the last flag in the mash - can't have a value
set errmsg "bad options for %caller%. Flag \"$mashflagname\" in mash \"$flagsupplied\" cannot have a value as only the last flag in a mash can have a value. The flag \"$mashflagname\" must be of type none. (1)"
#flag with no value - check for -typedefaults for the flag
#none / solo
if {[tcl::dict::exists $argstate $mashflagoptionset -typedefaults]} {
set tdflt [tcl::dict::get $argstate $mashflagoptionset -typedefaults]
} else {
#normal default for a solo is 1 unless overridden by -typedefaults
set tdflt 1
}
if {[tcl::dict::get $argstate $mashflagoptionset -multiple]} {
#puts stderr "Debug: flag '$mashflagname' in mash '$flagsupplied' is a multiple with typedefaults $tdflt -- api_opt: $api_opt flag_ident: $flag_ident flagsreceived: $flagsreceived multisreceived: $multisreceived"
if {$api_opt ni $flagsreceived} {
#override any default - don't lappend to it
tcl::dict::set opts $flag_ident $tdflt
} else {
tcl::dict::lappend opts $flag_ident $tdflt
}
if {$api_opt ni $multisreceived} {
lappend multisreceived $api_opt
}
} else {
#test parse_withdef_parsekey_repeat_ordering {Ensure last flag has precedence}
#tcl::dict::set opts $flag_ident $tdflt
if {$flag_ident_is_parsekey} {
#(shimmer - but required for ordering correctness during override)
puts stderr "Debug: flag '$mashflagname' in mash '$flagsupplied' flag_ident '$flag_ident' is the same as parsekey '$api_opt' tdflt: $tdflt - using lappend to ensure it ends up after any previous flag in the mash that had the same parsekey"
lappend opts $flag_ident $tdflt
puts stderr "opts after lappend: $opts"
} else {
tcl::dict::set opts $flag_ident $tdflt
}
}
#incr vals_remaining_possible -1
lappend solosreceived $api_opt ;#dups ok
}
}
lappend flagsreceived $api_opt
incr posn
}
#update vals_remaining_possible by one or 2 if the last flag took a value.
incr vals_remaining_possible -1
if {$flagval_included || $consume_value} {
incr vals_remaining_possible -1
}
#after processing the mash, we will have updated opts for each individual flag in the mash,
#and updated multisreceived and solo_received as needed based on the optionset entries for each individual flag in the mash
#we possibly need to incr i to skip a received value for the mash if the last flag in the mash had a value.
#or break if we have reached the end of the args after processing the mash
if {$flagval_included || $consume_value} {
#the last flag in the mash had a value - we have already processed it for that flag - so we need to skip it for the next iteration of the loop
incr i
if {$i > $maxidx} {
#we have reached the end of the args after processing the mash and its value - so we can break out of the loop
break
}
} else {
#no value included for the last flag in the mash - so we just continue to the next iteration of the loop to process the next arg
}
continue
}
}
}
default {
if {[dict exists $lookup_optset $flagname]} {
if {[dict exists $lookup_optset $flagname]} {
set optionset [dict get $lookup_optset $flagname]
set optionset [dict get $lookup_optset $flagname]
} else {
} else {
#we matched a prefix of all_opts - but it's not in the lookup_optset?
#review - this should not happen as we only match prefixes from all_opts which is derived from the keys of lookup_optset
puts stderr "Debug: matched prefix '$flagname' not in lookup_optset - this should not happen"
#(at least some "unsupported" test- commands don't provide a Usage line at all - e.g fossil help test-http)
foreach ln $basic_opt_lines {
set ln [string trim $ln]
if {$ln eq ""} {
continue
}
#the truncated description lines aren't useful here - but are always separated from the option info by more than one space.
set colbreak [string first " " $ln] ;#first occurrence of 2 spaces in a row - which is the separator between option info and description in fossil help output
set optinfo [string range $ln 0 $colbreak-1]
#this isn't the full help info for the option - but it's what we have available in the output of 'fossil help subcmd -o' - which is more concise and easier to parse than the full help for each option.
#todo - call fossil help <subcmd> and retrieve full help for each option.
set temphelp [string range $ln $colbreak end]
set opthelp [string trim $temphelp]
#we expect either one or two parts.
lassign $optinfo namepart typepart
#e.g --case-sensitive BOOL
#e.g -v|--verbose
#e.g -ci|--checkin VERSION (convert to -ci|--checkin=|--checkin -type VERSION)
if {$typepart ne ""} {
set optnames [split $namepart "|"]
#rebuild optnames as punkoptiondef string retaining dashes and pipes but adding in additional forms for longopts - e.g -ci|--checkin becomes -ci|--checkin=|--checkin
set punknames [list]
foreach n $optnames {
if {[string match --* $n]} {
#set n [list $n [string range $n 2 end]= [string range $n 2 end]]
lappend punknames $n ${n}=
} elseif {[string match -* $n]} {
lappend punknames $n
} else {
error "Unexpected option format: $n"
}
}
set typepart "-type $typepart"
} else {
#use as is if the flag doesn't have an argument - e.g -v|--verbose
error "punk::args::resolve - bad optionspecs line - unable to parse first word of record '$trimrec' id:$DEF_definition_id"
}
}
set record_values [lassign $trimrec firstword] ;#after first word, the remaining list elements up to the first newline that isn't inside a value, form a dict
#set record_values [lassign $trimrec firstword]
if {[llength $record_values] % 2 != 0} {
if {[llength $record_values] % 2 != 0} {
#todo - avoid raising an error - store invalid defs keyed on id
#todo - avoid raising an error - store invalid defs keyed on id
error "punk::args::resolve - bad optionspecs line for record '$firstword' Remaining items on line must be in paired option-value format - received '$record_values' id:$DEF_definition_id"
error "punk::args::resolve - bad optionspecs line for record '$firstword' Remaining items on line must be in paired option-value format - received '$record_values' id:$DEF_definition_id"
#OPTSPEC_DEFAULTS are the base defaults for options - these can be overridden by @opts lines
#we may still need to test some of these defaults for validity, e.g -mash true can only apply if the argname has at least one single-character alias (e.g -x or -x|--xxx)
set spec_merged [dict get $F $fid OPTSPEC_DEFAULTS]
set spec_merged [dict get $F $fid OPTSPEC_DEFAULTS]
#allow when any alt in argname is a single letter flag such s -a or -Z
#single letter flags do not have to be -type none to allow -mash to be set true.
#a mash can be supplied where the last flag in the mash is a value-taking flag.
if {$specval} {
set has_single_letter_flag 0
foreach alias $optaliases {
if {[string length $alias] == 2 && [string match -* $alias]} {
set has_single_letter_flag 1
break
}
}
if {!$has_single_letter_flag} {
error "punk::args::resolve - invalid use of -mash for argument '$argname'. -mash can only be true if at least one alias in the argname is a single-letter flag (e.g -a or -Z) @id:$DEF_definition_id"
#todo - we also have to set -mash false when processing defaults from @opts if the argname doesn't contain any single-letter flags
if {[tcl::dict::get $spec_merged -type] eq "none"} {
if {[tcl::dict::get $spec_merged -type] eq "none"} {
dict set F $fid OPT_SOLOS [list {*}[dict get $F $fid OPT_SOLOS] $argname]
dict set F $fid OPT_SOLOS [list {*}[dict get $F $fid OPT_SOLOS] $argname]
}
}
if {[tcl::dict::get $spec_merged -mash]} {
#The value for -mash might be true only due to a default from @opts - in which case we need to check the argname for validity of -mash as described above and if not valid, set -mash false in the ARG_INFO for this argname
if {$argname eq "--"} {
#force -mash false - in case no -mash was specified on the flag itself and @opts -mash is true
tcl::dict::set spec_merged -mash false
} else {
set has_single_letter_flag 0
foreach alias $optaliases {
if {[string length $alias] == 2 && [string match -* $alias]} {
set has_single_letter_flag 1
break
}
}
if {!$has_single_letter_flag} {
#force -mash false in ARG_INFO for this argname - in case no -mash was specified and @opts -mash is true by default but argname doesn't contain any single-letter flags
tcl::dict::set spec_merged -mash false
}
}
#re-test state of -mash after any adjustments based on argname validity and defaults
if {[tcl::dict::get $spec_merged -mash]} {
#we add the whole argname with all aliases to the OPT_MASHES list - this is used during parsing to check if any of the aliases for a given flag are mashable
dict set F $fid OPT_MASHES [list {*}[dict get $F $fid OPT_MASHES] $argname]
}
}
} else {
} else {
tcl::dict::set F $fid ARG_CHECKS $argname\
tcl::dict::set F $fid ARG_CHECKS $argname\
[tcl::dict::remove $spec_merged -form -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi -choicecolumns -group -typesynopsis -help -ARGTYPE] ;#leave things like -range -minsize
[tcl::dict::remove $spec_merged -form -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi -choicecolumns -group -typesynopsis -help -ARGTYPE] ;#leave things like -range -minsize
set trie [punk::trie::trieclass new {*}$all_opts --]
set trie [punk::trie::trieclass new {*}$all_opts --]
set idents [dict get [$trie shortest_idents ""] scanned]
set idents [dict get [$trie shortest_idents ""] scanned]
if {[llength [dict get $form_dict OPT_MASHES]]} {
set all_mash_letters [dict get $form_dict OPT_ALL_MASH_LETTERS]
#now extend idents to be at least as long as the number of mash/bundle flags that exist.
#(when the flag itself is longer than number of mash flags
# - e.g for flags -x -v -c -f -collection, the ident for -collection would be -co normally
# but if we have 4 mash flags, we want it to be -colle to satisfy the requirement that it is longer then the number of mash flags
# unless it is an exact match.)
#
#e.g if all the single letter flags are configured with -mash true:
#our prefix calculation might give us the following idents:
# idents: -cabinet -ca -a -a -b -b -c -c -- --
#we need only to extend -cabinet to -cabi to satisfy the requirement that it is longer than the number of mash flags (3 in this example because -- is never a mash flag)
#note all_opts will necessarily not include mashed flags (e.g -abc) when only -a -b -c are defined - but we will detect and break those down in the main loop below
#flagsupplied when --longopt=x is --longopt (may still be a prefix)
#flagsupplied when --longopt=x is --longopt (may still be a prefix)
#get full flagname from possible prefix $flagsupplied
#get full flagname from possible prefix $flagsupplied
set flagname [tcl::prefix match -error "" [list {*}$all_opts --] $flagsupplied]
set flagname [tcl::prefix match -error "" [list {*}$all_opts --] $flagsupplied]
if {$flagname eq "--"} {
#The prefix matching above doesn't consider that mashed flags can make shorter prefixes an invalid match for the whole flag.
#if the length of our matched flagname is less than the length of $OPT_ALL_MASH_LETTERS, then we may have a mash of other flags,
#not a valid match for some longer flag that just happens to share the same prefix as the start of the mash.
#we have defined valid prefix matches in the presence of mashed flags to be only those that are longer than any possible mash of flags
#(review - for small numbers of mashed flags we could be more precise, but the combinatoric explosion of longer mash lengths makes it
#simpler to just say any match that is shorter than the length of the longest possible mash is invalid
# we may need consider what common utilities do in practice regarding allowing prefixes in the presence of mashed flags
#- but it seems likely that they would either not allow prefixes at all, or only allow prefixes that are longer than any possible mash of flags)
#So if we have a match that isn't exact and is shorter than the length of the longest possible mash, we need to check if it's actually a mash of valid flags rather than a valid prefix match for a longer flag.
if {$flagname ne $flagsupplied && [llength $OPT_MASHES] && (([string length $flagsupplied] -1) <= [llength $OPT_ALL_MASH_LETTERS])} {
#invalidate the match
set flagname ""
}
switch -- $flagname {
-- {
set optionset ""
}
"" {
#no match for flagname - could be a mashed flag e.g -abc where only -a -b -c are defined
if {![llength $OPT_MASHES]} {
#no mashed flags defined - so this probably isn't a flag - could be a value
set optionset ""
} else {
#check if every letter after the first matches a defined opt - if so treat as mashed flags
set mashflags [string range $flagsupplied 1 end]
set mashletters [split $mashflags ""]
set all_mashable true
foreach mf $mashletters {
if {$mf ni $OPT_ALL_MASH_LETTERS} {
set all_mashable false
break
}
}
#todo - move block below up here.
if {!$all_mashable} {
puts stderr "Debug: flagsupplied '$flagsupplied' not a valid flagname and not a valid mash of flags - treating as value"
#- probably isn't a flag at all - could be a value
#treat as value
set optionset ""
set optionset ""
} else {
} else {
#puts stderr "Debug: flagsupplied '$flagsupplied' not a valid flagname but is a valid mash of flags - treating as mash of flags"
#treat as mashed flags - we will break down into individual flags and process each one in turn
set optionset $flagsupplied
#the -mash option means we may have to process multiple flags as received for one arg that looks like a flag
#we can still use the lookup_optset dict to get the optionset for each individual flag - as the keys of lookup_optset are all the individual flags (not mashed together)
#we need to update:
# vals_remaining_possible after processing all matchletters (by -1 or -2 depending on whether the mash includes a flag with an attached value (trailing=<val>) or accepts a value.)
# multisreceived
# soloreceived (if any of the flags in the mash are solo)
# flagsreceived (add the mash as received - but also add each individual flag in the mash as received for the purposes of checking for multiple and solo)
# opts (for each flag in the mash)
set posn 0
set consume_value 0 ;#if last mash flag accepts a value, we will consume the next arg as its value
foreach mf $mashletters {
set matchopt [dict get $lookup_optset -$mf]
if {$matchopt eq ""} {
#this should not happen as we have already checked all letters are mashable - but check just in case
puts stderr "Debug: mash letter '-$mf' not in lookup_optset - this should not happen"
} else {
#process each mashed flag as if it were received separately
#- we can reuse the same flagval for each as they won't be expected to have values (as they are single letter flags)
#we will still need to check for multiple and defaults for each individual flag
#we can also still use the same argstate entries for each individual flag as the optionset will be the same for each of the mashed flags (as they will all be defined in the same optionset e.g -a|-b|-c)
set mashflagname -$mf
set mashflagoptionset [dict get $lookup_optset $mashflagname]
set raw_optionset_members [split $mashflagoptionset |]
#set mashflagapiopt [dict get $argstate $mashflagoptionset -parsekey]
if {[tcl::dict::get $argstate $mashflagoptionset -parsekey] ne ""} {
set api_opt [dict get $argstate $mashflagoptionset -parsekey]
} else {
set api_opt [string trimright [lindex $raw_optionset_members end] =]
}
if {$api_opt eq $flagname} {
set flag_ident $api_opt
set flag_ident_is_parsekey 0
} else {
#initially key our opts on a long form allowing us to know which specific flag was used
#(for when multiple map to same parsekey e.g lsearch)
#e.g -increasing|-SORTOPTION
set flag_ident $flagname|$api_opt
set flag_ident_is_parsekey 1
}
set optionset_type [tcl::dict::get $argstate $mashflagoptionset -type]
#only the last flag in a mash can be allowed to have a value, and the other flags must be of type none.
#flags are by default optional.
if {$optionset_type ne "none"} {
#A flag with a value - only allowed for the last flag in a mash
if {$posn != [expr {[llength $mashletters] - 1}]} {
#not the last flag in the mash - can't have a value
set errmsg "bad options for %caller%. Flag \"$mashflagname\" in mash \"$flagsupplied\" cannot have a value as only the last flag in a mash can have a value. The flag \"$mashflagname\" must be of type none. (1)"
#flag with no value - check for -typedefaults for the flag
#none / solo
if {[tcl::dict::exists $argstate $mashflagoptionset -typedefaults]} {
set tdflt [tcl::dict::get $argstate $mashflagoptionset -typedefaults]
} else {
#normal default for a solo is 1 unless overridden by -typedefaults
set tdflt 1
}
if {[tcl::dict::get $argstate $mashflagoptionset -multiple]} {
#puts stderr "Debug: flag '$mashflagname' in mash '$flagsupplied' is a multiple with typedefaults $tdflt -- api_opt: $api_opt flag_ident: $flag_ident flagsreceived: $flagsreceived multisreceived: $multisreceived"
if {$api_opt ni $flagsreceived} {
#override any default - don't lappend to it
tcl::dict::set opts $flag_ident $tdflt
} else {
tcl::dict::lappend opts $flag_ident $tdflt
}
if {$api_opt ni $multisreceived} {
lappend multisreceived $api_opt
}
} else {
#test parse_withdef_parsekey_repeat_ordering {Ensure last flag has precedence}
#tcl::dict::set opts $flag_ident $tdflt
if {$flag_ident_is_parsekey} {
#(shimmer - but required for ordering correctness during override)
puts stderr "Debug: flag '$mashflagname' in mash '$flagsupplied' flag_ident '$flag_ident' is the same as parsekey '$api_opt' tdflt: $tdflt - using lappend to ensure it ends up after any previous flag in the mash that had the same parsekey"
lappend opts $flag_ident $tdflt
puts stderr "opts after lappend: $opts"
} else {
tcl::dict::set opts $flag_ident $tdflt
}
}
#incr vals_remaining_possible -1
lappend solosreceived $api_opt ;#dups ok
}
}
lappend flagsreceived $api_opt
incr posn
}
#update vals_remaining_possible by one or 2 if the last flag took a value.
incr vals_remaining_possible -1
if {$flagval_included || $consume_value} {
incr vals_remaining_possible -1
}
#after processing the mash, we will have updated opts for each individual flag in the mash,
#and updated multisreceived and solo_received as needed based on the optionset entries for each individual flag in the mash
#we possibly need to incr i to skip a received value for the mash if the last flag in the mash had a value.
#or break if we have reached the end of the args after processing the mash
if {$flagval_included || $consume_value} {
#the last flag in the mash had a value - we have already processed it for that flag - so we need to skip it for the next iteration of the loop
incr i
if {$i > $maxidx} {
#we have reached the end of the args after processing the mash and its value - so we can break out of the loop
break
}
} else {
#no value included for the last flag in the mash - so we just continue to the next iteration of the loop to process the next arg
}
continue
}
}
}
default {
if {[dict exists $lookup_optset $flagname]} {
if {[dict exists $lookup_optset $flagname]} {
set optionset [dict get $lookup_optset $flagname]
set optionset [dict get $lookup_optset $flagname]
} else {
} else {
#we matched a prefix of all_opts - but it's not in the lookup_optset?
#review - this should not happen as we only match prefixes from all_opts which is derived from the keys of lookup_optset
puts stderr "Debug: matched prefix '$flagname' not in lookup_optset - this should not happen"
#(at least some "unsupported" test- commands don't provide a Usage line at all - e.g fossil help test-http)
foreach ln $basic_opt_lines {
set ln [string trim $ln]
if {$ln eq ""} {
continue
}
#the truncated description lines aren't useful here - but are always separated from the option info by more than one space.
set colbreak [string first " " $ln] ;#first occurrence of 2 spaces in a row - which is the separator between option info and description in fossil help output
set optinfo [string range $ln 0 $colbreak-1]
#this isn't the full help info for the option - but it's what we have available in the output of 'fossil help subcmd -o' - which is more concise and easier to parse than the full help for each option.
#todo - call fossil help <subcmd> and retrieve full help for each option.
set temphelp [string range $ln $colbreak end]
set opthelp [string trim $temphelp]
#we expect either one or two parts.
lassign $optinfo namepart typepart
#e.g --case-sensitive BOOL
#e.g -v|--verbose
#e.g -ci|--checkin VERSION (convert to -ci|--checkin=|--checkin -type VERSION)
if {$typepart ne ""} {
set optnames [split $namepart "|"]
#rebuild optnames as punkoptiondef string retaining dashes and pipes but adding in additional forms for longopts - e.g -ci|--checkin becomes -ci|--checkin=|--checkin
set punknames [list]
foreach n $optnames {
if {[string match --* $n]} {
#set n [list $n [string range $n 2 end]= [string range $n 2 end]]
lappend punknames $n ${n}=
} elseif {[string match -* $n]} {
lappend punknames $n
} else {
error "Unexpected option format: $n"
}
}
set typepart "-type $typepart"
} else {
#use as is if the flag doesn't have an argument - e.g -v|--verbose
error "punk::args::resolve - bad optionspecs line - unable to parse first word of record '$trimrec' id:$DEF_definition_id"
}
}
set record_values [lassign $trimrec firstword] ;#after first word, the remaining list elements up to the first newline that isn't inside a value, form a dict
#set record_values [lassign $trimrec firstword]
if {[llength $record_values] % 2 != 0} {
if {[llength $record_values] % 2 != 0} {
#todo - avoid raising an error - store invalid defs keyed on id
#todo - avoid raising an error - store invalid defs keyed on id
error "punk::args::resolve - bad optionspecs line for record '$firstword' Remaining items on line must be in paired option-value format - received '$record_values' id:$DEF_definition_id"
error "punk::args::resolve - bad optionspecs line for record '$firstword' Remaining items on line must be in paired option-value format - received '$record_values' id:$DEF_definition_id"
#OPTSPEC_DEFAULTS are the base defaults for options - these can be overridden by @opts lines
#we may still need to test some of these defaults for validity, e.g -mash true can only apply if the argname has at least one single-character alias (e.g -x or -x|--xxx)
set spec_merged [dict get $F $fid OPTSPEC_DEFAULTS]
set spec_merged [dict get $F $fid OPTSPEC_DEFAULTS]
#allow when any alt in argname is a single letter flag such s -a or -Z
#single letter flags do not have to be -type none to allow -mash to be set true.
#a mash can be supplied where the last flag in the mash is a value-taking flag.
if {$specval} {
set has_single_letter_flag 0
foreach alias $optaliases {
if {[string length $alias] == 2 && [string match -* $alias]} {
set has_single_letter_flag 1
break
}
}
if {!$has_single_letter_flag} {
error "punk::args::resolve - invalid use of -mash for argument '$argname'. -mash can only be true if at least one alias in the argname is a single-letter flag (e.g -a or -Z) @id:$DEF_definition_id"
#todo - we also have to set -mash false when processing defaults from @opts if the argname doesn't contain any single-letter flags
if {[tcl::dict::get $spec_merged -type] eq "none"} {
if {[tcl::dict::get $spec_merged -type] eq "none"} {
dict set F $fid OPT_SOLOS [list {*}[dict get $F $fid OPT_SOLOS] $argname]
dict set F $fid OPT_SOLOS [list {*}[dict get $F $fid OPT_SOLOS] $argname]
}
}
if {[tcl::dict::get $spec_merged -mash]} {
#The value for -mash might be true only due to a default from @opts - in which case we need to check the argname for validity of -mash as described above and if not valid, set -mash false in the ARG_INFO for this argname
if {$argname eq "--"} {
#force -mash false - in case no -mash was specified on the flag itself and @opts -mash is true
tcl::dict::set spec_merged -mash false
} else {
set has_single_letter_flag 0
foreach alias $optaliases {
if {[string length $alias] == 2 && [string match -* $alias]} {
set has_single_letter_flag 1
break
}
}
if {!$has_single_letter_flag} {
#force -mash false in ARG_INFO for this argname - in case no -mash was specified and @opts -mash is true by default but argname doesn't contain any single-letter flags
tcl::dict::set spec_merged -mash false
}
}
#re-test state of -mash after any adjustments based on argname validity and defaults
if {[tcl::dict::get $spec_merged -mash]} {
#we add the whole argname with all aliases to the OPT_MASHES list - this is used during parsing to check if any of the aliases for a given flag are mashable
dict set F $fid OPT_MASHES [list {*}[dict get $F $fid OPT_MASHES] $argname]
}
}
} else {
} else {
tcl::dict::set F $fid ARG_CHECKS $argname\
tcl::dict::set F $fid ARG_CHECKS $argname\
[tcl::dict::remove $spec_merged -form -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi -choicecolumns -group -typesynopsis -help -ARGTYPE] ;#leave things like -range -minsize
[tcl::dict::remove $spec_merged -form -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi -choicecolumns -group -typesynopsis -help -ARGTYPE] ;#leave things like -range -minsize
set trie [punk::trie::trieclass new {*}$all_opts --]
set trie [punk::trie::trieclass new {*}$all_opts --]
set idents [dict get [$trie shortest_idents ""] scanned]
set idents [dict get [$trie shortest_idents ""] scanned]
if {[llength [dict get $form_dict OPT_MASHES]]} {
set all_mash_letters [dict get $form_dict OPT_ALL_MASH_LETTERS]
#now extend idents to be at least as long as the number of mash/bundle flags that exist.
#(when the flag itself is longer than number of mash flags
# - e.g for flags -x -v -c -f -collection, the ident for -collection would be -co normally
# but if we have 4 mash flags, we want it to be -colle to satisfy the requirement that it is longer then the number of mash flags
# unless it is an exact match.)
#
#e.g if all the single letter flags are configured with -mash true:
#our prefix calculation might give us the following idents:
# idents: -cabinet -ca -a -a -b -b -c -c -- --
#we need only to extend -cabinet to -cabi to satisfy the requirement that it is longer than the number of mash flags (3 in this example because -- is never a mash flag)
#note all_opts will necessarily not include mashed flags (e.g -abc) when only -a -b -c are defined - but we will detect and break those down in the main loop below
#flagsupplied when --longopt=x is --longopt (may still be a prefix)
#flagsupplied when --longopt=x is --longopt (may still be a prefix)
#get full flagname from possible prefix $flagsupplied
#get full flagname from possible prefix $flagsupplied
set flagname [tcl::prefix match -error "" [list {*}$all_opts --] $flagsupplied]
set flagname [tcl::prefix match -error "" [list {*}$all_opts --] $flagsupplied]
if {$flagname eq "--"} {
#The prefix matching above doesn't consider that mashed flags can make shorter prefixes an invalid match for the whole flag.
#if the length of our matched flagname is less than the length of $OPT_ALL_MASH_LETTERS, then we may have a mash of other flags,
#not a valid match for some longer flag that just happens to share the same prefix as the start of the mash.
#we have defined valid prefix matches in the presence of mashed flags to be only those that are longer than any possible mash of flags
#(review - for small numbers of mashed flags we could be more precise, but the combinatoric explosion of longer mash lengths makes it
#simpler to just say any match that is shorter than the length of the longest possible mash is invalid
# we may need consider what common utilities do in practice regarding allowing prefixes in the presence of mashed flags
#- but it seems likely that they would either not allow prefixes at all, or only allow prefixes that are longer than any possible mash of flags)
#So if we have a match that isn't exact and is shorter than the length of the longest possible mash, we need to check if it's actually a mash of valid flags rather than a valid prefix match for a longer flag.
if {$flagname ne $flagsupplied && [llength $OPT_MASHES] && (([string length $flagsupplied] -1) <= [llength $OPT_ALL_MASH_LETTERS])} {
#invalidate the match
set flagname ""
}
switch -- $flagname {
-- {
set optionset ""
}
"" {
#no match for flagname - could be a mashed flag e.g -abc where only -a -b -c are defined
if {![llength $OPT_MASHES]} {
#no mashed flags defined - so this probably isn't a flag - could be a value
set optionset ""
} else {
#check if every letter after the first matches a defined opt - if so treat as mashed flags
set mashflags [string range $flagsupplied 1 end]
set mashletters [split $mashflags ""]
set all_mashable true
foreach mf $mashletters {
if {$mf ni $OPT_ALL_MASH_LETTERS} {
set all_mashable false
break
}
}
#todo - move block below up here.
if {!$all_mashable} {
puts stderr "Debug: flagsupplied '$flagsupplied' not a valid flagname and not a valid mash of flags - treating as value"
#- probably isn't a flag at all - could be a value
#treat as value
set optionset ""
set optionset ""
} else {
} else {
#puts stderr "Debug: flagsupplied '$flagsupplied' not a valid flagname but is a valid mash of flags - treating as mash of flags"
#treat as mashed flags - we will break down into individual flags and process each one in turn
set optionset $flagsupplied
#the -mash option means we may have to process multiple flags as received for one arg that looks like a flag
#we can still use the lookup_optset dict to get the optionset for each individual flag - as the keys of lookup_optset are all the individual flags (not mashed together)
#we need to update:
# vals_remaining_possible after processing all matchletters (by -1 or -2 depending on whether the mash includes a flag with an attached value (trailing=<val>) or accepts a value.)
# multisreceived
# soloreceived (if any of the flags in the mash are solo)
# flagsreceived (add the mash as received - but also add each individual flag in the mash as received for the purposes of checking for multiple and solo)
# opts (for each flag in the mash)
set posn 0
set consume_value 0 ;#if last mash flag accepts a value, we will consume the next arg as its value
foreach mf $mashletters {
set matchopt [dict get $lookup_optset -$mf]
if {$matchopt eq ""} {
#this should not happen as we have already checked all letters are mashable - but check just in case
puts stderr "Debug: mash letter '-$mf' not in lookup_optset - this should not happen"
} else {
#process each mashed flag as if it were received separately
#- we can reuse the same flagval for each as they won't be expected to have values (as they are single letter flags)
#we will still need to check for multiple and defaults for each individual flag
#we can also still use the same argstate entries for each individual flag as the optionset will be the same for each of the mashed flags (as they will all be defined in the same optionset e.g -a|-b|-c)
set mashflagname -$mf
set mashflagoptionset [dict get $lookup_optset $mashflagname]
set raw_optionset_members [split $mashflagoptionset |]
#set mashflagapiopt [dict get $argstate $mashflagoptionset -parsekey]
if {[tcl::dict::get $argstate $mashflagoptionset -parsekey] ne ""} {
set api_opt [dict get $argstate $mashflagoptionset -parsekey]
} else {
set api_opt [string trimright [lindex $raw_optionset_members end] =]
}
if {$api_opt eq $flagname} {
set flag_ident $api_opt
set flag_ident_is_parsekey 0
} else {
#initially key our opts on a long form allowing us to know which specific flag was used
#(for when multiple map to same parsekey e.g lsearch)
#e.g -increasing|-SORTOPTION
set flag_ident $flagname|$api_opt
set flag_ident_is_parsekey 1
}
set optionset_type [tcl::dict::get $argstate $mashflagoptionset -type]
#only the last flag in a mash can be allowed to have a value, and the other flags must be of type none.
#flags are by default optional.
if {$optionset_type ne "none"} {
#A flag with a value - only allowed for the last flag in a mash
if {$posn != [expr {[llength $mashletters] - 1}]} {
#not the last flag in the mash - can't have a value
set errmsg "bad options for %caller%. Flag \"$mashflagname\" in mash \"$flagsupplied\" cannot have a value as only the last flag in a mash can have a value. The flag \"$mashflagname\" must be of type none. (1)"
#flag with no value - check for -typedefaults for the flag
#none / solo
if {[tcl::dict::exists $argstate $mashflagoptionset -typedefaults]} {
set tdflt [tcl::dict::get $argstate $mashflagoptionset -typedefaults]
} else {
#normal default for a solo is 1 unless overridden by -typedefaults
set tdflt 1
}
if {[tcl::dict::get $argstate $mashflagoptionset -multiple]} {
#puts stderr "Debug: flag '$mashflagname' in mash '$flagsupplied' is a multiple with typedefaults $tdflt -- api_opt: $api_opt flag_ident: $flag_ident flagsreceived: $flagsreceived multisreceived: $multisreceived"
if {$api_opt ni $flagsreceived} {
#override any default - don't lappend to it
tcl::dict::set opts $flag_ident $tdflt
} else {
tcl::dict::lappend opts $flag_ident $tdflt
}
if {$api_opt ni $multisreceived} {
lappend multisreceived $api_opt
}
} else {
#test parse_withdef_parsekey_repeat_ordering {Ensure last flag has precedence}
#tcl::dict::set opts $flag_ident $tdflt
if {$flag_ident_is_parsekey} {
#(shimmer - but required for ordering correctness during override)
puts stderr "Debug: flag '$mashflagname' in mash '$flagsupplied' flag_ident '$flag_ident' is the same as parsekey '$api_opt' tdflt: $tdflt - using lappend to ensure it ends up after any previous flag in the mash that had the same parsekey"
lappend opts $flag_ident $tdflt
puts stderr "opts after lappend: $opts"
} else {
tcl::dict::set opts $flag_ident $tdflt
}
}
#incr vals_remaining_possible -1
lappend solosreceived $api_opt ;#dups ok
}
}
lappend flagsreceived $api_opt
incr posn
}
#update vals_remaining_possible by one or 2 if the last flag took a value.
incr vals_remaining_possible -1
if {$flagval_included || $consume_value} {
incr vals_remaining_possible -1
}
#after processing the mash, we will have updated opts for each individual flag in the mash,
#and updated multisreceived and solo_received as needed based on the optionset entries for each individual flag in the mash
#we possibly need to incr i to skip a received value for the mash if the last flag in the mash had a value.
#or break if we have reached the end of the args after processing the mash
if {$flagval_included || $consume_value} {
#the last flag in the mash had a value - we have already processed it for that flag - so we need to skip it for the next iteration of the loop
incr i
if {$i > $maxidx} {
#we have reached the end of the args after processing the mash and its value - so we can break out of the loop
break
}
} else {
#no value included for the last flag in the mash - so we just continue to the next iteration of the loop to process the next arg
}
continue
}
}
}
default {
if {[dict exists $lookup_optset $flagname]} {
if {[dict exists $lookup_optset $flagname]} {
set optionset [dict get $lookup_optset $flagname]
set optionset [dict get $lookup_optset $flagname]
} else {
} else {
#we matched a prefix of all_opts - but it's not in the lookup_optset?
#review - this should not happen as we only match prefixes from all_opts which is derived from the keys of lookup_optset
puts stderr "Debug: matched prefix '$flagname' not in lookup_optset - this should not happen"
#(at least some "unsupported" test- commands don't provide a Usage line at all - e.g fossil help test-http)
foreach ln $basic_opt_lines {
set ln [string trim $ln]
if {$ln eq ""} {
continue
}
#the truncated description lines aren't useful here - but are always separated from the option info by more than one space.
set colbreak [string first " " $ln] ;#first occurrence of 2 spaces in a row - which is the separator between option info and description in fossil help output
set optinfo [string range $ln 0 $colbreak-1]
#this isn't the full help info for the option - but it's what we have available in the output of 'fossil help subcmd -o' - which is more concise and easier to parse than the full help for each option.
#todo - call fossil help <subcmd> and retrieve full help for each option.
set temphelp [string range $ln $colbreak end]
set opthelp [string trim $temphelp]
#we expect either one or two parts.
lassign $optinfo namepart typepart
#e.g --case-sensitive BOOL
#e.g -v|--verbose
#e.g -ci|--checkin VERSION (convert to -ci|--checkin=|--checkin -type VERSION)
if {$typepart ne ""} {
set optnames [split $namepart "|"]
#rebuild optnames as punkoptiondef string retaining dashes and pipes but adding in additional forms for longopts - e.g -ci|--checkin becomes -ci|--checkin=|--checkin
set punknames [list]
foreach n $optnames {
if {[string match --* $n]} {
#set n [list $n [string range $n 2 end]= [string range $n 2 end]]
lappend punknames $n ${n}=
} elseif {[string match -* $n]} {
lappend punknames $n
} else {
error "Unexpected option format: $n"
}
}
set typepart "-type $typepart"
} else {
#use as is if the flag doesn't have an argument - e.g -v|--verbose