1use 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 ktlint [ARGS ...] Exactly the `ktlint` drop-in, flags and exit codes as ktlint's CLI
24 (for build tools that bundle only `ktrs`)
25 ktrs --version
26
27Format options:
28 --style <meta|google|kotlinlang> Code style (default: meta)
29 --check Don't write; list files that would change and exit 1 if any
30 --keep-unused-imports Leave unused imports in place
31 --editorconfig Apply .editorconfig overrides (max_line_length, indent sizes,
32 ktfmt_trailing_comma_management_strategy); for stdin, at
33 --stdin-name
34 --stdin-name <name> Name (path) of the stdin input, for messages and .editorconfig
35 -v, --verbose Report each formatted file
36
37{LINT_HELP}
38
39`ktfmt` and `ktlint` binaries with those tools' exact flags ship alongside, for existing scripts
40and integrations.";
41
42fn help() -> String {
43 HELP_TEMPLATE.replace("{LINT_HELP}", crate::ktrs_lint::HELP)
44}
45
46pub fn run(args: &[String]) -> i32 {
47 match args.first().map(String::as_str) {
48 Some("fmt") => match parse_fmt_args(&args[1..]) {
49 Ok(parsed) => {
50 ktrs_syntax::caught_panic::silence_caught_panics();
51 Main::new(io::stdin(), io::stdout(), io::stderr()).run_parsed(&parsed)
52 }
53 Err(message) => {
54 eprintln!("error: {message}\n\n{}", help());
55 2
56 }
57 },
58 Some("lint") => crate::ktrs_lint::run(&args[1..]).unwrap_or_else(|message| {
59 eprintln!("error: {message}\n\n{}", help());
60 2
61 }),
62 Some("ktlint") => crate::ktlint::main(&args[1..]),
64 Some("serve") if args.len() == 1 => crate::serve::run(io::stdin().lock(), io::stdout().lock()),
65 Some("--version" | "-V") => {
66 println!("ktrs {} (formats like ktfmt {KTFMT_VERSION})", env!("CARGO_PKG_VERSION"));
67 0
68 }
69 Some("help" | "--help" | "-h") => {
70 println!("{}", help());
71 0
72 }
73 _ => {
74 eprintln!("{}", help());
75 2
76 }
77 }
78}
79
80pub fn parse_fmt_args(args: &[String]) -> Result<ParsedArgs, String> {
81 let mut parsed = ParsedArgs {
82 file_names: Vec::new(),
83 formatting_options: META_FORMAT,
84 dry_run: false,
85 set_exit_if_changed: false,
86 stdin_name: None,
87 editor_config: false,
88 quiet: true,
89 };
90 let mut keep_unused_imports = false;
91 let mut args = args.iter();
92 while let Some(arg) = args.next() {
93 let (flag, inline_value) = match arg.split_once('=') {
94 Some((flag, value)) if arg.starts_with("--") => (flag, Some(value.to_owned())),
95 _ => (arg.as_str(), None),
96 };
97 let mut value = || inline_value.clone().or_else(|| args.next().cloned()).ok_or(format!("{flag} needs a value"));
98 match flag {
99 "--style" => parsed.formatting_options = style(&value()?)?,
100 "--check" => (parsed.dry_run, parsed.set_exit_if_changed) = (true, true),
101 "--keep-unused-imports" => keep_unused_imports = true,
102 "--editorconfig" => parsed.editor_config = true,
103 "--stdin-name" => parsed.stdin_name = Some(value()?),
104 "-v" | "--verbose" => parsed.quiet = false,
105 "-" => parsed.file_names.push(arg.clone()),
106 _ if flag.starts_with('-') => return Err(format!("unknown option {arg}")),
107 _ => parsed.file_names.push(arg.clone()),
108 }
109 }
110 parsed.formatting_options.remove_unused_imports = !keep_unused_imports;
111 if parsed.file_names.is_empty() {
112 parsed.file_names.push(".".to_owned());
113 }
114 let reads_stdin = parsed.file_names.iter().any(|f| f == "-");
115 if reads_stdin && parsed.file_names.len() > 1 {
116 return Err("cannot read from stdin and files in the same run".to_owned());
117 }
118 if parsed.stdin_name.is_some() && !reads_stdin {
119 return Err("--stdin-name can only be used when reading from stdin (-)".to_owned());
120 }
121 if let (true, true, Some(name)) = (reads_stdin, parsed.editor_config, &parsed.stdin_name) {
124 parsed.formatting_options =
125 editor_config_resolver::resolve_formatting_options(Path::new(name), &parsed.formatting_options);
126 }
127 Ok(parsed)
128}
129
130pub(crate) fn style(name: &str) -> Result<FormattingOptions, String> {
131 match name {
132 "meta" => Ok(META_FORMAT),
133 "google" => Ok(GOOGLE_FORMAT),
134 "kotlinlang" => Ok(KOTLINLANG_FORMAT),
135 _ => Err(format!("unknown style '{name}' (expected meta, google or kotlinlang)")),
136 }
137}