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 */
executenull102 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 */
subCmdsRequestingHelpnull153 private fun subCmdsRequestingHelp(): List<ParseableCommand> =
154 parser.subCommands.filter { it.cmd.help }.map { it.cmd }
155
156 /** Override to do something when parsing fails */
onParseFailednull157 open fun onParseFailed(error: ArgParseError) {}
158
159 /** Override to print a usage clause. E.g. `usage: my-cmd <arg1> <arg2>` */
usagenull160 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 */
helpnull195 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
flagnull240 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
paramnull261 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
subCommandnull283 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
checkLongNamenull298 private fun checkLongName(long: String): Boolean {
299 return !long.startsWith("-")
300 }
301
302 companion object {
Iterablenull303 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
getValuenull325 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
describenull349 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 */
printDoubleBoxednull369 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 */
printBoxednull384 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
indentednull391 fun IndentingPrintWriter.indented(block: () -> Unit) {
392 increaseIndent()
393 block()
394 decreaseIndent()
395 }
396