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; `ktrs lint`
3//! does the same over the `ktlint` drop-in (`crate::ktrs_lint`); `ktrs ktlint` is that drop-in itself.
4
5use std::io;
6use std::path::Path;
7
8use ktrs_fmt::{FormattingOptions, GOOGLE_FORMAT, KOTLINLANG_FORMAT, META_FORMAT};
9
10use crate::ktfmt::{KTFMT_VERSION, Main, ParsedArgs, editor_config_resolver};
11
12const HELP_TEMPLATE: &str = "\
13ktrs - fast Kotlin tooling
14
15Usage:
16  ktrs fmt [OPTIONS] [PATH ...]    Format .kt/.kts files in place (default PATH: .)
17  ktrs fmt [OPTIONS] -             Format stdin to stdout
18  ktrs lint [OPTIONS] [PATH ...]   Check .kt/.kts files with ktlint's rules (default PATH: .); exit 1
19                                     on violations. Rules are being ported: `ktrs lint --list-rules`
20  ktrs lint [OPTIONS] -            Check stdin (with --format: fixed code to stdout)
21  ktrs serve                      Format requests framed on stdin until it closes (for build tools;
22                                     protocol: crates/ktrs-cli/src/serve.rs)
23  ktrs lsp                        Language server on stdin/stdout: ktlint diagnostics, fixes and
24                                     suppressions, ktfmt or ktlint formatting (settings:
25                                     crates/ktrs-lsp/src/lib.rs)
26  ktrs ktlint [ARGS ...]          Exactly the `ktlint` drop-in, flags and exit codes as ktlint's CLI
27                                     (for build tools that bundle only `ktrs`)
28  ktrs migrate [--write] [PATH]   Switch the build's ktfmt/ktlint plugins to the ktrs drop-ins: a diff,
29                                     exit 1 if any (`ktrs migrate --help`)
30  ktrs --version
31
32Format options:
33  --style <meta|google|kotlinlang>  Code style (default: meta)
34  --check                           Don't write; list files that would change and exit 1 if any
35  --keep-unused-imports             Leave unused imports in place
36  --editorconfig                    Apply .editorconfig overrides (max_line_length, indent sizes,
37                                      ktfmt_trailing_comma_management_strategy); for stdin, at
38                                      --stdin-name
39  --stdin-name <name>               Name (path) of the stdin input, for messages and .editorconfig
40  -v, --verbose                     Report each formatted file
41
42{LINT_HELP}
43
44`ktfmt` and `ktlint` binaries with those tools' exact flags ship alongside, for existing scripts
45and integrations.";
46
47fn help() -> String {
48    HELP_TEMPLATE.replace("{LINT_HELP}", crate::ktrs_lint::HELP)
49}
50
51pub fn run(args: &[String]) -> i32 {
52    match args.first().map(String::as_str) {
53        Some("fmt") => match parse_fmt_args(&args[1..]) {
54            Ok(parsed) => {
55                ktrs_syntax::caught_panic::silence_caught_panics();
56                Main::new(io::stdin(), io::stdout(), io::stderr()).run_parsed(&parsed)
57            }
58            Err(message) => {
59                eprintln!("error: {message}\n\n{}", help());
60                2
61            }
62        },
63        Some("lint") => crate::ktrs_lint::run(&args[1..]).unwrap_or_else(|message| {
64            eprintln!("error: {message}\n\n{}", help());
65            2
66        }),
67        // No `java_launcher` wildcard expansion: callers pass literal paths, not a shell's command line.
68        Some("ktlint") => crate::ktlint::main(&args[1..]),
69        Some("migrate") => crate::migrate::run(&args[1..], &mut io::stdout().lock(), &mut io::stderr().lock()),
70        Some("serve") if args.len() == 1 => crate::serve::run(io::stdin().lock(), io::stdout().lock()),
71        // `--stdio` is what VS Code's client and others pass; stdio is the only transport.
72        Some("lsp") if args[1..].iter().all(|a| a == "--stdio") => ktrs_lsp::run_stdio(),
73        Some("--version" | "-V") => {
74            println!("ktrs {} (formats like ktfmt {KTFMT_VERSION})", env!("CARGO_PKG_VERSION"));
75            0
76        }
77        Some("help" | "--help" | "-h") => {
78            println!("{}", help());
79            0
80        }
81        _ => {
82            eprintln!("{}", help());
83            2
84        }
85    }
86}
87
88pub fn parse_fmt_args(args: &[String]) -> Result<ParsedArgs, String> {
89    let mut parsed = ParsedArgs {
90        file_names: Vec::new(),
91        formatting_options: META_FORMAT,
92        dry_run: false,
93        set_exit_if_changed: false,
94        stdin_name: None,
95        editor_config: false,
96        quiet: true,
97    };
98    let mut keep_unused_imports = false;
99    let mut args = args.iter();
100    while let Some(arg) = args.next() {
101        let (flag, inline_value) = match arg.split_once('=') {
102            Some((flag, value)) if arg.starts_with("--") => (flag, Some(value.to_owned())),
103            _ => (arg.as_str(), None),
104        };
105        let mut value = || inline_value.clone().or_else(|| args.next().cloned()).ok_or(format!("{flag} needs a value"));
106        match flag {
107            "--style" => parsed.formatting_options = style(&value()?)?,
108            "--check" => (parsed.dry_run, parsed.set_exit_if_changed) = (true, true),
109            "--keep-unused-imports" => keep_unused_imports = true,
110            "--editorconfig" => parsed.editor_config = true,
111            "--stdin-name" => parsed.stdin_name = Some(value()?),
112            "-v" | "--verbose" => parsed.quiet = false,
113            "-" => parsed.file_names.push(arg.clone()),
114            _ if flag.starts_with('-') => return Err(format!("unknown option {arg}")),
115            _ => parsed.file_names.push(arg.clone()),
116        }
117    }
118    parsed.formatting_options.remove_unused_imports = !keep_unused_imports;
119    if parsed.file_names.is_empty() {
120        parsed.file_names.push(".".to_owned());
121    }
122    let reads_stdin = parsed.file_names.iter().any(|f| f == "-");
123    if reads_stdin && parsed.file_names.len() > 1 {
124        return Err("cannot read from stdin and files in the same run".to_owned());
125    }
126    if parsed.stdin_name.is_some() && !reads_stdin {
127        return Err("--stdin-name can only be used when reading from stdin (-)".to_owned());
128    }
129    // Unlike ktfmt 0.64 (the `ktfmt` binary), resolve .editorconfig for stdin at --stdin-name, as
130    // ktfmt's next release will: editors format buffers through stdin.
131    if let (true, true, Some(name)) = (reads_stdin, parsed.editor_config, &parsed.stdin_name) {
132        parsed.formatting_options =
133            editor_config_resolver::resolve_formatting_options(Path::new(name), &parsed.formatting_options);
134    }
135    Ok(parsed)
136}
137
138pub(crate) fn style(name: &str) -> Result<FormattingOptions, String> {
139    match name {
140        "meta" => Ok(META_FORMAT),
141        "google" => Ok(GOOGLE_FORMAT),
142        "kotlinlang" => Ok(KOTLINLANG_FORMAT),
143        _ => Err(format!("unknown style '{name}' (expected meta, google or kotlinlang)")),
144    }
145}