I’ve been working on a dotnet core global tool. One of the things that such a tool has, is many command line options. I found Argu to parse these command line options. However I ran into the problem that I was left with a very large amount of if/else statements. Perhaps I don’t know what the “proper” way is of handling this, but this was my naive approach.
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
|
let hasInfo = arguments.Contains Info
let hasPostalCode = arguments.Contains PostalCode
let hasUpdate = arguments.Contains Update
let hasList = arguments.Contains List
let hasClearDatabase = arguments.Contains ClearDatabase
if hasInfo
then printfn "info"
elif hasPostalCode && hasUpdate
then printfn "%s" <| parser.PrintUsage()
elif hasPostalCode && not hasUpdate
then getPostalCodeInformation <| arguments.GetResult PostalCode
elif hasList
then
match arguments.GetResult List with
| None -> printfn "%s" <| parser.PrintUsage()
| Some list ->
match list with
| Supported -> ConsolePrinter.printCountries DataDownload.supportedCountries
| Available ->
Database.ensureDatabase()
match Query.getAvailableCountries () with
| Ok countries ->
match countries with
| None -> printfn "No countries have been updated yet"
| Some c -> ConsolePrinter.printCountries c
| Error e ->
ConsolePrinter.printErrorMessage e ErrorLog.writeException
elif hasClearDatabase
then
Database.clearDatabase()
elif not hasPostalCode && hasUpdate
then
match arguments.GetResult Update with
| None -> printfn "%s" <| parser.PrintUsage()
| Some countryCode ->
updateCountry countryCode
|
The entire time I was thinking that there must be a better way. After mulling over it awhile, I suddenly remembered Active Patterns. What’s interesting to note, is that I had never used Active Patterns before, but I still somehow knew that this was finally a perfect use case for these. This allowed me to decouple the command line option, from its functionality into easily maintained blocks.
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
|
let (|ShowInformation|HasPostalCode|UpdateCountry|ListAvailable|ListSupported| HasClearDatabase|HelpRequested|)
(input: ParseResults<Arguments>) =
if input.Contains Info then ShowInformation
elif input.Contains PostalCode then HasPostalCode
elif input.Contains Update then UpdateCountry
elif input.Contains List
then
let listRequest = input.GetResult List
let isAvailableRequest = listRequest |> Option.exists (fun l -> l = Available)
let isSupportedRequest = listRequest |> Option.exists (fun l -> l = Supported)
if not isAvailableRequest && not isSupportedRequest
then HelpRequested
elif isAvailableRequest
then ListAvailable
else ListSupported
elif input.Contains ClearDatabase then HasClearDatabase
else HelpRequested
|
Here we have the definition of the Active Pattern. Each command line option is is own possible pattern, then we have a much more manageable and readable block that returns the selected options. Now, we only need to define what should happen with these options.
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
|
match arguments with
| ShowInformation -> printfn "info"
| HasPostalCode -> getPostalCodeInformation <| arguments.GetResult PostalCode
| UpdateCountry ->
match arguments.GetResult Update with
| None -> printfn "%s" <| parser.PrintUsage()
| Some countryCode ->
updateCountry countryCode
| ListAvailable ->
Database.ensureDatabase()
match Query.getAvailableCountries () with
| Ok countries ->
match countries with
| None -> printfn "No countries have been updated yet"
| Some c -> ConsolePrinter.printCountries c
| Error e ->
ConsolePrinter.printErrorMessage e ErrorLog.writeException
| ListSupported -> ConsolePrinter.printCountries DataDownload.supportedCountries
| HasClearDatabase -> Database.clearDatabase()
| _ -> printfn "%s" <| parser.PrintUsage()
|
Much better. This is a lot more clear to read and understand which command line options map to what functionality.
val hasInfo : 'a
val hasPostalCode : 'a
val hasUpdate : 'a
val hasList : 'a
val hasClearDatabase : 'a
val input : 'a
active recognizer ShowInformation: 'a -> Microsoft.FSharp.Core.Choice<'b,'c,'d,'e,'f,'g,'h>
active recognizer HasPostalCode: 'a -> Microsoft.FSharp.Core.Choice<'b,'c,'d,'e,'f,'g,'h>
active recognizer UpdateCountry: 'a -> Microsoft.FSharp.Core.Choice<'b,'c,'d,'e,'f,'g,'h>
val None : 'a
active recognizer ListAvailable: 'a -> Microsoft.FSharp.Core.Choice<'b,'c,'d,'e,'f,'g,'h>
active recognizer ListSupported: 'a -> Microsoft.FSharp.Core.Choice<'b,'c,'d,'e,'f,'g,'h>
active recognizer HasClearDatabase: 'a -> Microsoft.FSharp.Core.Choice<'b,'c,'d,'e,'f,'g,'h>