1

I'm trying to use F# and System.CommandLine to make a little CLI tool. A command can have a handler callback that is called when the command is used. Usually one can define several flags for a command and the name of the flag is used to bind to an argument of the handler function with the same name.

Example

myApp foo --a

will call the handler for the foo command

let handler (a: bool) (b: bool) = // a should be true, b should be false
   ...

However this doesn't work and both a and b are false when I bind the handler that is a let binding:

let fooCommand = Command ...
fooCommand.Handler <- CommandHandler.Create handler
// will call handler false false

But, when I use a lambda directly it works fine and the function arguments have the correct values

let fooCommand = Command ...
fooCommand.Handler <- CommandHandler.Create (fun (a: bool) (b: bool) -> handler a b)
// will call handler true false

Why is that? Why does the handler work as a lambda but not as a let binding?

Here is a MRE

#r "nuget: System.CommandLine, 2.0.0-beta1.21308.1"

open System.CommandLine
open System.CommandLine.Invocation
open System.CommandLine.Parsing

let opts = [Option<bool>([|"--a"|]); Option<bool>([|"--b"|])]

let fooCmd = Command("foo", "")
List.iter fooCmd.AddOption opts

let barCmd = Command("bar", "")
List.iter barCmd.AddOption opts

let handler (a: bool) (b: bool) = 
    printfn "%A" {| a = a; b = b|}

fooCmd.Handler <- CommandHandler.Create handler

barCmd.Handler <- CommandHandler.Create (fun (a: bool) (b: bool) -> handler a b)

let root = RootCommand("")
root.Add fooCmd
root.Add barCmd

printfn "foo --a: "
root.Invoke("foo --a")

printfn "bar --a: "
root.Invoke("bar --a")

which prints

foo --a: 
{ a = false
  b = false }
bar --a: 
{ a = true
  b = false }
Timo
  • 9,269
  • 2
  • 28
  • 58
  • 2
    I see you already figured it out! It seems that System.CommandLine uses some fragile reflection magic to figure out what the parameter names are. Just in case you wanted to avoid that, there is an F# library for command line argument parsing, Argu, which lets you define the commands using a somewhat more clear structure of a discriminated union: http://fsprojects.github.io/Argu/ – Tomas Petricek Sep 10 '21 at 10:56
  • @TomasPetricek thank you so much for that link. This library looks way more F# friendly :) – Timo Sep 10 '21 at 12:44

1 Answers1

1

I found that the issue lies in the delegate creation from a let binding. If I create an Action<bool, bool> from the let binding, parameter names are lost. If the Action is created from a lambda directly, parameter names are preserved:

open System
open System.Reflection

let foo (a: bool) (b: bool) = ()

let x = Action<bool, bool>(foo)
x.Method.GetParameters() |> Seq.iter (printfn "%A") 

let y = Action<bool, bool>(fun (a: bool) (b: bool) -> ())
y.Method.GetParameters() |> Seq.iter (printfn "%A")

prints

Boolean delegateArg0
Boolean delegateArg1
Boolean a
Boolean b

I guess let bindings just don't keep metadata like this, probably because it would've too big of a runtime impact, especially with currying.

Please correct me if there is a different reason for this.

Timo
  • 9,269
  • 2
  • 28
  • 58