diff --git a/Cli.lean b/Cli.lean
index ec8c71e..44206f4 100644
--- a/Cli.lean
+++ b/Cli.lean
@@ -1,4 +1,5 @@
module
public import Cli.Basic
-public import Cli.Extensions
\ No newline at end of file
+public import Cli.Extensions
+public import Cli.Typed
\ No newline at end of file
diff --git a/Cli/Basic.lean b/Cli/Basic.lean
index 2b1e254..a350f63 100644
--- a/Cli/Basic.lean
+++ b/Cli/Basic.lean
@@ -1018,17 +1018,20 @@ section Macro
("EXTENSIONS: " sepBy(term, ";", "; "))?
"\n]" : term
- meta def expandNameableStringArg (t : TSyntax `Cli.nameableStringArg) : MacroM Term :=
+ meta def expandNameableStringArg (t : TSyntax ``Cli.nameableStringArg) : MacroM Term :=
pure ⟨t.raw[0]⟩
- meta def expandLiteralIdent (t : TSyntax `Cli.literalIdent) : MacroM Term :=
+ meta def extractLiteralIdent (t : TSyntax ``Cli.literalIdent) : String :=
let s := t.raw[0]
if s.getKind == identKind then
- pure <| quote s.getId.toString
+ s.getId.toString
else
- pure ⟨s⟩
+ s.isStrLit?.get!
- meta def expandRunFun (runFun : TSyntax `Cli.runFun) : MacroM Term :=
+ meta def expandLiteralIdent (t : TSyntax ``Cli.literalIdent) : MacroM Term :=
+ pure <| quote <| extractLiteralIdent t
+
+ meta def expandRunFun (runFun : TSyntax ``Cli.runFun) : MacroM Term :=
match runFun with
| `(Cli.runFun| VIA $run) =>
`($run)
@@ -1036,17 +1039,17 @@ section Macro
`(fun _ => pure 0)
| _ => Macro.throwUnsupported
- meta def expandPositionalArg (positionalArg : TSyntax `Cli.positionalArg) : MacroM Term := do
+ meta def expandPositionalArg (positionalArg : TSyntax ``Cli.positionalArg) : MacroM Term := do
let `(Cli.positionalArg| $name : $type; $description) := positionalArg
| Macro.throwUnsupported
`(Arg.mk $(← expandLiteralIdent name) $(← expandNameableStringArg description) $type)
- meta def expandVariableArg (variableArg : TSyntax `Cli.variableArg) : MacroM Term := do
+ meta def expandVariableArg (variableArg : TSyntax ``Cli.variableArg) : MacroM Term := do
let `(Cli.variableArg| ...$name : $type; $description) := variableArg
| Macro.throwUnsupported
`(Arg.mk $(← expandLiteralIdent name) $(← expandNameableStringArg description) $type)
- meta def expandFlag (flag : TSyntax `Cli.flag) : MacroM Term := do
+ meta def expandFlag (flag : TSyntax ``Cli.flag) : MacroM Term := do
let `(Cli.flag| $flagName1 $[, $flagName2]? $[ : $type]?; $description) := flag
| Macro.throwUnsupported
let mut shortName := quote (none : Option String)
@@ -1061,31 +1064,53 @@ section Macro
| some type => type
`(Flag.mk $shortName $(← expandLiteralIdent longName) $(← expandNameableStringArg description) $type)
+ meta def mkCmdInstantiationTerm
+ (name : TSyntax ``Cli.literalIdent)
+ (version? : Option (TSyntax ``Cli.nameableStringArg))
+ (description : TSyntax ``Cli.nameableStringArg)
+ (flags? : Option (TSyntaxArray ``Cli.flag))
+ (positionalArgs? : Option (TSyntaxArray ``Cli.positionalArg))
+ (variableArg? : Option (Option (TSyntax ``Cli.variableArg)))
+ (subCommands? : Option (Lean.Syntax.TSepArray `ident ";"))
+ (extensions? : Option (Lean.Syntax.TSepArray `term ";"))
+ (run : Term)
+ : MacroM Term := do
+ `(Cmd.mk
+ (name := $(← expandLiteralIdent name))
+ (version? := $(quote (← version?.mapM expandNameableStringArg)))
+ (description := $(← expandNameableStringArg description))
+ (flags := $(quote (← flags?.getD #[] |>.mapM expandFlag)))
+ (positionalArgs := $(quote (← positionalArgs?.getD #[] |>.mapM expandPositionalArg)))
+ (variableArg? := $(quote (← (Option.join variableArg?).mapM expandVariableArg)))
+ (run := $run)
+ (subCmds := $(quote ((subCommands?.getD ⟨#[]⟩).getElems : TSyntaxArray `term)))
+ (extension? := some <| Array.foldl Extension.then { : Extension } <| Array.qsort
+ $(quote (extensions?.getD ⟨#[]⟩).getElems) (·.priority > ·.priority)))
+
macro_rules
| `(`[Cli|
$name $run:runFun; $[[$version?]]?
$description
$[FLAGS:
- $flags*
+ $flags?*
]?
$[ARGS:
- $positionalArgs*
- $[$variableArg]?
+ $positionalArgs?*
+ $[$variableArg?]?
]?
- $[SUBCOMMANDS: $subCommands;*]?
- $[EXTENSIONS: $extensions;*]?
+ $[SUBCOMMANDS: $subCommands?;*]?
+ $[EXTENSIONS: $extensions?;*]?
]) => do
- `(Cmd.mk
- (name := $(← expandLiteralIdent name))
- (version? := $(quote (← version?.mapM expandNameableStringArg)))
- (description := $(← expandNameableStringArg description))
- (flags := $(quote (← flags.getD #[] |>.mapM expandFlag)))
- (positionalArgs := $(quote (← positionalArgs.getD #[] |>.mapM expandPositionalArg)))
- (variableArg? := $(quote (← (Option.join variableArg).mapM expandVariableArg)))
- (run := $(← expandRunFun run))
- (subCmds := $(quote ((subCommands.getD ⟨#[]⟩).getElems : TSyntaxArray `term)))
- (extension? := some <| Array.foldl Extension.then { : Extension } <| Array.qsort
- $(quote (extensions.getD ⟨#[]⟩).getElems) (·.priority > ·.priority)))
+ mkCmdInstantiationTerm
+ name
+ version?
+ description
+ flags?
+ positionalArgs?
+ variableArg?
+ subCommands?
+ extensions?
+ (← expandRunFun run)
end Macro
section Info
diff --git a/Cli/Typed.lean b/Cli/Typed.lean
new file mode 100644
index 0000000..0f184b9
--- /dev/null
+++ b/Cli/Typed.lean
@@ -0,0 +1,184 @@
+module
+
+public import Cli.Basic
+meta import Init.Data.Slice.Array.Iterator
+meta import Init.Data.Slice.Array.Basic
+
+section
+
+namespace Cli
+
+open Lean
+
+section TypedCli
+ private meta def kebabToCamelCase (s : String) : String := Id.run do
+ let parts := s.split "-" |>.toArray
+ let mut result : String := parts[0]!.toString
+ for part in parts[1...*] do
+ let some frontChar := part.front?
+ | continue
+ let frontChar := frontChar.toUpper
+ let rest := part.drop 1
+ result := result.push frontChar ++ rest
+ return result
+
+ private meta def mkTypedParsedDecls
+ (name : TSyntax ``Cli.literalIdent)
+ (version? : Option (TSyntax ``Cli.nameableStringArg))
+ (description : TSyntax ``Cli.nameableStringArg)
+ (flags? : Option (TSyntaxArray ``Cli.flag))
+ (positionalArgs? : Option (TSyntaxArray ``Cli.positionalArg))
+ (variableArg? : Option (Option (TSyntax ``Cli.variableArg)))
+ (subCommands? : Option (Lean.Syntax.TSepArray `ident ";"))
+ (extensions? : Option (Lean.Syntax.TSepArray `term ";"))
+ : MacroM (Array Syntax) := do
+ let cmdName := .mkSimple <| extractLiteralIdent name
+ let cmdConstructorIdent := mkIdent (.str cmdName "mkCmd")
+ let typedParsedDeclName := .str cmdName "Parsed"
+ let typedParsedDeclIdent := mkIdent typedParsedDeclName
+ let typedParsedDeclRawConstructorIdent := mkIdent (.str typedParsedDeclName "mk")
+
+ let typedParsedDecl ← `(
+ structure $typedParsedDeclIdent where
+ raw : Cli.Parsed
+ deriving Inhabited
+ )
+
+ let mut typedParsedDeclAccessors : Array Syntax := #[]
+ for flag in flags?.getD #[] do
+ let `(Cli.flag| $flagName1 $[, $flagName2?]? $[ : $flagType?]?; $_) := flag
+ | Macro.throwUnsupported
+ let longFlagName := extractLiteralIdent <| flagName2?.getD flagName1
+ let longFlagNameTerm := quote longFlagName
+ let flagAccessorName := kebabToCamelCase longFlagName
+ let flagAccessor ←
+ match flagType? with
+ | none =>
+ let flagAccessorIdent := mkIdent (.str typedParsedDeclName flagAccessorName)
+ `(
+ @[inline] def $flagAccessorIdent (p : $typedParsedDeclIdent) : Bool :=
+ p.raw.hasFlag $longFlagNameTerm
+ )
+ | some flagType =>
+ let flagAccessorIdent := mkIdent (.str typedParsedDeclName (flagAccessorName ++ "?"))
+ `(
+ @[inline] def $flagAccessorIdent (p : $typedParsedDeclIdent) : Option $flagType :=
+ p.raw.flag? $longFlagNameTerm |>.map (·.as! $flagType)
+ )
+ typedParsedDeclAccessors := typedParsedDeclAccessors.push flagAccessor
+ for arg in positionalArgs?.getD #[] do
+ let `(Cli.positionalArg| $argName : $argType; $_) := arg
+ | Macro.throwUnsupported
+ let argName := extractLiteralIdent argName
+ let argNameTerm := quote argName
+ let argAccessorName := kebabToCamelCase argName
+ let argAccessorIdent := mkIdent (.str typedParsedDeclName argAccessorName)
+ let argAccessor ← `(
+ @[inline] def $argAccessorIdent (p : $typedParsedDeclIdent) : $argType :=
+ p.raw.positionalArg! $argNameTerm |>.as! $argType
+ )
+ typedParsedDeclAccessors := typedParsedDeclAccessors.push argAccessor
+ if let some variableArg := Option.join variableArg? then
+ let `(Cli.variableArg| ...$variableArgName : $variableArgType; $_) := variableArg
+ | Macro.throwUnsupported
+ let variableArgAccessorName := extractLiteralIdent variableArgName |> kebabToCamelCase
+ let variableArgAccessorIdent := mkIdent (.str typedParsedDeclName variableArgAccessorName)
+ let variableArgAccessor ← `(
+ @[inline] def $variableArgAccessorIdent (p : $typedParsedDeclIdent) : Array $variableArgType :=
+ p.raw.variableArgsAs! $variableArgType
+ )
+ typedParsedDeclAccessors := typedParsedDeclAccessors.push variableArgAccessor
+
+ let cmdInstantiationTerm ← mkCmdInstantiationTerm
+ name
+ version?
+ description
+ flags?
+ positionalArgs?
+ variableArg?
+ subCommands?
+ extensions?
+ (← `(fun raw => handler ($typedParsedDeclRawConstructorIdent raw)))
+ let cmdConstructor ← `(
+ def $cmdConstructorIdent
+ (handler : $typedParsedDeclIdent → IO UInt32)
+ : Cmd :=
+ $cmdInstantiationTerm
+ )
+
+ return #[typedParsedDecl] ++ typedParsedDeclAccessors ++ #[cmdConstructor]
+
+ syntax "cli_def " literalIdent runFun "; " ("[" nameableStringArg "]")?
+ nameableStringArg
+ ("FLAGS:\n" withPosition((flag)*))?
+ ("ARGS:\n" withPosition((positionalArg)* (variableArg)?))?
+ ("SUBCOMMANDS: " sepBy(ident, ";", "; "))?
+ ("EXTENSIONS: " sepBy(term, ";", "; "))?
+ : command
+
+ syntax "cli_def " literalIdent "; " ("[" nameableStringArg "]")?
+ nameableStringArg
+ ("FLAGS:\n" withPosition((flag)*))?
+ ("ARGS:\n" withPosition((positionalArg)* (variableArg)?))?
+ ("SUBCOMMANDS: " sepBy(ident, ";", "; "))?
+ ("EXTENSIONS: " sepBy(term, ";", "; "))?
+ (" RUN " term)?
+ : command
+
+ macro_rules
+ | `(cli_def $name $run:runFun; $[[$version?]]?
+ $description
+ $[FLAGS:
+ $flags?*
+ ]?
+ $[ARGS:
+ $positionalArgs?*
+ $[$variableArg?]?
+ ]?
+ $[SUBCOMMANDS: $subCommands?;*]?
+ $[EXTENSIONS: $extensions?;*]? ) => do
+ let cmdIdent := mkIdent (.mkSimple (extractLiteralIdent name))
+ let cmdMk ← mkCmdInstantiationTerm
+ name
+ version?
+ description
+ flags?
+ positionalArgs?
+ variableArg?
+ subCommands?
+ extensions?
+ (← expandRunFun run)
+ `(def $cmdIdent : Cmd := $cmdMk)
+
+ macro_rules
+ | `(cli_def $name; $[[$version?]]?
+ $description
+ $[FLAGS:
+ $flags?*
+ ]?
+ $[ARGS:
+ $positionalArgs?*
+ $[$variableArg?]?
+ ]?
+ $[SUBCOMMANDS: $subCommands?;*]?
+ $[EXTENSIONS: $extensions?;*]?
+ $[RUN $handlerTerm?]?) => do
+ let typedParsedDecls ← mkTypedParsedDecls
+ name
+ version?
+ description
+ flags?
+ positionalArgs?
+ variableArg?
+ subCommands?
+ extensions?
+ let some handlerTerm := handlerTerm?
+ | return mkNullNode typedParsedDecls
+ let cmdName := .mkSimple <| extractLiteralIdent name
+ let cmdIdent := mkIdent cmdName
+ let cmdConstructorIdent := mkIdent (Name.mkStr cmdName "mkCmd")
+ let cmd ← `(def $cmdIdent : Cmd := $cmdConstructorIdent $handlerTerm)
+ return mkNullNode (typedParsedDecls ++ #[cmd])
+end TypedCli
+
+end Cli
diff --git a/CliTest/Example.lean b/CliTest/Example.lean
index af86a39..4b69977 100644
--- a/CliTest/Example.lean
+++ b/CliTest/Example.lean
@@ -2,43 +2,14 @@ import Cli
open Cli
-def runExampleCmd (p : Parsed) : IO UInt32 := do
- let input : String := p.positionalArg! "input" |>.as! String
- let outputs : Array String := p.variableArgsAs! String
- IO.println <| "Input: " ++ input
- IO.println <| "Outputs: " ++ toString outputs
-
- if p.hasFlag "verbose" then
- IO.println "Flag `--verbose` was set."
- if p.hasFlag "invert" then
- IO.println "Flag `--invert` was set."
- if p.hasFlag "optimize" then
- IO.println "Flag `--optimize` was set."
-
- let priority : Nat := p.flag! "priority" |>.as! Nat
- IO.println <| "Flag `--priority` always has at least a default value: " ++ toString priority
-
- if p.hasFlag "module" then
- let moduleName : ModuleName := p.flag! "module" |>.as! ModuleName
- IO.println <| s!"Flag `--module` was set to `{moduleName}`."
-
- if let some setPathsFlag := p.flag? "set-paths" then
- IO.println <| toString <| setPathsFlag.as! (Array String)
- return 0
-
-def installCmd := `[Cli|
- installCmd NOOP;
+cli_def installCmd NOOP;
"installCmd provides an example for a subcommand without flags or arguments that does nothing. \
Versions can be omitted."
-]
-def testCmd := `[Cli|
- testCmd NOOP;
+cli_def testCmd NOOP;
"testCmd provides another example for a subcommand without flags or arguments that does nothing."
-]
-def exampleCmd : Cmd := `[Cli|
- exampleCmd VIA runExampleCmd; ["0.0.1"]
+cli_def exampleCmd; ["0.0.1"]
"This string denotes the description of `exampleCmd`."
FLAGS:
@@ -51,7 +22,7 @@ def exampleCmd : Cmd := `[Cli|
which can be used to reference Lean modules like `Init.Data.Array` \
or Lean files using a relative path like `Init/Data/Array.lean`."
"set-paths" : Array String; "Declares a flag `--set-paths` \
- that takes an argument of type `Array Nat`. \
+ that takes an argument of type `Array String`. \
Quotation marks allow the use of hyphens."
ARGS:
@@ -70,7 +41,30 @@ def exampleCmd : Cmd := `[Cli|
EXTENSIONS:
author "mhuisi";
defaultValues! #[("priority", "0")]
-]
+
+def runExampleCmd (p : exampleCmd.Parsed) : IO UInt32 := do
+ IO.println <| "Input: " ++ p.input
+ IO.println <| "Outputs: " ++ toString p.outputs
+
+ if p.verbose then
+ IO.println "Flag `--verbose` was set."
+ if p.invert then
+ IO.println "Flag `--invert` was set."
+ if p.optimize then
+ IO.println "Flag `--optimize` was set."
+
+ let priority : Nat := p.priority?.getD 0
+ IO.println <| "Flag `--priority` always has at least a default value: " ++ toString priority
+
+ if let some moduleName := p.module? then
+ IO.println <| s!"Flag `--module` was set to `{moduleName}`."
+
+ if let some setPaths := p.setPaths? then
+ IO.println <| toString setPaths
+
+ return 0
+
+def exampleCmd : Cmd := exampleCmd.mkCmd runExampleCmd
def main (args : List String) : IO UInt32 :=
exampleCmd.validate args
@@ -106,7 +100,6 @@ Yields:
0.0.1
-/
-
#eval main <| "-h".splitOn " "
/-
Yields:
@@ -135,7 +128,7 @@ Yields:
or Lean files using a relative path like
`Init/Data/Array.lean`.
--set-paths : Array String Declares a flag `--set-paths` that takes an
- argument of type `Array Nat`. Quotation marks
+ argument of type `Array String`. Quotation marks
allow the use of hyphens.
ARGS:
diff --git a/CliTest/Tests.lean b/CliTest/Tests.lean
index bfd41f0..cc1c020 100644
--- a/CliTest/Tests.lean
+++ b/CliTest/Tests.lean
@@ -1,5 +1,6 @@
import Cli.Basic
import Cli.Extensions
+import Cli.Typed
namespace Cli
@@ -416,4 +417,503 @@ end InvalidInputs
end ModuleName
+section CliDef
+
+-- Test cli_def VIA (backward-compatible mode)
+cli_def viaSubCmd VIA doNothing;
+ "a subcommand defined with cli_def VIA"
+
+-- Test cli_def NOOP
+cli_def noopSubCmd NOOP;
+ "a subcommand defined with cli_def NOOP"
+
+-- Test cli_def VIA with flags, args, extensions
+cli_def viaCmd VIA doNothing; ["1.0.0"]
+ "a command defined with cli_def VIA"
+
+ FLAGS:
+ verbose; "verbose flag"
+ n, count : Nat; "a typed flag"
+
+ ARGS:
+ input : String; "an input"
+
+ SUBCOMMANDS: viaSubCmd; noopSubCmd
+
+ EXTENSIONS:
+ defaultValues! #[("count", "0")]
+
+section ViaValidInputs
+
+/--
+info: "cmd: viaCmd; flags: #[--count=0]; positionalArgs: #[]; variableArgs: #[]"
+-/
+#guard_msgs in
+#eval viaCmd.processParsed "hello"
+
+/--
+info: "cmd: viaCmd; flags: #[--verbose, --count=5]; positionalArgs: #[]; variableArgs: #[]"
+-/
+#guard_msgs in
+#eval viaCmd.processParsed "--verbose -n5 hello"
+
+/--
+info: "cmd: viaCmd noopSubCmd; flags: #[]; positionalArgs: #[]; variableArgs: #[]"
+-/
+#guard_msgs in
+#eval viaCmd.processParsed "noopSubCmd"
+
+end ViaValidInputs
+
+section ViaInvalidInputs
+
+/-- info: "Missing positional argument `.`" -/
+#guard_msgs in
+#eval viaCmd.processParsed ""
+
+end ViaInvalidInputs
+
+-- Test cli_def RUN: typed struct generation and accessor correctness
+
+-- A simple subcommand used by the typed command
+cli_def typedSubCmd NOOP;
+ "a subcommand for the typed command"
+
+-- The main typed command exercises all feature combinations:
+-- parameterless flags, typed flags, short aliases, hyphenated names,
+-- positional args, variable args, subcommands, and extensions.
+cli_def typedCmd; ["2.0.0"]
+ "a command defined with cli_def RUN"
+
+ FLAGS:
+ verbose; "verbose flag"
+ x, unknown1; "flag with short name"
+ t, typed1 : String; "typed flag"
+ "p-n", "level-param" : Nat; "hyphenated flag"
+
+ ARGS:
+ input1 : String; "a string input"
+ input2 : Array Nat; "an array input"
+ ...outputs : Nat; "varargs"
+
+ SUBCOMMANDS: typedSubCmd
+
+ EXTENSIONS:
+ defaultValues! #[("level-param", "0")]
+
+ RUN fun p => do
+ -- Exercise every accessor to verify types are correct
+ IO.println <| "input1=" ++ p.input1
+ IO.println <| "input2=" ++ toString p.input2
+ IO.println <| "outputs=" ++ toString p.outputs
+ IO.println <| "verbose=" ++ toString p.verbose
+ IO.println <| "unknown1=" ++ toString p.unknown1
+ IO.println <| "typed1=" ++ toString p.typed1?
+ IO.println <| "levelParam=" ++ toString p.levelParam?
+ return 0
+
+section TypedStructExists
+
+-- Verify the Parsed struct exists and is Inhabited
+#check typedCmd.Parsed
+#check (inferInstance : Inhabited typedCmd.Parsed)
+
+-- Verify accessor types
+#check @typedCmd.Parsed.verbose -- typedCmd.Parsed → Bool
+#check @typedCmd.Parsed.unknown1 -- typedCmd.Parsed → Bool
+#check @typedCmd.Parsed.typed1? -- typedCmd.Parsed → Option String
+#check @typedCmd.Parsed.levelParam? -- typedCmd.Parsed → Option Nat
+#check @typedCmd.Parsed.input1 -- typedCmd.Parsed → String
+#check @typedCmd.Parsed.input2 -- typedCmd.Parsed → Array Nat
+#check @typedCmd.Parsed.outputs -- typedCmd.Parsed → Array Nat
+#check @typedCmd.Parsed.raw -- typedCmd.Parsed → Cli.Parsed
+
+end TypedStructExists
+
+section TypedValidInputs
+
+/--
+info: input1=foo
+input2=#[1, 2, 3]
+outputs=#[4, 5]
+verbose=true
+unknown1=false
+typed1=(some hello)
+levelParam=(some 7)
+---
+info: 0
+-/
+#guard_msgs in
+#eval typedCmd.validate <| "--verbose -t hello --level-param=7 foo 1,2,3 4 5".splitOn " "
+
+/--
+info: input1=foo
+input2=#[1]
+outputs=#[]
+verbose=false
+unknown1=true
+typed1=(some bar)
+levelParam=(some 0)
+---
+info: 0
+-/
+#guard_msgs in
+#eval typedCmd.validate <| "-x -tbar foo 1".splitOn " "
+
+/--
+info: input1=foo
+input2=#[1]
+outputs=#[]
+verbose=false
+unknown1=false
+typed1=none
+levelParam=(some 0)
+---
+info: 0
+-/
+#guard_msgs in
+#eval typedCmd.validate <| "foo 1".splitOn " "
+
+end TypedValidInputs
+
+section TypedParsing
+
+-- Verify that the typed command produces the same parse results as a
+-- manually-defined equivalent using `[Cli| ...]`
+def typedCmdEquiv : Cmd := `[Cli|
+ typedCmd VIA doNothing; ["2.0.0"]
+ "a command defined with cli_def RUN"
+
+ FLAGS:
+ verbose; "verbose flag"
+ x, unknown1; "flag with short name"
+ t, typed1 : String; "typed flag"
+ "p-n", "level-param" : Nat; "hyphenated flag"
+
+ ARGS:
+ input1 : String; "a string input"
+ input2 : Array Nat; "an array input"
+ ...outputs : Nat; "varargs"
+
+ SUBCOMMANDS: typedSubCmd
+
+ EXTENSIONS:
+ defaultValues! #[("level-param", "0")]
+]
+
+/--
+info: "cmd: typedCmd; flags: #[--verbose, --typed1=hello, --level-param=0]; positionalArgs: #[, ]; variableArgs: #[, ]"
+-/
+#guard_msgs in
+#eval typedCmd.processParsed "--verbose -t hello foo 1,2,3 4 5"
+
+-- Same parse result from the equivalent command
+/--
+info: "cmd: typedCmd; flags: #[--verbose, --typed1=hello, --level-param=0]; positionalArgs: #[, ]; variableArgs: #[, ]"
+-/
+#guard_msgs in
+#eval typedCmdEquiv.processParsed "--verbose -t hello foo 1,2,3 4 5"
+
+end TypedParsing
+
+section TypedInvalidInputs
+
+/-- info: "Missing positional argument `.`" -/
+#guard_msgs in
+#eval typedCmd.processParsed ""
+
+/-- info: "Missing positional argument `.`" -/
+#guard_msgs in
+#eval typedCmd.processParsed "foo"
+
+/-- info: "Invalid type of argument `abc` for positional argument ``." -/
+#guard_msgs in
+#eval typedCmd.processParsed "foo abc"
+
+/-- info: "Unknown flag `--nonexistent`." -/
+#guard_msgs in
+#eval typedCmd.processParsed "foo 1 --nonexistent"
+
+end TypedInvalidInputs
+
+section TypedInfo
+
+/--
+info: "typedCmd [2.0.0]\na command defined with cli_def RUN\n\nUSAGE:\n typedCmd [SUBCOMMAND] [FLAGS] ...\n\nFLAGS:\n -h, --help Prints this message.\n --version Prints the version.\n --verbose verbose flag\n -x, --unknown1 flag with short name\n -t, --typed1 : String typed flag\n -p-n, --level-param : Nat hyphenated flag [Default: `0`]\n\nARGS:\n input1 : String a string input\n input2 : Array Nat an array input\n outputs : Nat varargs\n\nSUBCOMMANDS:\n typedSubCmd a subcommand for the typed command"
+-/
+#guard_msgs in
+#eval typedCmd.extendedHelp
+
+end TypedInfo
+
+-- Test: cli_def RUN with no flags and no variable args
+cli_def minimalCmd;
+ "a minimal command with only a positional arg"
+
+ ARGS:
+ name : String; "your name"
+
+ RUN fun p => do
+ IO.println <| "Hello, " ++ p.name
+ return 0
+
+/--
+info: Hello, World
+---
+info: 0
+-/
+#guard_msgs in
+#eval minimalCmd.validate <| ["World"]
+
+-- Test: cli_def RUN with only flags, no args
+cli_def flagsOnlyCmd;
+ "a command with only flags"
+
+ FLAGS:
+ a, alpha; "first flag"
+ b, beta; "second flag"
+
+ RUN fun p => do
+ IO.println <| "alpha=" ++ toString p.alpha ++ " beta=" ++ toString p.beta
+ return 0
+
+/--
+info: alpha=true beta=false
+---
+info: 0
+-/
+#guard_msgs in
+#eval flagsOnlyCmd.validate <| ["-a"]
+
+/--
+info: alpha=true beta=true
+---
+info: 0
+-/
+#guard_msgs in
+#eval flagsOnlyCmd.validate <| ["-ab"]
+
+-- Test: hyphenToCamelCase conversion for hyphenated arg names
+cli_def hyphenCmd;
+ "tests hyphenated names"
+
+ FLAGS:
+ "my-flag"; "a hyphenated flag"
+ "another-long-flag" : String; "a typed hyphenated flag"
+
+ ARGS:
+ "my-input" : String; "a hyphenated arg"
+
+ RUN fun p => do
+ IO.println <| "myFlag=" ++ toString p.myFlag
+ IO.println <| "anotherLongFlag=" ++ toString p.anotherLongFlag?
+ IO.println <| "myInput=" ++ p.myInput
+ return 0
+
+/--
+info: myFlag=true
+anotherLongFlag=(some hello)
+myInput=world
+---
+info: 0
+-/
+#guard_msgs in
+#eval hyphenCmd.validate <| "--my-flag --another-long-flag=hello world".splitOn " "
+
+-- Test: raw accessor provides access to underlying Cli.Parsed
+cli_def rawAccessCmd;
+ "tests raw accessor"
+
+ ARGS:
+ input : String; "an input"
+
+ RUN fun p => do
+ -- Access via typed accessor
+ IO.println <| "typed: " ++ p.input
+ -- Access via raw Cli.Parsed
+ IO.println <| "raw: " ++ (p.raw.positionalArg! "input" |>.as! String)
+ return 0
+
+/--
+info: typed: hello
+raw: hello
+---
+info: 0
+-/
+#guard_msgs in
+#eval rawAccessCmd.validate <| ["hello"]
+
+-- Test: signature-only cli_def generates struct + mkCmd but no `def`
+cli_def sigOnlyCmd; ["1.0.0"]
+ "a command defined with signature-only cli_def"
+
+ FLAGS:
+ verbose; "verbose flag"
+ n, count : Nat; "a count flag"
+
+ ARGS:
+ input : String; "an input"
+ ...extras : String; "extra args"
+
+section SigOnlyStructExists
+-- Verify the struct and mkCmd exist
+#check sigOnlyCmd.Parsed
+#check (inferInstance : Inhabited sigOnlyCmd.Parsed)
+#check @sigOnlyCmd.Parsed.verbose -- sigOnlyCmd.Parsed → Bool
+#check @sigOnlyCmd.Parsed.count? -- sigOnlyCmd.Parsed → Option Nat
+#check @sigOnlyCmd.Parsed.input -- sigOnlyCmd.Parsed → String
+#check @sigOnlyCmd.Parsed.extras -- sigOnlyCmd.Parsed → Array String
+#check @sigOnlyCmd.mkCmd -- (sigOnlyCmd.Parsed → IO UInt32) → Cmd
+end SigOnlyStructExists
+
+-- Define the handler as a standalone function using the generated type
+def runSigOnlyCmd (p : sigOnlyCmd.Parsed) : IO UInt32 := do
+ IO.println <| "input=" ++ p.input
+ IO.println <| "extras=" ++ toString p.extras
+ IO.println <| "verbose=" ++ toString p.verbose
+ IO.println <| "count=" ++ toString p.count?
+ return 0
+
+-- Attach the handler via mkCmd
+def sigOnlyCmd : Cmd := sigOnlyCmd.mkCmd runSigOnlyCmd
+
+/--
+info: input=hello
+extras=#[a, b]
+verbose=true
+count=(some 42)
+---
+info: 0
+-/
+#guard_msgs in
+#eval sigOnlyCmd.validate <| "--verbose --count=42 hello a b".splitOn " "
+
+/--
+info: input=world
+extras=#[]
+verbose=false
+count=none
+---
+info: 0
+-/
+#guard_msgs in
+#eval sigOnlyCmd.validate <| ["world"]
+
+-- Verify help output includes all metadata
+/--
+info: "sigOnlyCmd [1.0.0]\na command defined with signature-only cli_def\n\nUSAGE:\n sigOnlyCmd [FLAGS] ...\n\nFLAGS:\n -h, --help Prints this message.\n --version Prints the version.\n --verbose verbose flag\n -n, --count : Nat a count flag\n\nARGS:\n input : String an input\n extras : String extra args"
+-/
+#guard_msgs in
+#eval sigOnlyCmd.extendedHelp
+
+-- Test: signature-only with RUN-equivalent pattern produces same results
+-- as inline RUN
+cli_def sigEquivCmd;
+ "equivalent command"
+
+ FLAGS:
+ verbose; "verbose"
+
+ ARGS:
+ name : String; "a name"
+
+ RUN fun p => do
+ IO.println <| "name=" ++ p.name ++ " verbose=" ++ toString p.verbose
+ return 0
+
+cli_def sigEquivCmd2;
+ "equivalent command"
+
+ FLAGS:
+ verbose; "verbose"
+
+ ARGS:
+ name : String; "a name"
+
+def sigEquivCmd2 : Cmd := sigEquivCmd2.mkCmd fun p => do
+ IO.println <| "name=" ++ p.name ++ " verbose=" ++ toString p.verbose
+ return 0
+
+-- Both should produce the same output
+/--
+info: name=Alice verbose=true
+---
+info: 0
+-/
+#guard_msgs in
+#eval sigEquivCmd.validate <| "--verbose Alice".splitOn " "
+
+/--
+info: name=Alice verbose=true
+---
+info: 0
+-/
+#guard_msgs in
+#eval sigEquivCmd2.validate <| "--verbose Alice".splitOn " "
+
+-- Test: signature-only cli_def with subcommands
+cli_def sigSubA NOOP;
+ "subcommand A"
+
+cli_def sigSubB;
+ "subcommand B"
+
+ FLAGS:
+ force; "force flag"
+
+ RUN fun p => do
+ IO.println <| "subB force=" ++ toString p.force
+ return 0
+
+cli_def sigParentCmd; ["3.0.0"]
+ "a parent command with subcommands, defined signature-only"
+
+ FLAGS:
+ verbose; "verbose flag"
+
+ ARGS:
+ target : String; "a target"
+
+ SUBCOMMANDS:
+ sigSubA;
+ sigSubB
+
+def runSigParentCmd (p : sigParentCmd.Parsed) : IO UInt32 := do
+ IO.println <| "target=" ++ p.target ++ " verbose=" ++ toString p.verbose
+ return 0
+
+def sigParentCmd : Cmd := sigParentCmd.mkCmd runSigParentCmd
+
+-- Run parent command
+/--
+info: target=foo verbose=true
+---
+info: 0
+-/
+#guard_msgs in
+#eval sigParentCmd.validate <| "--verbose foo".splitOn " "
+
+-- Run subcommand A (NOOP)
+/-- info: 0 -/
+#guard_msgs in
+#eval sigParentCmd.validate <| "sigSubA".splitOn " "
+
+-- Run subcommand B
+/--
+info: subB force=true
+---
+info: 0
+-/
+#guard_msgs in
+#eval sigParentCmd.validate <| "sigSubB --force".splitOn " "
+
+-- Verify help lists subcommands
+/--
+info: "sigParentCmd [3.0.0]\na parent command with subcommands, defined signature-only\n\nUSAGE:\n sigParentCmd [SUBCOMMAND] [FLAGS] \n\nFLAGS:\n -h, --help Prints this message.\n --version Prints the version.\n --verbose verbose flag\n\nARGS:\n target : String a target\n\nSUBCOMMANDS:\n sigSubA subcommand A\n sigSubB subcommand B"
+-/
+#guard_msgs in
+#eval sigParentCmd.extendedHelp
+
+end CliDef
+
end Cli
diff --git a/README.md b/README.md
index 099320f..2fef019 100644
--- a/README.md
+++ b/README.md
@@ -5,24 +5,21 @@ This project is maintained by [@mhuisi](https://github.com/mhuisi).
See [the documentation of Lake](https://github.com/leanprover/lean4/blob/master/src/lake/README.md).
### Configuration
-Commands are configured with a lightweight DSL. The following declarations define a command `exampleCmd` with two subcommands `installCmd` and `testCmd`. `runExampleCmd` denotes a handler that is called when the command is run and is described further down below in the **Command Handlers** subsection.
+Commands are configured with a lightweight DSL. The following declarations define a command `exampleCmd` with two subcommands `installCmd` and `testCmd`.
+
+The signature-only `cli_def` generates a typed `exampleCmd.Parsed` wrapper struct with accessor functions for each flag and argument, and an `exampleCmd.mkCmd` constructor that takes a typed handler. The handler is then defined as a standalone function and attached via `mkCmd`.
```Lean
open Cli
-def installCmd := `[Cli|
- installCmd NOOP;
+cli_def installCmd NOOP;
"installCmd provides an example for a subcommand without flags or arguments that does nothing. \
Versions can be omitted."
-]
-def testCmd := `[Cli|
- testCmd NOOP;
+cli_def testCmd NOOP;
"testCmd provides another example for a subcommand without flags or arguments that does nothing."
-]
-def exampleCmd : Cmd := `[Cli|
- exampleCmd VIA runExampleCmd; ["0.0.1"]
+cli_def exampleCmd; ["0.0.1"]
"This string denotes the description of `exampleCmd`."
FLAGS:
@@ -35,7 +32,7 @@ def exampleCmd : Cmd := `[Cli|
which can be used to reference Lean modules like `Init.Data.Array` \
or Lean files using a relative path like `Init/Data/Array.lean`."
"set-paths" : Array String; "Declares a flag `--set-paths` \
- that takes an argument of type `Array Nat`. \
+ that takes an argument of type `Array String`. \
Quotation marks allow the use of hyphens."
ARGS:
@@ -54,36 +51,35 @@ def exampleCmd : Cmd := `[Cli|
EXTENSIONS:
author "mhuisi";
defaultValues! #[("priority", "0")]
-]
```
### Command handlers
-The command handler `runExampleCmd` demonstrates how to use the parsed user input.
+The command handler `runExampleCmd` demonstrates how to use the parsed input.
```Lean
-def runExampleCmd (p : Parsed) : IO UInt32 := do
- let input : String := p.positionalArg! "input" |>.as! String
- let outputs : Array String := p.variableArgsAs! String
- IO.println <| "Input: " ++ input
- IO.println <| "Outputs: " ++ toString outputs
+def runExampleCmd (p : exampleCmd.Parsed) : IO UInt32 := do
+ IO.println <| "Input: " ++ p.input
+ IO.println <| "Outputs: " ++ toString p.outputs
- if p.hasFlag "verbose" then
+ if p.verbose then
IO.println "Flag `--verbose` was set."
- if p.hasFlag "invert" then
+ if p.invert then
IO.println "Flag `--invert` was set."
- if p.hasFlag "optimize" then
+ if p.optimize then
IO.println "Flag `--optimize` was set."
- let priority : Nat := p.flag! "priority" |>.as! Nat
+ let priority : Nat := p.priority?.getD 0
IO.println <| "Flag `--priority` always has at least a default value: " ++ toString priority
- if p.hasFlag "module" then
- let moduleName : ModuleName := p.flag! "module" |>.as! ModuleName
+ if let some moduleName := p.module? then
IO.println <| s!"Flag `--module` was set to `{moduleName}`."
- if let some setPathsFlag := p.flag? "set-paths" then
- IO.println <| toString <| setPathsFlag.as! (Array String)
+ if let some setPaths := p.setPaths? then
+ IO.println <| toString setPaths
+
return 0
+
+def exampleCmd : Cmd := exampleCmd.mkCmd runExampleCmd
```
### Running the command
@@ -154,8 +150,8 @@ FLAGS:
or Lean files using a relative path like
`Init/Data/Array.lean`.
--set-paths : Array String Declares a flag `--set-paths` that takes an
- argument of type `Array String`. Quotation marks
- allow the use of hyphens.
+ argument of type `Array String`. Quotation
+ marks allow the use of hyphens.
ARGS:
input : String Declares a positional argument that takes an
@@ -173,7 +169,7 @@ SUBCOMMANDS:
The full example can be found under `./CliTest/Example.lean`.
## Ad Hoc Documentation
-This section documents only the most common features of the library. For the full documentation, peek into `./Cli/Basic.lean` and `./Cli/Extensions.lean`! All definitions below live in the `Cli` namespace.
+This section documents only the most common features of the library. For the full documentation, peek into `./Cli/Basic.lean`, `./Cli/Typed.lean` and `./Cli/Extensions.lean`! All definitions below live in the `Cli` namespace.
```Lean
-- In a `nameableStringArg`, string literals can either be used directly or an identifier can be
@@ -191,17 +187,30 @@ syntax variableArg := colGe "..." literalIdent " : " term "; " nameableStringArg
syntax flag := colGe literalIdent ("," literalIdent)? (" : " term)? "; " nameableStringArg
-syntax "`[Cli|\n"
- literalIdent runFun "; " ("[" nameableStringArg "]")?
+-- Untyped variant: generates `def : Cmd` with the given handler or a no-op handler.
+-- Does not generate typed accessors.
+syntax "cli_def " literalIdent runFun "; " ("[" nameableStringArg "]")?
nameableStringArg
("FLAGS:\n" withPosition((flag)*))?
("ARGS:\n" withPosition((positionalArg)* (variableArg)?))?
("SUBCOMMANDS: " sepBy(ident, ";", "; "))?
("EXTENSIONS: " sepBy(term, ";", "; "))?
- "\n]" : term
-```
+ : command
+
+-- Typed variant: generates `.Parsed` (a typed struct), accessors, and
+-- `.mkCmd : (.Parsed → IO UInt32) → Cmd`.
+-- With `RUN`: additionally generates `def : Cmd := .mkCmd `.
+-- Without `RUN`: the user defines their handler separately
+-- and attaches it via `def : Cmd := .mkCmd `.
+syntax "cli_def " literalIdent "; " ("[" nameableStringArg "]")?
+ nameableStringArg
+ ("FLAGS:\n" withPosition((flag)*))?
+ ("ARGS:\n" withPosition((positionalArg)* (variableArg)?))?
+ ("SUBCOMMANDS: " sepBy(ident, ";", "; "))?
+ ("EXTENSIONS: " sepBy(term, ";", "; "))?
+ (" RUN " term)?
+ : command
-```Lean
/--
Validates `args` by `Cmd.process?`ing the input according to `c`.
Note that `args` designates the list `` in `somebinary `.
@@ -211,157 +220,5 @@ If neither of these flags were passed and processing was successful, the `run` h
called command is executed.
In the case of a processing error, the error is printed to stderr and an exit code of `1` is returned.
-/
-def validate (c : Cmd) (args : List String) : IO UInt32 := do
- let result := c.process args
- match result with
- | .ok (cmd, parsed) =>
- if parsed.hasFlag "help" then
- parsed.printHelp
- return 0
- if parsed.cmd.meta.hasVersion ∧ parsed.hasFlag "version" then
- parsed.printVersion!
- return 0
- cmd.run parsed
- | .error (cmd, err) =>
- cmd.printError err
- return 1
-```
-```Lean
- /-- Represents parsed user input data. -/
- structure Parsed where
- /-- Recursive meta-data of the associated command. -/
- cmd : Parsed.Cmd
- /-- Parent of the associated command. -/
- parent? : Option Parsed.Cmd
- /-- Parsed flags. -/
- flags : Array Parsed.Flag
- /-- Parsed positional arguments. -/
- positionalArgs : Array Parsed.Arg
- /-- Parsed variable arguments. -/
- variableArgs : Array Parsed.Arg
- deriving Inhabited
-
-namespace Parsed
- /-- Parent of the associated command. -/
- def parent! (p : Parsed) : Parsed.Cmd
-
- /-- Checks whether the associated command has a parent, i.e. whether it is not the root command. -/
- def hasParent (p : Parsed) : Bool
-
- /-- Finds the parsed flag in `p` with the corresponding `longName`. -/
- def flag? (p : Parsed) (longName : String) : Option Flag
- /-- Finds the parsed positional argument in `p` with the corresponding `name`. -/
- def positionalArg? (p : Parsed) (name : String) : Option Arg
-
- /-- Finds the parsed flag in `p` with the corresponding `longName`. -/
- def flag! (p : Parsed) (longName : String) : Flag
- /-- Finds the parsed positional argument in `p` with the corresponding `name`. -/
- def positionalArg! (p : Parsed) (name : String) : Arg
-
- /-- Checks whether `p` has a parsed flag with the corresponding `longName`. -/
- def hasFlag (p : Parsed) (longName : String) : Bool
- /-- Checks whether `p` has a positional argument with the corresponding `longName`. -/
- def hasPositionalArg (p : Parsed) (name : String) : Bool
-
- /--
- Converts all `p.variableArgs` values to `τ`, which should be the same type
- that was designated in the corresponding `Cli.Arg`.
- Yields `none` if the conversion was unsuccessful, which can only
- happen if `τ` is not the same type as the one designated in the corresponding `Cli.Arg`.
- -/
- def variableArgsAs? (p : Parsed) (τ) [ParseableType τ] : Option (Array τ)
-
- /--
- Converts all `p.variableArgs` values to `τ`, which should be the same type
- that was designated in the corresponding `Cli.Arg`.
- Panics if the conversion was unsuccessful, which can only
- happen if `τ` is not the same type as the one designated in the corresponding `Cli.Arg`.
- -/
- def variableArgsAs! (p : Parsed) (τ) [Inhabited τ] [ParseableType τ] : Array τ
-end Parsed
-```
-```Lean
-namespace Parsed
- /--
- Represents a flag and its parsed value.
- Use `Parsed.Flag.as!` to convert the value to some `ParseableType`.
- -/
- structure Flag where
- /-- Associated flag meta-data. -/
- flag : Flag
- /-- Parsed value that was validated and conforms to `flag.type`. -/
- value : String
-
- namespace Flag
- /--
- Converts `f.value` to `τ`, which should be the same type
- that was designated in `f.flag.type`.
- Yields `none` if the conversion was unsuccessful, which can only
- happen if `τ` is not the same type as the one designated in `f.flag.type`.
- -/
- def as? (f : Flag) (τ) [ParseableType τ] : Option τ
- /--
- Converts `f.value` to `τ`, which should be the same type
- that was designated in `f.flag.type`.
- Panics if the conversion was unsuccessful, which can only
- happen if `τ` is not the same type as the one designated in `f.flag.type`.
- -/
- def as! (f : Flag) (τ) [Inhabited τ] [ParseableType τ] : τ
- end Flag
-
- /--
- Represents an argument and its parsed value.
- Use `Parsed.Arg.as!` to convert the value to some `ParseableType`.
- -/
- structure Arg where
- /-- Associated argument meta-data. -/
- arg : Arg
- /-- Parsed value that was validated and conforms to `arg.type`. -/
- value : String
-
- namespace Arg
- /--
- Converts `a.value` to `τ`, which should be the same type
- that was designated in `a.arg.type`.
- Yields `none` if the conversion was unsuccessful, which can only
- happen if `τ` is not the same type as the one designated in `a.arg.type`.
- -/
- def as? (a : Arg) (τ) [ParseableType τ] : Option τ
- /--
- Converts `a.value` to `τ`, which should be the same type
- that was designated in `a.arg.type`.
- Panics if the conversion was unsuccessful, which can only
- happen if `τ` is not the same type as the one designated in `a.arg.type`.
- -/
- def as! (a : Arg) (τ) [Inhabited τ] [ParseableType τ] : τ
- end Arg
-end Parsed
-```
-```Lean
-/--
-Creates a new command. Adds a `-h, --help` and a `--version` flag if a version is designated.
-Updates the `parentNames` of all subcommands.
-- `name`: Name that is displayed in the help.
-- `version?`: Version that is displayed in the help and when the version is queried.
-- `description`: Description that is displayed in the help.
-- `furtherInformation?`: Information appended to the end of the help. Useful for command extensions.
-- `flags`: Supported flags ("options" in standard terminology).
-- `positionalArgs`: Supported positional arguments ("operands" in standard terminology).
-- `variableArg?`: Variable argument at the end of the positional arguments.
-- `run`: Handler to run when the command is called and flags/arguments have been successfully processed.
-- `subCmds`: Subcommands.
-- `extension?`: Extension of the Cli library.
--/
-def mk
- (name : String)
- (version? : Option String)
- (description : String)
- (furtherInformation? : Option String := none)
- (flags : Array Flag := #[])
- (positionalArgs : Array Arg := #[])
- (variableArg? : Option Arg := none)
- (run : Parsed → IO UInt32)
- (subCmds : Array Cmd := #[])
- (extension? : Option Extension := none)
- : Cmd
+def validate (c : Cmd) (args : List String) : IO UInt32
```