Skip to content

Commit 3bfca63

Browse files
committed
Add some comments to explain how ConsoleApp parsing works
1 parent 249d8fb commit 3bfca63

File tree

1 file changed

+87
-4
lines changed

1 file changed

+87
-4
lines changed

src/Azure.Functions.Cli/ConsoleApp.cs

+87-4
Original file line numberDiff line numberDiff line change
@@ -36,16 +36,21 @@ public static async Task RunAsync<T>(string[] args, IContainer container)
3636
{
3737
var app = new ConsoleApp(args, typeof(T).Assembly, container);
3838
var action = app.Parse();
39+
// If all goes well, we will have an action to run.
40+
// This action can be an actual action, or just a HelpAction, but this method doesn't care
41+
// since HelpAction is still an IAction.
3942
if (action != null)
4043
{
4144
try
4245
{
46+
// All Actions are async. No return value is expected from any action.
4347
await action.RunAsync();
4448
}
4549
catch (Exception ex)
4650
{
4751
if (Environment.GetEnvironmentVariable(Constants.CliDebug) == "1")
4852
{
53+
// If CLI is in debug mode, display full call stack.
4954
ColoredConsole.Error.WriteLine(ErrorColor(ex.ToString()));
5055
}
5156
else
@@ -56,19 +61,40 @@ public static async Task RunAsync<T>(string[] args, IContainer container)
5661
}
5762
}
5863

64+
/// <summary>
65+
/// This function essentially takes in an IAction object, and builds the
66+
/// command line that ought to be used to run that action but elevated.
67+
/// The IAction however has to mention the name of each property that maps to
68+
/// LongName and ShortName in the option Description. See InternalAction for an example.
69+
/// This method also doesn't support actions that take an untyped paramater
70+
/// like functionAppName, functionName, setting keys and values, storageAccount, etc.
71+
/// </summary>
5972
public static bool RelaunchSelfElevated(IAction action, out string errors)
6073
{
74+
// A command is:
75+
// func <context: optional> \
76+
// <subcontext: valid only if there is a context, optional> \
77+
// <action: not optional> --options
6178
errors = string.Empty;
6279
var attribute = action.GetType().GetCustomAttribute<ActionAttribute>();
6380
if (attribute != null)
6481
{
82+
// First extract the contexts for the given action
6583
Func<Context, string> getContext = c => c == Context.None ? string.Empty : c.ToString();
6684
var context = getContext(attribute.Context);
6785
var subContext = getContext(attribute.Context);
86+
87+
// Get the actual action name to use on the command line.
6888
var name = attribute.Name;
89+
90+
// Every action is expected to return a ICommandLineParserResult that contains
91+
// a collection UnMatchedOptions that the action accepts.
92+
// That means this method doesn't support actions that have untyped ordered options.
93+
// This however can be updated to support them easily just like help does now.
6994
var args = action
7095
.ParseArgs(Array.Empty<string>())
7196
.UnMatchedOptions
97+
// Description is expected to contain the name of the POCO's property holding the value.
7298
.Select(o => new { Name = o.Description, ParamName = o.HasLongName ? $"--{o.LongName}" : $"-{o.ShortName}" })
7399
.Select(n =>
74100
{
@@ -88,6 +114,10 @@ public static bool RelaunchSelfElevated(IAction action, out string errors)
88114

89115
var command = $"{context} {subContext} {name} {args}";
90116

117+
// Since the process will be elevated, we won't be able to redirect stdout\stdin to
118+
// our process if we are not elevated too, which is most probably the case.
119+
// Therefore I use shell redirection >> to a temp file, then read the content after
120+
// the process exists.
91121
var logFile = Path.GetTempFileName();
92122
var exeName = Process.GetCurrentProcess().MainModule.FileName;
93123
command = $"/c \"{exeName}\" {command} >> {logFile}";
@@ -117,38 +147,65 @@ internal ConsoleApp(string[] args, Assembly assembly, IContainer container)
117147
{
118148
_args = args;
119149
_container = container;
150+
// TypeAttributePair is just a typed tuple of an IAction type and one of its action attribute.
120151
_actionAttributes = assembly
121152
.GetTypes()
122153
.Where(t => typeof(IAction).IsAssignableFrom(t) && !t.IsAbstract)
123154
.Select(type => type.GetCustomAttributes<ActionAttribute>().Select(a => new TypeAttributePair { Type = type, Attribute = a }))
124155
.SelectMany(i => i);
125156
}
126157

158+
/// <summary>
159+
/// This method parses _args into an IAction.
160+
/// </summary>
127161
internal IAction Parse()
128162
{
163+
// If there is no args are passed, display help.
164+
// If 1 arg is passed and it matched any of the strings in _helpArgs, display help.
165+
// Otherwise, continue parsing.
129166
if (_args.Length == 0 ||
130167
(_args.Length == 1 && _helpArgs.Any(ha => _args[0].Replace("-", "").Equals(ha, StringComparison.OrdinalIgnoreCase)))
131168
)
132169
{
133170
return new HelpAction(_actionAttributes, CreateAction);
134171
}
135172

173+
// this supports the format:
174+
// `func help <context: optional> <subContext: optional> <action: optional>`
175+
// but help has to be the first word. So `func azure help` for example doesn't work
176+
// but `func help azure` should work.
136177
var isHelp = _args.First().Equals("help", StringComparison.OrdinalIgnoreCase)
137178
? true
138179
: false;
139-
140-
var argsStack = new Stack<string>((isHelp ? _args.Skip(1) : _args).Reverse());
141-
var contextStr = argsStack.Peek();
180+
181+
// We'll need to grab context arg: string, subcontext arg: string, action arg: string
182+
var contextStr = string.Empty;
142183
var subContextStr = string.Empty;
184+
var actionStr = string.Empty;
185+
186+
// These start out as None, but if contextStr and subContextStr hold a value, they'll
187+
// get parsed into these
143188
var context = Context.None;
144189
var subContext = Context.None;
145-
var actionStr = string.Empty;
190+
191+
// If isHelp, skip one and parse the rest of the command as usual.
192+
var argsStack = new Stack<string>((isHelp ? _args.Skip(1) : _args).Reverse());
193+
194+
// Grab the first string, but don't pop it off the stack.
195+
// If it's indeed a valid context, will remove it later.
196+
// Otherwise, it could be just an action. Actions are allowed not to have contexts.
197+
contextStr = argsStack.Peek();
146198

147199
if (Enum.TryParse(contextStr, true, out context))
148200
{
201+
// It is a valid context, so pop it out of the stack.
149202
argsStack.Pop();
150203
if (argsStack.Any())
151204
{
205+
// We still have items in the stack, do the same again for subContext.
206+
// This means we only support 2 levels of contexts only. Main, and Sub.
207+
// There is currently no way to declaratively specify any more.
208+
// If we ever need more than 2 contexts, we should switch to a more generic mechanism.
152209
subContextStr = argsStack.Peek();
153210
if (Enum.TryParse(subContextStr, true, out subContext))
154211
{
@@ -159,46 +216,72 @@ internal IAction Parse()
159216

160217
if (argsStack.Any())
161218
{
219+
// If there are still more items in the stack, then it's an actionStr
162220
actionStr = argsStack.Pop();
163221
}
164222

165223
if (string.IsNullOrEmpty(actionStr) || isHelp)
166224
{
225+
// At this point we have all we need to create an IAction:
226+
// context
227+
// subContext
228+
// action
229+
// However, if isHelp is true, then display help for that context.
230+
// Action Name is ignored with help since we don't have action specific help yet.
231+
// There is no need so far for action specific help since general context help displays
232+
// the help for all the actions in that context anyway.
167233
return new HelpAction(_actionAttributes, CreateAction, contextStr, subContextStr);
168234
}
169235

236+
// Find the matching action type.
237+
// We expect to find 1 and only 1 IAction that matches all 3 (context, subContext, action)
170238
var actionType = _actionAttributes
171239
.Where(a => a.Attribute.Name.Equals(actionStr, StringComparison.OrdinalIgnoreCase) &&
172240
a.Attribute.Context == context &&
173241
a.Attribute.SubContext == subContext)
174242
.SingleOrDefault();
175243

244+
// If none is found, display help passing in all the info we have right now.
176245
if (actionType == null)
177246
{
178247
return new HelpAction(_actionAttributes, CreateAction, contextStr, subContextStr);
179248
}
180249

250+
// Create the IAction
181251
var action = CreateAction(actionType.Type);
252+
253+
// Grab whatever is left in the stack of args into an array.
254+
// This will be passed into the action as actions can optionally take args for their options.
182255
var args = argsStack.ToArray();
183256
try
184257
{
258+
// Give the action a change to parse its args.
185259
var parseResult = action.ParseArgs(args);
186260
if (parseResult.HasErrors)
187261
{
262+
// There was an error with the args, pass it to the HelpAction.
188263
return new HelpAction(_actionAttributes, CreateAction, action, parseResult);
189264
}
190265
else
191266
{
267+
// Action is ready to run.
192268
return action;
193269
}
194270
}
195271
catch (CliArgumentsException ex)
196272
{
273+
// TODO: we can probably display help here as well.
274+
// This happens for actions that expect an ordered untyped options.
197275
ColoredConsole.Error.WriteLine(ex.Message);
198276
return null;
199277
}
200278
}
201279

280+
/// <summary>
281+
/// This method instantiates an IAction object from a Type.
282+
/// It uses _container to injects dependencies into the created IAction if needed.
283+
/// <param name="type"> Type is expected to be an IAction type </param>
284+
/// </summary>
202285
internal IAction CreateAction(Type type)
203286
{
204287
var ctor = type.GetConstructors()?.SingleOrDefault();

0 commit comments

Comments
 (0)