• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.statusbar.commandline
18 
19 /**
20  * [CommandParser] defines the collection of tokens which can be parsed from an incoming command
21  * list, and parses them into their respective containers. Supported tokens are of the following
22  * forms:
23  * ```
24  * Flag: boolean value, false by default. always optional.
25  * Param: named parameter, taking N args all of a given type. Currently only single arg parameters
26  *        are supported.
27  * SubCommand: named command created by adding a command to a parent. Supports all fields above, but
28  *             not other subcommands.
29  * ```
30  *
31  * Tokens are added via the factory methods for each token type. They can be made `required` by
32  * calling the [require] method for the appropriate type, as follows:
33  * ```
34  * val requiredParam = parser.require(parser.param(...))
35  * ```
36  *
37  * The reason for having an explicit require is so that generic type arguments can be handled
38  * properly. See [SingleArgParam] and [SingleArgParamOptional] for the difference between an
39  * optional parameter and a required one.
40  *
41  * Typical usage of a required parameter, however, will occur within the context of a
42  * [ParseableCommand], which defines a convenience `require()` method:
43  * ```
44  * class MyCommand : ParseableCommand {
45  *   val requiredParam = param(...).require()
46  * }
47  * ```
48  *
49  * This parser defines two modes of parsing, both of which validate for required parameters.
50  * 1. [parse] is a top-level parsing method. This parser will walk the given arg list and populate
51  *    all of the delegate classes based on their type. It will handle SubCommands, and after parsing
52  *    will check for any required-but-missing SubCommands or Params.
53  *
54  *    **This method requires that every received token is represented in its grammar.**
55  * 2. [parseAsSubCommand] is a second-level parsing method suitable for any [SubCommand]. This
56  *    method will handle _only_ flags and params. It will return parsing control to its parent
57  *    parser on the first unknown token rather than throwing.
58  */
59 class CommandParser {
60     private val _flags = mutableListOf<Flag>()
61     val flags: List<Flag> = _flags
62     private val _params = mutableListOf<Param>()
63     val params: List<Param> = _params
64     private val _subCommands = mutableListOf<SubCommand>()
65     val subCommands: List<SubCommand> = _subCommands
66 
67     private val tokenSet = mutableSetOf<String>()
68 
69     /**
70      * Parse the arg list into the fields defined in the containing class.
71      *
72      * @return true if all required fields are present after parsing
73      * @throws ArgParseError on any failure to process args
74      */
parsenull75     fun parse(args: List<String>): Boolean {
76         if (args.isEmpty()) {
77             // An empty args list might be valid here if there are no required inputs
78             return validateRequiredParams()
79         }
80 
81         val iterator = args.listIterator()
82         var tokenHandled: Boolean
83         while (iterator.hasNext()) {
84             val token = iterator.next()
85             tokenHandled = false
86 
87             flags
88                 .find { it.matches(token) }
89                 ?.let {
90                     it.inner = true
91                     tokenHandled = true
92                 }
93 
94             if (tokenHandled) continue
95 
96             params
97                 .find { it.matches(token) }
98                 ?.let {
99                     it.parseArgsFromIter(iterator)
100                     tokenHandled = true
101                 }
102 
103             if (tokenHandled) continue
104 
105             subCommands
106                 .find { it.matches(token) }
107                 ?.let {
108                     it.parseSubCommandArgs(iterator)
109                     tokenHandled = true
110                 }
111 
112             if (!tokenHandled) {
113                 throw ArgParseError("Unknown token: $token")
114             }
115         }
116 
117         return validateRequiredParams()
118     }
119 
120     /**
121      * Parse a subset of the commands that came in from the top-level [parse] method, for the
122      * subcommand that this parser represents. Note that subcommands may not contain other
123      * subcommands. But they may contain flags and params.
124      *
125      * @return true if all required fields are present after parsing
126      * @throws ArgParseError on any failure to process args
127      */
parseAsSubCommandnull128     fun parseAsSubCommand(iter: ListIterator<String>): Boolean {
129         // arg[-1] is our subcommand name, so the rest of the args are either for this
130         // subcommand, OR for the top-level command to handle. Therefore, we bail on the first
131         // failure, but still check our own required params
132 
133         // The mere presence of a subcommand (similar to a flag) is a valid subcommand
134         if (flags.isEmpty() && params.isEmpty()) {
135             return validateRequiredParams()
136         }
137 
138         var tokenHandled: Boolean
139         while (iter.hasNext()) {
140             val token = iter.next()
141             tokenHandled = false
142 
143             flags
144                 .find { it.matches(token) }
145                 ?.let {
146                     it.inner = true
147                     tokenHandled = true
148                 }
149 
150             if (tokenHandled) continue
151 
152             params
153                 .find { it.matches(token) }
154                 ?.let {
155                     it.parseArgsFromIter(iter)
156                     tokenHandled = true
157                 }
158 
159             if (!tokenHandled) {
160                 // Move the cursor position backwards since we've arrived at a token
161                 // that we don't own
162                 iter.previous()
163                 break
164             }
165         }
166 
167         return validateRequiredParams()
168     }
169 
170     /**
171      * If [parse] or [parseAsSubCommand] does not produce a valid result, generate a list of errors
172      * based on missing elements
173      */
generateValidationErrorMessagesnull174     fun generateValidationErrorMessages(): List<String> {
175         val missingElements = mutableListOf<String>()
176 
177         if (unhandledParams.isNotEmpty()) {
178             val names = unhandledParams.map { it.longName }
179             missingElements.add("No values passed for required params: $names")
180         }
181 
182         if (unhandledSubCmds.isNotEmpty()) {
183             missingElements.addAll(unhandledSubCmds.map { it.longName })
184             val names = unhandledSubCmds.map { it.shortName }
185             missingElements.add("No values passed for required sub-commands: $names")
186         }
187 
188         return missingElements
189     }
190 
191     /** Check for any missing, required params, or any invalid subcommands */
validateRequiredParamsnull192     private fun validateRequiredParams(): Boolean =
193         unhandledParams.isEmpty() && unhandledSubCmds.isEmpty() && unvalidatedSubCmds.isEmpty()
194 
195     // If any required param (aka non-optional) hasn't handled a field, then return false
196     private val unhandledParams: List<Param>
197         get() = params.filter { (it is SingleArgParam<*>) && !it.handled }
198 
199     private val unhandledSubCmds: List<SubCommand>
<lambda>null200         get() = subCommands.filter { (it is RequiredSubCommand<*> && !it.handled) }
201 
202     private val unvalidatedSubCmds: List<SubCommand>
<lambda>null203         get() = subCommands.filter { !it.validationStatus }
204 
checkCliNamesnull205     private fun checkCliNames(short: String?, long: String): String? {
206         if (short != null && tokenSet.contains(short)) {
207             return short
208         }
209 
210         if (tokenSet.contains(long)) {
211             return long
212         }
213 
214         return null
215     }
216 
subCommandContainsSubCommandsnull217     private fun subCommandContainsSubCommands(cmd: ParseableCommand): Boolean =
218         cmd.parser.subCommands.isNotEmpty()
219 
220     private fun registerNames(short: String?, long: String) {
221         if (short != null) {
222             tokenSet.add(short)
223         }
224         tokenSet.add(long)
225     }
226 
227     /**
228      * Turns a [SingleArgParamOptional]<T> into a [SingleArgParam] by converting the [T?] into [T]
229      *
230      * @return a [SingleArgParam] property delegate
231      */
requirenull232     fun <T : Any> require(old: SingleArgParamOptional<T>): SingleArgParam<T> {
233         val newParam =
234             SingleArgParam(
235                 longName = old.longName,
236                 shortName = old.shortName,
237                 description = old.description,
238                 valueParser = old.valueParser,
239             )
240 
241         replaceWithRequired(old, newParam)
242         return newParam
243     }
244 
replaceWithRequirednull245     private fun <T : Any> replaceWithRequired(
246         old: SingleArgParamOptional<T>,
247         new: SingleArgParam<T>,
248     ) {
249         _params.remove(old)
250         _params.add(new)
251     }
252 
253     /**
254      * Turns an [OptionalSubCommand] into a [RequiredSubCommand] by converting the [T?] in to [T]
255      *
256      * @return a [RequiredSubCommand] property delegate
257      */
requirenull258     fun <T : ParseableCommand> require(optional: OptionalSubCommand<T>): RequiredSubCommand<T> {
259         val newCmd = RequiredSubCommand(optional.cmd)
260         replaceWithRequired(optional, newCmd)
261         return newCmd
262     }
263 
replaceWithRequirednull264     private fun <T : ParseableCommand> replaceWithRequired(
265         old: OptionalSubCommand<T>,
266         new: RequiredSubCommand<T>,
267     ) {
268         _subCommands.remove(old)
269         _subCommands.add(new)
270     }
271 
flagnull272     internal fun flag(longName: String, shortName: String? = null, description: String = ""): Flag {
273         checkCliNames(shortName, longName)?.let {
274             throw IllegalArgumentException("Detected reused flag name ($it)")
275         }
276         registerNames(shortName, longName)
277 
278         val flag = Flag(shortName, longName, description)
279         _flags.add(flag)
280         return flag
281     }
282 
paramnull283     internal fun <T : Any> param(
284         longName: String,
285         shortName: String? = null,
286         description: String = "",
287         valueParser: ValueParser<T>,
288     ): SingleArgParamOptional<T> {
289         checkCliNames(shortName, longName)?.let {
290             throw IllegalArgumentException("Detected reused param name ($it)")
291         }
292         registerNames(shortName, longName)
293 
294         val param =
295             SingleArgParamOptional(
296                 shortName = shortName,
297                 longName = longName,
298                 description = description,
299                 valueParser = valueParser,
300             )
301         _params.add(param)
302         return param
303     }
304 
subCommandnull305     internal fun <T : ParseableCommand> subCommand(command: T): OptionalSubCommand<T> {
306         checkCliNames(null, command.name)?.let {
307             throw IllegalArgumentException("Cannot re-use name for subcommand ($it)")
308         }
309 
310         if (subCommandContainsSubCommands(command)) {
311             throw IllegalArgumentException(
312                 "SubCommands may not contain other SubCommands. $command"
313             )
314         }
315 
316         registerNames(null, command.name)
317 
318         val subCmd = OptionalSubCommand(command)
319         _subCommands.add(subCmd)
320         return subCmd
321     }
322 }
323