Hi,
Attached is my attempt at a process-cmd-args function, as well as a bunch of 
tests.  It handles most of the spec, except for -abc (either as -a=bc or as -a 
-b -c), and except for "exact" P6 forms (:foo<1> or :foo(1)).

Also the line that sets the "but False" variations is commented out for now, 
because "but" isn't working.  The tests might need fixing too, since I'm not 
sure whether eqv (as used by is_deeply) would cover that, or whether it would 
take a separate test in bool context.

Instead of a %boolean-names hash, I used an array to hold those parameter 
names, as well as @arrays to hold the array-parameters.  Or it would be pretty 
easy to change to a hash that maps the parameter names onto their types, or 
something like that.  I also added a %shorts param that maps short arg-names 
onto their full names.

I have a few questions about the spec that are worth clarifying on p6l, but 
despite some slow going, I'm pleased to find that some aspects of P6 have 
already settled nicely in my brain.


-David
module process-cmd-args;    # call it something so we can test it

our sub process-cmd-args
        ( @args,        # args as received from the command-line
          @booleans?,   # list of parameter-names that were declared as Bools
          @arrays?,     # list of parameter-names that were declared as Arrays
          %shorts? )    # hash mapping short parameter-names onto their full names
{
    my @posns;          # hold the resulting positional arguments, in the same order they came in
    my %names;          # hold the resulting named arguments
    
    my Bool $stopped;   # flag will be turned on when we stop looking for options (e.g. hit a "--")
    my $collector;      # holds the name of an option if it wants to collect multiple values (declared as @)
                        # (the idea of the "collector" is to let us know when we have: --foo v1 v2 ...
                        # so that we can collect the v1, v2, etc. for the "foo" parameter as we loop 
                        # over them; when not defined, it means we're not trying to collect anything)
    
    for @args -> $arg
    {
        if $arg eq "--" { $stopped = True; undefine $collector; next }
        
        my (Str $name, Str $value, Bool $autobool, Bool $short);
        my Bool $flag = True;   # default value for flags (we can set it to False if the option is negated)
        
        # Parse an argument into name and value pieces
        grammar Options         # (need the grammar because Rakudo doesn't handle lexical regexes yet)
        {
            token TOP { <name>'='<value> || <name> || <value> }
            
            regex name 
            # A parameter name
            # Note that we allow funny chars (that are not valid Perl identifiers) 
            # because they can always go in the slurpy-hash
            # (but we don't allow '=' because that has to signify an assignment)
            {
                ('--'|':'|'-') ('/')? (<-[=]>*)         # --foo or :foo or -f, with optional / for negation
                { $name = ~$2; $flag = !~$1; $short = $0 eq "-"; $autobool = !$flag || $short || $0 eq ":"; } 
                # We assign some values right here: $name gets the name-part without leading -- or :, etc.
                # If negated ("/"), sets the bool value for a flag to False
                # If "-" then flag this as a short name
                # If ":" or "-" or negated then be prepared to treat it like a bool 
                #   (":foo", etc. becomes T/F, while only the "--foo" format can get its value from the subsequent arg)
            }
            
            regex value 
            # A value string
            # Can be anything, including an empty string
            # (because --foo=$FOO on the command line should be allowed even if $FOO was empty)
            # Value will be split into a list if it contains commas
            # but if not, returns a single Str, not a one-element list
            {
                .* 
                {   my @value = split / \s*\,\s* /, ~$/;                    # split on commas with optional whitespace
                    $value = @value.elems==1?? @value[0] !! eager @value    # eager to flatten out the spliterator!
                }   
            }
        }
        
        
        # If we've stopped collecting options, then subsequent args are all "values", whether they start with -- or not
        $stopped?? ($value = $arg) !! Options.parse($arg);      # otherwise parsing the arg will set $name and $value, etc.
        
        # If we have a parameter name, then set it up
        if defined $name
        {
            $short and ($name = $_ if $_ given %shorts{$name});     # if we got a short "-n", use its full name
            
            # Fix up value for params declared as arrays or bools
            if defined $value { $value = @($value) if $name ~~ any(@arrays) }   # force list even for a single value if we're assigning to an array
            else { $value = $flag if $name ~~ any(@booleans) or $autobool }     # no value given, so default to True (or False, with --/) if it's a Bool or something in bool-only format
            
            if defined $value   # name and a value means we can set it directly
            {
### FIX THIS LINE WHEN "but False" WORKS:
                %names{$name} = $value; ## = $flag?? $value !! $value but False;
                undefine $collector;            # we got --name=value, so no more collecting subsequent values
            }
            else                # no value (including defaulting to a bool value), so start collecting
            {
                $collector = $name;             # save the name so we know what we're collecting for
                %names{$name} = $name ~~ any(@arrays)?? () !! True;     
                # start an empty array if suitable so we can push onto it;
                # if not an array parameter, just set True in case we run out (if there is a subsequent value, it will overwrite)
                # (note that we never set to False here because a negated param never collects subsequent args, so would be already dealt with)
            }
        }
        # Otherwise, we have just a value
        else
        {
            if $collector  # then we're collecting this value for a previous --foo param
            {
                if $collector ~~ any(@arrays) { push %names{$collector}, $value }   # push if an array
                else { %names{$collector} = $value; undefine $collector; }          # or else just assign
                # Note that the parameter must be declared as an array to collect multiple values;
                # otherwise, we turn of collection after getting just one
            }
            else # this is just a plain old positional arg
            {
                push @posns, $value;
                $stopped = True;        # no more named switches after the first non-switch
            }
        }
    }
    
    return @posns, %names;
}

Attachment: process-cmd-args.t
Description: Binary data

Reply via email to