First of all thanks for creating this nice package. Been using it for a lot of projects and it's working fine as always :)
I was wondering if it's possible to read options from environment variables as well as from command params?
When I'm running my app in a docker container I would like to be able to edit my env variables in the container and then reflect that to my startup options.
In the old repo, I found this issue https://github.com/gsscoder/commandline/issues/58 but can't seem to find the implementation in the new repo. Was it never implemented or planned as a feature or are they other ways to resolve my "problem"?
If not I can always just read the env variables myself and update the startup options if needed.
I think the consensus in that thread was "Separation of concerns means this library should stick to commandline parsing only", and any implementation of environment variable defaults should be left up to the users who need it, and/or another library if it's a common need.
BTW, if you're implementing this yourself you'll need to decide whether command line args should override environment variables, or environment variables override command line args, in cases when both are present. IMHO command line args should always override environment variables. The simplest way to achieve that is probably to do things like this in your options class:
[Option('f', "--foo", DefaultValue=System.Environment.GetEnvironment("MYPROG_FOO") ?? "")
string Foo { get; set; }
That way --foo=bar overrides MYPROG_FOO=quux, but if no --foo option is given on the command line then the value will be "quux". (And if neither is specified, it will be the empty string rather than null, so you're safe from NullReferenceErrors).
Actually, that wouldn't work since attribute properties must be constants that can be known at compile time (see https://github.com/commandlineparser/commandline/issues/524#issuecomment-544639778). But it might be possible if a DefaultFactory attribute was added similar to the idea I sketched out in https://github.com/commandlineparser/commandline/issues/530#issuecomment-676019847 - would that feature, if implemented, meet your needs?
any movement on this, not being able to use env as defaults is a pretty big thing. It prevents the tooling from being useful,
I would be okay with something like DefaultFunc = () => Environment.GetEnvironmentVariable("ENV")
The problem is you can't just have another library because the processing order needs to be
Args > Env > Defaults, if I have another library then either Env gets precedence over Args or Defaults get precedence over Env.
Yes exactly, the order of precedence makes this problematic. This requirement of constant value for Default makes this really cumbersome to implement.
tumbleweed blowing by
I have worked around this by pre-processing the string[] args array.
args = AppendEnvironmentVariables(args); before processing.{ASSEMBLY}_{VERB}_{OPTION}.It works by:
Here's the code, which is totally untested beyond my use case, so YMMV:
```c#
static void Main(string[] args)
{
args = AppendEnvironmentVariables(args);
...
}
private static string[] AppendEnvironmentVariables(string[] args)
{
if (args.Length == 0)
{
return args;
}
var verb = args[0];
if (!TryGetOptions(verb, out var options))
{
return args;
}
var newArgs = new List<string>(args);
foreach (var unusedOption in FilterOptions(args, options))
{
var value = Environment.GetEnvironmentVariable($"{Assembly.GetExecutingAssembly().GetName().Name.ToUpperInvariant()}_{verb.ToUpperInvariant()}_{unusedOption.LongName.ToUpperInvariant()}");
if (value != null)
{
newArgs.Add($"--{unusedOption.LongName}={value}");
}
}
return newArgs.ToArray();
}
private static bool TryGetOptions(string verb, out OptionAttribute[] options)
{
foreach (var type in Assembly.GetExecutingAssembly().GetTypes().Where(type => !type.IsAbstract))
{
var verbAttribute = type.GetCustomAttributes
if (verbAttribute != null)
{
if (verbAttribute.Name == verb)
{
options = type.GetProperties()
.Select(property => property.GetCustomAttributes
.Where(option => option != null).ToArray();
return true;
}
}
}
options = null;
return false;
}
private static OptionAttribute[] FilterOptions(string[] args, OptionAttribute[] options)
{
var usedLongNames = new HashSet
var usedShortNames = new HashSet
foreach (var arg in args)
{
if (arg.StartsWith("--"))
{
var longName = arg.Substring(2);
if (longName.Contains('='))
{
longName = longName.Substring(0, longName.IndexOf('='));
}
usedLongNames.Add(longName);
}
else if (arg.StartsWith("-"))
{
var shortName = arg.Substring(1);
if (shortName.Contains('='))
{
shortName = shortName.Substring(0, shortName.IndexOf('='));
}
usedShortNames.Add(shortName);
}
}
return options.Where(option => !usedLongNames.Contains(option.LongName) && !usedShortNames.Contains(option.ShortName)).ToArray();
}
```
Hope this helps someone else!
@avenema that's amazing love it! :D,
however I am a bit curious what's the reasoning about naming spacing it by the executing assembly? :thinking:
@jannickj good question. I did it that way to avoid collisions with other projects. It's clear from the environment variable name that the value is specific to a particular executable and verb.
I'm using this approach in these three projects:
... and there's a possibility that at some point a verb/option pair might collide.
@avenema hehe fair enough :grin: , and I guess this is sort of an exercise left to anyone wanting to improve it however I think in general when it comes to namespacing it's preferable to have it be optional e.g. the AppendEnvironmentVariables could have an optional argument string? namespace = null
Most helpful comment
any movement on this, not being able to use env as defaults is a pretty big thing. It prevents the tooling from being useful,
I would be okay with something like
DefaultFunc = () => Environment.GetEnvironmentVariable("ENV")The problem is you can't just have another library because the processing order needs to be
Args > Env > Defaults, if I have another library then either Env gets precedence over Args or Defaults get precedence over Env.