Skip to content

Commit 5e14601

Browse files
authored
Merge pull request #76 from Tarmil/tarmil-75
Fix #75: Add SubCommand attribute for nullary subcommands
2 parents 78cfbb6 + 94048e9 commit 5e14601

File tree

7 files changed

+64
-12
lines changed

7 files changed

+64
-12
lines changed

src/Argu/Attributes.fs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,13 @@ type FirstAttribute () = inherit CliPositionAttribute (CliPosition.First)
8989
[<AttributeUsage(AttributeTargets.Property, AllowMultiple = false)>]
9090
type LastAttribute () = inherit CliPositionAttribute (CliPosition.Last)
9191

92+
/// Declares that argument is a subcommand.
93+
/// A parse exception will be raised if the argument has parameters
94+
/// and their type is not ParseResults<_>.
95+
/// Implicit if the argument does have a parameter of type ParseResults<_>.
96+
[<AttributeUsage(AttributeTargets.Property, AllowMultiple = false)>]
97+
type SubCommandAttribute () = inherit Attribute()
98+
9299
/// Declares that argument is the main command of the CLI syntax.
93100
/// Arguments are specified without requiring a switch.
94101
[<AttributeUsage(AttributeTargets.Property, AllowMultiple = false)>]

src/Argu/Parsers/Cli.fs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ type CliParseResultAggregator internal (argInfo : UnionArgInfo, stack : CliParse
103103
error argInfo ErrorCode.CommandLine "argument '%s' has been specified more than once." result.CaseInfo.Name.Value
104104

105105
if result.CaseInfo.ArgumentType = ArgumentType.SubCommand then
106+
if isSubCommandDefined then
107+
error argInfo ErrorCode.CommandLine "cannot run multiple subcommands."
106108
isSubCommandDefined <- true
107109

108110
agg.Add result
@@ -403,6 +405,9 @@ let rec private parseCommandLinePartial (state : CliParseState) (argInfo : Union
403405

404406
aggregator.AppendResult caseInfo name [|result|]
405407

408+
| NullarySubCommand ->
409+
aggregator.AppendResult caseInfo name [||]
410+
406411
and private parseCommandLineInner (state : CliParseState) (argInfo : UnionArgInfo) =
407412
let results = state.ResultStack.CreateNextAggregator argInfo
408413
while not state.Reader.IsCompleted do parseCommandLinePartial state argInfo results
@@ -426,4 +431,4 @@ and parseCommandLine (argInfo : UnionArgInfo) (programName : string) (descriptio
426431
Exiter = exiter
427432
}
428433

429-
parseCommandLineInner state argInfo
434+
parseCommandLineInner state argInfo

src/Argu/Parsers/KeyValue.fs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ let private parseKeyValuePartial (state : KeyValueParseState) (caseInfo : UnionC
9292
let case = mkUnionCase caseInfo caseInfo.Tag ParseSource.AppSettings name [|results|]
9393
success [|case|]
9494

95+
| NullarySubCommand
9596
| SubCommand _ -> () // AppSettings will not handle subcommands
9697

9798
| _ -> ()

src/Argu/PreCompute.fs

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,7 @@ let rec private preComputeUnionCaseArgInfo (stack : Type list) (helpParam : Help
308308
let isGatherAll = lazy(hasAttribute<GatherAllSourcesAttribute> attributes.Value)
309309
let isRest = lazy(hasAttribute<RestAttribute> attributes.Value)
310310
let isHidden = lazy(hasAttribute<HiddenAttribute> attributes.Value)
311+
let isExplicitSubCommand = lazy(hasAttribute<SubCommandAttribute> attributes.Value)
311312

312313
let mainCommandName = lazy(
313314
match tryGetAttribute<MainCommandAttribute> attributes.Value with
@@ -374,21 +375,25 @@ let rec private preComputeUnionCaseArgInfo (stack : Type list) (helpParam : Help
374375
| [|NestedParseResults _|] -> ArgumentType.SubCommand
375376
| [|Optional _|] -> ArgumentType.Optional
376377
| [|List _|] -> ArgumentType.List
378+
| _ when isExplicitSubCommand.Value -> ArgumentType.SubCommand
377379
| _ -> ArgumentType.Primitive
378380

381+
let checkSubCommand() =
382+
if Option.isSome customAssignmentSeparator.Value then
383+
arguExn "CustomAssignment in '%O' not supported in subcommands." uci
384+
if isRest.Value then
385+
arguExn "Rest attribute in '%O' not supported in subcommands." uci
386+
if isMandatory.Value then
387+
arguExn "Mandatory attribute in '%O' not supported in subcommands." uci
388+
if isMainCommand.Value then
389+
arguExn "MainCommand attribute in '%O' not supported in subcommands." uci
390+
if isInherited.Value then
391+
arguExn "Inherit attribute in '%O' not supported in subcommands." uci
392+
379393
let parsers = lazy(
380394
match types with
381395
| [|NestedParseResults prt|] ->
382-
if Option.isSome customAssignmentSeparator.Value then
383-
arguExn "CustomAssignment in '%O' not supported in subcommands." uci
384-
if isRest.Value then
385-
arguExn "Rest attribute in '%O' not supported in subcommands." uci
386-
if isMandatory.Value then
387-
arguExn "Mandatory attribute in '%O' not supported in subcommands." uci
388-
if isMainCommand.Value then
389-
arguExn "MainCommand attribute in '%O' not supported in subcommands." uci
390-
if isInherited.Value then
391-
arguExn "Inherit attribute in '%O' not supported in subcommands." uci
396+
checkSubCommand()
392397

393398
let argInfo = preComputeUnionArgInfoInner stack helpParam tryGetCurrent prt
394399
let shape = ShapeArgumentTemplate.FromType prt
@@ -417,6 +422,12 @@ let rec private preComputeUnionCaseArgInfo (stack : Type list) (helpParam : Help
417422
ListParam(Existential.FromType t, getPrimitiveParserByType label t)
418423

419424
| _ ->
425+
if isExplicitSubCommand.Value then
426+
if not (Array.isEmpty fields) then
427+
arguExn "SubCommand in '%O' not supported for parameters other than ParseResults<_>." uci
428+
checkSubCommand()
429+
NullarySubCommand
430+
else
420431
let getParser (p : PropertyInfo) =
421432
let label = tryExtractUnionParameterLabel p
422433
getPrimitiveParserByType label p.PropertyType

src/Argu/UnParsers.fs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ let mkCommandLineSyntax (argInfo : UnionArgInfo) (prefix : string) (maxWidth : i
8585
| SubCommand (label = None) -> yield " <options>"
8686
| SubCommand (label = Some label) -> yield sprintf " <%s>" label
8787
| ListParam (_,parser) -> yield sprintf " [<%s>...]" parser.Description
88+
| NullarySubCommand -> ()
8889

8990
if not aI.IsMandatory.Value then yield ']'
9091
}
@@ -171,6 +172,7 @@ let mkArgUsage width (aI : UnionCaseArgInfo) = stringExpr {
171172

172173
| SubCommand(_,_,Some label) -> yield sprintf " <%s>" label
173174
| SubCommand(_,_,None) -> yield " <options>"
175+
| NullarySubCommand -> ()
174176

175177
let! finish = StringExpr.currentLength
176178
if finish - start >= descriptionOffset then
@@ -332,6 +334,7 @@ let rec mkCommandLineArgs (argInfo : UnionArgInfo) (args : seq<obj>) =
332334
if not aI.IsMainCommand then yield clName()
333335
let nestedResult = fields.[0] :?> IParseResult
334336
yield! mkCommandLineArgs nested (nestedResult.GetAllResults())
337+
| NullarySubCommand -> yield clName()
335338
}
336339

337340
args
@@ -363,6 +366,7 @@ let mkAppSettingsDocument (argInfo : UnionArgInfo) printComments (args : 'Templa
363366
| None -> [||]
364367
| Some key ->
365368
match aI.ParameterInfo.Value with
369+
| NullarySubCommand
366370
| SubCommand _ -> [||]
367371
| Primitives parsers ->
368372
let values =
@@ -419,4 +423,4 @@ let mkAppSettingsDocument (argInfo : UnionArgInfo) printComments (args : 'Templa
419423

420424
XDocument(
421425
XElement(XName.Get "configuration",
422-
XElement(XName.Get "appSettings", Seq.collect mkArgumentEntry args)))
426+
XElement(XName.Get "appSettings", Seq.collect mkArgumentEntry args)))

src/Argu/UnionArgInfo.fs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,12 +127,14 @@ and ParameterInfo =
127127
| OptionalParam of Existential * FieldParserInfo
128128
| ListParam of Existential * FieldParserInfo
129129
| SubCommand of ShapeArgumentTemplate * argInfo:UnionArgInfo * label:string option
130+
| NullarySubCommand
130131
with
131132
member pI.Type =
132133
match pI with
133134
| Primitives _ -> ArgumentType.Primitive
134135
| OptionalParam _ -> ArgumentType.Optional
135136
| ListParam _ -> ArgumentType.List
137+
| NullarySubCommand
136138
| SubCommand _ -> ArgumentType.SubCommand
137139

138140
and [<NoEquality; NoComparison>]

tests/Argu.Tests/Tests.fs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ module ``Argu Tests`` =
4949
type RequiredSubcommand =
5050
| Foo
5151
| [<CliPrefix(CliPrefix.None)>] Sub of ParseResults<CleanArgs>
52+
| [<SubCommand; CliPrefix(CliPrefix.None)>] Null_Sub
5253
with
5354
interface IArgParserTemplate with
5455
member this.Usage = "required"
@@ -89,6 +90,7 @@ module ``Argu Tests`` =
8990
| [<CliPrefix(CliPrefix.None)>] Clean of ParseResults<CleanArgs>
9091
| [<CliPrefix(CliPrefix.None)>] Required of ParseResults<RequiredSubcommand>
9192
| [<CliPrefix(CliPrefix.None)>] Unrecognized of ParseResults<GatherUnrecognizedSubcommand>
93+
| [<SubCommand; CliPrefix(CliPrefix.None)>] Nullary_Sub
9294
with
9395
interface IArgParserTemplate with
9496
member a.Usage =
@@ -115,6 +117,7 @@ module ``Argu Tests`` =
115117
| Clean _ -> "clean state"
116118
| Required _ -> "required subcommand"
117119
| Unrecognized _ -> "unrecognized subcommand"
120+
| Nullary_Sub -> "nullary subcommand"
118121
| List _ -> "variadic params"
119122
| Optional _ -> "optional params"
120123
| A | B | C -> "misc arguments"
@@ -413,6 +416,25 @@ module ``Argu Tests`` =
413416
raisesWith<ArguParseException> <@ parser.ParseCommandLine(args, ignoreMissing = true) @>
414417
(fun e -> <@ e.FirstLine.Contains "subcommand" @>)
415418

419+
[<Fact>]
420+
let ``Nullary subcommand`` () =
421+
let args = [|"nullary-sub"|]
422+
let results = parser.ParseCommandLine(args, ignoreMissing = true)
423+
test <@ results.TryGetSubCommand() = Some Nullary_Sub @>
424+
425+
[<Fact>]
426+
let ``Required subcommand should succeed on nullary subcommand`` () =
427+
let args = [|"required"; "null-sub"|]
428+
let results = parser.ParseCommandLine(args, ignoreMissing = true)
429+
let nested = results.GetResult <@ Required @>
430+
test <@ nested.TryGetSubCommand() = Some Null_Sub @>
431+
432+
[<Fact>]
433+
let ``Calling both a nullary subcommand a normal one should fail`` () =
434+
let args = [|"required"; "null-sub"; "sub"; "-fdx"|]
435+
raisesWith<ArguParseException> <@ parser.ParseCommandLine(args, ignoreMissing = true) @>
436+
(fun e -> <@ e.FirstLine.Contains "subcommand" @>)
437+
416438
[<Fact>]
417439
let ``GatherUnrecognized attribute`` () =
418440
let args = [|"--mandatory-arg" ; "true" ; "unrecognized" ; "uarg1" ; "--switch1" ; "uarg2"|]

0 commit comments

Comments
 (0)