Skip to content

Commit

Permalink
Fix enumerable options grabbing too many values
Browse files Browse the repository at this point in the history
Fixes #687
Fixes #619
Fixes #617
Fixes #510
Fixes #454
Fixes #420
Fixes #396
Fixes #91
  • Loading branch information
rmunn committed Aug 20, 2020
1 parent 570d7b7 commit 47618e0
Show file tree
Hide file tree
Showing 7 changed files with 339 additions and 30 deletions.
35 changes: 22 additions & 13 deletions src/CommandLine/Core/GetoptTokenizer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,19 +94,28 @@ public static Result<IEnumerable<Token>, Error> ExplodeOptionList(
{
var tokens = tokenizerResult.SucceededWith().Memoize();

var replaces = tokens.Select((t, i) =>
optionSequenceWithSeparatorLookup(t.Text)
.MapValueOrDefault(sep => Tuple.Create(i + 1, sep),
Tuple.Create(-1, '\0'))).SkipWhile(x => x.Item1 < 0).Memoize();

var exploded = tokens.Select((t, i) =>
replaces.FirstOrDefault(x => x.Item1 == i).ToMaybe()
.MapValueOrDefault(r => t.Text.Split(r.Item2).Select(Token.Value),
Enumerable.Empty<Token>().Concat(new[] { t })));

var flattened = exploded.SelectMany(x => x);

return Result.Succeed(flattened, tokenizerResult.SuccessMessages());
var exploded = new List<Token>(tokens is ICollection<Token> coll ? coll.Count : tokens.Count());
var nothing = Maybe.Nothing<char>(); // Re-use same Nothing instance for efficiency
var separator = nothing;
foreach (var token in tokens) {
if (token.IsName()) {
separator = optionSequenceWithSeparatorLookup(token.Text);
exploded.Add(token);
} else {
// Forced values are never considered option values, so they should not be split
if (separator.MatchJust(out char sep) && sep != '\0' && !token.IsValueForced()) {
if (token.Text.Contains(sep)) {
exploded.AddRange(token.Text.Split(sep).Select(Token.ValueFromSeparator));
} else {
exploded.Add(token);
}
} else {
exploded.Add(token);
}
separator = nothing; // Only first value after a separator can possibly be split
}
}
return Result.Succeed(exploded as IEnumerable<Token>, tokenizerResult.SuccessMessages());
}

public static Func<
Expand Down
34 changes: 30 additions & 4 deletions src/CommandLine/Core/Token.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,12 @@ public static Token Value(string text, bool explicitlyAssigned)

public static Token ValueForced(string text)
{
return new Value(text, false, true);
return new Value(text, false, true, false);
}

public static Token ValueFromSeparator(string text)
{
return new Value(text, false, false, true);
}

public TokenType Tag
Expand Down Expand Up @@ -86,29 +91,45 @@ class Value : Token, IEquatable<Value>
{
private readonly bool explicitlyAssigned;
private readonly bool forced;
private readonly bool fromSeparator;

public Value(string text)
: this(text, false, false)
: this(text, false, false, false)
{
}

public Value(string text, bool explicitlyAssigned)
: this(text, explicitlyAssigned, false)
: this(text, explicitlyAssigned, false, false)
{
}

public Value(string text, bool explicitlyAssigned, bool forced)
public Value(string text, bool explicitlyAssigned, bool forced, bool fromSeparator)
: base(TokenType.Value, text)
{
this.explicitlyAssigned = explicitlyAssigned;
this.forced = forced;
this.fromSeparator = fromSeparator;
}

/// <summary>
/// Whether this value came from a long option with "=" separating the name from the value
/// </summary>
public bool ExplicitlyAssigned
{
get { return explicitlyAssigned; }
}

/// <summary>
/// Whether this value came from a sequence specified with a separator (e.g., "--files a.txt,b.txt,c.txt")
/// </summary>
public bool FromSeparator
{
get { return fromSeparator; }
}

/// <summary>
/// Whether this value came from args after the -- separator (when EnableDashDash = true)
/// </summary>
public bool Forced
{
get { return forced; }
Expand Down Expand Up @@ -153,6 +174,11 @@ public static bool IsValue(this Token token)
return token.Tag == TokenType.Value;
}

public static bool IsValueFromSeparator(this Token token)
{
return token.IsValue() && ((Value)token).FromSeparator;
}

public static bool IsValueForced(this Token token)
{
return token.IsValue() && ((Value)token).Forced;
Expand Down
20 changes: 20 additions & 0 deletions src/CommandLine/Core/TokenPartitioner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,18 @@ public static Tuple<IEnumerable<Token>, IEnumerable<Token>, IEnumerable<Token>,
var count = new Dictionary<Token, int>();
var max = new Dictionary<Token, Maybe<int>>();
var state = SequenceState.TokenSearch;
var separatorSeen = false;
Token nameToken = null;
foreach (var token in tokens)
{
if (token.IsValueForced())
{
separatorSeen = false;
nonOptionTokens.Add(token);
}
else if (token.IsName())
{
separatorSeen = false;
if (typeLookup(token.Text).MatchJust(out var info))
{
switch (info.TargetType)
Expand Down Expand Up @@ -96,12 +99,14 @@ public static Tuple<IEnumerable<Token>, IEnumerable<Token>, IEnumerable<Token>,
case SequenceState.TokenSearch:
case SequenceState.ScalarTokenFound when nameToken == null:
case SequenceState.SequenceTokenFound when nameToken == null:
separatorSeen = false;
nameToken = null;
nonOptionTokens.Add(token);
state = SequenceState.TokenSearch;
break;

case SequenceState.ScalarTokenFound:
separatorSeen = false;
nameToken = null;
scalarTokens.Add(token);
state = SequenceState.TokenSearch;
Expand All @@ -116,6 +121,20 @@ public static Tuple<IEnumerable<Token>, IEnumerable<Token>, IEnumerable<Token>,
nonOptionTokens.Add(token);
state = SequenceState.TokenSearch;
}
else if (token.IsValueFromSeparator())
{
separatorSeen = true;
sequence.Add(token);
count[nameToken]++;
}
else if (separatorSeen)
{
// Previous token came from a separator but this one didn't: sequence is completed
separatorSeen = false;
nameToken = null;
nonOptionTokens.Add(token);
state = SequenceState.TokenSearch;
}
else
{
sequence.Add(token);
Expand All @@ -125,6 +144,7 @@ public static Tuple<IEnumerable<Token>, IEnumerable<Token>, IEnumerable<Token>,
else
{
// Should never get here, but just in case:
separatorSeen = false;
sequences[nameToken] = new List<Token>(new[] { token });
count[nameToken] = 0;
max[nameToken] = Maybe.Nothing<int>();
Expand Down
35 changes: 22 additions & 13 deletions src/CommandLine/Core/Tokenizer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,19 +62,28 @@ public static Result<IEnumerable<Token>, Error> ExplodeOptionList(
{
var tokens = tokenizerResult.SucceededWith().Memoize();

var replaces = tokens.Select((t, i) =>
optionSequenceWithSeparatorLookup(t.Text)
.MapValueOrDefault(sep => Tuple.Create(i + 1, sep),
Tuple.Create(-1, '\0'))).SkipWhile(x => x.Item1 < 0).Memoize();

var exploded = tokens.Select((t, i) =>
replaces.FirstOrDefault(x => x.Item1 == i).ToMaybe()
.MapValueOrDefault(r => t.Text.Split(r.Item2).Select(Token.Value),
Enumerable.Empty<Token>().Concat(new[] { t })));

var flattened = exploded.SelectMany(x => x);

return Result.Succeed(flattened, tokenizerResult.SuccessMessages());
var exploded = new List<Token>(tokens is ICollection<Token> coll ? coll.Count : tokens.Count());
var nothing = Maybe.Nothing<char>(); // Re-use same Nothing instance for efficiency
var separator = nothing;
foreach (var token in tokens) {
if (token.IsName()) {
separator = optionSequenceWithSeparatorLookup(token.Text);
exploded.Add(token);
} else {
// Forced values are never considered option values, so they should not be split
if (separator.MatchJust(out char sep) && sep != '\0' && !token.IsValueForced()) {
if (token.Text.Contains(sep)) {
exploded.AddRange(token.Text.Split(sep).Select(Token.ValueFromSeparator));
} else {
exploded.Add(token);
}
} else {
exploded.Add(token);
}
separator = nothing; // Only first value after a separator can possibly be split
}
}
return Result.Succeed(exploded as IEnumerable<Token>, tokenizerResult.SuccessMessages());
}

public static IEnumerable<Token> Normalize(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information.

using System.Collections.Generic;

namespace CommandLine.Tests.Fakes
{
public class Options_For_Issue_91
{
[Value(0, Required = true)]
public string InputFileName { get; set; }

[Option('o', "output")]
public string OutputFileName { get; set; }

[Option('i', "include", Separator = ',')]
public IEnumerable<string> Included { get; set; }

[Option('e', "exclude", Separator = ',')]
public IEnumerable<string> Excluded { get; set; }
}

public class Options_For_Issue_454
{
[Option('c', "channels", Required = true, Separator = ':', HelpText = "Channel names")]
public IEnumerable<string> Channels { get; set; }

[Value(0, Required = true, MetaName = "file_path", HelpText = "Path of archive to be processed")]
public string ArchivePath { get; set; }
}

public class Options_For_Issue_510
{
[Option('a', "aa", Required = false, Separator = ',')]
public IEnumerable<string> A { get; set; }

[Option('b', "bb", Required = false)]
public string B { get; set; }

[Value(0, Required = true)]
public string C { get; set; }
}

public enum FMode { C, D, S };

public class Options_For_Issue_617
{
[Option("fm", Separator=',', Default = new[] { FMode.S })]
public IEnumerable<FMode> Mode { get; set; }

[Option('q')]
public bool q { get;set; }

[Value(0)]
public IList<string> Files { get; set; }
}

public class Options_For_Issue_619
{
[Option("verbose", Required = false, Default = false, HelpText = "Generate process tracing information")]
public bool Verbose { get; set; }

[Option("outdir", Required = false, Default = ".", HelpText = "Directory to look for object file")]
public string OutDir { get; set; }

[Option("modules", Required = true, Separator = ',', HelpText = "Directories to look for module file")]
public IEnumerable<string> ModuleDirs { get; set; }

[Option("ignore", Required = false, Separator = ' ', HelpText = "List of additional module name references to ignore")]
public IEnumerable<string> Ignores { get; set; }

[Value(0, Required = true, HelpText = "List of source files to process")]
public IEnumerable<string> Srcs { get; set; }
}
}
31 changes: 31 additions & 0 deletions tests/CommandLine.Tests/Fakes/Options_With_Similar_Names.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information.

using System.Collections.Generic;

namespace CommandLine.Tests.Fakes
{
public class Options_With_Similar_Names
{
[Option("deploy", Separator = ',', HelpText= "Projects to deploy")]
public IEnumerable<string> Deploys { get; set; }

[Option("profile", Required = true, HelpText = "Profile to use when restoring and publishing")]
public string Profile { get; set; }

[Option("configure-profile", Required = true, HelpText = "Profile to use for Configure")]
public string ConfigureProfile { get; set; }
}

public class Options_With_Similar_Names_And_Separator
{
[Option('f', "flag", HelpText = "Flag")]
public bool Flag { get; set; }

[Option('c', "categories", Required = false, Separator = ',', HelpText = "Categories")]
public IEnumerable<string> Categories { get; set; }

[Option('j', "jobId", Required = true, HelpText = "Texts.ExplainJob")]
public int JobId { get; set; }
}

}
Loading

0 comments on commit 47618e0

Please sign in to comment.