** Description ** [dbohdan] 2017-08-14: The following Tcl module automates processing of command line arguments. It was inspired by [Perl 6], which does a similar thing [https://perl6advent.wordpress.com/2010/12/02/day-2-interacting-with-the-command-line-with-main-subs/%|%quite elegantly]. In short, this module automatically generates a command line interface for your Tcl program based on the arguments that its main proc takes. It maps the proc's arguments whose names are prefixed with dashes (e.g., `-a` or `--arg`) to named command line arguments (options) and other proc arguments to positional command line arguments. E.g., `proc main {filename -a {--factor 2x}} ...` will result in `usage: yourscript.tcl -a A [--factor 2x] [--] filename`. Proc arguments with default values are mapped to optional command line arguments, named or positional; other arguments are considered mandatory. If some of the mandatory arguments are missing or unrecognized extra command line arguments are given, autoopts will output an informative error message, print an automatically generated usage note, and exit. It will also output a usage message if the user gives the command line argument `-h` or `--help`. To use this autoopts, source it and call `::autoopts::go ?description? ?your-main-proc?`. Download with [wiki-reaper]: `wiki-reaper 48940 2 > autoopts-0.2.0.tm` ** Use example ** *** Code *** ====== #! /usr/bin/env tclsh proc main {{input -} {--indent 4}} { set ch [expr {$input eq "-" ? "stdin" : [open $input]}] while {[gets $ch line] > 0} { puts [string repeat { } ${--indent}]$line } } source autoopts.tcl ::autoopts::go {indenter pro v1.0.1 -- indents input with spaces} ====== *** Shell transcript *** ====== $ ./autoopts-example.tcl --help indenter pro v1.0.1 - indents input with spaces usage: autoopts-example.tcl [--indent 4] [--] [input] $ ./autoopts-example.tcl --wrong unknown option: --wrong usage: autoopts-example.tcl [--indent 4] [--] [input] $ ./autoopts-example.tcl file1 file2 file3 too many positional arguments usage: autoopts-example.tcl [--indent 4] [--] [input] $ ./autoopts-example.tcl --indent missing value for option --indent usage: autoopts-example.tcl [--indent 4] [--] [input] $ ./autoopts-example.tcl ./autoopts-example.tcl --indent 11 #! /usr/bin/env tclsh proc main {{input -} {--indent 4}} { set ch [expr {$input eq "-" ? "stdin" : [open $input]}] while {[gets $ch line] > 0} { puts [string repeat { } ${--indent}]$line } } ====== ** Code ** ====== # autoopts v0.2.0 # This module automatically processes command line arguments and generates a # usage message based on the arguments that your main proc takes. To use it, # simply source this file and call # ::autoopts::go ?description? ?your-main-proc? # Copyright (c) 2017 dbohdan # License: MIT package require Tcl 8.5 namespace eval ::autoopts { variable version 0.2.0 } proc ::autoopts::has-prefix name { return [regexp {^--?} $name] } # Generate a command line argument spec based on the arguments the proc $proc # takes. The spec follows the format # {named {named1 ... namedN} positional {positional1 .. positionalM}}, where # each list item namedI or positionalJ is a dictionary {name foo ?default bar?}. proc ::autoopts::generate-spec proc { set spec {named {} positional {}} foreach arg [info args $proc] { set info {} dict set info name $arg if {[info default $proc $arg default]} { dict set info default $default } if {[has-prefix $arg]} { dict lappend spec named $info } else { dict lappend spec positional $info } } return $spec } proc ::autoopts::me {} { return [file tail [info script]] } # Take a spec generated by [generate-spec] and return a human-readable usage # message. proc ::autoopts::generate-usage spec { set usage "usage: [me]" foreach info [dict get $spec named] { set name [dict get $info name] if {[dict exists $info default]} { append usage " \[$name [dict get $info default]\]" } else { append usage " $name [string toupper [string trimleft $name -]]" } } if {[llength [dict get $spec positional]] > 0} { append usage { [--]} foreach info [dict get $spec positional] { set name [dict get $info name] if {[dict exists $info default]} { append usage " \[$name\]" } else { append usage " $name" } } } return $usage } # Parse the command line argument list $arguments according to the spec $spec. # Returns a dictionary {arg1 value1 ... argN valueN}. proc ::autoopts::parse {spec arguments} { set named [dict get $spec named] set positional [dict get $spec positional] set count [llength $arguments] set eon 0 ;# End of named arguments. set names {} set pos 0 ;# Number of positional arguments encountered. set values {} ;# Result. foreach info $named { lappend names [dict get $info name] } for {set i 0} {$i < $count} {incr i} { set arg [lindex $arguments $i] if {[has-prefix $arg] && !$eon} { if {$arg eq {--}} { set eon 1 continue } if {$arg ni $names} { error "unknown option: $arg" } if {$i + 1 < $count} { dict set values $arg [lindex $arguments $i+1] } else { error "missing value for option $arg" } incr i } else { if {$pos >= [llength $positional]} { error {too many positional arguments} } dict set values [dict get [lindex $positional $pos] name] $arg incr pos } } set keys [dict keys $values] for {set i $pos} {$i < [llength $positional]} {incr i} { set info [lindex $positional $i] if {[dict exists $info default]} { dict set values [dict get $info name] [dict get $info default] } else { error {too few positional arguments} } } foreach info $named { set name [dict get $info name] if {$name ni $keys} { if {[dict exists $info default]} { dict set values $name [dict get $info default] } else { error "missing required option $name" } } } return $values } # Inspect proc $proc for argument names and call it with argument values in the # dictionary $values. proc ::autoopts::invoke {proc values} { set procArgs {} foreach arg [info args $proc] { lappend procArgs [dict get $values $arg] } $proc {*}$procArgs } # Invoke $proc with arguments based on $::argv. If some mandatory command line # arguments are missing or there are too many of them, print an error message # and usage to stderr and exit with exit status 1. If $::argv consists of just # {-h} or {--help}, print usage to stderr and exit with status 0. proc ::autoopts::go {{description {}} {proc main}} { set spec [generate-spec $proc] if {[string trim $::argv] in {-h --help}} { puts stderr $description puts stderr [generate-usage $spec] exit 0 } if {[catch { set values [parse $spec $::argv] } err]} { puts stderr $err puts stderr [generate-usage $spec] exit 1 } invoke $proc $values } # Testing. proc ::autoopts::assert-equal {actual expected} { if {$actual ne $expected} { error "expected \"$expected\", but got \"$actual\"" } } proc ::autoopts::test {} { proc ::autoopts::foo {-n --hello} { return [list ${-n} ${--hello}] } proc ::autoopts::bar {inputfile {outputfile -} {--format pdf}} { return [list $inputfile $outputfile ${--format}] } set fooSpec {named {{name -n} {name --hello}} positional {}} set barSpec {named {{name --format default pdf}}\ positional {{name inputfile} {name outputfile default -}}} # generate-spec. set spec [generate-spec foo] assert-equal $spec $fooSpec assert-equal [generate-usage $spec] "usage: [me] -n N --hello HELLO" set spec [generate-spec bar] assert-equal $spec $barSpec assert-equal [generate-usage $spec] \ "usage: [me] \[--format pdf\] \[--\] inputfile \[outputfile\]" # parse. catch {parse $fooSpec {}} err assert-equal $err {missing required option -n} catch {parse $fooSpec {-n 5}} err assert-equal $err {missing required option --hello} catch {parse $fooSpec foo} err assert-equal $err {too many positional arguments} catch {parse $fooSpec {-n 5 --hello world --with-cheese}} err assert-equal $err {unknown option: --with-cheese} catch {parse $fooSpec {-n 5 --hello world --with-cheese no}} err assert-equal $err {unknown option: --with-cheese} assert-equal [parse $fooSpec {-n 5 --hello world}] {-n 5 --hello world} catch {parse $barSpec {}} err assert-equal $err {too few positional arguments} catch {parse $barSpec {x y z}} err assert-equal $err {too many positional arguments} catch {parse $barSpec {x --what? z}} err assert-equal $err {unknown option: --what?} assert-equal [parse $barSpec foo.doc] \ {inputfile foo.doc outputfile - --format pdf} assert-equal [parse $barSpec {foo.doc bar.pdf}] \ {inputfile foo.doc outputfile bar.pdf --format pdf} assert-equal [parse $barSpec {foo.doc bar.xlsx --format xlsx}] \ {inputfile foo.doc outputfile bar.xlsx --format xlsx} # invoke. assert-equal [invoke foo {-n 42 --hello universe}] {42 universe} assert-equal [invoke bar { inputfile /dev/zero outputfile /dev/null --format N/A }] {/dev/zero /dev/null N/A} rename ::autoopts::foo {} rename ::autoopts::bar {} } # If this is the main script... if {[info exists argv0] && ([file tail [info script]] eq [file tail $argv0])} { ::autoopts::test } ====== <>Argument Processing | Package