adamabdelhamed / PowerArgs

The ultimate .NET Standard command line argument parser
MIT License
568 stars 56 forks source link

Sub-actions? #138

Open kdubau opened 5 years ago

kdubau commented 5 years ago

I am looking to be able to have sub-action methods. Something like mytool.exe secret set {ArgPosition0} {ArgPosition1} and mytool.exe secret get {ArgPosition0}.

I wasn't able to figure out how to achieve this so perhaps it's not supported? If it is can you please describe and I can PR an update to the README.md.

If not, feel free to close this or leave it open to track feature implementation (depending on complexity I can likely help).

vnekatesharao commented 5 years ago

I am able to do this with the following

Program.cs class Program { static void Main(string[] args) { Console.WriteLine("Hello World!"); Args.InvokeAction(args); } }

Subcmd1.cs

public class Subsubcmd1Args
{
    [ArgRequired, ArgDescription("The first operand to process"), ArgPosition(1)]
    public string message { get; set; }
}

public class Subsubcmd1
{
    //public string[] Args { get; set; }

    [ArgActionMethod, ArgDescription("Adds the two operands")]
    public void SubSubCmd1(Subsubcmd1Args args)
    {
        Console.WriteLine($"{args.message}");
    }
}

[ArgExceptionBehavior(ArgExceptionPolicy.StandardExceptionHandling)]
public class Subcmd1
{
    [HelpHook, ArgShortcut("-?"), ArgDescription("Shows this help")]
    public bool Help { get; set; }

    [ArgActionMethod, ArgDescription("Adds the two operands")]
    public void SubCmd1(string[] args)
    {
        Args.InvokeAction<Subsubcmd1>(args);
        //Console.WriteLine($"{args.message}");
    }
}

So with this code in place and if we build and run as following

CMD> dotnet OptionParser.dll SubCmd1 subsubcmd1 "My message Here"

This works good for positional parameter. The problem here is we cannot pass the message as named parameter something like following

CMD> dotnet OptionParser.dll SubCmd1 subsubcmd1 -message "My message Here" 

I am getting error like

Unexpected named argument: message

I thing there is something to improve here for powerarg library..

adamabdelhamed commented 5 years ago

Not currently supported and it would be a fairly big change. I'll leave the issue open and if I have time in the future maybe I'll add this. You could try to fake it by using a positional arg, but there would be side effects.

workabyte commented 3 years ago

@adamabdelhamed what would your thoughts be on something like

  1. add arg to parent action with attr ArgSubAction, this would then be set with all args passed in excluding the current action
  2. then in the action handler for the "main action" you could then call Args.InvokeAction<SubAction>(args.SubActionArgs)

not sure how the args documentation portion of this would get flushed out 🤔 just an initial thought 🤷‍♂️

workabyte commented 2 years ago

@adamabdelhamed / @vnekatesharao / @kdubau: would love to get your feedback on this 😄 so this is the working solution I have, I did borrow some ideas from how kubernet is changing passing args to pods using -- as a separator.

so my "tricks" here were to

  1. use [ArgShortcut("--")] as the shortcut for my "sub args" string[]
  2. using a different arg prefix for sub action args (in this case :)
  3. using type string[] for the Args property of the "first level" action
  4. build up the new "args" array based on SubAction and Args being populated

the following commands have been tested (yes is exe) and worked as expected

  1. util hs -h (help for hello sign actions printed)
  2. util hs -- ls -h (help for sub actions printed)
  3. util hs ls -- :f (ran the sub action for ls and IncludeFields was set to true

this does only seem to solve for one level but I suppose you could repeat the concept with new separators for each level but I am not sure that will be fun 😢 quick app as a test so be gentle :)

    static void Main(string[] args)
    {
        try
        {
            Args.InvokeAction<UtilActions>(args);
        }
        catch (DbEntityValidationException dbEx)
        {
            var sdSb = new StringBuilder();
            foreach (var eve in dbEx.EntityValidationErrors)
            {
                sdSb.AppendLine(
                    $"Entity of type {eve.Entry.Entity.GetType().Name} in state {eve.Entry.State} has the following validation errors:");
                foreach (var ve in eve.ValidationErrors)
                {
                    sdSb.AppendLine(
                        $"- Property: {ve.PropertyName}, Value: {eve.Entry.CurrentValues.GetValue<object>(ve.PropertyName)}, Error: {ve.ErrorMessage}");
                }
            }

            sdSb.ToString()
                .ToConsoleString(ConsoleColor.Red)
                .ToConsole();
        }
        catch (Exception e)
        {
                e.ToConsoleString(ConsoleColor.Red)
                .ToConsole();
        }
    }

then My tope level action looks like so

public partial class UtilActions
{
    [ArgShortcut("hs")]
    [ArgActionMethod, ArgDescription("create md5 hash of all values")]
    public void HelloSign(HelloSignArgs action)
    {
        action.SubAction.ToConsole();
        try
        {
            var args = new List<string>();
            if (action.SubAction.IsNotNullOrWhiteSpace())
            {
                args.Add(action.SubAction);
            }

            if (action.Args.IsNotNull())
            {
                args.AddRange(action.Args
                    .Select(a => a.Replace(":", "--"))
                    .ToList());
            }

            Args.InvokeAction<HelloSignSubActions>(args.ToArray());
        }
        catch (Exception e)
        {
            e.ToConsole();
          }
      }
  }

and here is the related Args Class

public class HelloSignArgs
{
    [ArgPosition(1)]
    public string SubAction {get; set;}

    [ArgShortcut("--")]
    [ArgDescription("separator for arguments intended to be passed to the sub action")]
    public string[] Args {get; set;}
}

given the test I used was running with hs ls -f I do get the -f properly set in the sub actions

public class HelloSignSubActions
{
    [HelpHook]
    [ArgShortcut("?")]
    [ArgShortcut("h")]
    [ArgShortcut("--?")]
    [ArgShortcut("--h")]
    [ArgShortcut("--help")]
    [ArgDescription("Shows this help")]
    public bool Help {get; set;}

    [ArgShortcut("ls")]
    [ArgActionMethod, ArgDescription("create md5 hash of all values")]
    public void List(HelloSignSubActionArgs args)
    {
        args.IncludeFields.ToConsole();
    }
}

with the Args Class as so:

public class HelloSignSubActionArgs
{
    [ArgShortcut("f")]
    [ArgDescription("include fields with template output")]
    public bool IncludeFields {get; set;}
}
workabyte commented 2 years ago

by using a diff prefix it solved the second issue with parsing I was running into @adamabdelhamed funny but custom prefix attr here would help to solve this "cleanly" but not required

asking for [ArgPrefix("-", "--")]

https://github.com/adamabdelhamed/PowerArgs/issues/67

workabyte commented 2 years ago

@adamabdelhamed might be interesting to do something like [ArgActions("git")] where git would be the "action prefix" for the method

kinda like .net attr routing, set the "root" path for the "controller" (action class) and then each action

if that seems like something worth doing I can start another ticket

If I were to PR #67 would you approve it?