views:

157

answers:

2

I'm trying to write a dynamic sort of command line processor where I have a dictionary with keys being possible parameters, and the member being an Action where the string is the text between the parameters passed on the command line. Want to be able to add parameters just by adding the params array, and writing the action in the dictionary.

Yes I realize this is a pointless exercise in overcomplicating implementation to simplify maintenance. Mostly just trying to stress myself to learn more linq.

Here's my dictionary:

    private static Dictionary<string[], Action<string>> _commandLineParametersProcessor = new Dictionary<string[], Action<string>>()
{
    {
        new string[] {"-l", "--l", "-log", "--log"},
        (logFile) =>  
            {
                _blaBla.LogFilePath = logFile;
            }
    },
    {
        new string[] { "-s", "--s", "-server", "--server" },
        (server) =>  
            {
                ExecuteSomething(server);
                _blaBla.Server = server;
            }
    }
};

What's the most elegant mechanism to take string[] args and not just correlate the members that fall within any of the dictionary key arrays, but Aggregate((x,y) => string.Format("{0} {1}", x, y)) the sequence of elements (was thinking TakeWhile() fits in here somehow) inbetween the args[] members that would be Contain()ed in any of the keys arrays, and handing them into the action of the respective key's value member.

We have all written these little command line processors countless times, and while obviously a simple loop and switch is always more than adequate, this is again as I said an exercise trying to stress my linq skills. So please no complaints that I'm overengineering, that part is obvious.

Update: To make this maybe a little easier, here is a non-linq way of doing what I'm looking for (may be imperfect, this is just winging it):

Action<string> currentAction;
string currentActionParameter;
for(int i = 0; i < e.Args.Length; i++)
{
    bool isParameterSwitch = _commandLineParametersProcessor.Keys.Any((parameterChoices) => parameterChoices.Contains(e.Args[i]));

    if (isParameterSwitch)
    {
        if (!string.IsNullOrEmpty(currentActionParameter) && currentAction != null)
        {
            currentAction(currentActionParameter);

            currentAction = null;
            currentActionParameter = "";
        }
        currentAction = _commandLineParametersProcessor[_commandLineParametersProcessor.Keys.Single((parameterChoices) => parameterChoices.Contains(e.Args[i]))];
    }
    else
    {
        currentActionParameter = string.Format("{0} {1}", currentActionParameter, e.Args[i]);
    }
}

This is not an altogether bad approach, I just wonder if anyone can maybe simplify it a little using linq or otherwise, though this may be the simplest form i guess..

+2  A: 

Assuming you know that every command has a corresponding argument (so 'args' will always be in the format of

cmd arg (repeated)

You could do something ridiculous like this...

var output = args.Select((value, idx) => new { Value = value, Group = idx / 2 })
            .GroupBy(x => x.Group)
            .Select(g => new 
             { 
                 Command = commands.FirstOrDefault(kvp => 
                    kvp.Key.Contains(g.First().Value)).Value, 
                 Argument = g.Last().Value 
             })
            .Where(c => c.Command != null)
            .Aggregate(
                new StringBuilder(), 
                (builder, value) => 
                { 
                    builder.AppendLine(value.Command(value.Argument)); 
                    return builder; 
                }).ToString();

But that is, frankly, the most obtuse bit of C# that I can recall ever writing, and not a very good way to teach yourself LINQ. Nonetheless, it will do what you're asking.

EDIT

Just realized (thanks to David B) that your key is a string[], not just a string, so I added some even more obtuse code that deals with that.

Adam Robinson
I can definitely say that's the most obtuse C# I can ever recall *reading*. :D
Mike Caron
+2  A: 

Borrowing half of Adam Robinson's answer (+1 btw), but realizing that the Dictionary will never be accessed by key, and you just want to run the Actions instead of building up a string...

var inputCommands = args
    .Select((value, idx) => new { Value = value, Group = idx / 2 })
    .GroupBy(x => x.Group) 
    .Select(g => new  
    {  
      Command = g.First().Value,  
      Argument = g.Last().Value  
    }).ToList();

inputCommands.ForEach(x => 
{
  Action<string> theAction = 
  (
    from kvp in commands
    where kvp.Key.Contains(x.Command)
    select kvp.Value
  ).FirstOrDefault();
  if (theAction != null)
  {
    theAction(x.Argument);
  }
}

kvp.Key.Contains really defeats the whole point of Dictionary. I'd re-design that to be a Dictionary<string, Action<string>>. Then you could say

inputCommands.ForEach(x => 
{
  if (commands.ContainsKey(x.Command))
  {
    commands[x.Command](x.Argument);
  }
}

PS: I can recall much more obtuse C# code that I have written than this.


I must admit the possibility that you want to collect the actions, instead of running them. Here is that code:

var todo =
(
  from x in inputCommands
  let theAction = 
  (
    from kvp in commands
    where kvp.Key.Contains(x.Command)
    select kvp.Value
  ).FirstOrDefault()
  where theAction != null
  select new { TheAction = theAction, Argument = x.Argument }
).ToList();
David B
+1, though you're not doing everything inline ;)
Adam Robinson
The idea is to just execute the action, though as I understand it, you're still missing the concatenation of args between parameter switches, e.g. bla.exe -l some junk for l --server server.com "some junk for l" needs to be passed to the action whos key contains -l in the dictionary, though in the args array that's 3 members, the variability of what needs to be concattenated as parameters is what makes this really tricky.
Jimmy Hoffa