diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a78f14c..d5451235 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,148 @@ This name should be decided amongst the team before the release. * [#535](https://github.com/tweag/topiary/pull/535) Improved error message when idempotency fails due to invalid output in the first pass. * [#533](https://github.com/tweag/topiary/pull/533) Update tree-sitter-ocaml to 0.20.3 * [#576](https://github.com/tweag/topiary/pull/576) Allows prepending/appending `@begin_scope` and `@end_scope` +* [#583](https://github.com/tweag/topiary/pull/583) Modernisation of the command line interface (see [below](#cli-migration-guide), for details) + +#### CLI Migration Guide + +Full documentation for the CLI can be found in the project's +[`README`](/README.md). Herein we summarise how the v0.2.3 functionality +maps to the new interface, to aid migration. + +##### Formatting + +###### From Files, In Place + +Before: +``` +topiary [--skip-idempotence] \ + [--tolerate-parsing-errors] \ + --in-place \ + --input-files INPUT_FILES... +``` + +After: +``` +topiary fmt [--skip-idempotence] \ + [--tolerate-parsing-errors] \ + INPUT_FILES... +``` + +###### From File, To New File + +Before: +``` +topiary [--skip-idempotence] \ + [--tolerate-parsing-errors] \ + (--langauge LANGUAGE | --query QUERY) \ + --input-files INPUT_FILE \ + --output-file OUTPUT_FILE +``` + +After (use IO redirection): +``` +topiary [--skip-idempotence] \ + [--tolerate-parsing-errors] \ + (--langauge LANGUAGE | --query QUERY) \ + < INPUT_FILE \ + > OUTPUT_FILE +``` + +###### Involving Standard Input and Output + +Before: +``` +topariy [--skip-idempotence] \ + [--tolerate-parsing-errors] \ + (--langauge LANGUAGE | --query QUERY) \ + (--input-files - | < INPUT_FILE) \ + [--output-file -] +``` + +After (use IO redirection): +``` +topiary [--skip-idempotence] \ + [--tolerate-parsing-errors] \ + (--langauge LANGUAGE | --query QUERY) \ + < INPUT_FILE +``` + +##### Visualisation + +###### From File + +Before: +``` +topiary --visualise[=FORMAT] \ + --input-files INPUT_FILE \ + [--output-file OUTPUT_FILE | > OUTPUT_FILE] +``` + +After: +``` +topiary vis [--tolerate-parsing-errors] \ + [--format FORMAT] \ + INPUT_FILE \ + [> OUTPUT_FILE] +``` + +###### Involving Standard Input and Output + +Before: +``` +topiary --visualise[=FORMAT] \ + (--langauge LANGUAGE | --query QUERY) \ + < INPUT_FILE \ + [--output-file OUTPUT_FILE | > OUTPUT_FILE] +``` + +After (use IO redirection): +``` +topiary vis [--tolerate-parsing-errors] \ + [--format FORMAT] \ + (--langauge LANGUAGE | --query QUERY) \ + < INPUT_FILE \ + [> OUTPUT_FILE] +``` + +##### Configuration + +###### Custom Configuration + +To replicate the behaviour of v0.2.3, set the configuration collation +mode to `revise`. This can be done with the `TOPIARY_CONFIG_COLLATION` +environment variable, or the `--configuration-collation` argument. + +The new default collation method is `merge`, which is subtly different +when it comes to collating collections. + +###### Overriding Configuration + +Before (or using the `TOPIARY_CONFIGURATION_OVERRIDE` environment +variable): +``` +topiary --configuration-override CONFIG_FILE ... +``` + +After (or using a combination of `TOPIARY_CONFIG_FILE` and +`TOPIARY_CONFIG_COLLATION` environment variables): +``` +topiary --configuration CONFIG_FILE \ + --configuration-collation override \ + ... +``` + +###### Examining Computed Configuration + +Before (to standard error, then proceeding with other functions): +``` +topiary --output-configuration ... +``` + +After (to standard output, as a dedicated function): +``` +topiary cfg +``` ## v0.2.3 - Cyclic Cypress - 2023-06-20 diff --git a/README.md b/README.md index 01811200..d0d6f42b 100644 --- a/README.md +++ b/README.md @@ -127,7 +127,7 @@ set it to `/languages`, for example: ```console export TOPIARY_LANGUAGE_DIR=/home/me/tools/topiary/languages -topiary -i -f ./projects/helloworld/hello.ml +topiary fmt ./projects/helloworld/hello.ml ``` `TOPIARY_LANGUAGE_DIR` can alternatively be set at build time. Topiary will pick @@ -159,44 +159,206 @@ pre-commit-check = nix-pre-commit-hooks.run { ### Usage +The Topiary CLI uses a number of subcommands to delineate functionality. +These can be listed with `topiary --help`; each subcommand then has its +own, dedicated help text. + + + ``` CLI app for Topiary, the universal code formatter. -Usage: topiary [OPTIONS] <--language |--input-files [...]> +Usage: topiary [OPTIONS] + +Commands: + fmt Format inputs + vis Visualise the input's Tree-sitter parse tree + cfg Print the current configuration + help Print this message or the help of the given subcommand(s) Options: - -l, --language - Which language to parse and format [possible values: json, nickel, ocaml, ocaml-interface, ocamllex, toml] - -f, --input-files [...] - Path to an input file or multiple input files. If omitted, or equal to "-", read from standard input. If multiple files are provided, `in_place` is assumed [default: -] - -q, --query - Which query file to use - -o, --output-file - Path to an output file. If omitted, or equal to "-", write to standard output - -i, --in-place - Format the input files in place - -v, --visualise[=] - Visualise the syntax tree, rather than format [possible values: json, dot] + -C, --configuration + Configuration file + + [env: TOPIARY_CONFIG_FILE] + + --configuration-collation + Configuration collation mode + + [env: TOPIARY_CONFIG_COLLATION] + [default: merge] + + Possible values: + - merge: When multiple sources of configuration are available, matching items are updated from + the higher priority source, with collections merged as the union of sets + - revise: When multiple sources of configuration are available, matching items (including + collections) are superseded from the higher priority source + - override: When multiple sources of configuration are available, the highest priority source is + taken. All values from lower priority sources are discarded + + -h, --help + Print help (see a summary with '-h') + + -V, --version + Print version +``` + + +#### Format + + + +``` +Format inputs + +Usage: topiary fmt [OPTIONS] <--language |--query |FILES> + +Arguments: + [FILES]... + Input files and directories (omit to read from stdin) + +Options: + -t, --tolerate-parsing-errors + Consume as much as possible in the presence of parsing errors + -s, --skip-idempotence Do not check that formatting twice gives the same output - --output-configuration - Output the full configuration to stderr before continuing + + -l, --language + Topiary supported language (for formatting stdin) + + [possible values: json, nickel, ocaml, ocaml-interface, ocamllex, toml] + + -q, --query + Topiary query file (for formatting stdin) + + -C, --configuration + Configuration file + + [env: TOPIARY_CONFIG_FILE] + + --configuration-collation + Configuration collation mode + + [env: TOPIARY_CONFIG_COLLATION] + [default: merge] + + Possible values: + - merge: When multiple sources of configuration are available, matching items are updated from + the higher priority source, with collections merged as the union of sets + - revise: When multiple sources of configuration are available, matching items (including + collections) are superseded from the higher priority source + - override: When multiple sources of configuration are available, the highest priority source is + taken. All values from lower priority sources are discarded + + -h, --help + Print help (see a summary with '-h') +``` + + +When formatting inputs from disk, language selection is detected from +the input files' extensions. To format standard input, you must specify +either `--language` or `--query` arguments, omitting any input files. + +#### Visualise + + + +``` +Visualise the input's Tree-sitter parse tree + +Usage: topiary vis [OPTIONS] <--language |--query |FILE> + +Arguments: + [FILE] + Input file (omit to read from stdin) + +Options: -t, --tolerate-parsing-errors - Format as much as possible even if some of the input causes parsing errors - --configuration-override - Override all configuration with the provided file [env: TOPIARY_CONFIGURATION_OVERRIDE=] - -c, --configuration-file - Add the specified configuration file with the highest prority [env: TOPIARY_CONFIGURATION_FILE=] + Consume as much as possible in the presence of parsing errors + + -f, --format + Visualisation format + + [default: dot] + + Possible values: + - dot: GraphViz DOT serialisation + - json: JSON serialisation + + -l, --language + Topiary supported language (for formatting stdin) + + [possible values: json, nickel, ocaml, ocaml-interface, ocamllex, toml] + + -q, --query + Topiary query file (for formatting stdin) + + -C, --configuration + Configuration file + + [env: TOPIARY_CONFIG_FILE] + + --configuration-collation + Configuration collation mode + + [env: TOPIARY_CONFIG_COLLATION] + [default: merge] + + Possible values: + - merge: When multiple sources of configuration are available, matching items are updated from + the higher priority source, with collections merged as the union of sets + - revise: When multiple sources of configuration are available, matching items (including + collections) are superseded from the higher priority source + - override: When multiple sources of configuration are available, the highest priority source is + taken. All values from lower priority sources are discarded + -h, --help - Print help - -V, --version - Print version + Print help (see a summary with '-h') ``` + + +When visualising inputs from disk, language selection is detected from +the input file's extension. To visualise standard input, you must +specify either `--language` or `--query` arguments, omitting the input +file. The visualisation output is written to standard out. -Language selection is based on precedence, in the following order: -* A specified language -* Detected from the input file's extension -* A specified query file +#### Configuration + + + +``` +Print the current configuration + +Usage: topiary cfg [OPTIONS] + +Options: + -C, --configuration + Configuration file + + [env: TOPIARY_CONFIG_FILE] + + --configuration-collation + Configuration collation mode + + [env: TOPIARY_CONFIG_COLLATION] + [default: merge] + + Possible values: + - merge: When multiple sources of configuration are available, matching items are updated from + the higher priority source, with collections merged as the union of sets + - revise: When multiple sources of configuration are available, matching items (including + collections) are superseded from the higher priority source + - override: When multiple sources of configuration are available, the highest priority source is + taken. All values from lower priority sources are discarded + + -h, --help + Print help (see a summary with '-h') +``` + + +Please refer to the [Configuration](#configuration-1) section below to +understand the different sources of configuration and collation modes. #### Exit Codes @@ -204,7 +366,7 @@ The Topiary process will exit with a zero exit code upon successful formatting. Otherwise, the following exit codes are defined: | Reason | Code | -| :--------------------------- | ---- | +| :--------------------------- | ---: | | Unspecified error | 1 | | CLI argument parsing error | 2 | | I/O error | 3 | @@ -219,15 +381,15 @@ formatting. Otherwise, the following exit codes are defined: Once built, the program can be run like this: ```bash -echo '{"foo":"bar"}' | topiary --language json +echo '{"foo":"bar"}' | topiary fmt --language json ``` `topiary` can also be built and run from source via either Cargo or Nix, if you have those installed: ```bash -echo '{"foo":"bar"}' | cargo run -- --language json -echo '{"foo":"bar"}' | nix run . -- --language json +echo '{"foo":"bar"}' | cargo run -- fmt --language json +echo '{"foo":"bar"}' | nix run . -- fmt --language json ``` It will output the following formatted code: @@ -240,40 +402,53 @@ Set the `RUST_LOG=debug` environment variable if you want to enable debug logging. ## Configuration -Topiary is configured using `languages.toml` files. There are three -locations where Topiary checks for such a file. -### Locations +Topiary is configured using `languages.toml` files. There are up to four +sources where Topiary checks for such a file. + +### Configuration Sources + At buildtime the [languages.toml](./languages.toml) in the root of -this repository is included into Topiary. This file is parsed at +this repository is embedded into Topiary. This file is parsed at runtime. The purpose of this `languages.toml` file is to provide sane defaults for users of Topiary (both the library and the binary). -The other two are read by the Topiary binary at runtime and allow the user to +The next two are read by the Topiary binary at runtime and allow the user to configure Topiary to their needs. The first is intended to be user specific, and can thus be found in the configuration directory of the OS: -``` -Unix: /home/alice/.config/topiary/languages.toml -Windows: C:\Users\Alice\AppData\Roaming\Topiary\config\languages.toml -MacOS: /Users/Alice/Library/Application Support/Topiary/languages.toml -``` + +| OS | Typical Configuration Path | +| :------ | :---------------------------------------------------------------- | +| Unix | `/home/alice/.config/topiary/languages.toml` | +| Windows | `C:\Users\Alice\AppData\Roaming\Topiary\config\languages.toml` | +| macOS | `/Users/Alice/Library/Application Support/Topiary/languages.toml` | + This file is not automatically created by Topiary. -The last location is intended to be a project-specific settings file for -Topiary. When running Topiary in some directory, it will look up in the file -tree until it finds a .topiary directory. It will then read the `languages.toml` +The next source is intended to be a project-specific settings file for +Topiary. When running Topiary in some directory, it will ascend the file +tree until it finds a `.topiary` directory. It will then read any `languages.toml` file present in that directory. -The Topiary binary parses these file in the following order, any configuration -options defined earlier are overwritten by those defined later. +Finally, an explicit configuration file may be specified using the +`-C`/`--configuration` command line argument (or the +`TOPIARY_CONFIG_FILE` environment variable). This is intended for +driving Topiary under very specific use-cases. -1. The builtin configuration file -2. The user configuration file in the OS's configuration directory -3. The project specific topiary configuration +The Topiary binary parses these sources in the following order. The +action taken to coalesce matching items is dependent on the [collation +mode](#configuration-collation). + +1. The builtin configuration file. +2. The user configuration file in the OS's configuration directory. +3. The project specific Topiary configuration. +4. The explicit configuration file specified as a CLI argument. ### Configuration Options + The configuration file contains a list of languages, each language configuration headed by ``[[language]]``. For instance, the one for Nickel is defined as such: + ```toml [[language]] name = "nickel" @@ -281,7 +456,7 @@ extensions = ["ncl"] ``` The `name` field is used by Topiary to associate the language entry with the -query file and tree-sitter grammar. This field should be written lowercase. +query file and Tree-sitter grammar. This value should be written in lowercase. The `name` field is mandatory for every ``[[language]]`` block in every configuration file. @@ -290,10 +465,95 @@ need to exist in every configuration file. It is sufficient if, for every language, there is a single configuration file that defines the list of extensions for that language. -A final optional field called `indent` exists to define the indentation method +A final optional field, called `indent`, exists to define the indentation method for that language. Topiary defaults to two spaces `" "` if it cannot find the indent field in any configuration file for a specific language. +### Configuration Collation + +When parsing configuration from multiple sources, Topiary can collate +matching configuration items (matched on language name) in various ways. +The collation mode is set by the `--configuration-collation` command +line argument (or the `TOPIARY_CONFIG_COLLATION` environment variable). + +The different modes are best explained by example. Consider the +following two configurations, in priority order from lowest to highest +(comments have been added for illustrative purposes): + +```toml +# Lowest priority configuration + +[[language]] +name = "example" +extensions = ["eg"] + +[[language]] +name = "demo" +extensions = ["demo"] +``` + +```toml +# Highest priority configuration + +[[language]] +name = "example" +extensions = ["example"] +indent = " " +``` + +#### Merge Mode (Default) + +Matching items are updated from the higher priority source, with +collections merged as the union of sets. + +```toml +# For the "example" language: +# * The collated extensions is the union of the source extensions +# * The indentation is taken from the highest priority source +[[language]] +name = "example" +extensions = ["eg", "example"] +indent = " " + +# The "demo" language is unchanged +[[language]] +name = "demo" +extensions = ["demo"] +``` + +#### Revise Mode + +Matching items (including collections) are superseded from the higher +priority source. + +```toml +# The "example" language's values are taken from the highest priority source +[[language]] +name = "example" +extensions = ["example"] +indent = " " + +# The "demo" language is unchanged +[[language]] +name = "demo" +extensions = ["demo"] +``` + +#### Override Mode + +The highest priority source is taken. All values from lower priority +sources are discarded. + +```toml +# The "example" language's values are taken from the highest priority source +[[language]] +name = "example" +extensions = ["example"] +indent = " " + +# The "demo" language does not exist in the highest priority source, so is omitted +``` + ## Design As long as there is a [Tree-sitter grammar][tree-sitter-parsers] defined diff --git a/verify-documented-usage.sh b/verify-documented-usage.sh index de1f17a2..5de3836e 100755 --- a/verify-documented-usage.sh +++ b/verify-documented-usage.sh @@ -1,17 +1,56 @@ #!/usr/bin/env bash -usage="$(nix run . -- --help)" - -echo "$usage" | -{ - while IFS= read -r line - do - if ! grep -Fxq "$line" README.md - then - echo "Usage is not correctly documented in README.md. Update the file with the following:" - echo "$usage" - exit 1 - fi - done - - echo "Usage is correctly documented in README.md." + +set -euo pipefail + +readonly FENCE='```' + +get-cli-usage() { + # Get the help text from the CLI + local subcommand="${1-ROOT}" + + case "${subcommand}" in + "ROOT") nix run . -- --help;; + *) nix run . -- "${subcommand}" --help;; + esac } + +get-readme-usage() { + # Get the help text from the README + local subcommand="${1-ROOT}" + + sed --quiet " + /usage:start:${subcommand}/, /usage:end:${subcommand}/ { + //d # Delete the markers (last pattern) + /${FENCE}/d # Delete the code fences + p # Print anything else + } + " README.md +} + +diff-usage() { + # Generate a diff between the README and CLI help text + local subcommand="${1-ROOT}" + + diff --text \ + --ignore-all-space \ + <(get-readme-usage "${subcommand}") \ + <(get-cli-usage "${subcommand}") +} + +main() { + local -a subcommands=(ROOT fmt vis cfg) + + local _diff + local _subcommand + for _subcommand in "${subcommands[@]}"; do + if ! _diff=$(diff-usage "${_subcommand}"); then + >&2 echo "Usage is not correctly documented in README.md for the ${_subcommand} subcommand!" + echo "${_diff}" + exit 1 + fi + done + + >&2 echo "Usage is correctly documented in README.md" +} + +main