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