spectre.console icon indicating copy to clipboard operation
spectre.console copied to clipboard

A `boolean` flag followed by a positional argument leads the the error `Flags cannot be assigned a value.`

Open sandreas opened this issue 3 years ago • 2 comments

Information

  • OS: macOS
  • Version: 0.43.0
  • Terminal: macOS Term

Describe the bug A combination of a boolean flag and a positional argument leads the the error Flags cannot be assigned a value., which is wrong in my opinion.

To Reproduce Use the following settings:

// ...
    [Description("Input files or folders")]
    [CommandArgument(0, "[input]")]
    public string[] Input { get; set; } = Array.Empty<string>();

    [CommandOption("--dry-run")] public bool DryRun { get; init; } = false;
// ...

And call the app like this:

myapp --dry-run "my-input-folder/"

Expected behavior As --dry-run is a flag, I would expect spectre.console to handle the fact, taht there is a positional argument in the settings and parse this correctly. In my opinion this should work without error messages.

Workaround Putting the flag at the end works like expected:

myapp "my-input-folder/" --dry-run

sandreas avatar May 07 '22 05:05 sandreas

Added a small cli-tester to my open source project to demonstrate the issue: https://github.com/sandreas/tone/tree/main/cli-tester

# error, although --dry-run is a boolean flag without any value
cli-tester test --dry-run input.mp3

Error: Flags cannot be assigned a value.

       test --dry-run input.mp3
            ^^^^^^^^^ Can't assign value


# works like expected (just put the flag at the end)
cli-tester test input.mp3  --dry-run

Since this is an issue with UX, I think this is a serious one. I would really appreciate ANY feedback...?! Maybe I'm doing something wrong?

sandreas avatar Jun 05 '22 11:06 sandreas

@patriksvensson Sorry to adress you directly but I just wanted to ask, if this is the right place to report these kind of issues or am I doing something wrong?

sandreas avatar Jul 05 '22 16:07 sandreas

This seems a duplicate of https://github.com/spectreconsole/spectre.console/issues/193 @patriksvensson

FrankRay78 avatar Oct 18 '22 16:10 FrankRay78

@FrankRay78 You are right, sorry, I did not find this. If you are looking for a reason, why this is not working as expected, you should take a look this.

the tokenizer should not silently ignore valid chars or make invalid assumptions about the context (e.g. assume that every args value starting with - is automatically an option)... this is something the lexer, parser or context should be aware of...

I think the tokenizer treats every argument the same way and is not aware of it's context - which is a real problem when using flags (boolean), because the tokenzier / lexer only knows, that the next argument cannot be a value, if it is aware of the type of already parsed arguments...

In my opinion all these issues are related to each other. Maybe the right way to do this would be:

  • Take the next argument
  • Check if the argument matches a setting property
    • If yes, check if the argument needs a value (yes, if it is a setting, no if it is a flag)
      • If yes, take the next argument value as is (without parsing anything or assuming - is a setting argument or flag)
      • If no, set the flag and start over
    • If no, check if the argument already contains a value (with =)
      • If yes, parse and start over
      • If no, it is a parsing error

If I am right, currently the parser treats every argument the same without looking at the context.

sandreas avatar Oct 18 '22 16:10 sandreas

Hi @sandreas, it's interesting your comment above, and also your comment here https://github.com/spectreconsole/spectre.console/issues/959

I think we are saying the same thing but I would appreciate if you could cast your eyes over my most recent comment here: https://github.com/spectreconsole/spectre.console/issues/193

Also, the CommandTreeParser is a surprisingly long class with most of the methods marked as private (ie. small number of entry points combined with complex internal logic), which as you indicated elsewhere, doesn't bode well for test coverage.

FrankRay78 avatar Oct 18 '22 18:10 FrankRay78

I think we are saying the same thing but I would appreciate if you could cast your eyes over my most recent comment here: https://github.com/spectreconsole/spectre.console/issues/193

Thank you for your quick response. I did read your most recent comment - maybe I did not fully understand, because I'm not too much into the spectre.console code, but I think what you describe is only part of the problem...

The command line parser does indeed handle boolean options (flags) the wrong way under specific circumstances, but NOT ONLY flags. Afaik it does not distinguish between FLAGS, OPTIONS, VALUES, ARGUMENTS and OPTION=VALUE combinations, depending on the context it currently has. This seems to cause the flag issue, but also to the -, " and <whitespace> cannot be used as first char of a VALUE (because it goes through a parser, although it shouldn't and should be kept as is).

Example - let's say you have the following Options-Class:

public class TestCommandSettings: CommandSettings
{
    [CommandOption("--dry-run")] public bool? DryRun { get; init; } = false;
    
    [CommandOption("--order-by")] public string? OrderBy { get; set; }
    
    [Description("Input files or folders")]
    [CommandArgument(0, "[input]")]
    public string[] Input { get; set; } = Array.Empty<string>();
}
# 1. FLAG, next token MUST be OPTION, OPTION=VALUE, ARGUMENT or end of tokens
# --dry-run
cli-tester --dry-run

# 2. OPTION, next token MUST be VALUE (e.g. for OrderBy-Option)
# --order-by 
cli-tester --order-by "size"

# 3. OPTION=VALUE, next token MUST be OPTION, OPTION=VALUE, ARGUMENT or end of tokens
# --order-by="size"
cli-tester --order-by="size"

# 4. ARGUMENT, next token MUST be OPTION, OPTION=VALUE, ARGUMENT or end of tokens
# "my-directory/"
cli-tester "my-directory/"

# combined example
cli-tester --order-by "-size" --dry-run "my-directory"

So in my opinion the parser needs to be redesigned to:

  • Not look for VALUE after FLAG, OPTION=VALUE or ARGUMENT
  • Force having a VALUE after every valid OPTION
  • Never "parse" any VALUE or ARGUMENT (just keep as is)

The only real "parsing" needs to be done, when there is OPTION=VALUE, in every other case it just needs a check, if it is a valid FLAG or OPTION followed by an untouched VALUE or it can be an ARGUMENT.

In short: Why "parse" --order-by and "-size" separately, when we already know, that --order-by is a perfect match in our OPTION reference and it must always be followed by a VALUE, that does not need to be parsed anyway.

I hope this was understandable... pretty long text for a pretty simple algorithm ;)

sandreas avatar Oct 18 '22 20:10 sandreas

Excellent write up and example @sandreas. I plan to fix the other issue, which is one step forward and will let me learn the parser and improve code coverage. Then come back to this issue.

FrankRay78 avatar Oct 19 '22 08:10 FrankRay78

Some small additions / edge-cases:

  • What happens if there single quotes enclosing double quotes with equal sign: "--meta-description='\"What?\"'"
  • If there is multiple flags with short form --force|-f and --debug|-d, a common short form is -df to enable both flags
  • If there is an option with short form --order-by|-o, the value can be set via -osize

Just a few of my other thoughts...

sandreas avatar Oct 19 '22 11:10 sandreas

@sandreas -df is already supported.

patriksvensson avatar Oct 19 '22 11:10 patriksvensson

-df is already supported.

Didn't know that. thx

sandreas avatar Oct 19 '22 11:10 sandreas

@FrankRay78

Would it help, if I compose a set of edge case parameter sets (e.g. --option '"value" test') compile the following code as x64 for Windows, macOS and Linux and provide the output of these edge cases on all platforms?:

foreach (var arg in args)
{
    Console.WriteLine(arg);
}

sandreas avatar Oct 20 '22 17:10 sandreas

This issue can be closed/marked as complete @patriksvensson, now that PR https://github.com/spectreconsole/spectre.console/pull/1048 has been successfully merged.

FrankRay78 avatar Dec 11 '22 13:12 FrankRay78