nu_command/filesystem/idx/
search.rs1use super::state::stream_grep;
2use fff_search::GrepMode;
3use nu_engine::command_prelude::*;
4use nu_protocol::Range;
5use std::ops::Bound;
6
7#[derive(Clone)]
8pub struct IdxSearch;
9
10impl Command for IdxSearch {
11 fn name(&self) -> &str {
12 "idx search"
13 }
14
15 fn signature(&self) -> Signature {
16 Signature::build(self.name())
17 .rest(
18 "pattern",
19 SyntaxShape::String,
20 "One or more search patterns.",
21 )
22 .switch("regex", "Use regular-expression matching mode.", Some('r'))
23 .switch("fuzzy", "Use fuzzy line-matching mode.", Some('f'))
24 .named(
25 "limit",
26 SyntaxShape::Int,
27 "Maximum number of matches to collect.",
28 Some('l'),
29 )
30 .named(
31 "context",
32 SyntaxShape::OneOf(vec![SyntaxShape::Range, SyntaxShape::Int]),
33 "The number of context lines to include before and after each match can be specified as an integer or a range. An integer sets both the before and after context to that number, while a range uses a negative value for lines before and a positive value for lines after (e.g., -3..5).",
34 Some('c'),
35 )
36 .input_output_types(vec![(Type::Nothing, Type::List(Box::new(Type::record())))])
37 .category(Category::FileSystem)
38 }
39
40 fn description(&self) -> &str {
41 "Search indexed file contents."
42 }
43
44 fn extra_description(&self) -> &str {
45 "Mode selection: plain text is the default and treats each pattern literally, `--regex` evaluates the patterns as regular expressions, and `--fuzzy` performs approximate line matching."
46 }
47
48 fn examples(&self) -> Vec<Example<'_>> {
49 vec![
50 Example {
51 description: "Search indexed file contents for a plain text pattern.",
52 example: "idx search hello",
53 result: None,
54 },
55 Example {
56 description: "Search using a regular expression.",
57 example: "idx search --regex 'fn \\w+'",
58 result: None,
59 },
60 Example {
61 description: "Search with multiple patterns simultaneously.",
62 example: "idx search TODO FIXME HACK",
63 result: None,
64 },
65 Example {
66 description: "Include 2 lines of context before and 5 lines after each match.",
67 example: "idx search error -2..5",
68 result: None,
69 },
70 Example {
71 description: "Brackets and question marks are treated as literal text, not glob patterns.",
72 example: "idx search 'arr[0]'",
73 result: None,
74 },
75 Example {
76 description: "Glob patterns with a path separator filter which files to search.",
77 example: "idx search pattern tests/*",
78 result: None,
79 },
80 Example {
81 description: "Brace expansion globs also filter which files to search.",
82 example: "idx search pattern *.{rs,js}",
83 result: None,
84 },
85 ]
86 }
87
88 fn run(
89 &self,
90 engine_state: &EngineState,
91 stack: &mut Stack,
92 call: &Call,
93 _input: PipelineData,
94 ) -> Result<PipelineData, ShellError> {
95 let patterns: Vec<String> = call.rest(engine_state, stack, 0)?;
96 if patterns.is_empty() {
97 return Err(ShellError::MissingParameter {
98 param_name: "pattern".to_string(),
99 span: call.head,
100 });
101 }
102
103 let regex = call.has_flag(engine_state, stack, "regex")?;
104 let fuzzy = call.has_flag(engine_state, stack, "fuzzy")?;
105
106 if regex && fuzzy {
107 return Err(ShellError::IncompatibleParameters {
108 left_message: "--regex cannot be used with --fuzzy".to_string(),
109 left_span: call.get_flag_span(stack, "regex").unwrap_or(call.head),
110 right_message: "--fuzzy cannot be used with --regex".to_string(),
111 right_span: call.get_flag_span(stack, "fuzzy").unwrap_or(call.head),
112 });
113 }
114
115 let limit = call
116 .get_flag::<i64>(engine_state, stack, "limit")?
117 .and_then(|v| usize::try_from(v).ok())
118 .unwrap_or(50);
119
120 let mode = if fuzzy {
121 GrepMode::Fuzzy
122 } else if regex {
123 GrepMode::Regex
124 } else {
125 GrepMode::PlainText
126 };
127
128 let context_param: Option<Value> = call.get_flag(engine_state, stack, "context")?;
129 let (before_context, after_context) = match context_param {
130 Some(Value::Int { val: i, .. }) if i < 0 => {
131 return Err(ShellError::UnsupportedInput {
132 msg: "Context must be specified as an or a range (e.g. -3..5). Negative value for before-context, positive value for after-context.".into(),
133 input: "value originates from here".into(),
134 msg_span: call.head,
135 input_span: call.head,
136 });
137 }
138 Some(Value::Int { val: n, .. }) => (n as usize, n as usize),
139 Some(Value::Range { val: range, .. }) => {
140 match *range {
142 Range::IntRange(r) => {
143 if r.step() != 1 {
145 return Err(ShellError::UnsupportedInput {
146 msg: "Context range must not have an explicit step (e.g. use -3..5, not -3..1..5)".into(),
147 input: "value originates from here".into(),
148 msg_span: call.head,
149 input_span: call.head,
150 });
151 }
152
153 let start = r.start();
154 if start > 0 {
155 return Err(ShellError::UnsupportedInput {
156 msg: "Context range start must be <= 0 (use a negative value for before-context, e.g. -3..5)".into(),
157 input: "value originates from here".into(),
158 msg_span: call.head,
159 input_span: call.head,
160 });
161 }
162
163 let end_val = match r.end() {
164 Bound::Included(e) | Bound::Excluded(e) => e,
165 Bound::Unbounded => {
166 return Err(ShellError::UnsupportedInput {
167 msg: "Context range must have a bounded end (use a positive value for after-context, e.g. -3..5)".into(),
168 input: "value originates from here".into(),
169 msg_span: call.head,
170 input_span: call.head,
171 });
172 }
173 };
174
175 if end_val < 0 {
176 return Err(ShellError::UnsupportedInput {
177 msg: "Context range end must be >= 0 (use a positive value for after-context, e.g. -3..5)".into(),
178 input: "value originates from here".into(),
179 msg_span: call.head,
180 input_span: call.head,
181 });
182 }
183
184 let before = start.unsigned_abs() as usize;
185 let after = end_val as usize;
186 (before, after)
187 }
188 Range::FloatRange(_) => {
189 return Err(ShellError::UnsupportedInput {
190 msg: "Float ranges are not supported for context".into(),
191 input: "value originates from here".into(),
192 msg_span: call.head,
193 input_span: call.head,
194 });
195 }
196 }
197 }
198 Some(other) => {
199 return Err(ShellError::UnsupportedInput {
200 msg: format!(
201 "Context must be an integer or range, but got {}",
202 other.get_type()
203 ),
204 input: "value originates from here".into(),
205 msg_span: call.head,
206 input_span: call.head,
207 });
208 }
209 None => (0usize, 0usize),
210 };
211
212 let signals = engine_state.signals();
213 let cwd = engine_state.cwd(Some(stack))?.into_std_path_buf();
214 let ctx = super::state::GrepSearchContext {
215 patterns: &patterns,
216 mode,
217 page_limit: limit,
218 span: call.head,
219 before_context,
220 after_context,
221 cwd: Some(cwd.as_path()),
222 signals,
223 };
224 stream_grep(ctx)
225 }
226}