nu_lint/
external_command.rs1use std::collections::HashMap;
2
3use nu_protocol::ast::Expr;
4
5pub use crate::lint::Fix;
7use crate::{context::LintContext, lint::RuleViolation};
8
9#[must_use]
11pub fn extract_external_args(
12 args: &[nu_protocol::ast::ExternalArgument],
13 context: &LintContext,
14) -> Vec<String> {
15 args.iter()
16 .map(|arg| match arg {
17 nu_protocol::ast::ExternalArgument::Regular(expr) => {
18 context.source[expr.span.start..expr.span.end].to_string()
19 }
20 nu_protocol::ast::ExternalArgument::Spread(expr) => {
21 format!("...{}", &context.source[expr.span.start..expr.span.end])
22 }
23 })
24 .collect()
25}
26
27pub struct BuiltinAlternative {
29 pub command: &'static str,
30 pub note: Option<&'static str>,
31}
32
33impl BuiltinAlternative {
34 #[must_use]
35 pub fn simple(command: &'static str) -> Self {
36 Self {
37 command,
38 note: None,
39 }
40 }
41
42 #[must_use]
43 pub fn with_note(command: &'static str, note: &'static str) -> Self {
44 Self {
45 command,
46 note: Some(note),
47 }
48 }
49}
50
51pub type FixBuilder = fn(
53 cmd_text: &str,
54 alternative: &BuiltinAlternative,
55 args: &[nu_protocol::ast::ExternalArgument],
56 expr_span: nu_protocol::Span,
57 context: &LintContext,
58) -> Fix;
59
60fn get_custom_suggestion(
62 cmd_text: &str,
63 args: &[nu_protocol::ast::ExternalArgument],
64 context: &LintContext,
65) -> Option<(String, String)> {
66 match cmd_text {
67 "tail" => {
68 let args_text = extract_external_args(args, context);
69 if args_text.iter().any(|arg| arg == "--pid") {
70 let message = "Consider using Nushell's structured approach for process \
71 monitoring instead of external 'tail --pid'"
72 .to_string();
73 let suggestion = "Replace 'tail --pid $pid -f /dev/null' with Nushell process \
74 monitoring:\nwhile (ps | where pid == $pid | length) > 0 { \
75 sleep 1s }\n\nThis approach uses Nushell's built-in ps command \
76 with structured data filtering and is more portable across \
77 platforms."
78 .to_string();
79 return Some((message, suggestion));
80 }
81 }
82 "hostname" => {
83 let args_text = extract_external_args(args, context);
84 if args_text.iter().any(|arg| arg == "-I") {
85 let message = "Consider using Nushell's structured approach for getting IP \
86 addresses instead of external 'hostname -I'"
87 .to_string();
88 let suggestion = "Replace 'hostname -I' with Nushell network commands:\nsys net | \
89 get ip\n\nThis approach uses Nushell's built-in sys net command \
90 to get IP addresses in a structured format. You can filter \
91 specific interfaces or addresses as needed."
92 .to_string();
93 return Some((message, suggestion));
94 }
95 }
96 _ => {}
97 }
98 None
99}
100
101#[must_use]
103pub fn detect_external_commands<S: ::std::hash::BuildHasher>(
104 context: &LintContext,
105 rule_id: &'static str,
106 alternatives: &HashMap<&'static str, BuiltinAlternative, S>,
107 fix_builder: Option<FixBuilder>,
108) -> Vec<RuleViolation> {
109 context.collect_rule_violations(|expr, ctx| {
110 if let Expr::ExternalCall(head, args) = &expr.expr {
111 let cmd_text = &ctx.source[head.span.start..head.span.end];
112
113 if let Some((custom_message, custom_suggestion)) =
115 get_custom_suggestion(cmd_text, args, ctx)
116 {
117 return vec![
118 RuleViolation::new_dynamic(rule_id, custom_message, expr.span)
119 .with_suggestion_dynamic(custom_suggestion),
120 ];
121 }
122
123 if let Some(alternative) = alternatives.get(cmd_text) {
125 let message = format!(
126 "Consider using Nushell's built-in '{}' instead of external '^{}'",
127 alternative.command, cmd_text
128 );
129
130 let suggestion = match alternative.note {
131 Some(note) => format!(
132 "Replace '^{}' with built-in command: {}\nBuilt-in commands are more \
133 portable, faster, and provide better error handling.\n\nNote: {note}",
134 cmd_text, alternative.command
135 ),
136 None => format!(
137 "Replace '^{}' with built-in command: {}\nBuilt-in commands are more \
138 portable, faster, and provide better error handling.",
139 cmd_text, alternative.command
140 ),
141 };
142
143 let fix =
144 fix_builder.map(|builder| builder(cmd_text, alternative, args, expr.span, ctx));
145
146 let violation = RuleViolation::new_dynamic(rule_id, message, expr.span)
147 .with_suggestion_dynamic(suggestion);
148
149 let violation = match fix {
150 Some(f) => violation.with_fix(f),
151 None => violation,
152 };
153
154 return vec![violation];
155 }
156 }
157 vec![]
158 })
159}