Skip to main content

rippy_cli/
list.rs

1//! Implementation of `rippy list` subcommands.
2
3use std::collections::BTreeSet;
4use std::path::PathBuf;
5use std::process::ExitCode;
6
7use crate::cli::{ListArgs, ListTarget};
8use crate::error::RippyError;
9use crate::{allowlists, handlers, inspect};
10
11/// Entry point for `rippy list`.
12///
13/// # Errors
14///
15/// Returns `RippyError` if config loading fails for the `rules` subcommand.
16pub fn run(args: &ListArgs) -> Result<ExitCode, RippyError> {
17    match &args.target {
18        ListTarget::Safe => list_safe(),
19        ListTarget::Handlers => list_handlers(),
20        ListTarget::Rules(rules_args) => list_rules(rules_args.filter.as_deref())?,
21    }
22    Ok(ExitCode::SUCCESS)
23}
24
25fn list_safe() {
26    let safe = allowlists::all_simple_safe();
27    println!("Safe commands (auto-approved):");
28    print_columns(&safe);
29    println!("  ({} commands)\n", safe.len());
30
31    let wrappers = allowlists::all_wrappers();
32    println!("Wrapper commands (pass through to inner command):");
33    print_columns(&wrappers);
34    println!("  ({} commands)", wrappers.len());
35}
36
37fn list_handlers() {
38    let all_cmds = handlers::all_handler_commands();
39    let mut groups: BTreeSet<Vec<&str>> = BTreeSet::new();
40
41    for cmd in &all_cmds {
42        if let Some(handler) = handlers::get_handler(cmd) {
43            groups.insert(handler.commands().to_vec());
44        }
45    }
46
47    println!("Handler commands:");
48    for cmds in &groups {
49        let joined = cmds.join(", ");
50        println!("  {joined}");
51    }
52    println!(
53        "\n  ({} commands across {} handlers)",
54        all_cmds.len(),
55        groups.len()
56    );
57}
58
59fn list_rules(filter: Option<&str>) -> Result<(), RippyError> {
60    let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
61    let output = inspect::collect_list_data(&cwd, None)?;
62
63    println!("Rules:\n");
64
65    for source in &output.config_sources {
66        let rules: Vec<_> = source
67            .rules
68            .iter()
69            .filter(|r| matches_filter(r, filter))
70            .collect();
71        if rules.is_empty() {
72            continue;
73        }
74        println!("  {}:", source.path);
75        for rule in &rules {
76            let msg = rule
77                .message
78                .as_ref()
79                .map_or(String::new(), |m| format!("  \"{m}\""));
80            println!("    {:<6} {}{msg}", rule.action, rule.pattern);
81        }
82        println!();
83    }
84
85    for source in &output.cc_sources {
86        let rules: Vec<_> = source
87            .rules
88            .iter()
89            .filter(|r| matches_filter(r, filter))
90            .collect();
91        if rules.is_empty() {
92            continue;
93        }
94        println!("  {}:", source.path);
95        for rule in &rules {
96            println!("    {:<6} {}", rule.action, rule.pattern);
97        }
98        println!();
99    }
100    Ok(())
101}
102
103fn matches_filter(rule: &inspect::RuleDisplay, filter: Option<&str>) -> bool {
104    let Some(f) = filter else { return true };
105    rule.pattern.contains(f) || rule.action.contains(f)
106}
107
108/// Print items in multi-column layout, 6 per row.
109fn print_columns(items: &[&str]) {
110    for chunk in items.chunks(6) {
111        let row: Vec<String> = chunk.iter().map(|s| format!("{s:<14}")).collect();
112        println!("  {}", row.join(""));
113    }
114}
115
116#[cfg(test)]
117#[allow(clippy::unwrap_used)]
118mod tests {
119    use super::*;
120
121    #[test]
122    fn safe_list_is_sorted_and_nonempty() {
123        let safe = allowlists::all_simple_safe();
124        assert!(!safe.is_empty());
125        let mut sorted = safe.clone();
126        sorted.sort_unstable();
127        assert_eq!(safe, sorted);
128    }
129
130    #[test]
131    fn wrapper_list_is_sorted_and_nonempty() {
132        let wrappers = allowlists::all_wrappers();
133        assert!(!wrappers.is_empty());
134        let mut sorted = wrappers.clone();
135        sorted.sort_unstable();
136        assert_eq!(wrappers, sorted);
137    }
138
139    #[test]
140    fn handler_commands_is_sorted_and_nonempty() {
141        let cmds = handlers::all_handler_commands();
142        assert!(!cmds.is_empty());
143        let mut sorted = cmds.clone();
144        sorted.sort_unstable();
145        assert_eq!(cmds, sorted);
146    }
147
148    #[test]
149    fn filter_matches_pattern() {
150        let rule = inspect::RuleDisplay {
151            action: "allow".into(),
152            pattern: "git status".into(),
153            message: None,
154        };
155        assert!(matches_filter(&rule, Some("git")));
156        assert!(!matches_filter(&rule, Some("docker")));
157        assert!(matches_filter(&rule, None));
158    }
159
160    #[test]
161    fn filter_matches_action() {
162        let rule = inspect::RuleDisplay {
163            action: "deny".into(),
164            pattern: "rm -rf".into(),
165            message: None,
166        };
167        assert!(matches_filter(&rule, Some("deny")));
168        assert!(!matches_filter(&rule, Some("allow")));
169    }
170}