Skip to main content

loq_cli/
lib.rs

1//! Command-line interface for loq.
2//!
3//! Provides the main entry point and CLI argument handling for the loq tool.
4
5#![forbid(unsafe_code)]
6#![warn(missing_docs)]
7
8mod baseline;
9mod baseline_shared;
10mod check;
11mod cli;
12mod config_edit;
13mod init;
14mod output;
15mod relax;
16mod tighten;
17
18use std::ffi::OsString;
19use std::io::{self, Read, Write};
20use std::process::ExitCode;
21
22use clap::Parser;
23use termcolor::{ColorChoice, StandardStream, WriteColor};
24
25use baseline::run_baseline;
26use check::{output_mode, run_check};
27use init::run_init;
28use relax::run_relax;
29use tighten::run_tighten;
30
31pub use cli::{Cli, Command};
32
33/// Exit status for the CLI.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum ExitStatus {
36    /// All checks passed.
37    Success,
38    /// Violations found (errors).
39    Failure,
40    /// Runtime error occurred.
41    Error,
42}
43
44impl From<ExitStatus> for ExitCode {
45    fn from(status: ExitStatus) -> Self {
46        match status {
47            ExitStatus::Success => Self::from(0),
48            ExitStatus::Failure => Self::from(1),
49            ExitStatus::Error => Self::from(2),
50        }
51    }
52}
53
54/// Runs the CLI using environment args and stdio.
55#[must_use]
56pub fn run_env() -> ExitStatus {
57    let args = std::env::args_os();
58    let stdin = io::stdin();
59    let mut stdout = StandardStream::stdout(ColorChoice::Auto);
60    let mut stderr = StandardStream::stderr(ColorChoice::Auto);
61    run_with(args, stdin.lock(), &mut stdout, &mut stderr)
62}
63
64fn normalize_args<I>(args: I) -> Vec<OsString>
65where
66    I: IntoIterator<Item = OsString>,
67{
68    let mut iter = args.into_iter();
69    let mut normalized = Vec::new();
70
71    if let Some(program) = iter.next() {
72        normalized.push(program);
73    }
74
75    let mut subcommand_seen = false;
76    let mut in_check = false;
77    let mut rewrite_enabled = true;
78
79    for arg in iter {
80        if arg.as_os_str() == "--" {
81            rewrite_enabled = false;
82            normalized.push(arg);
83            continue;
84        }
85
86        let arg_str = arg.to_string_lossy();
87        if !subcommand_seen {
88            if arg_str == "check" {
89                subcommand_seen = true;
90                in_check = true;
91            } else if !arg_str.starts_with('-') {
92                subcommand_seen = true;
93            }
94            normalized.push(arg);
95            continue;
96        }
97
98        if rewrite_enabled && in_check && arg.as_os_str() == "-" {
99            normalized.push(OsString::from("--stdin"));
100        } else {
101            normalized.push(arg);
102        }
103    }
104
105    normalized
106}
107
108/// Runs the CLI with custom args and streams (for testing).
109pub fn run_with<I, R, W1, W2>(args: I, mut stdin: R, stdout: &mut W1, stderr: &mut W2) -> ExitStatus
110where
111    I: IntoIterator<Item = OsString>,
112    R: Read,
113    W1: WriteColor + Write,
114    W2: WriteColor,
115{
116    let cli = Cli::parse_from(normalize_args(args));
117    let mode = output_mode(&cli);
118
119    let default_check = Command::Check(cli::CheckArgs {
120        paths: vec![],
121        stdin: false,
122        staged: false,
123        diff: None,
124        no_cache: false,
125        output_format: cli::OutputFormat::Text,
126    });
127    match cli.command.as_ref().unwrap_or(&default_check) {
128        Command::Check(args) => run_check(args, &mut stdin, stdout, stderr, mode),
129        Command::Init(args) => run_init(args, stdout, stderr),
130        Command::Baseline(args) => run_baseline(args, stdout, stderr),
131        Command::Tighten(args) => run_tighten(args, stdout, stderr),
132        Command::Relax(args) => run_relax(args, stdout, stderr),
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139    use std::path::PathBuf;
140
141    fn check_command_details(cli: Cli) -> Option<(bool, Vec<PathBuf>)> {
142        if let Some(Command::Check(check)) = cli.command {
143            Some((check.stdin, check.paths))
144        } else {
145            None
146        }
147    }
148
149    #[test]
150    fn exit_status_to_exit_code() {
151        assert_eq!(ExitCode::from(ExitStatus::Success), ExitCode::from(0));
152        assert_eq!(ExitCode::from(ExitStatus::Failure), ExitCode::from(1));
153        assert_eq!(ExitCode::from(ExitStatus::Error), ExitCode::from(2));
154    }
155
156    #[test]
157    fn normalize_args_converts_stdin_dash_for_check() {
158        let args = vec!["loq", "check", "-", "src"]
159            .into_iter()
160            .map(OsString::from)
161            .collect::<Vec<_>>();
162        let normalized = normalize_args(args);
163        let cli = Cli::parse_from(normalized);
164
165        let details = check_command_details(cli);
166        assert_eq!(details, Some((true, vec![PathBuf::from("src")])));
167    }
168
169    #[test]
170    fn normalize_args_preserves_literal_dash_after_double_dash() {
171        let args = vec!["loq", "check", "--", "-"]
172            .into_iter()
173            .map(OsString::from)
174            .collect::<Vec<_>>();
175        let normalized = normalize_args(args);
176        let cli = Cli::parse_from(normalized);
177
178        let details = check_command_details(cli);
179        assert_eq!(details, Some((false, vec![PathBuf::from("-")])));
180    }
181
182    #[test]
183    fn normalize_args_converts_stdin_dash_with_global_flags() {
184        let args = vec!["loq", "--verbose", "check", "-"]
185            .into_iter()
186            .map(OsString::from)
187            .collect::<Vec<_>>();
188        let normalized = normalize_args(args);
189        let cli = Cli::parse_from(normalized);
190
191        let details = check_command_details(cli);
192        assert_eq!(details, Some((true, Vec::new())));
193    }
194
195    #[test]
196    fn check_command_details_returns_none_for_non_check_commands() {
197        let cli = Cli::parse_from(["loq", "init"]);
198        assert_eq!(check_command_details(cli), None);
199    }
200}