-
Notifications
You must be signed in to change notification settings - Fork 5.1k
Add ability to parse command line switches without a value #43481
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1a123e7
19ca1d8
93bbd5d
dd7790e
a6e18e8
3c99063
902aa2f
9b1be42
f9a692e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,6 +3,9 @@ | |
|
||
using System; | ||
using System.Collections.Generic; | ||
#if !NET461 | ||
using System.Runtime.InteropServices; | ||
#endif | ||
|
||
namespace Microsoft.Extensions.Configuration.CommandLine | ||
{ | ||
|
@@ -12,6 +15,12 @@ namespace Microsoft.Extensions.Configuration.CommandLine | |
public class CommandLineConfigurationProvider : ConfigurationProvider | ||
{ | ||
private readonly Dictionary<string, string> _switchMappings; | ||
private static bool s_isWindows = | ||
#if NET461 | ||
true; | ||
#else | ||
RuntimeInformation.IsOSPlatform(OSPlatform.Windows); | ||
#endif | ||
|
||
/// <summary> | ||
/// Initializes a new instance. | ||
|
@@ -31,104 +40,171 @@ public CommandLineConfigurationProvider(IEnumerable<string> args, IDictionary<st | |
/// <summary> | ||
/// The command line arguments. | ||
/// </summary> | ||
protected IEnumerable<string> Args { get; private set; } | ||
protected IEnumerable<string> Args { get; } | ||
|
||
/// <summary> | ||
/// Loads the configuration data from the command line args. | ||
/// </summary> | ||
public override void Load() | ||
{ | ||
var data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); | ||
string key, value; | ||
|
||
using (IEnumerator<string> enumerator = Args.GetEnumerator()) | ||
{ | ||
// Store 1st argument here and start while loop with the 2nd, | ||
// or 'Current', argument. This is so we can look at the | ||
// 'Current' argument in relation to the 1st while evaluating | ||
// the 1st. | ||
string previousArg = enumerator.MoveNext() ? enumerator.Current : null; | ||
|
||
// If no first arg, return empty dictionary | ||
if (previousArg == null) | ||
{ | ||
Data = data; | ||
return; | ||
} | ||
|
||
while (enumerator.MoveNext()) | ||
{ | ||
// 'Current' is now the 2nd argument in Args | ||
string currentArg = enumerator.Current; | ||
int keyStartIndex = 0; | ||
|
||
if (currentArg.StartsWith("--")) | ||
{ | ||
keyStartIndex = 2; | ||
} | ||
else if (currentArg.StartsWith("-")) | ||
// TryProcessArgs will return false if previousArg is invalid | ||
if (TryProcessArgs(previousArg, currentArg, out (string key, string value) loopPair)) | ||
{ | ||
keyStartIndex = 1; | ||
} | ||
else if (currentArg.StartsWith("/")) | ||
{ | ||
// "/SomeSwitch" is equivalent to "--SomeSwitch" when interpreting switch mappings | ||
// So we do a conversion to simplify later processing | ||
currentArg = string.Format("--{0}", currentArg.Substring(1)); | ||
keyStartIndex = 2; | ||
// Override value when key is duplicated, | ||
// so we always have the last argument win. | ||
data[loopPair.key] = loopPair.value; | ||
} | ||
previousArg = currentArg; | ||
} | ||
|
||
int separator = currentArg.IndexOf('='); | ||
// Process the last previousArg after exiting loop | ||
if (TryProcessArgs(previousArg, null, out (string key, string value) lastPair)) | ||
{ | ||
data[lastPair.key] = lastPair.value; | ||
} | ||
} | ||
|
||
if (separator < 0) | ||
{ | ||
// If there is neither equal sign nor prefix in current arugment, it is an invalid format | ||
if (keyStartIndex == 0) | ||
{ | ||
// Ignore invalid formats | ||
continue; | ||
} | ||
|
||
// If the switch is a key in given switch mappings, interpret it | ||
if (_switchMappings != null && _switchMappings.TryGetValue(currentArg, out string mappedKey)) | ||
{ | ||
key = mappedKey; | ||
} | ||
// If the switch starts with a single "-" and it isn't in given mappings , it is an invalid usage so ignore it | ||
else if (keyStartIndex == 1) | ||
{ | ||
continue; | ||
} | ||
// Otherwise, use the switch name directly as a key | ||
else | ||
{ | ||
key = currentArg.Substring(keyStartIndex); | ||
} | ||
|
||
string previousKey = enumerator.Current; | ||
if (!enumerator.MoveNext()) | ||
{ | ||
// ignore missing values | ||
continue; | ||
} | ||
|
||
value = enumerator.Current; | ||
} | ||
else | ||
{ | ||
string keySegment = currentArg.Substring(0, separator); | ||
|
||
// If the switch is a key in given switch mappings, interpret it | ||
if (_switchMappings != null && _switchMappings.TryGetValue(keySegment, out string mappedKeySegment)) | ||
{ | ||
key = mappedKeySegment; | ||
} | ||
// If the switch starts with a single "-" and it isn't in given mappings , it is an invalid usage | ||
else if (keyStartIndex == 1) | ||
{ | ||
throw new FormatException(SR.Format(SR.Error_ShortSwitchNotDefined, currentArg)); | ||
} | ||
// Otherwise, use the switch name directly as a key | ||
else | ||
{ | ||
key = currentArg.Substring(keyStartIndex, separator - keyStartIndex); | ||
} | ||
|
||
value = currentArg.Substring(separator + 1); | ||
} | ||
Data = data; | ||
} | ||
|
||
/// <summary> | ||
/// Reconcile two consecutive arguments into a key-value pair for the first. | ||
/// </summary> | ||
/// <remarks> | ||
/// Helper function to reduce repeated code in <see cref="Load"/> method. | ||
/// </remarks> | ||
/// <param name="previousArg">The first string argument</param> | ||
/// <param name="currentArg">The second string argument</param> | ||
/// <param name="pair"> | ||
/// A properly resolved configuration key-value pair, or null if previous argument is invalid. | ||
/// </param> | ||
/// <returns> | ||
/// True if the args can be resolved to a proper configuration key-value pair. | ||
/// </returns> | ||
private bool TryProcessArgs(string previousArg, string currentArg, out (string key, string value) pair) | ||
{ | ||
string key, value; | ||
int keyStartIndex = 0; | ||
|
||
// Override value when key is duplicated. So we always have the last argument win. | ||
data[key] = value; | ||
if (previousArg.StartsWith("--")) | ||
{ | ||
keyStartIndex = 2; | ||
} | ||
else if (previousArg.StartsWith("-")) | ||
{ | ||
keyStartIndex = 1; | ||
} | ||
else if (previousArg.StartsWith("/") | ||
&& s_isWindows | ||
&& previousArg.IndexOf("/", StringComparison.Ordinal) | ||
== previousArg.LastIndexOf("/", StringComparison.Ordinal)) // i.e. only one instance of '/' | ||
{ | ||
// On Windows, "/SomeSwitch" is equivalent to "--SomeSwitch" when interpreting switch mappings | ||
// So we do a conversion to simplify later processing | ||
previousArg = $"--{previousArg.Substring(1)}"; | ||
keyStartIndex = 2; | ||
} | ||
|
||
int separatorIndex = previousArg.IndexOf('='); | ||
|
||
if (separatorIndex < 0) | ||
{ | ||
// If there is neither equal sign nor prefix in previous argument, it is an invalid format | ||
if (keyStartIndex == 0) | ||
{ | ||
// Ignore invalid formats | ||
pair = default; | ||
return false; | ||
} | ||
|
||
// If the switch is a key in given switch mappings, interpret it | ||
if (_switchMappings != null | ||
&& _switchMappings.TryGetValue( | ||
previousArg, | ||
out string mappedKey)) | ||
{ | ||
key = mappedKey; | ||
} | ||
// If the switch starts with a single "-" and it isn't in given mappings, | ||
// or in any other case, use the switch name directly as a key | ||
else | ||
{ | ||
key = previousArg.Substring(keyStartIndex); | ||
} | ||
|
||
// If the argument is last in list, the next argument begins | ||
// with an arg delimiter, or the next argument contains '=', | ||
// then treat argument as switch and record value of "true" | ||
if (currentArg == null | ||
|| currentArg.StartsWith("--") | ||
|| currentArg.StartsWith("-") | ||
|| currentArg.StartsWith("/") | ||
&& s_isWindows | ||
&& currentArg.IndexOf("/", StringComparison.Ordinal) | ||
== currentArg.LastIndexOf("/", StringComparison.Ordinal) | ||
|| currentArg.Contains("=")) | ||
{ | ||
value = "true"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what would happen if the value expected for the key is not a boolean? Do we have validation for that? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure I understand the question. The
would generate the dictionary The absence of a switch without a value can be interpreted as a falsy value for that switch. For example, if my program is looking for a value for Key1 in Without a given value, |
||
} | ||
else | ||
{ | ||
value = currentArg; | ||
} | ||
} | ||
else | ||
{ | ||
string keySegment = previousArg.Substring(0, separatorIndex); | ||
|
||
Data = data; | ||
// If the switch is a key in given switch mappings, interpret it | ||
if (_switchMappings != null | ||
&& _switchMappings.TryGetValue( | ||
keySegment, | ||
out string mappedKeySegment)) | ||
{ | ||
key = mappedKeySegment; | ||
} | ||
// If the switch starts with a single "-" and it isn't in given mappings , it is an invalid usage | ||
else if (keyStartIndex == 1) | ||
{ | ||
throw new FormatException( | ||
SR.Format( | ||
SR.Error_ShortSwitchNotDefined, | ||
previousArg)); | ||
} | ||
// Otherwise, use the switch name directly as a key | ||
else | ||
{ | ||
key = previousArg.Substring( | ||
keyStartIndex, | ||
separatorIndex - keyStartIndex); | ||
} | ||
|
||
value = previousArg.Substring(separatorIndex + 1); | ||
} | ||
|
||
pair = (key, value); | ||
return true; | ||
} | ||
|
||
private Dictionary<string, string> GetValidatedSwitchMappingsCopy(IDictionary<string, string> switchMappings) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't this be calling
TryProcessArgs
with the last arg twice?previousArg
will be pointing toenumerator.Current
which is the same ascurrentArg
if it entered the while loop.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The variable
previousArg
gets assigned the value ofcurrentArg
at the end of the last loop in thewhile
block. When it reaches the point where it processes the last argument (outside the while loop),previousArg
andcurrentArg
are indeed the same value, but that value has not yet been passed as thepreviousArg
to theTryProcessArgs
method. It only goes through it once.