Skip to content

Commit

Permalink
Allow consuming *all* args as positionals, not just ones which look l…
Browse files Browse the repository at this point in the history
…ike `--foo` (#255)
  • Loading branch information
Smaug123 authored Sep 11, 2024
1 parent 913959a commit 5748ac3
Show file tree
Hide file tree
Showing 9 changed files with 620 additions and 121 deletions.
16 changes: 16 additions & 0 deletions ConsumePlugin/Args.fs
Original file line number Diff line number Diff line change
Expand Up @@ -195,3 +195,19 @@ type ManyLongForms =
type private IrrelevantDu =
| Foo
| Bar

[<ArgParser true>]
type FlagsIntoPositionalArgs =
{
A : string
[<PositionalArgs true>]
GrabEverything : string list
}

[<ArgParser true>]
type FlagsIntoPositionalArgs' =
{
A : string
[<PositionalArgs false>]
DontGrabEverything : string list
}
542 changes: 467 additions & 75 deletions ConsumePlugin/GeneratedArgs.fs

Large diffs are not rendered by default.

15 changes: 14 additions & 1 deletion WoofWare.Myriad.Plugins.Attributes/ArgParserAttributes.fs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,22 @@ type ArgParserAttribute (isExtensionMethod : bool) =

/// Attribute indicating that this field shall accumulate all unmatched args,
/// as well as any that appear after a bare `--`.
type PositionalArgsAttribute () =
///
/// Set `includeFlagLike = true` to include args that begin `--` in the
/// positional args.
/// (By default, `includeFlagLike = false` and we throw when encountering
/// an argument which looks like a flag but which we don't recognise.)
/// We will still interpret `--help` as requesting help, unless it comes after
/// a standalone `--` separator.
type PositionalArgsAttribute (includeFlagLike : bool) =
inherit Attribute ()

/// The default value of `isExtensionMethod`, the optional argument to the ArgParserAttribute constructor.
static member DefaultIncludeFlagLike = false

/// Shorthand for the "includeFlagLike = false" constructor; see documentation there for details.
new () = PositionalArgsAttribute PositionalArgsAttribute.DefaultIncludeFlagLike

/// Attribute indicating that this field shall have a default value derived
/// from calling an appropriately named static method on the type.
///
Expand Down
3 changes: 3 additions & 0 deletions WoofWare.Myriad.Plugins.Attributes/SurfaceBaseline.txt
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@ WoofWare.Myriad.Plugins.JsonSerializeAttribute.get_DefaultIsExtensionMethod [sta
WoofWare.Myriad.Plugins.ParseExactAttribute inherit System.Attribute
WoofWare.Myriad.Plugins.ParseExactAttribute..ctor [constructor]: string
WoofWare.Myriad.Plugins.PositionalArgsAttribute inherit System.Attribute
WoofWare.Myriad.Plugins.PositionalArgsAttribute..ctor [constructor]: bool
WoofWare.Myriad.Plugins.PositionalArgsAttribute..ctor [constructor]: unit
WoofWare.Myriad.Plugins.PositionalArgsAttribute.DefaultIncludeFlagLike [static property]: [read-only] bool
WoofWare.Myriad.Plugins.PositionalArgsAttribute.get_DefaultIncludeFlagLike [static method]: unit -> bool
WoofWare.Myriad.Plugins.RemoveOptionsAttribute inherit System.Attribute
WoofWare.Myriad.Plugins.RemoveOptionsAttribute..ctor [constructor]: unit
WoofWare.Myriad.Plugins.RestEase inherit obj
Expand Down
2 changes: 1 addition & 1 deletion WoofWare.Myriad.Plugins.Attributes/version.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "3.4",
"version": "3.5",
"publicReleaseRefSpec": [
"^refs/heads/main$"
],
Expand Down
45 changes: 45 additions & 0 deletions WoofWare.Myriad.Plugins.Test/TestArgParser/TestArgParser.fs
Original file line number Diff line number Diff line change
Expand Up @@ -618,3 +618,48 @@ Required argument '--exact' received no value"""
"""Help text requested.
--do-something-else / --anotherarg string
--turn-it-on / --dont-turn-it-off bool"""

[<Test>]
let ``Can collect *all* non-help args into positional args with includeFlagLike`` () =
let getEnvVar (_ : string) = failwith "do not call"

FlagsIntoPositionalArgs.parse' getEnvVar [ "--a" ; "foo" ; "--b=false" ; "--c=hi" ; "--" ; "--help" ]
|> shouldEqual
{
A = "foo"
GrabEverything = [ "--b=false" ; "--c=hi" ; "--help" ]
}

// Users might consider this eccentric!
// But we're only a simple arg parser; we don't look around to see whether this is "almost"
// a valid parse.
FlagsIntoPositionalArgs.parse' getEnvVar [ "--a" ; "--b=false" ; "--c=hi" ; "--" ; "--help" ]
|> shouldEqual
{
A = "--b=false"
GrabEverything = [ "--c=hi" ; "--help" ]
}

[<Test>]
let ``Can refuse to collect non-help args`` () =
let getEnvVar (_ : string) = failwith "do not call"

let exc =
Assert.Throws<exn> (fun () ->
FlagsIntoPositionalArgs'.parse' getEnvVar [ "--a" ; "foo" ; "--b=false" ; "--c=hi" ; "--" ; "--help" ]
|> ignore<FlagsIntoPositionalArgs'>
)

exc.Message
|> shouldEqual """Unable to process argument --b=false as key --b and value false"""

let exc =
Assert.Throws<exn> (fun () ->
FlagsIntoPositionalArgs'.parse' getEnvVar [ "--a" ; "--b=false" ; "--c=hi" ; "--" ; "--help" ]
|> ignore<FlagsIntoPositionalArgs'>
)

// Again perhaps eccentric!
// Again, we don't try to detect that the user has missed out the desired argument to `--a`.
exc.Message
|> shouldEqual """Unable to process argument --c=hi as key --c and value hi"""
110 changes: 66 additions & 44 deletions WoofWare.Myriad.Plugins/ArgParserGenerator.fs
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,8 @@ type private ParseFunction<'acc> =

[<RequireQualifiedAccess>]
type private ChoicePositional =
| Normal
| Choice
| Normal of includeFlagLike : SynExpr option
| Choice of includeFlagLike : SynExpr option

type private ParseFunctionPositional = ParseFunction<ChoicePositional>
type private ParseFunctionNonPositional = ParseFunction<Accumulation<ArgumentDefaultSpec>>
Expand Down Expand Up @@ -506,11 +506,14 @@ module internal ArgParserGenerator =

let positionalArgAttr =
attrs
|> List.tryFind (fun a ->
|> List.tryPick (fun a ->
match (List.last a.TypeName.LongIdent).idText with
| "PositionalArgsAttribute"
| "PositionalArgs" -> true
| _ -> false
| "PositionalArgs" ->
match a.ArgExpr with
| SynExpr.Const (SynConst.Unit, _) -> Some None
| a -> Some (Some a)
| _ -> None
)

let parseExactModifier =
Expand Down Expand Up @@ -580,7 +583,7 @@ module internal ArgParserGenerator =
| None ->

match positionalArgAttr with
| Some _ ->
| Some includeFlagLike ->
let getChoice (spec : ArgumentDefaultSpec option) : unit =
match spec with
| Some _ ->
Expand All @@ -607,7 +610,7 @@ module internal ArgParserGenerator =
FieldName = ident
Parser = parser
TargetVariable = Ident.create $"arg_%i{counter}"
Accumulation = ChoicePositional.Choice
Accumulation = ChoicePositional.Choice includeFlagLike
TargetType = parseTy
ArgForm = longForms
Help = helpText
Expand All @@ -619,7 +622,7 @@ module internal ArgParserGenerator =
FieldName = ident
Parser = parser
TargetVariable = Ident.create $"arg_%i{counter}"
Accumulation = ChoicePositional.Normal
Accumulation = ChoicePositional.Normal includeFlagLike
TargetType = parseTy
ArgForm = longForms
Help = helpText
Expand Down Expand Up @@ -855,8 +858,9 @@ module internal ArgParserGenerator =
|> SynExpr.pipeThroughFunction pos.Parser
|> fun p ->
match pos.Accumulation with
| ChoicePositional.Choice -> p |> SynExpr.pipeThroughFunction (SynExpr.createIdent "Choice1Of2")
| ChoicePositional.Normal -> p
| ChoicePositional.Choice _ ->
p |> SynExpr.pipeThroughFunction (SynExpr.createIdent "Choice1Of2")
| ChoicePositional.Normal _ -> p
|> SynExpr.pipeThroughFunction (
SynExpr.createLongIdent' [ pos.TargetVariable ; Ident.create "Add" ]
)
Expand Down Expand Up @@ -1000,6 +1004,50 @@ module internal ArgParserGenerator =
|> SynExpr.applyTo (SynExpr.createIdent "key")
|> SynExpr.applyTo (SynExpr.createIdent "value")

let processAsPositional =
SynExpr.sequential
[
SynExpr.createIdent "arg"
|> SynExpr.pipeThroughFunction leftoverArgParser
|> fun p ->
match leftoverArgAcc with
| ChoicePositional.Normal _ -> p
| ChoicePositional.Choice _ ->
p |> SynExpr.pipeThroughFunction (SynExpr.createIdent "Choice1Of2")
|> SynExpr.pipeThroughFunction (SynExpr.createLongIdent' [ leftoverArgs ; Ident.create "Add" ])

recurseKey
]

let notMatched =
let posAttr =
match leftoverArgAcc with
| ChoicePositional.Choice a
| ChoicePositional.Normal a -> a

let handleFailure =
[
SynMatchClause.create (SynPat.named "None") fail

SynMatchClause.create
(SynPat.nameWithArgs "Some" [ SynPat.named "msg" ])
(SynExpr.sequential
[
SynExpr.createIdent "sprintf"
|> SynExpr.applyTo (SynExpr.CreateConst "%s (at arg %s)")
|> SynExpr.applyTo (SynExpr.createIdent "msg")
|> SynExpr.applyTo (SynExpr.createIdent "arg")
|> SynExpr.pipeThroughFunction (SynExpr.dotGet "Add" (SynExpr.createIdent' errorAcc))

recurseKey
])
]
|> SynExpr.createMatch (SynExpr.createIdent "x")

match posAttr with
| None -> handleFailure
| Some posAttr -> SynExpr.ifThenElse posAttr handleFailure processAsPositional

let argStartsWithDashes =
SynExpr.createIdent "arg"
|> SynExpr.callMethodArg
Expand All @@ -1013,19 +1061,7 @@ module internal ArgParserGenerator =
let processKey =
SynExpr.ifThenElse
argStartsWithDashes
(SynExpr.sequential
[
SynExpr.createIdent "arg"
|> SynExpr.pipeThroughFunction leftoverArgParser
|> fun p ->
match leftoverArgAcc with
| ChoicePositional.Normal -> p
| ChoicePositional.Choice ->
p |> SynExpr.pipeThroughFunction (SynExpr.createIdent "Choice1Of2")
|> SynExpr.pipeThroughFunction (SynExpr.createLongIdent' [ leftoverArgs ; Ident.create "Add" ])

recurseKey
])
processAsPositional
(SynExpr.ifThenElse
(SynExpr.equals (SynExpr.createIdent "arg") (SynExpr.CreateConst "--help"))
(SynExpr.createLet
Expand Down Expand Up @@ -1061,23 +1097,9 @@ module internal ArgParserGenerator =
[
SynMatchClause.create (SynPat.nameWithArgs "Ok" [ SynPat.unit ]) recurseKey

SynMatchClause.create (SynPat.nameWithArgs "Error" [ SynPat.named "None" ]) fail
SynMatchClause.create
(SynPat.nameWithArgs
"Error"
[ SynPat.nameWithArgs "Some" [ SynPat.named "msg" ] |> SynPat.paren ])
(SynExpr.sequential
[
SynExpr.createIdent "sprintf"
|> SynExpr.applyTo (SynExpr.CreateConst "%s (at arg %s)")
|> SynExpr.applyTo (SynExpr.createIdent "msg")
|> SynExpr.applyTo (SynExpr.createIdent "arg")
|> SynExpr.pipeThroughFunction (
SynExpr.dotGet "Add" (SynExpr.createIdent' errorAcc)
)

recurseKey
])
(SynPat.nameWithArgs "Error" [ SynPat.named "x" ])
notMatched
]))
(SynExpr.createIdent "args" |> SynExpr.pipeThroughFunction recurseValue)))
(SynExpr.createIdent "helpText"
Expand Down Expand Up @@ -1189,8 +1211,8 @@ module internal ArgParserGenerator =
)
|> fun p ->
match leftoverArgAcc with
| ChoicePositional.Normal -> p
| ChoicePositional.Choice ->
| ChoicePositional.Normal _ -> p
| ChoicePositional.Choice _ ->
p
|> SynExpr.pipeThroughFunction (
SynExpr.applyFunction
Expand Down Expand Up @@ -1262,9 +1284,9 @@ module internal ArgParserGenerator =
SynType.string
| Some pf ->
match pf.Accumulation with
| ChoicePositional.Choice ->
| ChoicePositional.Choice _ ->
pf.TargetVariable, pf.Parser, SynType.app "Choice" [ pf.TargetType ; pf.TargetType ]
| ChoicePositional.Normal -> pf.TargetVariable, pf.Parser, pf.TargetType
| ChoicePositional.Normal _ -> pf.TargetVariable, pf.Parser, pf.TargetType

let bindings =
SynExpr.createIdent "ResizeArray"
Expand Down Expand Up @@ -1492,7 +1514,7 @@ module internal ArgParserGenerator =

let leftoverArgAcc =
match pos with
| None -> ChoicePositional.Normal
| None -> ChoicePositional.Normal None
| Some pos -> pos.Accumulation

[
Expand Down
5 changes: 5 additions & 0 deletions WoofWare.Myriad.Plugins/SynExpr/SynExpr.fs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@ module internal SynExpr =
let equals (a : SynExpr) (b : SynExpr) =
SynExpr.CreateAppInfix (SynExpr.CreateLongIdent SynLongIdent.eq, a) |> applyTo b

/// {a} && {b}
let booleanAnd (a : SynExpr) (b : SynExpr) =
SynExpr.CreateAppInfix (SynExpr.CreateLongIdent SynLongIdent.booleanAnd, a)
|> applyTo b

/// {a} + {b}
let plus (a : SynExpr) (b : SynExpr) =
SynExpr.CreateAppInfix (
Expand Down
3 changes: 3 additions & 0 deletions WoofWare.Myriad.Plugins/SynExpr/SynLongIdent.fs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ module internal SynLongIdent =
let eq =
SynLongIdent.SynLongIdent ([ Ident.create "op_Equality" ], [], [ Some (IdentTrivia.OriginalNotation "=") ])

let booleanAnd =
SynLongIdent.SynLongIdent ([ Ident.create "op_BooleanAnd" ], [], [ Some (IdentTrivia.OriginalNotation "&&") ])

let pipe =
SynLongIdent.SynLongIdent ([ Ident.create "op_PipeRight" ], [], [ Some (IdentTrivia.OriginalNotation "|>") ])

Expand Down

0 comments on commit 5748ac3

Please sign in to comment.