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