Skip to main content

ktrs_cli/
ktrs.rs

1//! The native `ktrs` command. `ktrs fmt` maps its flags onto ktfmt's [`ParsedArgs`] and runs the
2//! same [`Main`], so output, messages and exit codes stay those of the `ktfmt` drop-in.
3
4use std::io;
5use std::path::Path;
6
7use ktrs_fmt::{FormattingOptions, GOOGLE_FORMAT, KOTLINLANG_FORMAT, META_FORMAT};
8
9use crate::ktfmt::{KTFMT_VERSION, Main, ParsedArgs, editor_config_resolver};
10
11const HELP: &str = "\
12ktrs - fast Kotlin tooling
13
14Usage:
15  ktrs fmt [OPTIONS] [PATH ...]    Format .kt/.kts files in place (default PATH: .)
16  ktrs fmt [OPTIONS] -             Format stdin to stdout
17  ktrs serve                       Format requests framed on stdin until it closes (for build tools;
18                                     protocol: crates/ktrs-cli/src/serve.rs)
19  ktrs --version
20
21Format options:
22  --style <meta|google|kotlinlang>  Code style (default: meta)
23  --check                           Don't write; list files that would change and exit 1 if any
24  --keep-unused-imports             Leave unused imports in place
25  --editorconfig                    Apply .editorconfig overrides (max_line_length, indent sizes,
26                                      ktfmt_trailing_comma_management_strategy); for stdin, at
27                                      --stdin-name
28  --stdin-name <name>               Name (path) of the stdin input, for messages and .editorconfig
29  -v, --verbose                     Report each formatted file
30
31A ktfmt-compatible `ktfmt` binary ships alongside, for existing scripts and integrations.";
32
33pub fn run(args: &[String]) -> i32 {
34    match args.first().map(String::as_str) {
35        Some("fmt") => match parse_fmt_args(&args[1..]) {
36            Ok(parsed) => Main::new(io::stdin(), io::stdout(), io::stderr()).run_parsed(&parsed),
37            Err(message) => {
38                eprintln!("error: {message}\n\n{HELP}");
39                2
40            }
41        },
42        Some("serve") if args.len() == 1 => crate::serve::run(io::stdin().lock(), io::stdout().lock()),
43        Some("--version" | "-V") => {
44            println!("ktrs {} (formats like ktfmt {KTFMT_VERSION})", env!("CARGO_PKG_VERSION"));
45            0
46        }
47        Some("help" | "--help" | "-h") => {
48            println!("{HELP}");
49            0
50        }
51        _ => {
52            eprintln!("{HELP}");
53            2
54        }
55    }
56}
57
58pub fn parse_fmt_args(args: &[String]) -> Result<ParsedArgs, String> {
59    let mut parsed = ParsedArgs {
60        file_names: Vec::new(),
61        formatting_options: META_FORMAT,
62        dry_run: false,
63        set_exit_if_changed: false,
64        stdin_name: None,
65        editor_config: false,
66        quiet: true,
67    };
68    let mut keep_unused_imports = false;
69    let mut args = args.iter();
70    while let Some(arg) = args.next() {
71        let (flag, inline_value) = match arg.split_once('=') {
72            Some((flag, value)) if arg.starts_with("--") => (flag, Some(value.to_owned())),
73            _ => (arg.as_str(), None),
74        };
75        let mut value = || inline_value.clone().or_else(|| args.next().cloned()).ok_or(format!("{flag} needs a value"));
76        match flag {
77            "--style" => parsed.formatting_options = style(&value()?)?,
78            "--check" => (parsed.dry_run, parsed.set_exit_if_changed) = (true, true),
79            "--keep-unused-imports" => keep_unused_imports = true,
80            "--editorconfig" => parsed.editor_config = true,
81            "--stdin-name" => parsed.stdin_name = Some(value()?),
82            "-v" | "--verbose" => parsed.quiet = false,
83            "-" => parsed.file_names.push(arg.clone()),
84            _ if flag.starts_with('-') => return Err(format!("unknown option {arg}")),
85            _ => parsed.file_names.push(arg.clone()),
86        }
87    }
88    parsed.formatting_options.remove_unused_imports = !keep_unused_imports;
89    if parsed.file_names.is_empty() {
90        parsed.file_names.push(".".to_owned());
91    }
92    let reads_stdin = parsed.file_names.iter().any(|f| f == "-");
93    if reads_stdin && parsed.file_names.len() > 1 {
94        return Err("cannot read from stdin and files in the same run".to_owned());
95    }
96    if parsed.stdin_name.is_some() && !reads_stdin {
97        return Err("--stdin-name can only be used when reading from stdin (-)".to_owned());
98    }
99    // Unlike ktfmt 0.64 (the `ktfmt` binary), resolve .editorconfig for stdin at --stdin-name, as
100    // ktfmt's next release will: editors format buffers through stdin.
101    if let (true, true, Some(name)) = (reads_stdin, parsed.editor_config, &parsed.stdin_name) {
102        parsed.formatting_options =
103            editor_config_resolver::resolve_formatting_options(Path::new(name), &parsed.formatting_options);
104    }
105    Ok(parsed)
106}
107
108pub(crate) fn style(name: &str) -> Result<FormattingOptions, String> {
109    match name {
110        "meta" => Ok(META_FORMAT),
111        "google" => Ok(GOOGLE_FORMAT),
112        "kotlinlang" => Ok(KOTLINLANG_FORMAT),
113        _ => Err(format!("unknown style '{name}' (expected meta, google or kotlinlang)")),
114    }
115}