dynamic_cli/plugin/builtin/
env.rs1use crate::context::ExecutionContext;
28use crate::executor::CommandHandler;
29use crate::parser::ParsedArgs;
30use crate::plugin::Plugin;
31use crate::Result;
32
33const SENSITIVE_NAME_SUBSTRINGS: &[&str] = &[
36 "SECRET",
37 "TOKEN",
38 "KEY",
39 "PASS",
40 "CREDENTIAL",
41 "AUTH",
42 "PRIVATE",
43];
44
45fn is_sensitive_name(name: &str) -> bool {
48 let upper = name.to_uppercase();
49 SENSITIVE_NAME_SUBSTRINGS
50 .iter()
51 .any(|needle| upper.contains(needle))
52}
53
54pub struct EnvPlugin;
87
88impl EnvPlugin {
89 pub fn new() -> Self {
100 Self
101 }
102}
103
104impl Default for EnvPlugin {
105 fn default() -> Self {
106 Self::new()
107 }
108}
109
110impl Plugin for EnvPlugin {
111 fn name(&self) -> &str {
112 "env"
113 }
114
115 fn version(&self) -> &str {
116 env!("CARGO_PKG_VERSION")
117 }
118
119 fn description(&self) -> &str {
120 "Filtered environment variable display (feature-gated, #46)"
121 }
122
123 fn handlers(&self) -> Vec<(String, Box<dyn CommandHandler>)> {
124 vec![("env_show".to_string(), Box::new(EnvShowHandler))]
125 }
126}
127
128struct EnvShowHandler;
131
132impl CommandHandler for EnvShowHandler {
133 fn execute(&self, _ctx: &mut dyn ExecutionContext, _args: &ParsedArgs) -> Result<()> {
134 let mut vars: Vec<(String, String)> = std::env::vars().collect();
135 vars.sort_by(|a, b| a.0.cmp(&b.0));
136
137 let mut hidden_count = 0usize;
138
139 for (name, value) in &vars {
140 if is_sensitive_name(name) {
141 hidden_count += 1;
142 println!("{name} = <hidden>");
143 } else {
144 println!("{name} = {value}");
145 }
146 }
147
148 if hidden_count > 0 {
149 println!("\n{hidden_count} variable(s) hidden (name looked sensitive)");
150 }
151
152 Ok(())
153 }
154}
155
156#[cfg(test)]
161mod tests {
162 use super::*;
163 use std::any::Any;
164
165 #[derive(Default)]
166 struct TestContext;
167
168 impl ExecutionContext for TestContext {
169 fn as_any(&self) -> &dyn Any {
170 self
171 }
172 fn as_any_mut(&mut self) -> &mut dyn Any {
173 self
174 }
175 }
176
177 #[test]
178 fn test_env_plugin_metadata() {
179 let p = EnvPlugin::new();
180 assert_eq!(p.name(), "env");
181 assert!(!p.version().is_empty());
182 assert!(!p.description().is_empty());
183 }
184
185 #[test]
186 fn test_env_plugin_default() {
187 let p = EnvPlugin::default();
188 assert_eq!(p.name(), "env");
189 }
190
191 #[test]
192 fn test_env_plugin_handler_name() {
193 let handlers = EnvPlugin::new().handlers();
194 assert_eq!(handlers.len(), 1);
195 assert_eq!(handlers[0].0, "env_show");
196 }
197
198 #[test]
199 fn test_env_handler_executes() {
200 let handlers = EnvPlugin::new().handlers();
201 let (_, handler) = &handlers[0];
202 let mut ctx = TestContext;
203 assert!(handler
204 .execute(&mut ctx, &ParsedArgs::from_scalars(Default::default()))
205 .is_ok());
206 }
207
208 #[test]
209 fn test_env_plugin_is_send_sync() {
210 fn assert_send_sync<T: Send + Sync>(_: T) {}
211 assert_send_sync(EnvPlugin::new());
212 }
213
214 #[test]
215 fn test_is_sensitive_name_matches_expected_patterns() {
216 assert!(is_sensitive_name("API_SECRET"));
217 assert!(is_sensitive_name("AWS_ACCESS_KEY_ID"));
218 assert!(is_sensitive_name("DATABASE_PASSWORD"));
219 assert!(is_sensitive_name("GITHUB_TOKEN"));
220 assert!(is_sensitive_name("MY_APP_CREDENTIAL"));
221 assert!(is_sensitive_name("BASIC_AUTH_HEADER"));
222 assert!(is_sensitive_name("SSH_PRIVATE_KEY_PATH"));
223 assert!(is_sensitive_name("api_secret"));
225 assert!(is_sensitive_name("Api_Secret"));
226 }
227
228 #[test]
229 fn test_is_sensitive_name_leaves_ordinary_vars_alone() {
230 assert!(!is_sensitive_name("PATH"));
231 assert!(!is_sensitive_name("HOME"));
232 assert!(!is_sensitive_name("LANG"));
233 assert!(!is_sensitive_name("EDITOR"));
234 assert!(!is_sensitive_name("RUST_LOG"));
235 }
236}