• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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