Coalesce pkl format subcommands into the parent command. (#1263)

This commit is contained in:
Islon Scherer
2025-10-30 10:08:25 +01:00
committed by GitHub
parent 7bf150055c
commit 7df447924e
5 changed files with 192 additions and 205 deletions

View File

@@ -733,16 +733,22 @@ pkl shell-completion bash
pkl shell-completion zsh pkl shell-completion zsh
---- ----
[[command-format-check]] [[command-format]]
=== `pkl format check` === `pkl format`
*Synopsis*: `pkl format check <file-or-dir-path>` *Synopsis*: `pkl format <options> [<paths>]`
This command checks for format violations on the given file or directory and print their names to stdout. + This command formats or checks formatting of Pkl files. +
It returns a non-zero status code in case violations are found. Exit codes:
* 0: No violations found.
* 1: An unexpected error happened (ex.: IO error)
* 11: Violations were found.
If the path is a directory, recursively looks for files with a `.pkl` extension, or files named `PklProject`. If the path is a directory, recursively looks for files with a `.pkl` extension, or files named `PklProject`.
By default, the input files are formatted, and written to standard out.
==== Options ==== Options
.--grammar-version .--grammar-version
@@ -753,29 +759,22 @@ Select the grammar compatibility version for the formatter.
New versions are created for each backward incompatible grammar change. New versions are created for each backward incompatible grammar change.
==== ====
[[command-format-apply]]
=== `pkl format apply`
*Synopsis*: `pkl format apply [<options>] <file-or-dir-path>`
This command formats the given files overwriting them.
If the path is a directory, recursively looks for files with a `.pkl` extension, or files named `PklProject`.
==== Options
.-s, --silent .-s, --silent
[%collapsible] [%collapsible]
==== ====
Do not write the name of wrongly formatted files to stdout. Skip writing to standard out. Mutually exclusive with `--diff-name-only`.
==== ====
.--grammar-version .-w, --write
[%collapsible] [%collapsible]
==== ====
Default: `2` (latest version) + Format files in place, overwriting them. Implies `--diff-name-only`.
Select the grammar compatibility version for the formatter. ====
New versions are created for each backward incompatible grammar change.
.--diff-name-only
[%collapsible]
====
Write the path of files with formatting violations to stdout.
==== ====
[[common-options]] [[common-options]]

View File

@@ -1,59 +0,0 @@
/*
* Copyright © 2025 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.cli
import java.io.IOException
import java.nio.file.Files
import java.nio.file.Path
import kotlin.io.path.writeText
import org.pkl.commons.cli.CliBaseOptions
import org.pkl.commons.cli.CliException
import org.pkl.formatter.GrammarVersion
class CliFormatterApply(
cliBaseOptions: CliBaseOptions,
paths: List<Path>,
grammarVersion: GrammarVersion,
private val silent: Boolean,
) : CliFormatterCommand(cliBaseOptions, paths, grammarVersion) {
override fun doRun() {
var status = 0
for (path in paths()) {
val contents = Files.readString(path)
val (formatted, stat) = format(path, contents)
status = if (status == 0) stat else status
if (stat != 0 || contents == formatted) continue
if (!silent) {
consoleWriter.write(path.toAbsolutePath().toString())
consoleWriter.appendLine()
consoleWriter.flush()
}
try {
path.writeText(formatted, Charsets.UTF_8)
} catch (e: IOException) {
consoleWriter.write("Could not overwrite `$path`: ${e.message}")
consoleWriter.appendLine()
consoleWriter.flush()
status = 1
}
}
if (status != 0) {
throw CliException("Formatting violations found.", status)
}
}
}

View File

@@ -1,48 +0,0 @@
/*
* Copyright © 2025 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.cli
import java.nio.file.Files
import java.nio.file.Path
import org.pkl.commons.cli.CliBaseOptions
import org.pkl.commons.cli.CliException
import org.pkl.formatter.GrammarVersion
class CliFormatterCheck(
cliBaseOptions: CliBaseOptions,
paths: List<Path>,
grammarVersion: GrammarVersion,
) : CliFormatterCommand(cliBaseOptions, paths, grammarVersion) {
override fun doRun() {
var status = 0
for (path in paths()) {
val contents = Files.readString(path)
val (formatted, stat) = format(path, contents)
status = if (status == 0) stat else status
if (contents != formatted) {
consoleWriter.write(path.toAbsolutePath().toString())
consoleWriter.appendLine()
consoleWriter.flush()
status = 1
}
}
if (status != 0) {
throw CliException("Formatting violations found.", status)
}
}
}

View File

@@ -15,48 +15,144 @@
*/ */
package org.pkl.cli package org.pkl.cli
import java.io.File
import java.io.IOException
import java.io.Writer import java.io.Writer
import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
import kotlin.io.path.ExperimentalPathApi import java.util.stream.Stream
import kotlin.io.path.extension import kotlin.io.path.extension
import kotlin.io.path.isDirectory import kotlin.io.path.isDirectory
import kotlin.io.path.name import kotlin.io.path.name
import kotlin.io.path.walk import kotlin.io.path.writeText
import kotlin.math.max
import org.pkl.commons.cli.CliBaseOptions import org.pkl.commons.cli.CliBaseOptions
import org.pkl.commons.cli.CliCommand import org.pkl.commons.cli.CliCommand
import org.pkl.commons.cli.CliTestException
import org.pkl.core.ModuleSource
import org.pkl.core.runtime.VmUtils
import org.pkl.core.util.IoUtils
import org.pkl.formatter.Formatter import org.pkl.formatter.Formatter
import org.pkl.formatter.GrammarVersion import org.pkl.formatter.GrammarVersion
import org.pkl.parser.GenericParserError import org.pkl.parser.GenericParserError
abstract class CliFormatterCommand class CliFormatterCommand
@JvmOverloads @JvmOverloads
constructor( constructor(
options: CliBaseOptions, private val paths: List<Path>,
protected val paths: List<Path>, private val grammarVersion: GrammarVersion,
protected val grammarVersion: GrammarVersion, private val overwrite: Boolean,
protected val consoleWriter: Writer = System.out.writer(), private val diffNameOnly: Boolean,
) : CliCommand(options) { private val silent: Boolean,
protected fun format(file: Path, contents: String): Pair<String, Int> { private val consoleWriter: Writer = System.out.writer(),
try { private val errWriter: Writer = System.err.writer(),
return Formatter().format(contents, grammarVersion) to 0 ) : CliCommand(CliBaseOptions()) {
} catch (pe: GenericParserError) {
consoleWriter.write("Could not format `$file`: $pe") private fun format(contents: String): String {
consoleWriter.appendLine() return Formatter().format(contents, grammarVersion)
consoleWriter.flush() }
return "" to 1
private fun writeErr(error: String) {
errWriter.write(error)
errWriter.appendLine()
errWriter.flush()
}
private fun write(message: String) {
if (silent) return
consoleWriter.write(message)
consoleWriter.appendLine()
consoleWriter.flush()
}
private fun allSources(): Stream<ModuleSource> {
return paths.distinct().stream().flatMap { path ->
when {
path.toString() == "-" -> Stream.of(ModuleSource.text(IoUtils.readString(System.`in`)))
path.isDirectory() ->
Files.walk(path)
.filter { it.extension == "pkl" || it.name == "PklProject" }
.map(ModuleSource::path)
else -> Stream.of(ModuleSource.path(path))
}
} }
} }
@OptIn(ExperimentalPathApi::class) override fun doRun() {
protected fun paths(): Set<Path> { val status = Status(SUCCESS)
val allPaths = mutableSetOf<Path>()
for (path in paths) { handleSources(status)
if (path.isDirectory()) {
allPaths.addAll(path.walk().filter { it.extension == "pkl" || it.name == "PklProject" }) when (status.status) {
} else { FORMATTING_VIOLATION -> {
allPaths.add(path) // using CliTestException instead of CliException because we want full control on how to
// print errors
throw CliTestException("", status.status)
}
ERROR -> {
if (!silent) {
writeErr("An error occurred during formatting.")
}
throw CliTestException("", status.status)
}
}
}
private fun handleSources(status: Status) {
for (source in allSources()) {
val path = if (source.uri == VmUtils.REPL_TEXT_URI) Path.of("-") else Path.of(source.uri)
try {
val contents =
if (source.contents != null) {
if (overwrite) {
writeErr("Cannot write to stdin.")
throw CliTestException("", ERROR)
}
source.contents!!
} else {
File(source.uri).readText()
}
val formatted = format(contents)
if (contents != formatted) {
status.update(FORMATTING_VIOLATION)
if (diffNameOnly || overwrite) {
// if `--diff-name-only` or `-w` is specified, only write file names
write(path.toAbsolutePath().toString())
}
if (overwrite) {
path.writeText(formatted, Charsets.UTF_8)
}
}
if (!diffNameOnly && !overwrite) {
write(formatted)
}
} catch (pe: GenericParserError) {
writeErr("Could not format `$path`: $pe")
status.update(ERROR)
} catch (e: IOException) {
writeErr("IO error while reading `$path`: ${e.message}")
status.update(ERROR)
}
}
}
companion object {
private const val SUCCESS = 0
private const val FORMATTING_VIOLATION = 11
private const val ERROR = 1
private class Status(var status: Int) {
fun update(newStatus: Int) {
status =
when {
status == ERROR -> status
newStatus == ERROR -> newStatus
else -> max(status, newStatus)
}
} }
} }
return allPaths
} }
} }

View File

@@ -15,9 +15,8 @@
*/ */
package org.pkl.cli.commands package org.pkl.cli.commands
import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.Context import com.github.ajalt.clikt.core.Context
import com.github.ajalt.clikt.core.NoOpCliktCommand
import com.github.ajalt.clikt.core.subcommands
import com.github.ajalt.clikt.parameters.arguments.argument import com.github.ajalt.clikt.parameters.arguments.argument
import com.github.ajalt.clikt.parameters.arguments.multiple import com.github.ajalt.clikt.parameters.arguments.multiple
import com.github.ajalt.clikt.parameters.options.default import com.github.ajalt.clikt.parameters.options.default
@@ -26,75 +25,75 @@ import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.types.enum import com.github.ajalt.clikt.parameters.types.enum
import com.github.ajalt.clikt.parameters.types.path import com.github.ajalt.clikt.parameters.types.path
import java.nio.file.Path import java.nio.file.Path
import org.pkl.cli.CliFormatterApply import org.pkl.cli.CliFormatterCommand
import org.pkl.cli.CliFormatterCheck
import org.pkl.cli.commands.FormatterCheckCommand.Companion.grammarVersionHelp
import org.pkl.commons.cli.commands.BaseCommand
import org.pkl.formatter.GrammarVersion import org.pkl.formatter.GrammarVersion
class FormatterCommand : NoOpCliktCommand(name = "format") { class FormatterCommand : CliktCommand(name = "format") {
override fun help(context: Context) = "Run commands related to formatting" override fun help(context: Context) =
"""
Format or check formatting of Pkl files.
Examples:
```
# Overwrite all Pkl files inside `my/folder/`, recursively.
$ pkl format -w my/folder/
# Check formatting of all files, printing filenames with formatting violations to stdout.
# Exit with exit code `11` if formatting violations were found.
$ pkl format --diff-name-only my/folder/
# Format Pkl code from stdin.
$ echo "foo = 1" | pkl format -
```
"""
.trimIndent()
override fun helpEpilog(context: Context) = "For more information, visit $helpLink" override fun helpEpilog(context: Context) = "For more information, visit $helpLink"
init {
subcommands(FormatterCheckCommand(), FormatterApplyCommand())
}
}
class FormatterCheckCommand : BaseCommand(name = "check", helpLink = helpLink) {
override val helpString: String =
"Check if the given files are properly formatted, printing the file name to stdout in case they are not. Returns non-zero in case of failure."
val paths: List<Path> by val paths: List<Path> by
argument(name = "paths", help = "Files or directory to check.") argument(name = "paths", help = "Files or directory to check. Use `-` to read from stdin.")
.path(mustExist = true, canBeDir = true) .path(mustExist = false, canBeDir = true)
.multiple() .multiple()
val grammarVersion: GrammarVersion by val grammarVersion: GrammarVersion by
option(names = arrayOf("--grammar-version"), help = grammarVersionHelp) option(
names = arrayOf("--grammar-version"),
help =
"""
The grammar compatibility version to use.$NEWLINE
${GrammarVersion.entries.joinToString("$NEWLINE", prefix = " ") {
val default = if (it == GrammarVersion.latest()) " `(default)`" else ""
"`${it.version}`: ${it.versionSpan}$default"
}}
"""
.trimIndent(),
)
.enum<GrammarVersion> { "${it.version}" } .enum<GrammarVersion> { "${it.version}" }
.default(GrammarVersion.latest()) .default(GrammarVersion.latest())
override fun run() { val overwrite: Boolean by
CliFormatterCheck(baseOptions.baseOptions(emptyList()), paths, grammarVersion).run() option(
} names = arrayOf("-w", "--write"),
help = "Format files in place, overwriting them. Implies `---diff-name-only`.",
)
.flag(default = false)
companion object { val diffNameOnly: Boolean by
internal val grammarVersionHelp = option(
""" names = arrayOf("--diff-name-only"),
The grammar compatibility version to use.$NEWLINE help = "Write the path of files with formatting violations to stdout.",
${GrammarVersion.entries.joinToString("$NEWLINE", prefix = " ") { )
val default = if (it == GrammarVersion.latest()) " `(default)`" else "" .flag(default = false)
"`${it.version}`: ${it.versionSpan}$default"
}}
"""
.trimIndent()
}
}
class FormatterApplyCommand : BaseCommand(name = "apply", helpLink = helpLink) {
override val helpString: String =
"Overwrite all the files in place with the formatted version. Returns non-zero in case of failure."
val paths: List<Path> by
argument(name = "paths", help = "Files or directory to format.")
.path(mustExist = true, canBeDir = true)
.multiple()
val silent: Boolean by val silent: Boolean by
option( option(
names = arrayOf("-s", "--silent"), names = arrayOf("-s", "--silent"),
help = "Do not write the name of the files that failed formatting to stdout.", help = "Don't write to stdout or stderr. Mutually exclusive with `--diff-name-only`.",
) )
.flag() .flag(default = false)
val grammarVersion: GrammarVersion by
option(names = arrayOf("--grammar-version"), help = grammarVersionHelp)
.enum<GrammarVersion> { "${it.version}" }
.default(GrammarVersion.latest())
override fun run() { override fun run() {
CliFormatterApply(baseOptions.baseOptions(emptyList()), paths, grammarVersion, silent).run() CliFormatterCommand(paths, grammarVersion, overwrite, diffNameOnly, silent).run()
} }
} }