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             return false
78         }
79 
80         val iterator = args.listIterator()
81         var tokenHandled: Boolean
82         while (iterator.hasNext()) {
83             val token = iterator.next()
84             tokenHandled = false
85 
86             flags
87                 .find { it.matches(token) }
88                 ?.let {
89                     it.inner = true
90                     tokenHandled = true
91                 }
92 
93             if (tokenHandled) continue
94 
95             params
96                 .find { it.matches(token) }
97                 ?.let {
98                     it.parseArgsFromIter(iterator)
99                     tokenHandled = true
100                 }
101 
102             if (tokenHandled) continue
103 
104             subCommands
105                 .find { it.matches(token) }
106                 ?.let {
107                     it.parseSubCommandArgs(iterator)
108                     tokenHandled = true
109                 }
110 
111             if (!tokenHandled) {
112                 throw ArgParseError("Unknown token: $token")
113             }
114         }
115 
116         return validateRequiredParams()
117     }
118 
119     /**
120      * Parse a subset of the commands that came in from the top-level [parse] method, for the
121      * subcommand that this parser represents. Note that subcommands may not contain other
122      * subcommands. But they may contain flags and params.
123      *
124      * @return true if all required fields are present after parsing
125      * @throws ArgParseError on any failure to process args
126      */
parseAsSubCommandnull127     fun parseAsSubCommand(iter: ListIterator<String>): Boolean {
128         // arg[-1] is our subcommand name, so the rest of the args are either for this
129         // subcommand, OR for the top-level command to handle. Therefore, we bail on the first
130         // failure, but still check our own required params
131 
132         // The mere presence of a subcommand (similar to a flag) is a valid subcommand
133         if (flags.isEmpty() && params.isEmpty()) {
134             return validateRequiredParams()
135         }
136 
137         var tokenHandled: Boolean
138         while (iter.hasNext()) {
139             val token = iter.next()
140             tokenHandled = false
141 
142             flags
143                 .find { it.matches(token) }
144                 ?.let {
145                     it.inner = true
146                     tokenHandled = true
147                 }
148 
149             if (tokenHandled) continue
150 
151             params
152                 .find { it.matches(token) }
153                 ?.let {
154                     it.parseArgsFromIter(iter)
155                     tokenHandled = true
156                 }
157 
158             if (!tokenHandled) {
159                 // Move the cursor position backwards since we've arrived at a token
160                 // that we don't own
161                 iter.previous()
162                 break
163             }
164         }
165 
166         return validateRequiredParams()
167     }
168 
169     /**
170      * If [parse] or [parseAsSubCommand] does not produce a valid result, generate a list of errors
171      * based on missing elements
172      */
generateValidationErrorMessagesnull173     fun generateValidationErrorMessages(): List<String> {
174         val missingElements = mutableListOf<String>()
175 
176         if (unhandledParams.isNotEmpty()) {
177             val names = unhandledParams.map { it.longName }
178             missingElements.add("No values passed for required params: $names")
179         }
180 
181         if (unhandledSubCmds.isNotEmpty()) {
182             missingElements.addAll(unhandledSubCmds.map { it.longName })
183             val names = unhandledSubCmds.map { it.shortName }
184             missingElements.add("No values passed for required sub-commands: $names")
185         }
186 
187         return missingElements
188     }
189 
190     /** Check for any missing, required params, or any invalid subcommands */
validateRequiredParamsnull191     private fun validateRequiredParams(): Boolean =
192         unhandledParams.isEmpty() && unhandledSubCmds.isEmpty() && unvalidatedSubCmds.isEmpty()
193 
194     // If any required param (aka non-optional) hasn't handled a field, then return false
195     private val unhandledParams: List<Param>
196         get() = params.filter { (it is SingleArgParam<*>) && !it.handled }
197 
198     private val unhandledSubCmds: List<SubCommand>
<lambda>null199         get() = subCommands.filter { (it is RequiredSubCommand<*> && !it.handled) }
200 
201     private val unvalidatedSubCmds: List<SubCommand>
<lambda>null202         get() = subCommands.filter { !it.validationStatus }
203 
checkCliNamesnull204     private fun checkCliNames(short: String?, long: String): String? {
205         if (short != null && tokenSet.contains(short)) {
206             return short
207         }
208 
209         if (tokenSet.contains(long)) {
210             return long
211         }
212 
213         return null
214     }
215 
subCommandContainsSubCommandsnull216     private fun subCommandContainsSubCommands(cmd: ParseableCommand): Boolean =
217         cmd.parser.subCommands.isNotEmpty()
218 
219     private fun registerNames(short: String?, long: String) {
220         if (short != null) {
221             tokenSet.add(short)
222         }
223         tokenSet.add(long)
224     }
225 
226     /**
227      * Turns a [SingleArgParamOptional]<T> into a [SingleArgParam] by converting the [T?] into [T]
228      *
229      * @return a [SingleArgParam] property delegate
230      */
requirenull231     fun <T : Any> require(old: SingleArgParamOptional<T>): SingleArgParam<T> {
232         val newParam =
233             SingleArgParam(
234                 longName = old.longName,
235                 shortName = old.shortName,
236                 description = old.description,
237                 valueParser = old.valueParser,
238             )
239 
240         replaceWithRequired(old, newParam)
241         return newParam
242     }
243 
replaceWithRequirednull244     private fun <T : Any> replaceWithRequired(
245         old: SingleArgParamOptional<T>,
246         new: SingleArgParam<T>,
247     ) {
248         _params.remove(old)
249         _params.add(new)
250     }
251 
252     /**
253      * Turns an [OptionalSubCommand] into a [RequiredSubCommand] by converting the [T?] in to [T]
254      *
255      * @return a [RequiredSubCommand] property delegate
256      */
requirenull257     fun <T : ParseableCommand> require(optional: OptionalSubCommand<T>): RequiredSubCommand<T> {
258         val newCmd = RequiredSubCommand(optional.cmd)
259         replaceWithRequired(optional, newCmd)
260         return newCmd
261     }
262 
replaceWithRequirednull263     private fun <T : ParseableCommand> replaceWithRequired(
264         old: OptionalSubCommand<T>,
265         new: RequiredSubCommand<T>,
266     ) {
267         _subCommands.remove(old)
268         _subCommands.add(new)
269     }
270 
flagnull271     internal fun flag(
272         longName: String,
273         shortName: String? = null,
274         description: String = "",
275     ): Flag {
276         checkCliNames(shortName, longName)?.let {
277             throw IllegalArgumentException("Detected reused flag name ($it)")
278         }
279         registerNames(shortName, longName)
280 
281         val flag = Flag(shortName, longName, description)
282         _flags.add(flag)
283         return flag
284     }
285 
paramnull286     internal fun <T : Any> param(
287         longName: String,
288         shortName: String? = null,
289         description: String = "",
290         valueParser: ValueParser<T>,
291     ): SingleArgParamOptional<T> {
292         checkCliNames(shortName, longName)?.let {
293             throw IllegalArgumentException("Detected reused param name ($it)")
294         }
295         registerNames(shortName, longName)
296 
297         val param =
298             SingleArgParamOptional(
299                 shortName = shortName,
300                 longName = longName,
301                 description = description,
302                 valueParser = valueParser,
303             )
304         _params.add(param)
305         return param
306     }
307 
subCommandnull308     internal fun <T : ParseableCommand> subCommand(
309         command: T,
310     ): OptionalSubCommand<T> {
311         checkCliNames(null, command.name)?.let {
312             throw IllegalArgumentException("Cannot re-use name for subcommand ($it)")
313         }
314 
315         if (subCommandContainsSubCommands(command)) {
316             throw IllegalArgumentException(
317                 "SubCommands may not contain other SubCommands. $command"
318             )
319         }
320 
321         registerNames(null, command.name)
322 
323         val subCmd = OptionalSubCommand(command)
324         _subCommands.add(subCmd)
325         return subCmd
326     }
327 }
328