commandlineparser / commandline

The best C# command line parser that brings standardized *nix getopt style, for .NET. Includes F# support
MIT License
4.51k stars 477 forks source link

How to implement countable flags? Ref. -vvv for "triple verbosity" #523

Closed x10an14 closed 7 months ago

x10an14 commented 4 years ago

I'm not sure whether or not what I describe is part of the GNU getopt standard.

But ssh is a good example for what I'm trying to do.

Anyone using Github can perform the following (as long as they've set-up SSH keys):

-> $ ssh -T git@github.com
Hi x10an14! You've successfully authenticated, but GitHub does not provide shell access.

And....

Long SSH example output ```bash -> $ ssh -Tvv git@github.com OpenSSH_7.6p1 Ubuntu-4ubuntu0.3, OpenSSL 1.0.2n 7 Dec 2017 debug1: Reading configuration data /home/x10an14/.ssh/config debug1: Reading configuration data /etc/ssh/ssh_config debug1: /etc/ssh/ssh_config line 19: Applying options for * debug2: resolving "github.com" port 22 debug2: ssh_connect_direct: needpriv 0 debug1: Connecting to github.com [140.82.118.4] port 22. debug1: Connection established. debug1: identity file /home/x10an14/.ssh/id_rsa type 0 debug1: key_load_public: No such file or directory debug1: identity file /home/x10an14/.ssh/id_rsa-cert type -1 debug1: key_load_public: No such file or directory debug1: identity file /home/x10an14/.ssh/id_dsa type -1 debug1: key_load_public: No such file or directory debug1: identity file /home/x10an14/.ssh/id_dsa-cert type -1 debug1: key_load_public: No such file or directory debug1: identity file /home/x10an14/.ssh/id_ecdsa type -1 debug1: key_load_public: No such file or directory debug1: identity file /home/x10an14/.ssh/id_ecdsa-cert type -1 debug1: key_load_public: No such file or directory debug1: identity file /home/x10an14/.ssh/id_ed25519 type -1 debug1: key_load_public: No such file or directory debug1: identity file /home/x10an14/.ssh/id_ed25519-cert type -1 debug1: Local version string SSH-2.0-OpenSSH_7.6p1 Ubuntu-4ubuntu0.3 debug1: Remote protocol version 2.0, remote software version babeld-003ebee6 debug1: no match: babeld-003ebee6 debug2: fd 3 setting O_NONBLOCK debug1: Authenticating to github.com:22 as 'git' debug1: SSH2_MSG_KEXINIT sent debug1: SSH2_MSG_KEXINIT received debug2: local client KEXINIT proposal debug2: KEX algorithms: curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group-exchange-sha1,diffie-hellman-group14-sha256,diffie-hellman-group14-sha1,ext-info-c debug2: host key algorithms: ssh-rsa-cert-v01@openssh.com,rsa-sha2-512,rsa-sha2-256,ssh-rsa,ecdsa-sha2-nistp256-cert-v01@openssh.com,ecdsa-sha2-nistp384-cert-v01@openssh.com,ecdsa-sha2-nistp521-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,ssh-ed25519 debug2: ciphers ctos: chacha20-poly1305@openssh.com,aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com debug2: ciphers stoc: chacha20-poly1305@openssh.com,aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com debug2: MACs ctos: umac-64-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha1-etm@openssh.com,umac-64@openssh.com,umac-128@openssh.com,hmac-sha2-256,hmac-sha2-512,hmac-sha1 debug2: MACs stoc: umac-64-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha1-etm@openssh.com,umac-64@openssh.com,umac-128@openssh.com,hmac-sha2-256,hmac-sha2-512,hmac-sha1 debug2: compression ctos: none,zlib@openssh.com,zlib debug2: compression stoc: none,zlib@openssh.com,zlib debug2: languages ctos: debug2: languages stoc: debug2: first_kex_follows 0 debug2: reserved 0 debug2: peer server KEXINIT proposal debug2: KEX algorithms: curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256 debug2: host key algorithms: ssh-dss,rsa-sha2-512,rsa-sha2-256,ssh-rsa debug2: ciphers ctos: chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr,aes256-cbc,aes192-cbc,aes128-cbc debug2: ciphers stoc: chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr,aes256-cbc,aes192-cbc,aes128-cbc debug2: MACs ctos: hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha1-etm@openssh.com,hmac-sha2-256,hmac-sha2-512,hmac-sha1 debug2: MACs stoc: hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha1-etm@openssh.com,hmac-sha2-256,hmac-sha2-512,hmac-sha1 debug2: compression ctos: none,zlib,zlib@openssh.com debug2: compression stoc: none,zlib,zlib@openssh.com debug2: languages ctos: debug2: languages stoc: debug2: first_kex_follows 0 debug2: reserved 0 debug1: kex: algorithm: curve25519-sha256 debug1: kex: host key algorithm: rsa-sha2-512 debug1: kex: server->client cipher: chacha20-poly1305@openssh.com MAC: compression: none debug1: kex: client->server cipher: chacha20-poly1305@openssh.com MAC: compression: none debug1: expecting SSH2_MSG_KEX_ECDH_REPLY debug1: Server host key: ssh-rsa SHA256:nThbg6kXUpJWGl7E1IGOCspRomTxdCARLviKw6E5SY8 debug1: Host 'github.com' is known and matches the RSA host key. debug1: Found key in /home/x10an14/.ssh/known_hosts:3 debug2: set_newkeys: mode 1 debug1: rekey after 134217728 blocks debug1: SSH2_MSG_NEWKEYS sent debug1: expecting SSH2_MSG_NEWKEYS debug1: SSH2_MSG_NEWKEYS received debug2: set_newkeys: mode 0 debug1: rekey after 134217728 blocks debug2: key: /home/x10an14/.ssh/id_rsa (0x562872240bf0), agent debug2: key: /home/x10an14/.ssh/id_dsa ((nil)) debug2: key: /home/x10an14/.ssh/id_ecdsa ((nil)) debug2: key: /home/x10an14/.ssh/id_ed25519 ((nil)) debug1: SSH2_MSG_EXT_INFO received debug1: kex_input_ext_info: server-sig-algs= debug2: service_accept: ssh-userauth debug1: SSH2_MSG_SERVICE_ACCEPT received debug1: Authentications that can continue: publickey debug1: Next authentication method: publickey debug1: Offering public key: RSA SHA256:ll1aqkyno22aTX/zV1FtKkZ/id+7h8vn/uWY1bDWXbo /home/x10an14/.ssh/id_rsa debug2: we sent a publickey packet, wait for reply debug1: Server accepts key: pkalg ssh-rsa blen 1047 debug2: input_userauth_pk_ok: fp SHA256:ll1aqkyno22aTX/zV1FtKkZ/id+7h8vn/uWY1bDWXbo debug1: Authentication succeeded (publickey). Authenticated to github.com ([140.82.118.4]:22). debug1: channel 0: new [client-session] debug2: channel 0: send open debug1: Entering interactive session. debug1: pledge: network debug2: channel_input_open_confirmation: channel 0: callback start debug2: fd 3 setting TCP_NODELAY debug2: client_session2_setup: id 0 debug1: Sending environment. debug1: Sending env LC_MEASUREMENT = nb_NO.UTF-8 debug2: channel 0: request env confirm 0 debug1: Sending env LC_PAPER = nb_NO.UTF-8 debug2: channel 0: request env confirm 0 debug1: Sending env LC_MONETARY = nb_NO.UTF-8 debug2: channel 0: request env confirm 0 debug1: Sending env LANG = en_US.UTF-8 debug2: channel 0: request env confirm 0 debug1: Sending env LC_NAME = nb_NO.UTF-8 debug2: channel 0: request env confirm 0 debug1: Sending env LC_ADDRESS = nb_NO.UTF-8 debug2: channel 0: request env confirm 0 debug1: Sending env LC_NUMERIC = nb_NO.UTF-8 debug2: channel 0: request env confirm 0 debug1: Sending env LC_TELEPHONE = nb_NO.UTF-8 debug2: channel 0: request env confirm 0 debug1: Sending env LC_IDENTIFICATION = nb_NO.UTF-8 debug2: channel 0: request env confirm 0 debug2: channel 0: request shell confirm 1 debug2: channel_input_open_confirmation: channel 0: callback done debug2: channel 0: open confirm rwindow 32000 rmax 35000 debug2: channel_input_status_confirm: type 99 id 0 debug2: shell request accepted on channel 0 debug2: channel 0: rcvd ext data 89 debug1: client_input_channel_req: channel 0 rtype exit-status reply 0 debug2: channel 0: rcvd eof debug2: channel 0: output open -> drain debug2: channel 0: rcvd close debug2: channel 0: close_read debug2: channel 0: input open -> closed debug2: channel 0: obuf_empty delayed efd 7/(89) Hi x10an14! You've successfully authenticated, but GitHub does not provide shell access. debug2: channel 0: written 89 to efd 7 debug2: channel 0: obuf empty debug2: channel 0: close_write debug2: channel 0: output drain -> closed debug2: channel 0: almost dead debug2: channel 0: gc: notify user debug2: channel 0: gc: user detached debug2: channel 0: send close debug2: channel 0: is dead debug2: channel 0: garbage collecting debug1: channel 0: free: client-session, nchannels 1 Transferred: sent 5396, received 2996 bytes, in 0.2 seconds Bytes per second: sent 23560.2, received 13081.3 debug1: Exit status 1 ```

So in short, I can supply -v, -vv, or -vvv to the ssh command, and get increasing levels of verbosity. I enjoy the opportunity to supply -v (--verbose) and -q (--quiet) flags in CLIs I implement.

moh-hassan commented 4 years ago

Switches can be merged together:

   -tv   parsed as  -t -v

Using the following Option class

class Options
{   

     [Option('v')]
    public string Verbose { get; set; }
}

The commandline:

      -vv  =parsed to=>  -v v   =result to=> options.verbose = "v"
     -vvv  =parsed  to=>  -v vv  =result to=> options.verbose = "vv"
     -v   raise error  `Option 'v' has no value.`
x10an14 commented 4 years ago

Thanks! =)

x10an14 commented 4 years ago

Oh wait, I was a bit hasty, sorry!

-v raise error Option 'v' has no value.

Is that a necessity? It's not possible to give the flag -v (and just -v), and therefore set verbosityLevel to 1 as opposed to a default of 0?

Edit: To clarify, I'm under the impression now that what I quoted of your answer is something decided by the framework, not just an implementation detail you suggested.

moh-hassan commented 4 years ago

Verbose is defined as string not bool (so it's a Scalar option not a boolean Switch ) to accept string values as described above. Any scalar option should have a value in the commandline.

it's not possible to give the flag -v (and just -v), and therefore set verbosityLevel to 1 as opposed to a default of 0?

Do you mean: -v1 => -v 1 -v2 => -v 2

in that case Verbose can be integer.

x10an14 commented 4 years ago

Just to clarify (but I think we understand each other now):

"<nothing" => "verbosity level 0"
"-v" => "verbosity level 1"
"-vv" => "verbosity level 2"
"-vvv" => "verbosity level 3"

That's as easy as;

class Options
{   
     [Option(
         'v',
         MetaName = "verbosity")]
    public uint Verbosity { get; set; }
}

?

moh-hassan commented 4 years ago

In that case, verbose will be uint : v1 or v2 or v3 and no way to map against -v or -vv or -vvv Just, a trick, if you want to use triple vvv:

enum VerboseEnum
{
  v =1,
  vv,
  vvv
}
class Options
{   
     [Option(
         'v',
         MetaName = "verbosity",
                 Default=VerboseEnum.v)]
    public VerboseEnum Verbosity { get; set; }
}

The command can be:

           -vv /-vvv /-vvvv 

That is Quad verbosity not a triple ;) :)

Edit in case of enum, you can use both int and enum, all are valid:

    -v1   or -vv
    -v2    or -vvv
    -v3    or   -vvvv
x10an14 commented 4 years ago

I'm still not getting it =/

Here's my code; ```csharp namespace Slackbot { // Std libs using System; // External libs using CommandLine; public class Program { protected enum VerbosityEnum { v = 1, vv = 2, vvv = 3, } public static void Main(string[] args) { Console.WriteLine("Hello World!"); Parser.Default.ParseArguments(args) .WithParsed( options => { Console.WriteLine( $"Current Arguments: -v {options.Verbosity}" + "\nQuick start example."); }); } protected class Options { [Option( 'v', HelpText = "Verbosity level.", Default=VerbosityEnum.v)] public uint Verbosity { get; set; } } } } ```
Here's my output from testing; ```bash -> $ dotnet run -- -vv program.cs(22,37): warning SA1008: Opening parenthesis should not be followed by a space. [/home/x10an14/Documents/project/project.csproj] program.cs(13,13): warning SA1300: Element 'v' should begin with an uppercase letter [/home/x10an14/Documents/project/project.csproj] program.cs(14,13): warning SA1300: Element 'vv' should begin with an uppercase letter [/home/x10an14/Documents/project/project.csproj] program.cs(15,13): warning SA1300: Element 'vvv' should begin with an uppercase letter [/home/x10an14/Documents/project/project.csproj] Hello World! slackbot 1.0.0 Copyright (C) 2019 slackbot ERROR(S): Option 'v' is defined with a bad format. Error setting value to option 'v': Check if Option or Value attribute values are set properly for the given type. -v (Default: v) Verbosity level. --help Display this help screen. --version Display version information. -> $ dotnet run -- Hello World! slackbot 1.0.0 Copyright (C) 2019 slackbot ERROR(S): Error setting value to option 'v': Check if Option or Value attribute values are set properly for the given type. -v (Default: v) Verbosity level. --help Display this help screen. --version Display version information. -> $ dotnet run -- -v Hello World! slackbot 1.0.0 Copyright (C) 2019 slackbot ERROR(S): Option 'v' has no value. Error setting value to option 'v': Check if Option or Value attribute values are set properly for the given type. -v (Default: v) Verbosity level. --help Display this help screen. --version Display version information. -> $ ```
x10an14 commented 4 years ago

Here's a reference of how Rust's Structopt1 (based on Clap2) enables the same feature as I'm asking for

1: https://github.com/TeXitoi/structopt 2: https://github.com/clap-rs/clap

moh-hassan commented 4 years ago

I'm still not getting it

[Option( 'v',
       HelpText = "Verbosity level: v or vv or vvv Or int values 1 or 2 or 3",
       Default=VerbosityEnum.v)]
 public VerbosityEnum Verbosity { get; set; }

Edit: clab in Rust define the argument verbose with short_name(v) and compute the number of occurrence of the command (e.g for vvv ==>occurrence =3) using a method matches.occurrences_of (v)

nomicode commented 2 years ago

rename "term limit" to results limit, or else add both ngrams limit and results limit

this is also per brad's feedback

poizan42 commented 7 months ago

This issue seems to have been fixed by #684

You need to set FlagCounter=true in the attribute and enable GetoptMode on the parser. (there seems to have been some talk about changing GetoptMode to an enum instead in the future, see #690)

    var parser = new Parser(with => 
    {                   
        with.GetoptMode = true;
    });
...
class Options
{
    [Option('v', FlagCounter = true, HelpText = "Verbosity level: v or vv, vvv or vvvv")]
    public int Verbosity { get; set; }
}

Online demo: https://dotnetfiddle.net/E8Q3sL