Mailing List Archive: 49091 messages
  • Home
  • Script library
  • AltME Archive
  • Mailing list
  • Articles Index
  • Site search

[REBOL] Command Line Parser Module

From: greggirwin::mindspring::com at: 6-Oct-2003 22:10

Hi All, Here's an experimental command line parsing module. Let me know if you think it's useful (and worth pursuing further), what you like, and what you don't. -- Gregg Command Line Parser Module === Read-Me --- Introduction This is an *experimental* version of a command line parsing module for REBOL. It's so experimental that I don't even have a good name cooked up for it yet. :) The goal is to make it as easy as possible to define and process command line interfaces for REBOL scripts and applications. To that end, there is a dialect and one main function on the front end. The PARSE-COMMAND-LINE function takes input data--generally something like system/options/args--and a dialected spec block, very much like PARSE does, but using its own dialect. All you have to do after that is read the results. args: {--verbose -f %input.txt} ; << system/script/args options: parse-command-line args [ --verbose -v {verbose mode} -f input-file {The input file} ]
>> options/verbose
== true
>> options/input-file
== %input.txt To make your life even easier, standard usage and version options are built in (though crude right now). The command line syntax is meant to support standard Unix utility formats. I'm a Windows guy, but REBOL runs everywhere and I think this is the best option. Adding support for DOS "/" option syntax, as an alternative to "-" should be neither hard nor necessary. :) IMPORTANT NOTE! The input data is converted to a block! before it is parsed. --- Definitions :option - A switch that takes no arguments. :opt-arg - A switch that takes one or more arguments. :operand - A positional argument Options some in two varieties: short and long. Short options are a single character preceded by a single dash (-) and long options are a word precded by two dashes (--). -q --verbose Opt-Args consume one or more arguments following the option token. Right now, the input is converted to a block for easy processing, so standard REBOL lexical rules apply. For long options, single argument(s) must be separated from the token by a space or equal sign (=); multiple arguments have to be separate lexical items. If you use the "<opt>=<arg>" syntax, the argument will be seen as a string. You'll need to convert it yourself until datatype validations and coercions are in place. --file %input.txt --file=%input.txt Short options use the same form as long options, with one addition; you can put the argument immediately following the switch token. I don't care for it, but it's an accepted standard. -f %input.txt -f=input.txt -finput.txt Operands are just regular values that have no corresponding switch token associated with them. They are just accumulated in a block; there is no support for operands beyond that at this point. --- The Dialect You specify options and opt-args much as they would appear in documentation for your program. e.g. --quiet -q {quiet mode} --verbose -v {verbose mode} The rules are simple: Start with one or more words that begin with a single or double dash, which will be interpreted as option tokens. Next you can optionally include a word that *doesn't* start with a dash, which says you're defining an opt-arg instead of just a plain option. Next, put a string that will be displayed if the user asks for help on the program. Finally, you can include a spec block for the internal object that will be used during processing (See: Objects). The dialect is basically: some dash-word!s opt non-dash-word! string! opt block! Example: -C n { Shows n lines of context before and after each change. diff marks lines removed from path1 with -, lines added to path2 with + and lines changed in both files with !. This option conflicts with the -e and -f options. } [ name: 'num-context-lines ; overrides 'n args: integer! action: [value: to integer! arg] default: 3 ] \note You can put short strings on the same line as the tokens, but longer strings should be formatted over multiple lines as you want them to appear to the user. /note If multiple tokens are given for an option, the "name" of the token will be taken from the first one given. In the case of an opt-arg, the name will come from the arg name given after the option tokens. The name is important because that's how you're going to find it later. --- Objects When you call PARSE-COMMAND-LINE, giving it your command line spec, it will return a COMMAND-PARSER object that is filled with values that were parsed from the command line data. The name of each option becomes a word in that object, which is how you read out values that were set during the parsing process. For example: args: {--verbose -f %input.txt} options: parse-command-line args [ --verbose -v {verbose mode} -f input-file {The input file} ]
>> options/verbose
== true
>> options/input-file
== %input.txt In addition to the individual word values, you can get a list of all the custom words added for each option in the NAMES field.
>> options/names
== [verbose input-file help version] \note HELP and VERSION are built-in options that provide standard functionality for you at no charge. /note As your command line spec is parsed, internal objects are created for each option and opt-arg (and, likely, eventually operands). By providing a spec block for an option you can perform actions, override the name, provide default values, and tell it how many and what type of arguments it takes (though validation and type casting are not in place yet). --- Actions Actions are defined in the spec block for an option. -C n {Number of context lines} [ action: [value: to integer! arg] ] Actions are just blocks of REBOL code. The current implementation is... um...not all that great in regard to how these are handled. The thing you really need to know is that VALUE, ARG, and SPEC are special words in the context of an action. VALUE means the value of the option object where the action block is defined, ARG refers to the argument(s) consumed for the current opt-arg, and SPEC refers to the original command line spec you provided (it's used for automatic USAGE display). The default action, if none is given, will set the value of an option to true and the value of an opt-arg to the argument(s) consumed on its behalf. ;============================================================== REBOL [ Title: "Command Line Dialect: Experimental Version A" File: %cl-dialect-ex-a.r Author: "Gregg Irwin" Email: [greggirwin--acm--org] Date: 6-Oct-2003 Version: 0.0.1 Purpose: { Provide support for easy, automatic, command line parsing. You define the options in a dialect, which is used to build internal objects that are used to parse a command line. } Comment: { ***************************** *** THIS IS EXPERIMENTAL! *** ***************************** I include that caveat because I'm somewhat embarrassed by how I hacked it together. I have pages and pages of design ideas and notes, along with visions of an elegant PARSE-based implementation, but I was spending so much time thinking about the different ways it could be done, that I never got around to actually *doing* something with it. :\ So, I decided that I'd take some of the ideas and just whack something together to play with, and that's what this is. No, I'm not sure I like it, but it's something we can all use as a starting point, even if we only learn what *not* to do from it. :) One of the things we should use it for is to iron out the input dialect. There is a lot of other stuff to do: - General design needs to be re-thought to avoid all the ugly binding issues I created with this design (i.e. option/action contexts). - usage info for operands - conflict specification and handling - data type validation/coercion - what to do with unknown tokens - clean out and refactor unused idea bits - comment and explain things a lot more - CHOICE args (i.e. one of a set of options) - where best to get program name and version - match abbreviated long option names? - mulitple arg names for opt-args? - action handler for operands? - named operands? - more complete program info dialect? program-info: [ name: version: synopsis: description: options: operands: examples: environment-variables: diagnostics: messages: limits: ] I've looked at a number of modules in other languages that do this kind of thing, from getopt on up, but the heaviest influence was the Python Optik module by Greg Ward ( } ] option!: make object! [ type: 'option name: tokens: action: conflicts-with: desc: value: none ] opt-arg!: make option! [ type: 'opt-arg args: default: none ] operand!: make object! [ type: 'operand name: args: default: optional: ordinal: desc: value: none ] command-parser: make object! [ ; For internal use and debugging needs only. _option: copy [] _opt-arg: copy [] _operand: copy [] _token-map: copy [] _spec: none ; ; Add default options (if we decide not to do it in parse-command-line). ; append _option reduce [ ; 'help make option! [ ; name: 'help tokens: [--help -h] action: [show-usage] ; desc: "Show usage information" ; ] ; 'version make option! [ ; name: 'version tokens: [--version] action: [show-version] ; desc: "Show version information" ; ] ; ] names: copy [] ; custom option names added to the object. operands: does [:_operand] ; public access to operands. clean-token: func [ "Returns token less any attached arguments that come after an = sign." token ][ ; This fails if token is empty or has only spaces before the = sign. to word! first parse/all token "=" ] do-action: func [ "Execute the action associated with the token." token arg /local obj act ][ ;!! This routine is kludgey, because I put things together ; in such a way that binding/evaluation issues are problematic. ;print ["do-action" token arg] ;attempt [ obj: obj-from-token clean-token token either obj [ act: copy any [ obj/action ;?? Should we allow default values for options and use ; "not obj/default" here instead of true? [set in obj 'value either opt-arg? token [arg][true]] ] ;print mold act replace/all act 'arg either word? arg [to lit-word! arg][arg] ;!! YAK (yet another kludge). MOLDing to prevent evaluation. replace/all act 'spec mold _spec ;print ["x" token arg type? attempt [last act] mold act] do act ][ print ["Unknown token found:" token] ] ;] ] find-in-map: func [token][find _token-map to word! token] name-from-token: func [ "Returns an option name given any token that maps to it." token /local pos ][ either pos: find-in-map token [to word! first find pos lit-word!][none] ] obj-from-token: func [ "Returns an option name given any token that maps to it." token /local pos ][ either pos: find-in-map token [first find pos object!][none] ] opt-arg?: func [ "Returns true if the token maps to an opt-arg; false otherwise." token ][ attempt [select _opt-arg name-from-token clean-token token] ] parse-cl: func [ {Parses a command line according to the settings (options, etc.) in the parent command-parser object. Returns the object filled with data from the parse operation.} data /local ;-- funcs get-args get-opt-args process-long-opt process-operand process-opt process-short-opts ;-- vars args arg arg-str ][ ;-- Local Functions get-args: func [ {Returns the arguments for the given token.} token [string!] obj /short full-token [string!] ][ ;print [tab "get-arg:" token mold obj/args] any [ ; For short options, with a single arg, it can be butted ; right up against them. all [ short (2 < length? full-token) ; Allow things like "-qfin-file.txt", where ; -q is an option and -f is an opt-arg? (copy next find full-token last token) ] ; Both short and long args can have their args after an = sign. ; e.g. -a=on, --mode=text ; Handle opt-args using <opt>=<arg> format. pick parse/all token "=" 2 ; Get next <n> items from ARGS. get-opt-args obj ] ] get-opt-args: func [ {Consumes the number of arguments specified for the given opt-arg object from ARGS and returns them.} obj /local result num-args ][ ;print [tab tab "get-opt-args:" obj/name mold obj/args] num-args: either block? obj/args [length? obj/args][1] result: either num-args > 1 [ copy/part next args num-args ][ first next args ] args: skip args num-args result ] process-long-opt: func [ {Consumes any arguments for the option and performs its actions.} token [string!] /local obj arg ][ ;print ["Long Opt:" token] process-opt token ;print [tab "opt-arg?:" either obj [true][false] tab "arg:" arg] ] process-operand: func [arg] [ ;print ["Operand:" arg] append self/_operand arg ;do-action arg ??? ] process-opt: func [ {Inner option processor, for short opts that have an <opt>=<arg> format and all long opts.} token /local obj arg ][ if obj: opt-arg? token [arg: get-args token obj] ;print [ ; tab "process-opt:" token tab "opt-arg?:" ; either obj [true][false] tab "arg:" arg ;] do-action token arg ] process-short-opts: func [ {Consumes any arguments for the options and performs their actions. Handles both single and grouped tokens.} token [string!] /local obj tok arg ][ ;print ["Short Opts:" token] either find token #"=" [ process-opt token ][ foreach char next token [ ; skip leading "-" arg: none ;print [tab "Short Opt:" join "-" char] either obj: opt-arg? tok: join "-" char [ arg: get-args/short tok obj token do-action tok arg ;print [tab "Short opt-arg:" tok tab "arg:" arg] break ][ do-action tok arg ;print [tab "Short option:" tok] ] ] ] ] ;-- Processing args: to block! data while [not tail? args] [ arg-str: form arg: first args switch/default true reduce [ '-- = arg [