dotnet / command-line-api

Command line parsing, invocation, and rendering of terminal output.
https://github.com/dotnet/command-line-api/wiki
MIT License
3.42k stars 382 forks source link

System.CommandLine arguments without options #2247

Open daryl-williams opened 1 year ago

daryl-williams commented 1 year ago

Hello everyone,

I am new to .Net coming from a Unix background and I am developing a dotnet command line application that needs a couple of options, followed by a list of one or more input files, i.e:

mycmd —dpi 72 —grayscale input_file1.jpg input_file2.png

I have poured over the documentation and searched all over the web and have only been able to find examples of arguments that are part of an option, as in the —dpi 72 above. If someone could provide a simple example of how to define a standalone argument with an arity of OneOrMore or provide a link to some relevant documentation I would greatly appreciate it.

Best Regards,

Daryl

baronfel commented 1 year ago

This should be an Argument<FileInfo[]> (or CliArgument<FileInfo[]> if you're using the nightlies), but it could also be a string[], etc - 'accumulate multiple arguments' part comes from the fact that you should declare the type of the argument as an array/list/etc instead of just a single item.

daryl-williams commented 1 year ago

Hi @baronfel,

Thank you for your very quick reply. So if I understand correctly I should be able to declare something like the following:

var inputFileOption = new Argument<FileInfo[]?> (new[] { "--input_file" }, description: "One or more image filename(s) to work on: image_file1 image_file2 ...", getDefaultValue: () => { return ""; });

is that right? I get confused sometimes as there seem to be different ways to define options and I not yet seen an example using Argument instead of Option except for your example.

Thanks again for your help.

Daryl

baronfel commented 1 year ago

Yep! That should work well. Here's a simple example that I did in F# Interactive:

> let fileArg = Argument<FileInfo[]>("--input_file");;
val fileArg: Argument<FileInfo array> = Argument: --input_file

> fileArg.Parse([| "file1"; "file2"; "file3"; |]);;
val it: ParseResult =
  ParseResult: [ fsi <file1> <file2> <file3> ]
    {CommandResult = RootCommandResult: fsi file1 file2 file3;
     Directives = seq [];
     Errors = seq [];
     Parser = System.CommandLine.Parsing.Parser;
     RootCommandResult = RootCommandResult: fsi file1 file2 file3;
     Tokens = [|file1; file2; file3|];
     UnmatchedTokens = [||];
     UnparsedTokens = [||];}

> it.GetValueForArgument(fileArg);;
val it: FileInfo array =
  [|file1 {Attributes = -1;
           CreationTime = 12/31/1600 6:00:00 PM;
           CreationTimeUtc = 1/1/1601 12:00:00 AM;
           Directory = D:\Code\dotnet-docs;
           DirectoryName = "D:\Code\dotnet-docs";
           Exists = false;
           Extension = "";
           FullName = "D:\Code\dotnet-docs\file1";
           IsReadOnly = true;
           LastAccessTime = 12/31/1600 6:00:00 PM;
           LastAccessTimeUtc = 1/1/1601 12:00:00 AM;
           LastWriteTime = 12/31/1600 6:00:00 PM;
           LastWriteTimeUtc = 1/1/1601 12:00:00 AM;
           Length = ?;
           LinkTarget = null;
           Name = "file1";
           UnixFileMode = -1;};
    file2 {Attributes = -1;
           CreationTime = 12/31/1600 6:00:00 PM;
           CreationTimeUtc = 1/1/1601 12:00:00 AM;
           Directory = D:\Code\dotnet-docs;
           DirectoryName = "D:\Code\dotnet-docs";
           Exists = false;
           Extension = "";
           FullName = "D:\Code\dotnet-docs\file2";
           IsReadOnly = true;
           LastAccessTime = 12/31/1600 6:00:00 PM;
           LastAccessTimeUtc = 1/1/1601 12:00:00 AM;
           LastWriteTime = 12/31/1600 6:00:00 PM;
           LastWriteTimeUtc = 1/1/1601 12:00:00 AM;
           Length = ?;
           LinkTarget = null;
           Name = "file2";
           UnixFileMode = -1;};
    file3 {Attributes = -1;
           CreationTime = 12/31/1600 6:00:00 PM;
           CreationTimeUtc = 1/1/1601 12:00:00 AM;
           Directory = D:\Code\dotnet-docs;
           DirectoryName = "D:\Code\dotnet-docs";
           Exists = false;
           Extension = "";
           FullName = "D:\Code\dotnet-docs\file3";
           IsReadOnly = true;
           LastAccessTime = 12/31/1600 6:00:00 PM;
           LastAccessTimeUtc = 1/1/1601 12:00:00 AM;
           LastWriteTime = 12/31/1600 6:00:00 PM;
           LastWriteTimeUtc = 1/1/1601 12:00:00 AM;
           Length = ?;
           LinkTarget = null;
           Name = "file3";
           UnixFileMode = -1;}|]

You can see the definition (the important part is the FileInfo[] type definition), the parsing, and then the retrieval of the parsed values (which is a FileInfo []).

daryl-williams commented 1 year ago

I have tried the following:

var inputFileListArg = new Argument<string[]?> (new[] {"--input_file"}, description: "One or more image filename(s) to work on: image_file1 image_file2 ...");

but get the following build error:

error CS1503: Argument 1: cannot convert from 'string[]' to 'string?'

I am trying to save the input_file args to a struct which I have struct defined as:

public struct CmdArgs { public string[]? inputFileList; }

I don't think that has anything to do with compile error, but my confidence is low when it comes to my understanding of this new environment.

Regards,

D

baronfel commented 1 year ago

@daryl-williams do you have a complete sample code that we could look at to help more specifically?

daryl-williams commented 1 year ago

I have (hopefully) attached a file below that, although edited for brevity, includes all the relevant parts I have mentioned. Thanks again for your help. If I can provide further information please let me know.

D

ExampleCode.txt

baronfel commented 1 year ago

@daryl-williams looks like just a couple things to fix:

That should get you compiling!

daryl-williams commented 1 year ago

Hi @baronfel, I fixed the typo and changed the constructor to:

var inputFileListArg = new Argument<string> (new[] {"--input_file"}, description: "One or more image filename(s) to work on: image_file1 image_file2 ...");

but am still getting errors:

From the constructor declaration: error CS1503: Argument 1: cannot convert from 'string[]' to 'string?'

From the assignment to the struct (cmdArgs.inputFileList = inputFileListArg;): error CS0029: Cannot implicitly convert type 'string' to 'string[]'

D

baronfel commented 1 year ago

change new[] {"--input_file"} to just "input_file" and you should be good to go. Here is the full gist that I was able to create: https://gist.github.com/baronfel/a8cc681437b12a2d1984c1637ec958f5

daryl-williams commented 1 year ago

@baronfel Thank you so much for your help, I really appreciate it. I am still somewhat confused though about changing the type from string[] to string. Does mean the program can only have one input file? When I complied it gave me an error on there line where I save it to the CmdArgs.inputFileList var, which makes complete since they are now different types.

I guess I don't understand what you meant by "Arguments can't have aliases", I thought string[] is a Type? I realize I am very new to .Net and c# and have a lot to learn.

baronfel commented 1 year ago

I think it's easier to explain if I show you and option and an argument side-by-side:

var outputFileOption = new Option<string?> (new[] { "--output", "-o" }, description: "Output filename or basename when passing multiple output files, i.e. outputfile-1 outputfile-2 ...", getDefaultValue: () => { return ""; }); 
var inputFileListArg = new Argument<string[]> ("input_file", description: "One or more image filename(s) to work on: image_file1 image_file2 ...");

These are from your code - for an Option, the first parameter is a list of names for the option - the primary is the 'longest' name, and all other names are considered 'aliases'. This means a user could type in -o at the terminal or --output and get the same result.

However, Arguments don't have a concept of aliases - they really only have one name since they can't be explicitly passed like an Option is. This means the Argument constructor only needs a single string. Note that this has nothing to do with the type of the argument/option - that's all covered by the generic type parameter FileInfo when you created the argument using Argument<FileInfo> .

daryl-williams commented 1 year ago

I get it now, your explanation was very clear. I changed Argument type back to string[] and it compiled perfectly! Thanks again for your help, I learn as I go and this has been very helpful!

am11 commented 1 year ago

In cases where you want to control number of allowed values by the argument, Arity property can be used:

Argument<string[]> inputFileListArg = new("input_file(s)",
  description: "One or more image filename(s) to work on: image_file1 image_file2 ...")
  {
    Arity = ArgumentArity.OneOrMore
  };

note that the API has changed in the upcoming v2, so in the future, it shape will become:

CliArgument<string[]> inputFileListArg = new("input_file(s)")
{
  Description = "One or more image filename(s) to work on: image_file1 image_file2 ..."),
  Arity = ArgumentArity.OneOrMore
};
daryl-williams commented 1 year ago

Thank you Adele, I appreciate your input and information.On Aug 5, 2023, at 00:19, Adeel Mujahid @.***> wrote: In cases where you want to control number of allowed values by the argument, Arity property can be used: Argument<string[]> inputFileListArg = new("input_file(s)", description: "One or more image filename(s) to work on: image_file1 image_file2 ...") { Arity = ArgumentArity.OneOrMore }; note that the API has changed in the upcoming v2, so in the future, it shape will become: CliArgument<string[]> inputFileListArg = new("input_file(s)") { Description = "One or more image filename(s) to work on: image_file1 image_file2 ..."), Arity = ArgumentArity.OneOrMore };

—Reply to this email directly, view it on GitHub, or unsubscribe.You are receiving this because you were mentioned.Message ID: @.***>