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 import android.util.IndentingPrintWriter 20 import java.io.PrintWriter 21 import java.lang.IllegalArgumentException 22 import kotlin.properties.ReadOnlyProperty 23 import kotlin.reflect.KProperty 24 25 /** 26 * An implementation of [Command] that includes a [CommandParser] which can set all delegated 27 * properties. 28 * 29 * As the number of registrants to [CommandRegistry] grows, we should have a default mechanism for 30 * parsing common command line arguments. We are not expecting to build an arbitrarily-functional 31 * CLI, nor a GNU arg parse compliant interface here, we simply want to be able to empower clients 32 * to create simple CLI grammars such as: 33 * ``` 34 * $ my_command [-f|--flag] 35 * $ my_command [-a|--arg] <params...> 36 * $ my_command [subcommand1] [subcommand2] 37 * $ my_command <positional_arg ...> # not-yet implemented 38 * ``` 39 * 40 * Note that the flags `-h` and `--help` are reserved for the base class. It seems prudent to just 41 * avoid them in your implementation. 42 * 43 * Usage: 44 * 45 * The intended usage tries to be clever enough to enable good ergonomics, while not too clever as 46 * to be unmaintainable. Using the default parser is done using property delegates, and looks like: 47 * ``` 48 * class MyCommand( 49 * onExecute: (cmd: MyCommand, pw: PrintWriter) -> () 50 * ) : ParseableCommand(name) { 51 * val flag1 by flag( 52 * shortName = "-f", 53 * longName = "--flag", 54 * required = false, 55 * ) 56 * val param1: String by param( 57 * shortName = "-a", 58 * longName = "--args", 59 * valueParser = Type.String 60 * ).required() 61 * val param2: Int by param(..., valueParser = Type.Int) 62 * val subCommand by subCommand(...) 63 * 64 * override fun execute(pw: PrintWriter) { 65 * onExecute(this, pw) 66 * } 67 * 68 * companion object { 69 * const val name = "my_command" 70 * } 71 * } 72 * 73 * fun main() { 74 * fun printArgs(cmd: MyCommand, pw: PrintWriter) { 75 * pw.println("${cmd.flag1}") 76 * pw.println("${cmd.param1}") 77 * pw.println("${cmd.param2}") 78 * pw.println("${cmd.subCommand}") 79 * } 80 * 81 * commandRegistry.registerCommand(MyCommand.companion.name) { 82 * MyCommand() { (cmd, pw) -> 83 * printArgs(cmd, pw) 84 * } 85 * } 86 * } 87 * 88 * ``` 89 */ 90 abstract class ParseableCommand(val name: String, val description: String? = null) : Command { 91 val parser: CommandParser = CommandParser() 92 93 val help by flag(longName = "help", shortName = "h", description = "Print help and return") 94 95 /** 96 * After [execute(pw, args)] is called, this class goes through a parsing stage and sets all 97 * delegated properties. It is safe to read any delegated properties here. 98 * 99 * This method is never called for [SubCommand]s, since they are associated with a top-level 100 * command that handles [execute] 101 */ 102 abstract fun execute(pw: PrintWriter) 103 104 /** 105 * Given a command string list, [execute] parses the incoming command and validates the input. 106 * If this command or any of its subcommands is passed `-h` or `--help`, then execute will only 107 * print the relevant help message and exit. 108 * 109 * If any error is thrown during parsing, we will catch and log the error. This process should 110 * _never_ take down its process. Override [onParseFailed] to handle an [ArgParseError]. 111 * 112 * Important: none of the delegated fields can be read before this stage. 113 */ 114 override fun execute(pw: PrintWriter, args: List<String>) { 115 val success: Boolean 116 try { 117 success = parser.parse(args) 118 } catch (e: ArgParseError) { 119 pw.println(e.message) 120 onParseFailed(e) 121 return 122 } catch (e: Exception) { 123 pw.println("Unknown exception encountered during parse") 124 pw.println(e) 125 return 126 } 127 128 // Now we've parsed the incoming command without error. There are two things to check: 129 // 1. If any help is requested, print the help message and return 130 // 2. Otherwise, make sure required params have been passed in, and execute 131 132 val helpSubCmds = subCmdsRequestingHelp() 133 134 // Top-level help encapsulates subcommands. Otherwise, if _any_ subcommand requests 135 // help then defer to them. Else, just execute 136 if (help) { 137 help(pw) 138 } else if (helpSubCmds.isNotEmpty()) { 139 helpSubCmds.forEach { it.help(pw) } 140 } else { 141 if (!success) { 142 parser.generateValidationErrorMessages().forEach { pw.println(it) } 143 } else { 144 execute(pw) 145 } 146 } 147 } 148 149 /** 150 * Returns a list of all commands that asked for help. If non-empty, parsing will stop to print 151 * help. It is not guaranteed that delegates are fulfilled if help is requested 152 */ 153 private fun subCmdsRequestingHelp(): List<ParseableCommand> = 154 parser.subCommands.filter { it.cmd.help }.map { it.cmd } 155 156 /** Override to do something when parsing fails */ 157 open fun onParseFailed(error: ArgParseError) {} 158 159 /** Override to print a usage clause. E.g. `usage: my-cmd <arg1> <arg2>` */ 160 open fun usage(pw: IndentingPrintWriter) {} 161 162 /** 163 * Print out the list of tokens, their received types if any, and their description in a 164 * formatted string. 165 * 166 * Example: 167 * ``` 168 * my-command: 169 * MyCmd.description 170 * 171 * [optional] usage block 172 * 173 * Flags: 174 * -f 175 * description 176 * --flag2 177 * description 178 * 179 * Parameters: 180 * Required: 181 * -p1 [Param.Type] 182 * description 183 * --param2 [Param.Type] 184 * description 185 * Optional: 186 * same as above 187 * 188 * SubCommands: 189 * Required: 190 * ... 191 * Optional: 192 * ... 193 * ``` 194 */ 195 override fun help(pw: PrintWriter) { 196 val ipw = IndentingPrintWriter(pw) 197 ipw.printBoxed(name) 198 ipw.println() 199 200 // Allow for a simple `usage` block for clients 201 ipw.indented { usage(ipw) } 202 203 if (description != null) { 204 ipw.indented { ipw.println(description) } 205 ipw.println() 206 } 207 208 val flags = parser.flags 209 if (flags.isNotEmpty()) { 210 ipw.println("FLAGS:") 211 ipw.indented { 212 flags.forEach { 213 it.describe(ipw) 214 ipw.println() 215 } 216 } 217 } 218 219 val (required, optional) = parser.params.partition { it is SingleArgParam<*> } 220 if (required.isNotEmpty()) { 221 ipw.println("REQUIRED PARAMS:") 222 required.describe(ipw) 223 } 224 if (optional.isNotEmpty()) { 225 ipw.println("OPTIONAL PARAMS:") 226 optional.describe(ipw) 227 } 228 229 val (reqSub, optSub) = parser.subCommands.partition { it is RequiredSubCommand<*> } 230 if (reqSub.isNotEmpty()) { 231 ipw.println("REQUIRED SUBCOMMANDS:") 232 reqSub.describe(ipw) 233 } 234 if (optSub.isNotEmpty()) { 235 ipw.println("OPTIONAL SUBCOMMANDS:") 236 optSub.describe(ipw) 237 } 238 } 239 240 fun flag( 241 longName: String, 242 shortName: String? = null, 243 description: String = "", 244 ): Flag { 245 if (!checkShortName(shortName)) { 246 throw IllegalArgumentException( 247 "Flag short name must be one character long, or null. Got ($shortName)" 248 ) 249 } 250 251 if (!checkLongName(longName)) { 252 throw IllegalArgumentException("Flags must not start with '-'. Got $($longName)") 253 } 254 255 val short = shortName?.let { "-$shortName" } 256 val long = "--$longName" 257 258 return parser.flag(long, short, description) 259 } 260 261 fun <T : Any> param( 262 longName: String, 263 shortName: String? = null, 264 description: String = "", 265 valueParser: ValueParser<T>, 266 ): SingleArgParamOptional<T> { 267 if (!checkShortName(shortName)) { 268 throw IllegalArgumentException( 269 "Parameter short name must be one character long, or null. Got ($shortName)" 270 ) 271 } 272 273 if (!checkLongName(longName)) { 274 throw IllegalArgumentException("Parameters must not start with '-'. Got $($longName)") 275 } 276 277 val short = shortName?.let { "-$shortName" } 278 val long = "--$longName" 279 280 return parser.param(long, short, description, valueParser) 281 } 282 283 fun <T : ParseableCommand> subCommand( 284 command: T, 285 ) = parser.subCommand(command) 286 287 /** For use in conjunction with [param], makes the parameter required */ 288 fun <T : Any> SingleArgParamOptional<T>.required(): SingleArgParam<T> = parser.require(this) 289 290 /** For use in conjunction with [subCommand], makes the given [SubCommand] required */ 291 fun <T : ParseableCommand> OptionalSubCommand<T>.required(): RequiredSubCommand<T> = 292 parser.require(this) 293 294 private fun checkShortName(short: String?): Boolean { 295 return short == null || short.length == 1 296 } 297 298 private fun checkLongName(long: String): Boolean { 299 return !long.startsWith("-") 300 } 301 302 companion object { 303 fun Iterable<Describable>.describe(pw: IndentingPrintWriter) { 304 pw.indented { 305 forEach { 306 it.describe(pw) 307 pw.println() 308 } 309 } 310 } 311 } 312 } 313 314 /** 315 * A flag is a boolean value passed over the command line. It can have a short form or long form. 316 * The value is [Boolean.true] if the flag is found, else false 317 */ 318 data class Flag( 319 override val shortName: String? = null, 320 override val longName: String, 321 override val description: String? = null, 322 ) : ReadOnlyProperty<Any?, Boolean>, Describable { 323 var inner: Boolean = false 324 325 override fun getValue(thisRef: Any?, property: KProperty<*>) = inner 326 } 327 328 /** 329 * Named CLI token. Can have a short or long name. Note: consider renaming to "primary" and 330 * "secondary" names since we don't actually care what the strings are 331 * 332 * Flags and params will have [shortName]s that are always prefixed with a single dash, while 333 * [longName]s are prefixed by a double dash. E.g., `my_command -f --flag`. 334 * 335 * Subcommands do not do any prefixing, and register their name as the [longName] 336 * 337 * Can be matched against an incoming token 338 */ 339 interface CliNamed { 340 val shortName: String? 341 val longName: String 342 343 fun matches(token: String) = shortName == token || longName == token 344 } 345 346 interface Describable : CliNamed { 347 val description: String? 348 349 fun describe(pw: IndentingPrintWriter) { 350 if (shortName != null) { 351 pw.print("$shortName, ") 352 } 353 pw.print(longName) 354 pw.println() 355 if (description != null) { 356 pw.indented { pw.println(description) } 357 } 358 } 359 } 360 361 /** 362 * Print [s] inside of a unicode character box, like so: 363 * ``` 364 * ╔═══════════╗ 365 * ║ my-string ║ 366 * ╚═══════════╝ 367 * ``` 368 */ 369 fun PrintWriter.printDoubleBoxed(s: String) { 370 val length = s.length 371 println("╔${"═".repeat(length + 2)}╗") 372 println("║ $s ║") 373 println("╚${"═".repeat(length + 2)}╝") 374 } 375 376 /** 377 * Print [s] inside of a unicode character box, like so: 378 * ``` 379 * ┌───────────┐ 380 * │ my-string │ 381 * └───────────┘ 382 * ``` 383 */ 384 fun PrintWriter.printBoxed(s: String) { 385 val length = s.length 386 println("┌${"─".repeat(length + 2)}┐") 387 println("│ $s │") 388 println("└${"─".repeat(length + 2)}┘") 389 } 390 391 fun IndentingPrintWriter.indented(block: () -> Unit) { 392 increaseIndent() 393 block() 394 decreaseIndent() 395 } 396