commandline icon indicating copy to clipboard operation
commandline copied to clipboard

Clarify usage of boolean Options

Open jonnybee opened this issue 5 years ago • 20 comments

The Quick start sample in Readme.md gives the impression that boolean parameter can be specified with either a true or false value. However my attemps have given the following:

  • whenever the parameter exists on the commandline the value will always be true
  • omitting the parameter will set it to tits default value.
  • trying to specify either True or False as value is ignored.
  • having default value of true will always have the Option value set to true

Is this how it is supposed to work?

jonnybee avatar Oct 19 '20 13:10 jonnybee

Without an example, it is difficult to know what is going on.

My interpretation of it is: The default is (or should) be false - and only when the Boolean parameter is specified, it will become true... Would it be possible to re-write your parser arguments to work that way?

TPedersenAtClearwave avatar Nov 18 '20 21:11 TPedersenAtClearwave

The first Quick start sample clearly indicates that a boolean parameter can be specified as true or false. See: https://github.com/commandlineparser/commandline

But that's just not how it works.

jonnybee avatar Nov 19 '20 08:11 jonnybee

I agree with @jonnybee , having the same problem..

[Option('q', "quit", Required = false, Default = true, HelpText = "Quit after run")]
public bool Quit { get; set; }

You just can't make it false, it will always be true..

I've tried the following variations:

app.exe
app.exe -q 0
app.exe -q false
app.exe -q "false"
app.exe -q False
app.exe -q "False"

And of course also tried with the longname --quit as well.

https://github.com/commandlineparser/commandline/wiki/CommandLine-Grammar#example-2

Hmm, when it's nullable it DOES work as in the example, but you have to change the code a bit to do some HasValue checks, which is strange, because it will always have a value.

[Option('q', "quit", Required = false, Default = true, HelpText = "Quit after run")]
public bool? Quit { get; set; }

Besides that, "false" is a little bit too specific for C#, it would be nicer if -q 0 was also supported.

KoalaBear84 avatar Dec 23 '20 21:12 KoalaBear84

I just came here to report this also, you need to be able to specify the default as true then specifically set it to false.

Based on @KoalaBear84's Quit field definition:

[Option('q', "quit", Required = false, Default = true, HelpText = "Quit after run")]
public bool? Quit { get; set; }

I would recommend supporting these inputs:

Command Quit Value
app true
qpp -q true
app -q 1 true
app -q true true
app -q True true
app -q 0 false
app -q false false
app -q False false
qpp --quit true
app --quit 1 true
app --quit true true
app --quit True true
app --quit 0 false
app --quit false false
app --quit False false
app --no-quit false

justinmchase avatar Dec 31 '20 18:12 justinmchase

The workaround is to invert the field name and default to false:

[Option("no-quit", Required = false, Default = false, HelpText = "Do not quit after run")]
public bool? NoQuit { get; set; }

justinmchase avatar Dec 31 '20 18:12 justinmchase

The first Quick start sample clearly indicates that a boolean parameter can be specified as true or false. See: https://github.com/commandlineparser/commandline

But that's just not how it works.

I don't see that example in the https://github.com/commandlineparser/commandline README, but I do see something similar in https://github.com/commandlineparser/commandline/wiki/CommandLine-Grammar#example-2 where there's a nullable bool with default of true. The fact that it's nullable is key: if you have an option with a type of bool, CommandLineParser assumes that you want that to be a classic flag-type option, where the "true" or "false" value can be interpreted as "it was specified on the command line" or "it was not specified". If you want the user to actually provide the value "true" or "false" for the flag, then you need to make it a nullable bool, i.e. a bool? type rather than a bool type.

rmunn avatar Feb 06 '21 14:02 rmunn

This is from the README.MD:

C# Quick Start:

using System;
using CommandLine;

namespace QuickStart   
{
    class Program
    {
        public class Options
        {
            [Option('v', "verbose", Required = false, HelpText = "Set output to verbose messages.")]
            public bool Verbose { get; set; }
        }

        static void Main(string[] args)
        {
            Parser.Default.ParseArguments<Options>(args)
                   .WithParsed<Options>(o =>
                   {
                       if (o.Verbose)
                       {
                           Console.WriteLine($"Verbose output enabled. Current Arguments: -v {o.Verbose}");
                           Console.WriteLine("Quick Start Example! App is in Verbose mode!");
                       }
                       else
                       {
                           Console.WriteLine($"Current Arguments: -v {o.Verbose}");
                           Console.WriteLine("Quick Start Example!");
                       }
                   });
           }
      }
}

This sample does NOT use a nullable boolean - and the ONLY way to make verbose have a false value is to omit the parameter from the parameter line. And the sample clearly indicates that you should be able to specify a false value.

jonnybee avatar Feb 06 '21 15:02 jonnybee

Just to clarify the issue - with compiled program above, the C# Quick Start from README.MD:

QuickStart.exe ==> Current Arguments: -v False QuickStart.exe -v False ==> Verbose output enabled. Current Arguments: -v True QuickStart.exe -v ==> Verbose output enabled. Current Arguments: -v True QuickStart.exe -v True ==> Verbose output enabled. Current Arguments: -v True

Even worse if you would make:

        [Option('v', "verbose", Required = false, HelpText = "Set output to verbose messages.", Default = true)]
        public bool Verbose { get; set; }

Then there is absolutely NO WAY to make this parameter false.

jonnybee avatar Feb 06 '21 16:02 jonnybee

Ah, I see. You're misunderstanding the output of that sample program. (Which, to be fair, is badly written and easy to understand).

When the sample program outputs "Current Arguments: -v False" it does not mean that you specified -v false on the command line. It means that the value of the Verbose option is false, because -v was not given on the command line at all. That's the expected behavior for "flag" options: they default to false and become true if the option is given on the command line, which is the way most (not all, but most) boolean options are used in most software. What you were thinking it meant is that you can write -v true or -v false on the command line, but that's not how getopt (or CommandLineParser, which is designed to mimic getopt) works. When you write -v foo on the command line, that's read as two separate options: -v (meaning "Set Verbose to true") and "foo". Then if you had [Value(0)] string Filename { get; set; } in your options, the value of Filename would become "foo".

For those few cases where you have an option that defaults to true and you want to make it false, the common pattern is to have a --no-foo option. Let's say the "compile" step is the default, but sometimes users want to not compile. Then you'd write your program to take a --no-compile option, and if that is true, you skip the compile step. If it is false (the user didn't specify that on the command line), then you run the compile step.

Hopefully that clears up the misunderstanding. If there's more I need to explain, please let me know what else is confusing you and I'll do my best to explain it as well.

rmunn avatar Feb 07 '21 09:02 rmunn

...which is the way most (not all, but most) boolean options are used in most software.

Sorry citation needed :(

Head over here: https://npm.runkit.com/minimist

And try this code:

var minimist = require("minimist")
minimist(["-x", "--y", "false", "-z", "true", "--no-foo"], {
  // if true will treat all double hyphenated arguments without equal signs as boolean (e.g. affects --foo, not -f or --foo=bar)
  boolean: true,
  default: {
    foo: true,
    bar: true,
  }
})
{_: [], bar: true, foo: false, x: true, y: false, z: "true"}

image

This is the behavior of the main command line parsing tool in node.js, 41 million weekly downloads. What its doing is a little more complicated than what you're saying...

When you write -v foo on the command line, that's read as two separate options: -v (meaning "Set Verbose to true") and "foo".

With minimist if you do a -v foo it does indeed treat them as a single command not two separate as you're suggesting but it would interpret it as a string in that case and would not interpret -v true as a boolean true. It would however attempt to convert --verbose false into a boolean. It also automatically supports --no-verbose setting it as verbose: false in the parsed options.

I know this library is not that library and doesn't need to be exactly the same and that command line parsing is more of a convention then a specification and things do differ based on platforms and users... I get that but what I'm saying is that what you have here is unintuitive and complicated. In this .net system we have more information such as the Type and metadata such as the default value and when I see a field of type boolean and have set the default to true, then when I realize that its not possible to ever set this value to false now... that is confusing and seems like a bug.

It does seem like you should treat flags such as -v false as a single operation equivalent to -v=false, --verbose false, --verbose=false, --no-verbose, etc.

justinmchase avatar Feb 23 '21 15:02 justinmchase

...which is the way most (not all, but most) boolean options are used in most software.

Sorry citation needed :(

I guess I should have said "most getopt-using software" since the behavior getopt is what this library is meant to copy. Citation: let's look at some of the most common Linux command-line utilities, and see how they handle Boolean options. Off the top of my head, I thought of ls, gzip, and sed. Let's see how they handle flags (Boolean options). I'll pick flags that have both a short form and a long form so that we can test things like -v false, -vfalse, --verbose false, --verbose=false, and so on.

ls has a -a flag with long name --all. Let's see if we can pass "false" as a value:

$ ls
file1.txt  file2.txt
$ ls -a
.  ..  file1.txt  file2.txt
$ ls -a false
ls: cannot access 'false': No such file or directory
$ ls --all
.  ..  file1.txt  file2.txt
$ ls --no-all
ls: unrecognized option '--no-all'
Try 'ls --help' for more information.
$ ls --all false
ls: cannot access 'false': No such file or directory
$ ls --all=false
ls: option '--all' doesn't allow an argument
Try 'ls --help' for more information.
$ ls -afalse
ls: invalid option -- 'e'
Try 'ls --help' for more information.
$ 

gzip has a -d / --decompress flag. Let's see if we can do -d false or --decompress=false:

$ gzip -d
gzip: compressed data not read from a terminal. Use -f to force decompression.
For help, type: gzip -h
$ gzip --decompress
gzip: compressed data not read from a terminal. Use -f to force decompression.
For help, type: gzip -h
$ gzip -d false
gzip: false.gz: No such file or directory
$ gzip --decompress false
gzip: false.gz: No such file or directory
$ gzip --decompress=false
gzip: option '--decompress' doesn't allow an argument
Try `gzip --help' for more information.
$ gzip -dfalse
gzip: invalid option -- 's'
Try `gzip --help' for more information.
$ 

sed has rather few Boolean options, but there's a -s / --separate flag we can use to test with. I'll also pass sed a couple of other parameters and some input data so we can see it doing something:

$ echo boolean | sed -s -e 's/a/b/'
boolebn
$ echo boolean | sed --separate -e 's/a/b/'
boolebn
$ echo boolean | sed --separate=false -e 's/a/b/'
sed: option '--separate' doesn't allow an argument
$ echo boolean | sed --separate false -e 's/a/b/'
sed: can't read false: No such file or directory
$ echo boolean | sed -sfalse -e 's/a/b/'
sed: couldn't open file alse: No such file or directory
$ echo boolean | sed -s=false -e 's/a/b/'
sed: invalid option -- '='
$ 

You may find the behavior of getopt to be unintiutive and complicated, in which case you might want a different command-line parsing library. Because the purpose of this library is to reproduce the behavior of getopt as closely as possible, and that includes getopt's handling of Boolean options (they don't take values, they default to false, and passing them on the command line sets them to true).

rmunn avatar Feb 24 '21 08:02 rmunn

But those are all examples of booleans which default to false. The problem arises when you need a boolean which defaults to true.

Not all boolean options are flags, some are toggles.

For another thing getopt does support optional parameters, and it makes no distinction between options, flags and toggles. All of which are loosely defined across platforms and ecosystems.

Additionally, you have already implemented support for --long options which isn't even supported by all versions of getopt.

Here for example is a little script I wrote using getopt which will allow you to optionally toggle a boolean to false:

#!/bin/bash

ALL=true
function process_arg() {
    case "${1}" in
        a)
            case "${2}" in
                1)     ALL=true;;
                true)  ALL=true;;
                0)     ALL=false;;
                false) ALL=false;;
                *)     ALL=true;;
            esac
            ;;
        *)  ;;
    esac
}

while getopts ":a:" arg; do
    case "${arg}" in
        :) process_arg $OPTARG;;
        *) process_arg $arg $OPTARG;;
    esac
done
shift $((OPTIND-1))

echo "ALL = ${ALL}"
example          # ALL = true
example -a       # ALL = true
example -a 1     # ALL = true
example -a true  # ALL = true
example -a 0     # ALL = false
example -a false # ALL = false

The point is that getopt is itself very limited, confusing and inconsistent across platforms. Modern CLI applications have evolved beyond getopt somewhat; necessarily so to keep up with modern demands and sentiments. And while I agree that one should attempt to conform to conventions as much as possible, it doesn't behoove anyone to be unnecessarily pedantic about limiting ourselves simply to the features of a particular flavor of an ancient library.

This particular choice doesn't pass the principal of least astonishment in my opinion; perhaps it would be useful to consider booleans with parameters to be "toggles" instead of flags?

justinmchase avatar Feb 25 '21 20:02 justinmchase

@justinmchase - Having flags that take values doesn't pass the principle of least astonishment for me, because in my 10+ years of using Linux I've gotten very used to "flags are toggles that default to false" being the overwhelming majority. In the cases where that's not true, I'm used to seeing "--no-foo" syntax as a way to turn option "foo" off (there's no equivalent short option syntax).

I wouldn't object if someone made a PR to allow automatic creation and parsing of --no-foo syntax; right now if you want to do that, you'd have to do something like this:

public class Options
{
    [Option("no-foo", Default = false, Required = false, HelpText = "Turn foo off")]
    public bool NoFoo { get; set; }

    public bool Foo { get { return !Foo; } }
}

And now Foo is an option that defaults to true, and has a --no-foo syntax to turn it off. But if that extra little bit of boilerplate annoys you, go ahead and submit a PR to change the Option class to add a Negatable=true parameter, which would let you write:

public class Options
{
    [Option("foo", Default = true, Negatable = true, Required = false, HelpText = "Don't know what would go here")]
    public bool Foo { get; set; }
}

And that would create a --no-foo option. I don't know how you'd handle the HelpText, which is why I don't think I'll be writing such a PR myself; I'd have no idea how to do it right. But if you have a good idea for how to handle the HelpText, I'm sure such a PR would be accepted, because it would be in keeping with historical getopt behavior and usage. Booleans that take arguments, though, would not.

Oh, and there's one other way you could get CLP to do Booleans with arguments, though then they wouldn't behave as flags: write then as Nullable<bool>. Then you'd be required to do either --foo true or --foo false on the command line. The drawback then is that a plain --foo flag with no value would be an error. But that might be what you're looking for. It would be very weird command-line syntax, IMHO, but if that's what you're looking for, a Nullable<bool> is the way to do it, I think.

rmunn avatar Feb 26 '21 03:02 rmunn

Well if you had the negatable flag on an option you could just skip printing it in the help text altogether since it has a non-negated entry also or perhaps just indicate that the option is itself negatable. You could possibly also add a NegatedHelpText that is optional

In my case I would like to have an optional follow boolean that defaults to true.

Indicate flag is negatable

-f, --follow    (Default: true, Negatable) Follow log output, false to print existing logs and exit

Show --no-* flag without help text

-f, --follow     (Default: true) Follow log output, false to print existing logs and exit
 --no-follow

Show --no-* flag with constant help text

-f, --follow     (Default: true) Follow log output, false to print existing logs and exit
--no-follow      (follow: false)

Also you may be interested in knowing that the default Go lang flag parser also supports defaulting booleans to true but with the twist that you can only set it with a value specifically if there is an = in the assignment. Meaning -f false will not set the flag to false but -f=false will.

package main

import (
	"flag"
	"fmt"
)

var verbose = flag.Bool("v", true, "verbose")

func main() {
	flag.Parse()
	fmt.Println("verbose is", *verbose)
}
Args Result
true
-v true
-v 1 true
-v true true
-v 0 true
-v false true
-v=1 true
-v=true true
-v=0 false
-v=false false

justinmchase avatar Mar 05 '21 19:03 justinmchase

Any update on this? The only workaround is to make the field as nullable suggested by @KoalaBear84.

sreejith-ms avatar Mar 19 '21 10:03 sreejith-ms

My solution: (relatively clear) I use enumeration type instead of bool, and hope to support bool type as early as possible

public enum BoolType
{
    False,
    True
}
public class Options
{
    [Option("is-done", Required = true, Default = BoolType.False, HelpText = "Is Done?")]
    public BoolType IsDone { get; set; }
}

command: --is-done False

xeekst avatar Jul 16 '21 03:07 xeekst

Also a good alternative indeed @xeekst

I've tried to switch to other libraries because of this. I don't like the fact that it's not capable of the same features which are heavily used in the Unix environment/tools.

KoalaBear84 avatar Jul 16 '21 08:07 KoalaBear84

That this is not fixed still baffles me. I'd implement it myself if I had the time.

Sire avatar Nov 09 '22 10:11 Sire