I'm updating a project that uses CommandLineUtils to parse command line options. The options are defined by taking the CommandLineApplication and calling cmd.Option() on it, not with the options as properties in a class. For example:
public void Configure(CommandLineApplication cmd)
{
var aOpt = cmd.Option("-a", "Option A", CommandOptionType.SingleValue).IsRequired();
var bOpt = cmd.Option("-b", "Option B", CommandOptionType.SingleValue);
cmd.Command("subCommand1", subcmd =>
{
var cOpt = subcmd.Option("-c", "Option C", CommandOptionType.SingleValue).IsRequired();
var dOpt = subcmd.Option("-d", "Option D", CommandOptionType.SingleValue).IsRequired();
});
cmd.Command("subCommand2", subcmd =>
{
var fOpt = subcmd.Option("-f", "Option F", CommandOptionType.SingleValue).IsRequired();
});
}
I'm adding a case where one of two options are required but not both. For example, if those two options are -a and -c then:
These are valid option sets:
cmd -a
or
cmd -c
These are not valid option sets:
cmd -a -b
or
cmd
How can I specify that two options are related such that at least one of them is required? I know about .IsRequired() but that looks inflexible.
(My case is a little more complicated in that either -c is required, or -a -d are required together)
Custom validation can be added to a command by adding an ICommandValidator to cmd.Validators.
cmd.Validators.Add(new MyCommandValidator());
...
private class MyCommandValidator: ICommandValidator
{
public ValidationResult GetValidationResult(CommandLineApplication command, ValidationContext context)
{
return ValidationResult.Success;
}
}
In the example question there is a command with subcommands. This validator can check which command is being processed with command.Name if it is added to multiple commands. But it will only execute for the commands it is added to.
if (command.Name == "subCommand1") { ... }
To access the options use cmd.GetOptions(). This will provide a list of all available options, and include their values. cmd.Options can also be accessed, but is limited to the options specified by the user.
A specific option can be looked up by the ShortName or LongName.
var aOpt = command.GetOptions().First(o => o.ShortName == "a");
HasValue() or Value() can be used to see what options are set and whether they have a value set by the user. If the options are invalid return a new ValidationResult("Custom error");
Putting it all together:
// In subCommand1 definition
subcmd.Validators.Add(new MyCommandValidator());
...
private class MyCommandValidator: ICommandValidator
{
public ValidationResult GetValidationResult(CommandLineApplication command, ValidationContext context)
{
var allOpts = command.GetOptions();
var aOpt = allOpts.First(o => o.ShortName = "a");
var cOpt = allOpts.First(o => o.ShortName = "c");
var dOpt = allOpts.First(o => o.ShortName = "d");
// Either -a or -c is required
if (!cOpt.HasValue())
{
if (!aOpt.HasValue())
{
return new ValidationResult("Either -a or -c is required.");
}
// If -a is provided, -d is required
aOpt.IsRequired();
dOpt.IsRequired();
}
// If -c is provided, -a and -d are not allowed
else if (aOpt.HasValue() || dOpt.HasValue())
{
return new ValidationResult($"{cOpt.LongName} should not be used with {aOpt.LongName} or {dOpt.LongName}.");
}
// Options are valid
return ValidationResult.Success;
}
}
Note, if subCommand2 doesn't have complex validation and -a is always required, it is enough to add aOpt.IsRequired() in it's command definition.
Related
Scenario: I want to parse a console app's command line that has a number of options (that don't relate to a command), and a command that has a number of options. I've simplified what I'm trying to into a fictitious example. E.g. myapp --i infile.dat --o outfile.dat conversion --threshold 42
Problem: I'm finding that if I put the "conversion" command and it's option on the command-line then only its handler gets called, but not the handler for the root command. So I have no way to determine the values of the root command's --i and --o options.
(Conversely, if I omit the "conversion" command then root command's handler only is called - which is what i would expect.)
Example code:
public class Program
{
public static async Task<int> Main(string[] args)
{
// Conversion command
var thresholdOpt = new Option<int>("--threshold");
var conversionCommand = new Command("conversion") { thresholdOpt };
conversionCommand.SetHandler(
(int threshold) => { Console.WriteLine($"threshold={threshold}"); },
thresholdOpt);
// Root command
var infileOpt = new Option<string>("--i");
var outfileOpt = new Option<string>("--o");
var rootCommand = new RootCommand("test") { infileOpt, outfileOpt, conversionCommand };
rootCommand.SetHandler(
(string i, string o) => { Console.WriteLine($"i={i}, o={o}"); },
infileOpt, outfileOpt);
return await rootCommand.InvokeAsync(args);
}
}
Unexpected outputs:
> myapp --i infile.dat --o outfile.dat conversion --threshold 42
threshold=42
In the above I expect to see the value for the --i and --o options, as well as the threshold options associated with the conversion command, but the root command's handler isn't invoked.
Expected outputs:
> myapp --i infile.dat --o outfile.dat
i=infile.dat, o=outfile.dat
> myapp conversion --threshold 42
threshold=42
The above are what I'd expect to see.
Dependencies: I'm using System.CommandLine 2.0.0-beta3.22114.1, System.CommandLine.NamingConventionBinder v2.0.0-beta3.22114.1, .net 6.0, and Visual Studio 17.1.3.
I'd be grateful for help in understanding what I'm doing wrong. Thanks.
Based on the docs sample it seems only one verb gets executed. For example next:
var rootCommand = new RootCommand();
rootCommand.SetHandler(() => Console.WriteLine("root"));
var verbCommand = new Command("verb");
verbCommand.SetHandler(() => Console.WriteLine("verb"));
rootCommand.Add(verbCommand);
var childVerbCommand = new Command("childverb");
childVerbCommand.SetHandler(() => Console.WriteLine("childverb"));
verbCommand.Add(childVerbCommand);
return await rootCommand.InvokeAsync(args);
For no arguments will print root, for verb will print verb and for verb childverb will print childverb.
So if you need multiple actions performed it seems you will need to use another approach (for example manually processing rootCommand.Parse() result).
If you just want "--i" and "--o" accessible for conversion then add them to corresponding command:
// actually works without specifying infileOpt, outfileOpt on conversionCommand
// but should be still present on the root one
// also rootCommand.AddGlobalOption can be a more valid approach
var conversionCommand = new Command("conversion") { thresholdOpt, infileOpt, outfileOpt};
// add here for handler
conversionCommand.SetHandler(
(int threshold, string i, string o) => { Console.WriteLine($"threshold={threshold}i={i}, o={o}"); },
thresholdOpt, infileOpt, outfileOpt);
I have below command and it returns me null object . When I run the command separately in PowerShell window I get the right result. Below is my PowerShell method which is calling the command and the also the PowerShell command which I have defined. I am basically looking to return a string value. Please let me know what wrong am I doing?
C# method:
public string RunScript( string contentScript, Dictionary<string, EntityProperty> parameters)
{
List<string> parameterList = new List<string>();
foreach( var item in parameters )
{
parameterList.Add( item.Value.ToString() );
}
using( PowerShell ps = PowerShell.Create() )
{
ps.AddScript( contentScript );
// in ContentScript I get "Get-RowAndPartitionKey" on debugging
ps.AddParameters( parameterList );//I get list of strings
IAsyncResult async = ps.BeginInvoke();
StringBuilder stringBuilder = new StringBuilder();
foreach( PSObject result in ps.EndInvoke( async ) )
// here i get result empty in ps.EndInvoke(async)
{
stringBuilder.AppendLine( result.ToString() );
}
return stringBuilder.ToString();
}
}
}
My Powershell GetRowAndPartitionKey cmdlet definition, which the code above is trying to call:
public abstract class GetRowAndPartitionKey : PSCmdlet
{
[Parameter]
public List<string> Properties { get; set; } = new List<string>();
}
[Cmdlet( VerbsCommon.Get, "RowAndPartitionKey" )]
public class GetRowAndPartitionKeyCmd : GetRowAndPartitionKey
{
protected override void ProcessRecord()
{
string rowKey = string.Join( "_", Properties );
string pKey = string.Empty;
WriteObject( new
{
RowKey = rowKey,
PartitionKey = pKey
} );
}
}
}
When using the PowerShell SDK, if you want to pass parameters to a single command with .AddParameter() / .AddParameters() / AddArgument(), use .AddCommand(), not .AddScript()
.AddScript() is for passing arbitrary pieces of PowerShell code that is executed as a script block to which the parameters added with .AddParameters() are passed.
That is, your invocation is equivalent to & { Get-RowAndPartitionKey } <your-parameters>, and as you can see, your Get-RowAndPartitionKey command therefore doesn't receive the parameter values.
See this answer or more information.
Note: As a prerequisite for calling your custom Get-RowAndPartitionKey cmdlet, you may have to explicitly import the module (DLL) that contains it, which you can do:
either: with a separate, synchronous Import-Module call executed beforehand (for simplicity, I'm using .AddArgument() here, with passes an argument positionally, which binds to the -Name parameter (which also accepts paths)):
ps.AddCommand("Import-Module").AddArgument(#"<your-module-path-here>").Invoke();
or: as part of a single (in this case asynchronous) invocation - note the required .AddStatement() call to separate the two commands:
IAsyncResult async =
ps.AddCommand("Import-Module").AddArgument(#"<your-module-path-here>")
.AddStatement()
.AddCommand("GetRowAndPartitionKey").AddParameter("Properties", parameterList)
.BeginInvoke();
"<your-module-path-here>" refers to the full file-system path of the module that contains the Get-RowAndPartitionKey cmdlet; depending on how that module is implemented, it is either a path to the module's directory, its .psd1 module manifest, or to its .dll, if it is a stand-alone assembly.
Alternative import method, using the PowerShell SDK's dedicated .ImportPSModule() method:
This method obviates the need for an in-session Import-Module call, but requires extra setup:
Create a default session state.
Call .ImportPSModule() on it to import the module.
Pass this session state to PowerShell.Create()
var iss = InitialSessionState.CreateDefault();
iss.ImportPSModule(new string[] { #"<your-module-path-here>" });
var ps = PowerShell.Create(iss);
// Now the PowerShell commands submitted to the `ps` instance
// will see the module's exported commands.
Caveat: A PowerShell instance reflects its initial session state in .Runspace.InitialSessionState, but as a conceptually read-only property; the tricky part is that it is technically still modifiable, so that mistaken attempts to modify it are quietly ignored rather than resulting in exceptions.
To troubleshoot these calls:
Check ps.HadErrors after .Invoke() / .EndInvoke() to see if the PowerShell commands reported any (non-terminating) errors.
Enumerate ps.Streams.Errors to inspect the specific errors that occurred.
See this answer to a follow-up question for self-contained sample code that demonstrates these techniques.
While trying to squeeze Mono.Options into my Cake script, I noticed I wasn't entirely sure how to give it the original arguments string from the command line call that launched the Cake script in the first place. Mono.Options Parse method takes what would be a typical console app's string[] args parameter, so I need to feed it something it can work with.
I know I can query the context for specific arguments with ArgumentAlias calls, but is there any way to access the entire original string calling string?
Cake scripts are essentially just a regular .NET Process to you can access it through System.Environment.GetCommandLineArgs()
Example PoC
Quick n dirty example of one way you could use Mono.Options with Cake below
#addin nuget:?package=Mono.Options&version=5.3.0.1
using Mono.Options;
public static class MyOptions
{
public static bool ShouldShowHelp { get; set; } = false;
public static List<string> Names { get; set; } = new List<string>();
public static int Repeat { get; set; } = 1;
}
var p = new OptionSet {
{ "name=", "the name of someone to greet.", n => MyOptions.Names.Add (n) },
{ "repeat=", "the number of times to MyOptions.Repeat the greeting.", (int r) => MyOptions.Repeat = r },
// help is reserved cake command so using options instead
{ "options", "show this message and exit", h => MyOptions.ShouldShowHelp = h != null },
};
try {
p.Parse (
System.Environment.GetCommandLineArgs()
// Skip Cake.exe and potential cake file.
// i.e. "cake --name="Mattias""
// or "cake build.cake --name="Mattias""
.SkipWhile(arg=>arg.EndsWith(".exe", StringComparison.OrdinalIgnoreCase)||arg.EndsWith(".cake", StringComparison.OrdinalIgnoreCase))
.ToArray()
);
}
catch (OptionException e) {
Information("Options Sample: ");
Information (e.Message);
Information ("--options' for more information.");
return;
}
if (MyOptions.ShouldShowHelp || MyOptions.Names.Count == 0)
{
var sw = new StringWriter();
p.WriteOptionDescriptions (sw);
Information(
"Usage: [OPTIONS]"
);
Information(sw);
return;
}
string message = "Hello {0}!";
foreach (string name in MyOptions.Names) {
for (int i = 0; i < MyOptions.Repeat; ++i)
Information (message, name);
}
Example output
cake .\Mono.Options.cake will output help as no names specified
cake .\Mono.Options.cake --options will output "help"
cake .\Mono.Options.cake --name=Mattias will greet me
cake .\Mono.Options.cake --name="Mattias" --repeat=5 will greet me 5 times
cake .\Mono.Options.cake --name="Mattias" --repeat=sdss will fail and report because repeat not a number
I have a class:
class Options
{
// Remainder omitted (verb1, verb2, verb3)
[HelpVerbOption]
public string GetUsage(string verb)
{
return HelpText.AutoBuild(this, verb);
}
}
The docs say:
[...] The parser will pass null to master class GetUsage(string) also if
the user requested the help index with:
$ git help
or the verb command if the user requested explicitly
instructions on how to use a particular verb:
$ git help commit
[...]
Then, I typed MyApp.exe help verb1, but I could see only the base help (that looked like I typed the wrong verb, or help verb, or something). Rather, I expect it to show the help message related to specified verb. Why isn't it working properly?
For me it works using the mentioned approach, but only if I call my app without the --help-option (for instance MyApp batch). When I use MyApp --help batch the behaviour is as described by you.
However we can´t seem to get the same to work for the help-option.
EDIT: I managed to get this working by modifying the code of Commandline.Parser.cs with the following:
private bool TryParseHelpVerb(string[] args, object options, Pair<MethodInfo, HelpVerbOptionAttribute> helpInfo, OptionMap optionMap)
{
var helpWriter = _settings.HelpWriter;
if (helpInfo != null && helpWriter != null)
{
if (string.Compare(args[0], helpInfo.Right.LongName, GetStringComparison(_settings)) == 0)
{
// User explicitly requested help
var verb = args.FirstOrDefault(); // <----- change this to args[1];
if (verb != null)
{
var verbOption = optionMap[verb];
if (verbOption != null)
{
if (verbOption.GetValue(options) == null)
{
// We need to create an instance also to render help
verbOption.CreateInstance(options);
}
}
}
DisplayHelpVerbText(options, helpInfo, verb);
return true;
}
}
return false;
}
The problem appears at the line
var verb = args.FirstOrDefault();
As the very first argument (args[0]) is interpreteted as the verb or better the action (as described in the docs) verb will allways be help here. So we replace this by args[1] which contains the actual verb, for example commit.
EDIT2: To make this work for --help also we should also trim the first arg (args[0]) from the --character
if (string.Compare(args[0].Trim('-'), helpInfo.Right.LongName, GetStringComparison(_settings)) == 0)
When setting up a merge, the TortoiseSvn client has a wonderful checkbox labeled "Hide non-mergable revisions". I'm looking to reproduce the list of revisions that shows up when it's enabled using SharpSvn.
The TortoiseSvn documentation explains this checkbox:
When merge tracking is used, the log dialog will show previously merged revisions, and revisions pre-dating the common ancestor point, i.e. before the branch was copied, as greyed out. The Hide non-mergeable revisions checkbox allows you to filter out these revisions completely so you see only the revisions which can be merged.
How can I reproduce this functionality in SharpSvn code? I need a list of SvnLogEventArgs (or similar) that are candidates for merging.
Current status: I've only gotten as far as pulling the logs for both branches. I can't figure out how to get the appropriate svn:mergeinfo attribute or what to do with it once I get it.
I kept plugging away, and following links, and here's what I ended up with:
using (var client = new SvnClient())
{
var release = SvnTarget.FromUri(new Uri(#"https://******/branches/Release"));
var trunk = SvnTarget.FromUri(new Uri(#"https://******/trunk"));
string trunkMergeinfo, releaseMergeinfo;
client.GetProperty(release, "svn:mergeinfo", out releaseMergeinfo);
client.GetProperty(trunk, "svn:mergeinfo", out trunkMergeinfo);
var relInfos = releaseMergeinfo.Split("\n");
var trunkInfos = trunkMergeinfo.Split("\n");
// This is here because I don't know what will happen once I merge something into trunk.
Debug.Assert(relInfos.Except(trunkInfos).Count() == 1,"Too many unknown merge paths");
var trunklist = relInfos.SingleOrDefault(i => i.StartsWith("/trunk:"));
var revisions = trunklist.Replace("/trunk:", "").Split(",").SelectMany(t =>
{
// If the log contains a range, break it out to it's specific revisions.
if (t.Contains("-"))
{
var match = Regex.Match(t, #"(\d+)-(\d+)");
var start = int.Parse(match.Groups[1].Value);
var end = int.Parse(match.Groups[2].Value);
return Enumerable.Range(start, end - start + 1).ToArray();
}
else
return new[] { int.Parse(t) };
}).Select(x => (long)x);
Collection<SvnLogEventArgs> baseRevs;
// Why can't this take "trunk" or a property thereof as an argument?
client.GetLog(new Uri(#"https://******/trunk"), new SvnLogArgs { Start = 1725, End = SvnRevisionType.Head }, out baseRevs);
baseRevs.Reverse().Where(r => !revisions.Contains(r.Revision) ).Select(x => x.LogMessage).Dump();
}
Hopefully, this helps someone else, although I'll note that it does not have a lot of the sanity checking that I'd put in production code - this is the quick-and-dirty version.
Try SvnClient.ListMergesEligible:
http://sharpsvn.qqn.nl/current/html/M_SharpSvn_SvnClient_ListMergesEligible_1.htm
Edit.
SharpSVN seems bugged for me, so I went for cmd.
Check this out:
private static void mergelhetőVerziókListája()
{
string revíziók = cmd("svn", "mergeinfo --show-revs eligible \".../branches/dev\" \".../trunk\"");
}
private static string cmd(string utasítás, string paraméter)
{
StringBuilder eredmény = new StringBuilder();
Process cmd = new Process()
{
StartInfo = new ProcessStartInfo
{
FileName = utasítás,
Arguments = paraméter,
UseShellExecute = false,
RedirectStandardOutput = true,
CreateNoWindow = true
}
};
cmd.Start();
while (!cmd.StandardOutput.EndOfStream)
{
string kimenet = cmd.StandardOutput.ReadLine();
eredmény.AppendLine(kimenet); //...
}
return eredmény.ToString();
}