diff --git a/Argu.slnx b/Argu.slnx
index 4c0db3e..998efbc 100644
--- a/Argu.slnx
+++ b/Argu.slnx
@@ -40,6 +40,7 @@
+
diff --git a/Directory.Packages.props b/Directory.Packages.props
index b7691ac..1033116 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -10,6 +10,9 @@
+
+
+
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 58eaa7c..9e56101 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -3,13 +3,15 @@
* Fix Clarify exception in expr2Uci [#293](https://github.com/fsprojects/Argu/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/fsprojects/Argu/pull/297) [@dimension-zero](https://github.com/dimension-zero)
* Fix Limit min wordwrap column to 20 [#302](https://github.com/fsprojects/Argu/pull/302) [@dimension-zero](https://github.com/dimension-zero)
-* Fix [misrendering of usage when help blank](https://github.com/fsprojects/Argu/issues/173) [#323](https://github.com/fsprojects/Argu/pull/323) [@DominikL1999](https://github.com/DominikL1999)
+* Fix [misrendering of usage when parameter description blank](https://github.com/fsprojects/Argu/issues/173) [#323](https://github.com/fsprojects/Argu/pull/323) [@DominikL1999](https://github.com/DominikL1999)
* Add `Separator("=", orSpace = true)` and `Separator "="` attribute syntax to replace `Obsolete`d `EqualsAssignment`, `ColonAssignment`, `CustomAssignment`, `EqualsAssignmentOrSpaced`, `ColonAssignmentOrSpaced` and `CustomAssignmentOrSpaced` [#315](https://github.com/fsprojects/Argu/pull/315) [@dimension-zero](https://github.com/dimension-zero)
* Add `Argu.Samples.Introspect` sample [#298](https://github.com/fsprojects/Argu/pull/298) [@dimension-zero](https://github.com/dimension-zero)
* Add `ArgumentParser.Parse(ParseConfig)` [#307](https://github.com/fsprojects/Argu/pull/307) [@dimension-zero](https://github.com/dimension-zero)
* Add `ArgumentParser.PrintUsage(..., ?UsageStrings)` for localization support [#303](https://github.com/fsprojects/Argu/pull/303) [@dimension-zero](https://github.com/dimension-zero)
* Add AOT annotations [#314](https://github.com/fsprojects/Argu/pull/314) [@dimension-zero](https://github.com/dimension-zero)
* Add `SourceGenerator.ArguGenerate` marker [#318](https://github.com/fsprojects/Argu/pull/318) [@dimension-zero](https://github.com/dimension-zero)
+* Add `ConfigurationReader.FromEnvironmentVariables(prefix : String)` [#308](https://github.com/fsprojects/Argu/pull/308) [@dimension-zero](https://github.com/dimension-zero)
+* Add `ConfigurationReader.FromMicrosoftConfiguration(Microsoft.Extensions.Configuration.IConfiguration)` [#308](https://github.com/fsprojects/Argu/pull/308) [@dimension-zero](https://github.com/dimension-zero)
* Obsolete `EqualsAssignmentAttribute`, `ColonAssignmentAttribute`, `CustomAssignmentAttribute`, `EqualsAssignmentOrSpacedAttribute`, `ColonAssignmentOrSpacedAttribute` and `CustomAssignmentOrSpacedAttribute` [#315](https://github.com/fsprojects/Argu/pull/315) [@dimension-zero](https://github.com/dimension-zero)
* Obsolete `PostProcessResult`, `PostProcessResults`, `TryPostProcessResult` [#296](https://github.com/fsprojects/Argu/pull/296) [@dimension-zero](https://github.com/dimension-zero)
diff --git a/src/Argu.Extensions.Configuration/Argu.Extensions.Configuration.fsproj b/src/Argu.Extensions.Configuration/Argu.Extensions.Configuration.fsproj
new file mode 100644
index 0000000..dddb9de
--- /dev/null
+++ b/src/Argu.Extensions.Configuration/Argu.Extensions.Configuration.fsproj
@@ -0,0 +1,19 @@
+
+
+ netstandard2.0
+ true
+ true
+ Microsoft.Extensions.Configuration adapter for Argu — exposes any IConfiguration source as an Argu IConfigurationReader.
+ F#, argument, commandline, parser, configuration
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Argu.Extensions.Configuration/ConfigurationReader.fs b/src/Argu.Extensions.Configuration/ConfigurationReader.fs
new file mode 100644
index 0000000..9f845bd
--- /dev/null
+++ b/src/Argu.Extensions.Configuration/ConfigurationReader.fs
@@ -0,0 +1,26 @@
+namespace Argu.Extensions.Configuration
+
+open Microsoft.Extensions.Configuration
+
+open Argu
+
+///
+/// Argu backed by a
+/// .
+/// The reader returns null when a key is missing, matching the
+/// contract of every other Argu configuration reader.
+///
+type MicrosoftExtensionsConfigurationReader(configuration : IConfiguration, ?name : string) =
+ let name = defaultArg name "Microsoft.Extensions.Configuration reader"
+ interface IConfigurationReader with
+ member _.Name = name
+ member _.GetValue(key : string) =
+ // IConfiguration[key] returns null for missing keys, which matches Argu's expectation.
+ configuration[key]
+
+/// Factory helpers mirroring the shape of .
+[]
+type ConfigurationReader =
+ /// Create an Argu over an .
+ static member FromMicrosoftConfiguration(configuration : IConfiguration, ?name : string) : IConfigurationReader =
+ MicrosoftExtensionsConfigurationReader(configuration, ?name = name) :> _
diff --git a/src/Argu/ConfigReaders.fs b/src/Argu/ConfigReaders.fs
index 687e8d8..09f4858 100644
--- a/src/Argu/ConfigReaders.fs
+++ b/src/Argu/ConfigReaders.fs
@@ -1,9 +1,9 @@
namespace Argu
open System
-open System.IO
open System.Configuration
open System.Collections.Generic
+open System.IO
open System.Reflection
/// Abstract key/value configuration reader
@@ -11,16 +11,16 @@ type IConfigurationReader =
/// Configuration reader identifier
abstract Name : string
/// Gets value corresponding to supplied key
- abstract GetValue : key:string -> string | null
+ abstract GetValue : key : string -> string | null
/// Configuration reader that never returns a value
-type NullConfigurationReader() =
+type NullConfigurationReader () =
interface IConfigurationReader with
- member x.Name = "Null Configuration Reader"
- member x.GetValue _ = null
+ member _.Name = "Null Configuration Reader"
+ member _.GetValue _ = null
/// Environment variable-based configuration reader
-type EnvironmentVariableConfigurationReader() =
+type EnvironmentVariableConfigurationReader () =
// order of environment variable target lookup
let targets =
[| EnvironmentVariableTarget.Process
@@ -28,21 +28,22 @@ type EnvironmentVariableConfigurationReader() =
EnvironmentVariableTarget.Machine |]
interface IConfigurationReader with
- member x.Name = "Environment Variables Configuration Reader"
- member x.GetValue(key:string) =
+ member _.Name = "Environment Variables Configuration Reader"
+ member _.GetValue(key : string) =
+ // NOTE this logic varies from a targetless Get in that it will pick up changes that a given process tree
+ // has yet to propagate. A fresh shell should include machine and user level vars in the process env from the off
let folder curr (target : EnvironmentVariableTarget) =
match curr with
| null -> Environment.GetEnvironmentVariable(key, target)
| value -> value
-
- targets |> Array.fold folder null
+ (null, targets) ||> Array.fold folder
/// Configuration reader dictionary proxy
type DictionaryConfigurationReader (keyValueDictionary : IDictionary, ?name : string) =
let name = defaultArg name "Dictionary configuration reader."
interface IConfigurationReader with
member _.Name = name
- member _.GetValue(key:string) =
+ member _.GetValue(key : string) =
let ok,value = keyValueDictionary.TryGetValue key
if ok then value else null
@@ -51,23 +52,20 @@ type FunctionConfigurationReader (configFunc : string -> string option, ?name :
let name = defaultArg name "Function configuration reader."
interface IConfigurationReader with
member _.Name = name
- member _.GetValue(key:string) =
- match configFunc key with
- | None -> null
- | Some v -> v
+ member _.GetValue(key : string) = configFunc key |> Option.toObj
/// AppSettings XML configuration reader
type AppSettingsConfigurationReader () =
interface IConfigurationReader with
member _.Name = "AppSettings configuration reader"
- member _.GetValue(key:string) = ConfigurationManager.AppSettings[key]
+ member _.GetValue(key : string) = ConfigurationManager.AppSettings[key]
/// AppSettings XML configuration reader
type AppSettingsConfigurationFileReader private (xmlPath : string, kv : KeyValueConfigurationCollection) =
member _.Path = xmlPath
interface IConfigurationReader with
- member _.Name = sprintf "App.config configuration reader: %s" xmlPath
- member _.GetValue(key:string) =
+ member _.Name = $"App.config configuration reader: %s{xmlPath}"
+ member _.GetValue(key : string) =
match kv[key] with
| null -> null
| entry -> entry.Value
@@ -75,37 +73,50 @@ type AppSettingsConfigurationFileReader private (xmlPath : string, kv : KeyValue
/// Create used supplied XML file path
static member Create(path : string) =
if not <| File.Exists path then raise <| FileNotFoundException(path)
- let fileMap = ExeConfigurationFileMap()
- fileMap.ExeConfigFilename <- path
+ let fileMap = ExeConfigurationFileMap(ExeConfigFilename = path)
let config = ConfigurationManager.OpenMappedExeConfiguration(fileMap, ConfigurationUserLevel.None)
AppSettingsConfigurationFileReader(path, config.AppSettings.Settings)
/// Configuration reader implementations
+[]
type ConfigurationReader =
+
/// Create a configuration reader that always returns null
- static member NullReader = NullConfigurationReader() :> IConfigurationReader
+ static member NullReader : IConfigurationReader = NullConfigurationReader()
/// Create a configuration reader instance using an IDictionary instance
- static member FromDictionary(keyValueDictionary : IDictionary, ?name : string) =
- DictionaryConfigurationReader(keyValueDictionary, ?name = name) :> IConfigurationReader
+ static member FromDictionary(keyValueDictionary : IDictionary, ?name : string) : IConfigurationReader =
+ DictionaryConfigurationReader(keyValueDictionary, ?name = name)
/// Create a configuration reader instance using an F# function
- static member FromFunction(reader : string -> string option, ?name : string) =
- FunctionConfigurationReader(reader, ?name = name) :> IConfigurationReader
-
- /// Create a configuration reader instance using environment variables
- static member FromEnvironmentVariables() =
- EnvironmentVariableConfigurationReader() :> IConfigurationReader
+ static member FromFunction(reader : string -> string option, ?name : string) : IConfigurationReader =
+ FunctionConfigurationReader(reader, ?name = name)
+
+ /// Create a configuration reader instance using environment variables.
+ /// Reads Process variables, falling back to latest User or latest Machine targets if not found.
+ static member FromEnvironmentVariables() : IConfigurationReader = EnvironmentVariableConfigurationReader()
+
+ ///
+ /// Create a configuration reader that reads environment variables with a fixed prefix prepended to the requested key.
+ /// Maps --foo-bar arguments onto MYAPP_FOO_BAR environment variables.
+ /// As per FromEnvironmentVariables(), reads Process variables, falling back to latest User or latest Machine targets if not found.
+ ///
+ /// String prepended to each requested key (e.g. "MYAPP_").
+ static member FromEnvironmentVariables(prefix : string) : IConfigurationReader =
+ if isNull prefix then nullArg (nameof prefix)
+ let inner = EnvironmentVariableConfigurationReader() :> IConfigurationReader
+ let read (key : string) = inner.GetValue(prefix + key) |> Option.ofObj
+ FunctionConfigurationReader(read, name = $"Environment Variables (prefix=%s{prefix})")
/// Create a configuration reader instance using the application's resident AppSettings configuration
- static member FromAppSettings() = AppSettingsConfigurationReader() :> IConfigurationReader
+ static member FromAppSettings() : IConfigurationReader = AppSettingsConfigurationReader()
+
/// Create a configuration reader instance using a local xml App.Config file
- static member FromAppSettingsFile(path : string) = AppSettingsConfigurationFileReader.Create(path) :> IConfigurationReader
+ static member FromAppSettingsFile(path : string) : IConfigurationReader = AppSettingsConfigurationFileReader.Create path
+
/// Create a configuration reader instance using the location of an assembly file
- static member FromAppSettings(assembly : Assembly) =
+ static member FromAppSettings(assembly : Assembly) : IConfigurationReader =
let path = assembly.Location
if String.IsNullOrEmpty path then
- sprintf "Assembly location for '%O' is null or empty." assembly.Location
- |> invalidArg assembly.FullName
-
+ invalidArg assembly.FullName $"Assembly location for '{assembly.Location}' is null or empty."
AppSettingsConfigurationFileReader.Create(path + ".config") :> IConfigurationReader
diff --git a/tests/Argu.Tests/Argu.Tests.fsproj b/tests/Argu.Tests/Argu.Tests.fsproj
index 6090968..d0c2522 100644
--- a/tests/Argu.Tests/Argu.Tests.fsproj
+++ b/tests/Argu.Tests/Argu.Tests.fsproj
@@ -2,10 +2,12 @@
net10.0
Exe
+ enable
+
diff --git a/tests/Argu.Tests/CoverageTests.fs b/tests/Argu.Tests/CoverageTests.fs
index 19dd668..521b408 100644
--- a/tests/Argu.Tests/CoverageTests.fs
+++ b/tests/Argu.Tests/CoverageTests.fs
@@ -2,9 +2,9 @@
/// These are behavior-locking tests, not exhaustive functional tests.
module Argu.Tests.CoverageTests
+open Swensen.Unquote
open System
open System.Collections.Generic
-open Swensen.Unquote
open Xunit
open Argu
@@ -153,20 +153,15 @@ let ``AppSettings: ignoreMissing=true skips mandatory check`` () =
test <@ results.TryGetResult RequiredKey = None @>
-// === Env-var (covers Argu's existing EnvironmentVariableConfigurationReader) ===
-
type EnvArgs =
| [] Val of string
interface IArgParserTemplate with
member this.Usage = "value"
[]
-let ``EnvironmentVariableConfigurationReader: reads process env`` () =
- let key = "ARGU_TEST_VAL"
- let prior = Environment.GetEnvironmentVariable key
- try Environment.SetEnvironmentVariable(key, "from-env")
- let parser = ArgumentParser.Create()
- let reader = ConfigurationReader.FromEnvironmentVariables()
- let results = parser.ParseConfiguration(reader, ignoreMissing = true)
- test <@ results.GetResult Val = "from-env" @>
- finally Environment.SetEnvironmentVariable(key, prior)
+let ``ParseConfiguration fallback to EnvironmentVariableConfigurationReader picks up process env`` () =
+ use _ = EnvVarTests.envOverride "ARGU_TEST_VAL" "from-env"
+ let parser = ArgumentParser.Create()
+ let reader = ConfigurationReader.FromEnvironmentVariables()
+ let results = parser.ParseConfiguration reader
+ test <@ results.GetResult Val = "from-env" @>
diff --git a/tests/Argu.Tests/EnvVarTests.fs b/tests/Argu.Tests/EnvVarTests.fs
new file mode 100644
index 0000000..55764aa
--- /dev/null
+++ b/tests/Argu.Tests/EnvVarTests.fs
@@ -0,0 +1,50 @@
+module Argu.Tests.EnvVarTests
+
+open Swensen.Unquote
+open System
+open Xunit
+
+open Argu
+
+type Args =
+ | [] HostName of string
+ | [] Port of int
+ interface IArgParserTemplate with member this.Usage = "x"
+
+/// Sets an environment variable and restores its prior value on Dispose,
+/// so a test can scope the override with `use _ = envOverride key value`.
+let envOverride (key : string) (value : string) : IDisposable =
+ let prior = Environment.GetEnvironmentVariable key
+ Environment.SetEnvironmentVariable(key, value)
+ { new IDisposable with member _.Dispose() = Environment.SetEnvironmentVariable(key, prior) }
+
+[]
+let ``No-arg FromEnvironmentVariables works without prefix`` () =
+ use _ = envOverride "ARGU_TEST_PLAIN" "value"
+ let reader = ConfigurationReader.FromEnvironmentVariables()
+ test <@ reader.GetValue("ARGU_TEST_PLAIN") = "value" @>
+
+module Prefixed =
+
+ []
+ let ``FromEnvironmentVariables(prefix) prepends prefix to key`` () =
+ let reader = ConfigurationReader.FromEnvironmentVariables(prefix = "MYAPP_")
+ use _ = envOverride "MYAPP_HOST" "example.com"
+ test <@ reader.GetValue("HOST") = "example.com" @>
+
+ []
+ let ``FromEnvironmentVariables(prefix) returns null for missing keys`` () =
+ let reader = ConfigurationReader.FromEnvironmentVariables(prefix = "ARGU_TEST_MISSING_")
+ // Ensure no stray env var; SetEnvironmentVariable(null) clears it.
+ Environment.SetEnvironmentVariable("ARGU_TEST_MISSING_NOPE", null)
+ test <@ reader.GetValue("NOPE") = null @>
+
+ []
+ let ``FromEnvironmentVariables(prefix) parses round-trip through ArgumentParser`` () =
+ use _ = envOverride "DEMO_HOST" "host.local"
+ use _ = envOverride "DEMO_PORT" "443"
+ let parser = ArgumentParser.Create(programName = "demo")
+ let reader = ConfigurationReader.FromEnvironmentVariables(prefix = "DEMO_")
+ let results = parser.ParseConfiguration(reader, ignoreMissing = true)
+ test <@ results.GetResult(HostName) = "host.local" @>
+ test <@ results.GetResult(Port) = 443 @>