nu_command/filesystem/idx/
search.rs1use super::state::{GrepSearchContext, 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 --context -2..5 error",
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 .map(|value| {
118 usize::try_from(value)
119 .map_err(|_| ShellError::NeedsPositiveValue { span: call.head })
120 })
121 .transpose()?
122 .unwrap_or(50);
123
124 let mode = if fuzzy {
125 GrepMode::Fuzzy
126 } else if regex {
127 GrepMode::Regex
128 } else {
129 GrepMode::PlainText
130 };
131
132 let (before_context, after_context) = parse_context(
133 call.get_flag::<Value>(engine_state, stack, "context")?,
134 call.head,
135 )?;
136
137 let cwd = engine_state.cwd(Some(stack))?.into_std_path_buf();
138 stream_grep(GrepSearchContext {
139 patterns: &patterns,
140 mode,
141 page_limit: limit,
142 span: call.head,
143 before_context,
144 after_context,
145 cwd: Some(cwd.as_path()),
146 signals: engine_state.signals(),
147 })
148 }
149}
150
151fn parse_context(value: Option<Value>, span: Span) -> Result<(usize, usize), ShellError> {
153 let unsupported = |msg| ShellError::UnsupportedInput {
154 msg,
155 input: "value originates from here".into(),
156 msg_span: span,
157 input_span: span,
158 };
159
160 let Some(value) = value else {
161 return Ok((0, 0));
162 };
163
164 match value {
165 Value::Int { val, .. } => {
166 let count = usize::try_from(val).map_err(|_| {
167 unsupported("Context must be non-negative, or use a range such as -3..5".into())
168 })?;
169 Ok((count, count))
170 }
171 Value::Range { val, .. } => match *val {
172 Range::IntRange(range) => {
173 if range.step() != 1 {
174 return Err(unsupported(
175 "Context range must not have an explicit step (e.g. use -3..5, not -3..1..5)".into(),
176 ));
177 }
178
179 let start = range.start();
180 if start > 0 {
181 return Err(unsupported(
182 "Context range start must be <= 0 (use a negative value for before-context, e.g. -3..5)".into(),
183 ));
184 }
185
186 let end = match range.end() {
187 Bound::Included(end) | Bound::Excluded(end) => end,
188 Bound::Unbounded => {
189 return Err(unsupported(
190 "Context range must have a bounded end (use a positive value for after-context, e.g. -3..5)".into(),
191 ));
192 }
193 };
194 if end < 0 {
195 return Err(unsupported(
196 "Context range end must be >= 0 (use a positive value for after-context, e.g. -3..5)".into(),
197 ));
198 }
199
200 let before = usize::try_from(start.unsigned_abs())
201 .map_err(|_| unsupported("Context range start is too large".into()))?;
202 let after = usize::try_from(end)
203 .map_err(|_| unsupported("Context range end is too large".into()))?;
204 Ok((before, after))
205 }
206 Range::FloatRange(_) => Err(unsupported(
207 "Float ranges are not supported for context".into(),
208 )),
209 },
210 other => Err(unsupported(format!(
211 "Context must be an integer or range, but got {}",
212 other.get_type()
213 ))),
214 }
215}