• 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.tools.metalava.cli.common
18 
19 import com.android.tools.metalava.ProgressTracker
20 import com.github.ajalt.clikt.core.CliktCommand
21 import com.github.ajalt.clikt.core.NoSuchOption
22 import com.github.ajalt.clikt.core.PrintHelpMessage
23 import com.github.ajalt.clikt.core.PrintMessage
24 import com.github.ajalt.clikt.core.UsageError
25 import com.github.ajalt.clikt.core.context
26 import com.github.ajalt.clikt.parameters.arguments.argument
27 import com.github.ajalt.clikt.parameters.arguments.multiple
28 import com.github.ajalt.clikt.parameters.groups.provideDelegate
29 import com.github.ajalt.clikt.parameters.options.eagerOption
30 import com.github.ajalt.clikt.parameters.options.flag
31 import com.github.ajalt.clikt.parameters.options.option
32 import java.io.InputStream
33 import java.io.PrintWriter
34 
35 const val ARG_VERSION = "--version"
36 
37 /**
38  * Main metalava command.
39  *
40  * If no subcommand is specified in the arguments that are passed to [parse] then this will invoke
41  * the subcommand called [defaultCommandName] passing in all the arguments not already consumed by
42  * Clikt options.
43  */
44 internal open class MetalavaCommand(
45     internal val executionEnvironment: ExecutionEnvironment,
46 
47     /**
48      * The optional name of the default subcommand to run if no subcommand is provided in the
49      * command line arguments.
50      */
51     private val defaultCommandName: String? = null,
52     internal val progressTracker: ProgressTracker,
53 ) :
54     CliktCommand(
55         // Gather all the options and arguments into a list so that they can be handled by some
56         // non-Clikt option processor which it is assumed that the default command, if specified,
57         // has.
58         treatUnknownOptionsAsArgs = defaultCommandName != null,
59         // Call run on this command even if no sub-command is provided.
60         invokeWithoutSubcommand = true,
61         help =
62             """
63             Extracts metadata from source code to generate artifacts such as the signature files,
64             the SDK stub files, external annotations etc.
65         """
66                 .trimIndent()
67     ) {
68 
69     private val stdout = executionEnvironment.stdout
70     private val stderr = executionEnvironment.stderr
71 
72     init {
<lambda>null73         context {
74             console = MetalavaConsole(executionEnvironment)
75 
76             localization = MetalavaLocalization()
77 
78             /**
79              * Disable built in help.
80              *
81              * See [showHelp] for an explanation.
82              */
83             helpOptionNames = emptySet()
84 
85             // Override the help formatter to use Metalava's special formatter.
86             helpFormatter =
87                 MetalavaHelpFormatter(
88                     { common.terminal },
89                     localization,
90                 )
91         }
92 
93         // Print the version number if requested.
94         eagerOption(
95             help = "Show the version and exit",
96             names = setOf(ARG_VERSION),
97             // Abort the processing of options immediately to display the version and exit.
<lambda>null98             action = { throw PrintVersionException() }
99         )
100 
101         // Add the --print-stack-trace option.
102         eagerOption(
103             "--print-stack-trace",
104             help =
105                 """
106                     Print the stack trace of any exceptions that will cause metalava to exit.
107                     (default: no stack trace)
108                 """
109                     .trimIndent(),
<lambda>null110             action = { printStackTrace = true }
111         )
112     }
113 
114     /** Group of common options. */
115     val common by CommonOptions()
116 
117     /**
118      * True if a stack trace should be output for any exception that is thrown and causes metalava
119      * to exit.
120      *
121      * Uses a real property that is set by an eager option action rather than a normal Clikt option
122      * so that it will be readable even if metalava aborts before it has been processed. Otherwise,
123      * exceptions that were thrown before the option was processed would cause this field to be
124      * accessed to see whether their stack trace should be printed. That access would fail and
125      * obscure the original error.
126      */
127     private var printStackTrace: Boolean = false
128 
129     /**
130      * A custom, non-eager help option that allows [CommonOptions] like [CommonOptions.terminal] to
131      * be used when generating the help output.
132      *
133      * The built-in help option is eager and throws a [PrintHelpMessage] exception which aborts the
134      * processing of other options preventing their use when generating the help output.
135      *
136      * Currently, this does not support `-?` for help as Clikt considers that to be an invalid flag.
137      * However, `-?` is still supported for backwards compatibility using a workaround in
138      * [showHelpAndExitIfRequested].
139      */
140     private val showHelp by option("-h", "--help", help = "Show this message and exit").flag()
141 
142     /** Property into which all the arguments (and unknown options) are gathered. */
143     private val flags by
144         argument(
145                 name = "flags",
146                 help = "See below.",
147             )
148             .multiple()
149 
150     /** A list of actions to perform after the command has been executed. */
151     private val postCommandActions = mutableListOf<() -> Unit>()
152 
153     /** Process the command, handling [MetalavaCliException]s. */
processnull154     fun process(args: Array<String>): Int {
155         var exitCode = 0
156         try {
157             processThrowCliException(args)
158         } catch (e: PrintVersionException) {
159             // Print the version and exit.
160             stdout.println("\n$commandName version: ${Version.VERSION}")
161         } catch (e: MetalavaCliException) {
162             stdout.flush()
163             stderr.flush()
164 
165             if (printStackTrace) {
166                 e.printStackTrace(stderr)
167             } else {
168                 val prefix =
169                     if (e.exitCode != 0) {
170                         "Aborting: "
171                     } else {
172                         ""
173                     }
174 
175                 if (e.stderr.isNotBlank()) {
176                     stderr.println("\n${prefix}${e.stderr}")
177                 }
178                 if (e.stdout.isNotBlank()) {
179                     stdout.println("\n${prefix}${e.stdout}")
180                 }
181             }
182 
183             exitCode = e.exitCode
184         }
185 
186         // Perform any subcommand specific actions, e.g. flushing files they have opened, etc.
187         performPostCommandActions()
188 
189         return exitCode
190     }
191 
192     /**
193      * Register a command to run after the command has been executed and after any thrown
194      * [MetalavaCliException]s have been caught.
195      */
registerPostCommandActionnull196     fun registerPostCommandAction(action: () -> Unit) {
197         postCommandActions.add(action)
198     }
199 
200     /** Perform actions registered by [registerPostCommandAction]. */
performPostCommandActionsnull201     fun performPostCommandActions() {
202         postCommandActions.forEach { it() }
203     }
204 
205     /** Process the command, throwing [MetalavaCliException]s. */
processThrowCliExceptionnull206     fun processThrowCliException(args: Array<String>) {
207         try {
208             parse(args)
209         } catch (e: PrintHelpMessage) {
210             throw MetalavaCliException(
211                 stdout = e.command.getFormattedHelp(),
212                 exitCode = if (e.error) 1 else 0
213             )
214         } catch (e: PrintMessage) {
215             throw MetalavaCliException(
216                 stdout = e.message ?: "",
217                 exitCode = if (e.error) 1 else 0,
218                 cause = e,
219             )
220         } catch (e: NoSuchOption) {
221             val message = createNoSuchOptionErrorMessage(e)
222             throw MetalavaCliException(
223                 stderr = message,
224                 exitCode = e.statusCode,
225                 cause = e,
226             )
227         } catch (e: UsageError) {
228             val message = e.helpMessage()
229             throw MetalavaCliException(
230                 stderr = message,
231                 exitCode = e.statusCode,
232                 cause = e,
233             )
234         }
235     }
236 
237     /**
238      * Create an error message that incorporates the specific usage error as well as providing
239      * documentation for all the available options.
240      */
createNoSuchOptionErrorMessagenull241     private fun createNoSuchOptionErrorMessage(e: UsageError): String {
242         return buildString {
243             val errorContext = e.context ?: currentContext
244             e.message?.let { append(errorContext.localization.usageError(it)).append("\n\n") }
245             append(errorContext.command.getFormattedHelp())
246         }
247     }
248 
249     /**
250      * Perform this command's actions.
251      *
252      * This is called after the command line parameters are parsed. If one of the sub-commands is
253      * invoked then this is called before the sub-commands parameters are parsed.
254      */
runnull255     override fun run() {
256         // Make this available to all sub-commands.
257         currentContext.obj = this
258 
259         val subcommand = currentContext.invokedSubcommand
260         if (subcommand == null) {
261             showHelpAndExitIfRequested()
262 
263             if (defaultCommandName != null) {
264                 // Get any remaining arguments/options that were not handled by Clikt.
265                 val remainingArgs = flags.toTypedArray()
266 
267                 // Get the default command.
268                 val defaultCommand =
269                     registeredSubcommands().singleOrNull { it.commandName == defaultCommandName }
270                         ?: cliError(
271                             "Invalid default command name '$defaultCommandName', expected one of '${registeredSubcommandNames().joinToString("', '")}'"
272                         )
273 
274                 // No sub-command was provided so use the default subcommand.
275                 defaultCommand.parse(remainingArgs, currentContext)
276             }
277         }
278     }
279 
280     /**
281      * Show help and exit if requested.
282      *
283      * Help is requested if [showHelp] is true or [flags] contains `-?` or `-?`.
284      */
showHelpAndExitIfRequestednull285     private fun showHelpAndExitIfRequested() {
286         val remainingArgs = flags.toTypedArray()
287         // Output help and exit if requested.
288         if (showHelp || remainingArgs.contains("-?")) {
289             throw PrintHelpMessage(this)
290         }
291     }
292 
293     /**
294      * Exception to use for the --version option to use for aborting the processing of options
295      * immediately and allow the exception handling code to treat it specially.
296      */
297     private class PrintVersionException : RuntimeException()
298 }
299 
300 /**
301  * Get the containing [MetalavaCommand].
302  *
303  * It will always be set.
304  */
305 private val CliktCommand.metalavaCommand
306     get() = if (this is MetalavaCommand) this else currentContext.findObject()!!
307 
308 /** The [ExecutionEnvironment] within which the command is being run. */
309 val CliktCommand.executionEnvironment
310     get() = metalavaCommand.executionEnvironment
311 
312 /** The [PrintWriter] to use for error output from the command. */
313 val CliktCommand.stderr
314     get() = executionEnvironment.stderr
315 
316 /** The [PrintWriter] to use for non-error output from the command. */
317 val CliktCommand.stdout
318     get() = executionEnvironment.stdout
319 
320 /** The [InputStream] to use for input to the command. */
321 val CliktCommand.stdin
322     get() = executionEnvironment.stdin
323 
324 val CliktCommand.commonOptions
325     // Retrieve the CommonOptions that is made available by the containing MetalavaCommand.
326     get() = metalavaCommand.common
327 
328 val CliktCommand.terminal
329     // Retrieve the terminal from the CommonOptions.
330     get() = commonOptions.terminal
331 
332 val CliktCommand.progressTracker
333     // Retrieve the ProgressTracker that is made available by the containing MetalavaCommand.
334     get() = metalavaCommand.progressTracker
335 
CliktCommandnull336 fun CliktCommand.registerPostCommandAction(action: () -> Unit) {
337     metalavaCommand.registerPostCommandAction(action)
338 }
339