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) )