F#でコマンドライン引数

コマンドライン引数をパースするライブラリはすでにいくつも存在しますが、今回は簡単なものを実装してみました。

簡単のため、ロング名のみ、値は=での指定のみに対応します。

some.exe --enable --value=10


今回実装したものの利用例

コマンドライン引数用のレコード型とデフォルト値を準備し、parse関数で引数を解析します。

オプションの名前、説明、指定された時の挙動を渡して解析を行います。

type CommandLineOption =
    { Enabled : bool
      Name    : string
      Value   : int
    }

let defaultOption =
    { Enabled = false
      Name    = ""
      Value   = 0
    }

let commandOption =
    CommandLine.perse 
        [ CommandLine.noValue "enb" "enable something" 
            (fun s -> { s with Enabled = true })

          CommandLine.value "name" "set name"
            (fun s x -> Ok { s with Name = x })

          CommandLine.value "value" "set int value"
            (fun s x -> 
                String.parseInt32 x |> Result.ofOption (x + " is not int")
                |> Result.map (fun x -> { s with Value = x })
            )
        ]
        defaultOption
        (argv |> Array.toList)


parse関数

引数の解析を行い、OptionDefinition(後述)に基づいて入力レコードを更新していきます。

--helpオプションが指定された場合は、オプションの一覧を出力します。

オプションが重複して指定された場合や、オプションが存在しない場合はErrorを返します。

type PerseResult<'a> =
    | HelpPrinted
    | Persed of 'a

let perse (options : OptionDefinition<'a> list) (initialValue : 'a) (args : string list) : Result<PerseResult<'a>, string> =
    if args = [ "--help" ] then
        options |> List.iter (fun x -> printf "--%s %s\n" x.Name x.Description)
        Ok HelpPrinted
    else
        let optionMap = options |> List.map (fun x -> x.Name, x) |> Map.ofList
        parseArgs args
        |> Result.bind
            (List.foldResult
                (fun (state, processed) (name, value) ->
                    if processed |> Set.contains name then 
                        Error ("--" + name + " is set multiple times")
                    else if not (Map.containsKey name optionMap) then
                        Error ("invalid option --" + name)
                    else
                        optionMap.[name].ParseValue state value
                        |> Result.map (fun x -> x, processed |> Set.add name)
                )
                (initialValue, Set<string>([]))
            )
        |> Result.map (fun (state, _) -> Persed state)


OptionDefinition

ユーザーがオプションの定義をするのに利用する関数は下記の様に定義してあります。

type OptionDefinition<'a> =
    { Name        : string
      Description : string
      ParseValue  : 'a -> string -> Result<'a, string>
    }

let value name description (f : 'a -> string -> Result<'a, string>) : OptionDefinition<'a> = 
    { Name        = name 
      Description = description
      ParseValue  = 
        fun state x ->
            if x = "" then Error ("--" + name + " must have a value")
            else           f state x
    }

let noValue name description (f : 'a -> 'a) : OptionDefinition<'a> =
    { Name        = name
      Description = description
      ParseValue  = 
        fun state x ->  
            if x = "" then Ok (f state)
            else           Error ("--" + name + " can not have a value")
    }


parseArgs

parse内で利用されているparseArgsは下記の様に定義しています。

let (|Regex|_|) pattern str = 
   let result = Regex.Match(str, pattern)
   if result.Success then Some (List.tail [ for x in result.Groups -> x.Value ])
   else                   None

let parseArgs (args : string list) : Result<(string * string) list, string> =
    args |> List.mapResult
        (function
         | Regex "--(..+?)=(.+)" [ x; y ] -> Ok (x, y)
         | Regex "--(..+)"       [ x ]    -> Ok (x, "")
         | x                              -> Error ("invalid argument " + x)
        )