From 5e3eb50f35e900172513f7c8175090f0f44341da Mon Sep 17 00:00:00 2001 From: dimension-zero Date: Sat, 23 May 2026 18:01:20 +0100 Subject: [PATCH 1/3] feat(ParseConfig): Add record + Parse(config) overload (additive) * ArgumentParser.fs: Introduce public ParseConfig record carrying Inputs, ConfigurationReader, IgnoreMissing, IgnoreUnrecognized, RaiseOnUsage. Provide ParseConfig.Default reproducing the historical Parse(...) defaults. * New Parse(config: ParseConfig) overload delegates to the existing Parse(?inputs, ?configurationReader, ...) implementation. All current overloads remain untouched, so existing callers see no shape change. Useful when hosts want to layer configuration programmatically (e.g. merging container-provided defaults with user overrides) rather than juggling many optional method arguments. --- src/Argu/ArgumentParser.fs | 43 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/src/Argu/ArgumentParser.fs b/src/Argu/ArgumentParser.fs index f5816773..bfa16da4 100644 --- a/src/Argu/ArgumentParser.fs +++ b/src/Argu/ArgumentParser.fs @@ -3,6 +3,36 @@ open FSharp.Quotations open Argu.UnionArgInfo +/// Configuration record for . +/// Each field carries the same meaning as the matching optional parameter on +/// the existing Parse overload. Use +/// as a starting point and override only the fields you care about. +[] +type ParseConfig = + { + /// The command line input. None takes the inputs from System.Environment. + Inputs : string [] option + /// Configuration reader used to source AppSettings-style arguments. + /// None uses the AppSettings configuration of the current process. + ConfigurationReader : IConfigurationReader option + /// Ignore errors caused by the Mandatory attribute. + IgnoreMissing : bool + /// Ignore CLI arguments that do not match the schema. + IgnoreUnrecognized : bool + /// Treat '--help' parameters as parse errors. + RaiseOnUsage : bool + } + /// Default parse configuration, matching the historical Parse(...) defaults: + /// inputs and configurationReader inherited from the environment, do not ignore + /// missing or unrecognized arguments, and raise on '--help'. + static member Default : ParseConfig = { + Inputs = None + ConfigurationReader = None + IgnoreMissing = false + IgnoreUnrecognized = false + RaiseOnUsage = true + } + /// The Argu type generates an argument parser given a type argument /// that is an F# discriminated union. It can then be used to parse command line arguments /// or XML configuration. @@ -156,6 +186,19 @@ and [] with ParserExn (errorCode, msg) -> errorHandler.Exit (msg, errorCode) + /// Parse both command line args and supplied configuration reader, using + /// a record. Useful when callers want to construct + /// the parameter set programmatically (e.g. layering host defaults over user + /// overrides) without juggling many optional method arguments. + /// The parse configuration. See ParseConfig.Default. + member ap.Parse (config : ParseConfig) : ParseResults<'Template> = + ap.Parse( + ?inputs = config.Inputs, + ?configurationReader = config.ConfigurationReader, + ignoreMissing = config.IgnoreMissing, + ignoreUnrecognized = config.IgnoreUnrecognized, + raiseOnUsage = config.RaiseOnUsage) + /// Parse both command line args and supplied configuration reader. /// Results are merged with command line args overriding configuration parameters. /// The command line input. Taken from System.Environment if not specified. From f62862fcc5da298ddaa9f5e4aa72a3b9ee469c9e Mon Sep 17 00:00:00 2001 From: dimension-zero Date: Sat, 23 May 2026 18:51:56 +0100 Subject: [PATCH 2/3] test(ParseConfig): Add coverage for ParseConfig record + Parse(config) 6 new tests: * ParseConfig.Default holds the historical defaults (None inputs, None reader, false IgnoreMissing/IgnoreUnrecognized, true RaiseOnUsage). * Parse(config with explicit inputs) parses those inputs. * Parse(config) matches Parse(?inputs, ...) for the same shape. * IgnoreMissing=true skips the mandatory check on empty inputs. * IgnoreUnrecognized=true gathers unknown args into UnrecognizedCliParams. * ConfigurationReader provided via config sources AppSettings entries. A separate non-mandatory schema is used for the AppSettings test because Argu's missing-mandatory check fires from CLI parsing even when AppSettings provides a value (existing behaviour, not introduced by PR 19). Net suite size on this branch: 112 -> 118. # Conflicts: # tests/Argu.Tests/Argu.Tests.fsproj --- tests/Argu.Tests/Argu.Tests.fsproj | 1 + tests/Argu.Tests/ParseConfigTests.fs | 91 ++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 tests/Argu.Tests/ParseConfigTests.fs diff --git a/tests/Argu.Tests/Argu.Tests.fsproj b/tests/Argu.Tests/Argu.Tests.fsproj index 343246e8..9cb686f7 100644 --- a/tests/Argu.Tests/Argu.Tests.fsproj +++ b/tests/Argu.Tests/Argu.Tests.fsproj @@ -7,6 +7,7 @@ + diff --git a/tests/Argu.Tests/ParseConfigTests.fs b/tests/Argu.Tests/ParseConfigTests.fs new file mode 100644 index 00000000..48f79e9e --- /dev/null +++ b/tests/Argu.Tests/ParseConfigTests.fs @@ -0,0 +1,91 @@ +namespace Argu.Tests + +open System.Collections.Generic +open Xunit +open Swensen.Unquote + +open Argu + +/// Tests for the ParseConfig record + Parse(config) overload (PR 19). +module ``Argu Tests ParseConfig`` = + + type Args = + | [] Port of int + | Verbose + | Tag of string + interface IArgParserTemplate with + member this.Usage = + match this with + | Port _ -> "port" + | Verbose -> "verbose" + | Tag _ -> "tag" + + let private parser () = ArgumentParser.Create(programName = "app") + + [] + let ``ParseConfig.Default holds historical defaults`` () = + let d = ParseConfig.Default + test <@ d.Inputs = None @> + test <@ d.ConfigurationReader = None @> + test <@ d.IgnoreMissing = false @> + test <@ d.IgnoreUnrecognized = false @> + test <@ d.RaiseOnUsage = true @> + + [] + let ``Parse(config with explicit inputs) parses those inputs`` () = + let p = parser () + let argv = [| "--port"; "8080"; "--verbose" |] + let cfg = { ParseConfig.Default with Inputs = Some argv ; RaiseOnUsage = false } + let results = p.Parse(cfg) + test <@ results.GetResult(Port) = 8080 @> + test <@ results.Contains(Verbose) @> + + [] + let ``Parse(config) matches Parse(?inputs, ...) for the same parameters`` () = + let p = parser () + let argv = [| "--port"; "1234"; "--tag"; "v1" |] + let viaConfig = + let cfg = { ParseConfig.Default with Inputs = Some argv ; RaiseOnUsage = false } + p.Parse(cfg) + let viaOptional = p.Parse(inputs = argv, raiseOnUsage = false) + test <@ viaConfig.GetResult(Port) = viaOptional.GetResult(Port) @> + test <@ viaConfig.GetResult(Tag) = viaOptional.GetResult(Tag) @> + + [] + let ``Parse(config with IgnoreMissing=true) skips mandatory check`` () = + let p = parser () + let cfg = { ParseConfig.Default with Inputs = Some [||] ; IgnoreMissing = true } + let results = p.Parse(cfg) + test <@ results.TryGetResult(Port) = None @> + + [] + let ``Parse(config with IgnoreUnrecognized=true) collects unknown args`` () = + let p = parser () + let cfg = + { ParseConfig.Default with + Inputs = Some [| "--port"; "1"; "--bogus" |] + IgnoreUnrecognized = true + RaiseOnUsage = false } + let results = p.Parse(cfg) + test <@ results.UnrecognizedCliParams |> List.contains "--bogus" @> + + /// Argu's missing-mandatory check fires from the CLI even when + /// AppSettings provides a value (pre-existing behaviour, unrelated + /// to PR 19), so the AppSettings round-trip test uses a + /// non-mandatory schema. + type AppSettingsArgs = + | TagKey of string + interface IArgParserTemplate with member this.Usage = "tag" + + [] + let ``Parse(config with ConfigurationReader) sources AppSettings`` () = + let p = ArgumentParser.Create(programName = "app") + let dict = Dictionary() + dict["tagkey"] <- "v1" + let reader = ConfigurationReader.FromDictionary dict + let cfg = + { ParseConfig.Default with + Inputs = Some [||] + ConfigurationReader = Some reader } + let results = p.Parse(cfg) + test <@ results.GetResult(TagKey) = "v1" @> From 99f1f252333d94ab907c31893b92e2710c4c95f0 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Thu, 4 Jun 2026 15:30:25 +0100 Subject: [PATCH 3/3] polish --- RELEASE_NOTES.md | 3 +- src/Argu/ArgumentParser.fs | 21 ++-- tests/Argu.Tests/ParseConfigTests.fs | 148 +++++++++++++-------------- 3 files changed, 83 insertions(+), 89 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index a3f2c4ff..5ee49664 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -2,8 +2,9 @@ * Fix NRE in derivation of programName introduced in 6.2.2 [#292](https://github.com/SwensenSoftware/unquote/pull/292) [@dimension-zero](https://github.com/dimension-zero) * Fix Clarify exception in expr2Uci [#293](https://github.com/SwensenSoftware/unquote/pull/293) [@dimension-zero](https://github.com/dimension-zero) * Fix Report all missing args in error message, not just first level [#297](https://github.com/SwensenSoftware/unquote/pull/297) [@dimension-zero](https://github.com/dimension-zero) -* Add `Argu.Samples.Introspect` sample [#298](https://github.com/SwensenSoftware/unquote/pull/298) [@dimension-zero](https://github.com/dimension-zero) * Fix Limit min wordwrap column to 20 [#302](https://github.com/SwensenSoftware/unquote/pull/302) [@dimension-zero](https://github.com/dimension-zero) +* Add `Argu.Samples.Introspect` sample [#298](https://github.com/SwensenSoftware/unquote/pull/298) [@dimension-zero](https://github.com/dimension-zero) +* Add `ArgumentParser.Parse(ParseConfig)` [#307](https://github.com/SwensenSoftware/unquote/pull/307) [@dimension-zero](https://github.com/dimension-zero) ### 6.2.5 * Drop Package `FSharp.Core` dependency to `6.0.0` [#264](https://github.com/fsprojects/Argu/pull/264) diff --git a/src/Argu/ArgumentParser.fs b/src/Argu/ArgumentParser.fs index bfa16da4..cf185651 100644 --- a/src/Argu/ArgumentParser.fs +++ b/src/Argu/ArgumentParser.fs @@ -25,13 +25,12 @@ type ParseConfig = /// Default parse configuration, matching the historical Parse(...) defaults: /// inputs and configurationReader inherited from the environment, do not ignore /// missing or unrecognized arguments, and raise on '--help'. - static member Default : ParseConfig = { - Inputs = None - ConfigurationReader = None - IgnoreMissing = false - IgnoreUnrecognized = false - RaiseOnUsage = true - } + static member Default : ParseConfig = + { Inputs = None + ConfigurationReader = None + IgnoreMissing = false + IgnoreUnrecognized = false + RaiseOnUsage = true } /// The Argu type generates an argument parser given a type argument /// that is an F# discriminated union. It can then be used to parse command line arguments @@ -191,8 +190,8 @@ and [] /// the parameter set programmatically (e.g. layering host defaults over user /// overrides) without juggling many optional method arguments. /// The parse configuration. See ParseConfig.Default. - member ap.Parse (config : ParseConfig) : ParseResults<'Template> = - ap.Parse( + member self.Parse (config : ParseConfig) : ParseResults<'Template> = + self.Parse( ?inputs = config.Inputs, ?configurationReader = config.ConfigurationReader, ignoreMissing = config.IgnoreMissing, @@ -272,8 +271,8 @@ and [] mkCommandLineArgs argInfo (Seq.cast args) |> Seq.toArray /// Prints parameters in command line format. Useful for argument string generation. - member ap.PrintCommandLineArgumentsFlat (args : 'Template list) : string = - ap.PrintCommandLineArguments args |> flattenCliTokens + member self.PrintCommandLineArgumentsFlat (args : 'Template list) : string = + self.PrintCommandLineArguments args |> flattenCliTokens /// Prints parameters in App.Config format. /// The parameters that fill out the XML document. diff --git a/tests/Argu.Tests/ParseConfigTests.fs b/tests/Argu.Tests/ParseConfigTests.fs index 48f79e9e..68edf22e 100644 --- a/tests/Argu.Tests/ParseConfigTests.fs +++ b/tests/Argu.Tests/ParseConfigTests.fs @@ -1,91 +1,85 @@ -namespace Argu.Tests +module Argu.Tests.ParseConfigTests open System.Collections.Generic -open Xunit open Swensen.Unquote +open Xunit open Argu -/// Tests for the ParseConfig record + Parse(config) overload (PR 19). -module ``Argu Tests ParseConfig`` = +type Args = + | [] Port of int + | Verbose + | Tag of string + interface IArgParserTemplate with + member this.Usage = + match this with + | Port _ -> "port" + | Verbose -> "verbose" + | Tag _ -> "tag" - type Args = - | [] Port of int - | Verbose - | Tag of string - interface IArgParserTemplate with - member this.Usage = - match this with - | Port _ -> "port" - | Verbose -> "verbose" - | Tag _ -> "tag" +let private parser () = ArgumentParser.Create() +[] +let ``ParseConfig.Default holds historical defaults`` () = + let d = ParseConfig.Default + test <@ d.Inputs = None @> + test <@ d.ConfigurationReader = None @> + test <@ d.IgnoreMissing = false @> + test <@ d.IgnoreUnrecognized = false @> + test <@ d.RaiseOnUsage = true @> - let private parser () = ArgumentParser.Create(programName = "app") +[] +let ``Parse(config with explicit inputs) parses those inputs`` () = + let p = parser () + let argv = [| "--port"; "8080"; "--verbose" |] + let cfg = { ParseConfig.Default with Inputs = Some argv ; RaiseOnUsage = false } + let results = p.Parse(cfg) + test <@ results.GetResult(Port) = 8080 @> + test <@ results.Contains(Verbose) @> - [] - let ``ParseConfig.Default holds historical defaults`` () = - let d = ParseConfig.Default - test <@ d.Inputs = None @> - test <@ d.ConfigurationReader = None @> - test <@ d.IgnoreMissing = false @> - test <@ d.IgnoreUnrecognized = false @> - test <@ d.RaiseOnUsage = true @> - - [] - let ``Parse(config with explicit inputs) parses those inputs`` () = - let p = parser () - let argv = [| "--port"; "8080"; "--verbose" |] +[] +let ``Parse(config) matches Parse(?inputs, ...) for the same parameters`` () = + let p = parser () + let argv = [| "--port"; "1234"; "--tag"; "v1" |] + let viaConfig = let cfg = { ParseConfig.Default with Inputs = Some argv ; RaiseOnUsage = false } - let results = p.Parse(cfg) - test <@ results.GetResult(Port) = 8080 @> - test <@ results.Contains(Verbose) @> - - [] - let ``Parse(config) matches Parse(?inputs, ...) for the same parameters`` () = - let p = parser () - let argv = [| "--port"; "1234"; "--tag"; "v1" |] - let viaConfig = - let cfg = { ParseConfig.Default with Inputs = Some argv ; RaiseOnUsage = false } - p.Parse(cfg) - let viaOptional = p.Parse(inputs = argv, raiseOnUsage = false) - test <@ viaConfig.GetResult(Port) = viaOptional.GetResult(Port) @> - test <@ viaConfig.GetResult(Tag) = viaOptional.GetResult(Tag) @> + p.Parse(cfg) + let viaOptional = p.Parse(inputs = argv, raiseOnUsage = false) + test <@ viaConfig.GetResult(Port) = viaOptional.GetResult(Port) @> + test <@ viaConfig.GetResult(Tag) = viaOptional.GetResult(Tag) @> - [] - let ``Parse(config with IgnoreMissing=true) skips mandatory check`` () = - let p = parser () - let cfg = { ParseConfig.Default with Inputs = Some [||] ; IgnoreMissing = true } - let results = p.Parse(cfg) - test <@ results.TryGetResult(Port) = None @> +[] +let ``Parse(config with IgnoreMissing=true) skips mandatory check`` () = + let p = parser () + let cfg = { ParseConfig.Default with Inputs = Some [||] ; IgnoreMissing = true } + let results = p.Parse(cfg) + test <@ results.TryGetResult(Port) = None @> - [] - let ``Parse(config with IgnoreUnrecognized=true) collects unknown args`` () = - let p = parser () - let cfg = - { ParseConfig.Default with - Inputs = Some [| "--port"; "1"; "--bogus" |] - IgnoreUnrecognized = true - RaiseOnUsage = false } - let results = p.Parse(cfg) - test <@ results.UnrecognizedCliParams |> List.contains "--bogus" @> +[] +let ``Parse(config with IgnoreUnrecognized=true) collects unknown args`` () = + let p = parser () + let cfg = + { ParseConfig.Default with + Inputs = Some [| "--port"; "1"; "--bogus" |] + IgnoreUnrecognized = true + RaiseOnUsage = false } + let results = p.Parse(cfg) + test <@ results.UnrecognizedCliParams |> List.contains "--bogus" @> - /// Argu's missing-mandatory check fires from the CLI even when - /// AppSettings provides a value (pre-existing behaviour, unrelated - /// to PR 19), so the AppSettings round-trip test uses a - /// non-mandatory schema. - type AppSettingsArgs = - | TagKey of string - interface IArgParserTemplate with member this.Usage = "tag" +/// Argu's missing-mandatory check fires from the CLI even when AppSettings provides a value (pre-existing behavior), +/// so the AppSettings round-trip test uses a non-mandatory schema. +type AppSettingsArgs = + | TagKey of string + interface IArgParserTemplate with member this.Usage = "tag" - [] - let ``Parse(config with ConfigurationReader) sources AppSettings`` () = - let p = ArgumentParser.Create(programName = "app") - let dict = Dictionary() - dict["tagkey"] <- "v1" - let reader = ConfigurationReader.FromDictionary dict - let cfg = - { ParseConfig.Default with - Inputs = Some [||] - ConfigurationReader = Some reader } - let results = p.Parse(cfg) - test <@ results.GetResult(TagKey) = "v1" @> +[] +let ``Parse(config with ConfigurationReader) sources AppSettings`` () = + let p = ArgumentParser.Create() + let dict = Dictionary() + dict["tagkey"] <- "v1" + let reader = ConfigurationReader.FromDictionary dict + let cfg = + { ParseConfig.Default with + Inputs = Some [||] + ConfigurationReader = Some reader } + let results = p.Parse(cfg) + test <@ results.GetResult(TagKey) = "v1" @>