views:

229

answers:

3

I'm trying to parse command line arguments in an F# application. I'm using pattern matching over parameters list to accomplish it. Something like:

let rec parseCmdLnArgs = 
  function
  | [] -> { OutputFile = None ; OtherParam = None }
  | "/out" :: fileName :: rest -> let parsedRest = parseCmdLnArgs rest
                                  { OutputFile = Some(fileName) with parsedRest }

The problem is I want to make "/out" match case insensitive while preserving the case of other stuff. That means I can't alter the input and match the lowercase version of the input against it (this will lose the fileName case information).

I have thought about several solutions:

  • Resort to when clauses which is less than ideal.
  • Match a tuple each time, the first would be the actual parameter (which I'll just save for further processing and will wildcard match it) and the second would be the lowercased version used in such matchings. This looks worse than the first.
  • Use active patterns but that looks too verbose. I'll have to repeat things like ToLower "/out" before every item.

Is there a better option/pattern for doing these kind of stuff? I think this is a common problem and there should be a good way to handle it.

+2  A: 

I might do some pre-processing to allow for either "-" or "/" at the beginning of keywords, and to normalize the case:

let normalize (arg:string) =
    if arg.[0] = '/' || arg.[0] = '-' then 
        ("-" + arg.[1..].ToLower())
    else arg
let normalized = args |> List.map normalize

It's perhaps not ideal, but it's not like any user is going to have enough patience to type so many command-line parameters that looping through them twice is noticeably slow.

Joel Mueller
+2  A: 

You can use guards to match your deal:

let rec parseCmdLnArgs = 
  function
  | [] -> { OutputFile = None ; OtherParam = None }
  | root :: fileName :: rest when root.ToUpper() = "/OUT" -> let parsedRest = parseCmdLnArgs rest
                                  { OutputFile = Some(fileName) with parsedRest }
ssp
+9  A: 

I quite like your idea of using F# active patterns to solve this. It is a bit more verbose than using pre-processing, but I think it's quite elegant. Also, according to some BCL guidelines, you shouldn't be using ToLower when comparing strings (ignoring the case). The right approach is to use OrdinalIgnoreCase flag. You can still define a nice active pattern to do this for you:

open System

let (|InvariantEqual|_|) (str:string) arg = 
  if String.Compare(str, arg, StringComparison.OrdinalIgnoreCase) = 0
    then Some() else None

match "HellO" with
| InvariantEqual "hello" -> printfn "yep!"
| _ -> printfn "Nop!"    

You're right that it's more verbose, but it nicely hides the logic and it gives you enough power to use the recommended coding style (I'm not sure how this could be done using pre-processing).

Tomas Petricek
Yeah, the name 'ToLower' might have been misleading here. Basically, I meant the same thing as you wrote when using active pattern. It's verbose but apparently there's no better way.
Mehrdad Afshari