@@ -36,16 +36,21 @@ public static async Task RunAsync<T>(string[] args, IContainer container)
36
36
{
37
37
var app = new ConsoleApp ( args , typeof ( T ) . Assembly , container ) ;
38
38
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.
39
42
if ( action != null )
40
43
{
41
44
try
42
45
{
46
+ // All Actions are async. No return value is expected from any action.
43
47
await action . RunAsync ( ) ;
44
48
}
45
49
catch ( Exception ex )
46
50
{
47
51
if ( Environment . GetEnvironmentVariable ( Constants . CliDebug ) == "1" )
48
52
{
53
+ // If CLI is in debug mode, display full call stack.
49
54
ColoredConsole . Error . WriteLine ( ErrorColor ( ex . ToString ( ) ) ) ;
50
55
}
51
56
else
@@ -56,19 +61,40 @@ public static async Task RunAsync<T>(string[] args, IContainer container)
56
61
}
57
62
}
58
63
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>
59
72
public static bool RelaunchSelfElevated ( IAction action , out string errors )
60
73
{
74
+ // A command is:
75
+ // func <context: optional> \
76
+ // <subcontext: valid only if there is a context, optional> \
77
+ // <action: not optional> --options
61
78
errors = string . Empty ;
62
79
var attribute = action . GetType ( ) . GetCustomAttribute < ActionAttribute > ( ) ;
63
80
if ( attribute != null )
64
81
{
82
+ // First extract the contexts for the given action
65
83
Func < Context , string > getContext = c => c == Context . None ? string . Empty : c . ToString ( ) ;
66
84
var context = getContext ( attribute . Context ) ;
67
85
var subContext = getContext ( attribute . Context ) ;
86
+
87
+ // Get the actual action name to use on the command line.
68
88
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.
69
94
var args = action
70
95
. ParseArgs ( Array . Empty < string > ( ) )
71
96
. UnMatchedOptions
97
+ // Description is expected to contain the name of the POCO's property holding the value.
72
98
. Select ( o => new { Name = o . Description , ParamName = o . HasLongName ? $ "--{ o . LongName } " : $ "-{ o . ShortName } " } )
73
99
. Select ( n =>
74
100
{
@@ -88,6 +114,10 @@ public static bool RelaunchSelfElevated(IAction action, out string errors)
88
114
89
115
var command = $ "{ context } { subContext } { name } { args } ";
90
116
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.
91
121
var logFile = Path . GetTempFileName ( ) ;
92
122
var exeName = Process . GetCurrentProcess ( ) . MainModule . FileName ;
93
123
command = $ "/c \" { exeName } \" { command } >> { logFile } ";
@@ -117,38 +147,65 @@ internal ConsoleApp(string[] args, Assembly assembly, IContainer container)
117
147
{
118
148
_args = args ;
119
149
_container = container ;
150
+ // TypeAttributePair is just a typed tuple of an IAction type and one of its action attribute.
120
151
_actionAttributes = assembly
121
152
. GetTypes ( )
122
153
. Where ( t => typeof ( IAction ) . IsAssignableFrom ( t ) && ! t . IsAbstract )
123
154
. Select ( type => type . GetCustomAttributes < ActionAttribute > ( ) . Select ( a => new TypeAttributePair { Type = type , Attribute = a } ) )
124
155
. SelectMany ( i => i ) ;
125
156
}
126
157
158
+ /// <summary>
159
+ /// This method parses _args into an IAction.
160
+ /// </summary>
127
161
internal IAction Parse ( )
128
162
{
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.
129
166
if ( _args . Length == 0 ||
130
167
( _args . Length == 1 && _helpArgs . Any ( ha => _args [ 0 ] . Replace ( "-" , "" ) . Equals ( ha , StringComparison . OrdinalIgnoreCase ) ) )
131
168
)
132
169
{
133
170
return new HelpAction ( _actionAttributes , CreateAction ) ;
134
171
}
135
172
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.
136
177
var isHelp = _args . First ( ) . Equals ( "help" , StringComparison . OrdinalIgnoreCase )
137
178
? true
138
179
: 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 ;
142
183
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
143
188
var context = Context . None ;
144
189
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 ( ) ;
146
198
147
199
if ( Enum . TryParse ( contextStr , true , out context ) )
148
200
{
201
+ // It is a valid context, so pop it out of the stack.
149
202
argsStack . Pop ( ) ;
150
203
if ( argsStack . Any ( ) )
151
204
{
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.
152
209
subContextStr = argsStack . Peek ( ) ;
153
210
if ( Enum . TryParse ( subContextStr , true , out subContext ) )
154
211
{
@@ -159,46 +216,72 @@ internal IAction Parse()
159
216
160
217
if ( argsStack . Any ( ) )
161
218
{
219
+ // If there are still more items in the stack, then it's an actionStr
162
220
actionStr = argsStack . Pop ( ) ;
163
221
}
164
222
165
223
if ( string . IsNullOrEmpty ( actionStr ) || isHelp )
166
224
{
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.
167
233
return new HelpAction ( _actionAttributes , CreateAction , contextStr , subContextStr ) ;
168
234
}
169
235
236
+ // Find the matching action type.
237
+ // We expect to find 1 and only 1 IAction that matches all 3 (context, subContext, action)
170
238
var actionType = _actionAttributes
171
239
. Where ( a => a . Attribute . Name . Equals ( actionStr , StringComparison . OrdinalIgnoreCase ) &&
172
240
a . Attribute . Context == context &&
173
241
a . Attribute . SubContext == subContext )
174
242
. SingleOrDefault ( ) ;
175
243
244
+ // If none is found, display help passing in all the info we have right now.
176
245
if ( actionType == null )
177
246
{
178
247
return new HelpAction ( _actionAttributes , CreateAction , contextStr , subContextStr ) ;
179
248
}
180
249
250
+ // Create the IAction
181
251
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.
182
255
var args = argsStack . ToArray ( ) ;
183
256
try
184
257
{
258
+ // Give the action a change to parse its args.
185
259
var parseResult = action . ParseArgs ( args ) ;
186
260
if ( parseResult . HasErrors )
187
261
{
262
+ // There was an error with the args, pass it to the HelpAction.
188
263
return new HelpAction ( _actionAttributes , CreateAction , action , parseResult ) ;
189
264
}
190
265
else
191
266
{
267
+ // Action is ready to run.
192
268
return action ;
193
269
}
194
270
}
195
271
catch ( CliArgumentsException ex )
196
272
{
273
+ // TODO: we can probably display help here as well.
274
+ // This happens for actions that expect an ordered untyped options.
197
275
ColoredConsole . Error . WriteLine ( ex . Message ) ;
198
276
return null ;
199
277
}
200
278
}
201
279
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>
202
285
internal IAction CreateAction ( Type type )
203
286
{
204
287
var ctor = type . GetConstructors ( ) ? . SingleOrDefault ( ) ;
0 commit comments