1use std::ops::Bound;
2
3use crate::{grapheme_flags, grapheme_flags_const};
4use nu_cmd_base::input_handler::{CmdArgument, operate};
5use nu_engine::command_prelude::*;
6use nu_protocol::{IntRange, engine::StateWorkingSet};
7use unicode_segmentation::UnicodeSegmentation;
8
9struct Arguments {
10 end: bool,
11 substring: String,
12 range: Option<Spanned<IntRange>>,
13 cell_paths: Option<Vec<CellPath>>,
14 graphemes: bool,
15}
16
17impl CmdArgument for Arguments {
18 fn take_cell_paths(&mut self) -> Option<Vec<CellPath>> {
19 self.cell_paths.take()
20 }
21}
22
23#[derive(Clone)]
24pub struct StrIndexOf;
25
26impl Command for StrIndexOf {
27 fn name(&self) -> &str {
28 "str index-of"
29 }
30
31 fn signature(&self) -> Signature {
32 Signature::build("str index-of")
33 .input_output_types(vec![
34 (Type::String, Type::Int),
35 (Type::List(Box::new(Type::String)), Type::List(Box::new(Type::Int))),
36 (Type::table(), Type::table()),
37 (Type::record(), Type::record()),
38 ])
39 .allow_variants_without_examples(true)
40 .required("string", SyntaxShape::String, "The string to find in the input.")
41 .switch(
42 "grapheme-clusters",
43 "count indexes using grapheme clusters (all visible chars have length 1)",
44 Some('g'),
45 )
46 .switch(
47 "utf-8-bytes",
48 "count indexes using UTF-8 bytes (default; non-ASCII chars have length 2+)",
49 Some('b'),
50 )
51 .rest(
52 "rest",
53 SyntaxShape::CellPath,
54 "For a data structure input, search strings at the given cell paths, and replace with result.",
55 )
56 .named(
57 "range",
58 SyntaxShape::Range,
59 "optional start and/or end index",
60 Some('r'),
61 )
62 .switch("end", "search from the end of the input", Some('e'))
63 .category(Category::Strings)
64 }
65
66 fn description(&self) -> &str {
67 "Returns start index of first occurrence of string in input, or -1 if no match."
68 }
69
70 fn search_terms(&self) -> Vec<&str> {
71 vec!["match", "find", "search"]
72 }
73
74 fn is_const(&self) -> bool {
75 true
76 }
77
78 fn run(
79 &self,
80 engine_state: &EngineState,
81 stack: &mut Stack,
82 call: &Call,
83 input: PipelineData,
84 ) -> Result<PipelineData, ShellError> {
85 let substring: Spanned<String> = call.req(engine_state, stack, 0)?;
86 let cell_paths: Vec<CellPath> = call.rest(engine_state, stack, 1)?;
87 let cell_paths = (!cell_paths.is_empty()).then_some(cell_paths);
88 let args = Arguments {
89 substring: substring.item,
90 range: call.get_flag(engine_state, stack, "range")?,
91 end: call.has_flag(engine_state, stack, "end")?,
92 cell_paths,
93 graphemes: grapheme_flags(engine_state, stack, call)?,
94 };
95 operate(action, args, input, call.head, engine_state.signals())
96 }
97
98 fn run_const(
99 &self,
100 working_set: &StateWorkingSet,
101 call: &Call,
102 input: PipelineData,
103 ) -> Result<PipelineData, ShellError> {
104 let substring: Spanned<String> = call.req_const(working_set, 0)?;
105 let cell_paths: Vec<CellPath> = call.rest_const(working_set, 1)?;
106 let cell_paths = (!cell_paths.is_empty()).then_some(cell_paths);
107 let args = Arguments {
108 substring: substring.item,
109 range: call.get_flag_const(working_set, "range")?,
110 end: call.has_flag_const(working_set, "end")?,
111 cell_paths,
112 graphemes: grapheme_flags_const(working_set, call)?,
113 };
114 operate(
115 action,
116 args,
117 input,
118 call.head,
119 working_set.permanent().signals(),
120 )
121 }
122
123 fn examples(&self) -> Vec<Example<'_>> {
124 vec![
125 Example {
126 description: "Returns index of string in input",
127 example: " 'my_library.rb' | str index-of '.rb'",
128 result: Some(Value::test_int(10)),
129 },
130 Example {
131 description: "Count length using grapheme clusters",
132 example: "'๐ฏ๐ตใปใ ใตใ ใดใ' | str index-of --grapheme-clusters 'ใตใ'",
133 result: Some(Value::test_int(4)),
134 },
135 Example {
136 description: "Returns index of string in input within a`rhs open range`",
137 example: " '.rb.rb' | str index-of '.rb' --range 1..",
138 result: Some(Value::test_int(3)),
139 },
140 Example {
141 description: "Returns index of string in input within a lhs open range",
142 example: " '123456' | str index-of '6' --range ..4",
143 result: Some(Value::test_int(-1)),
144 },
145 Example {
146 description: "Returns index of string in input within a range",
147 example: " '123456' | str index-of '3' --range 1..4",
148 result: Some(Value::test_int(2)),
149 },
150 Example {
151 description: "Returns index of string in input",
152 example: " '/this/is/some/path/file.txt' | str index-of '/' -e",
153 result: Some(Value::test_int(18)),
154 },
155 ]
156 }
157}
158
159fn action(
160 input: &Value,
161 Arguments {
162 substring,
163 range,
164 end,
165 graphemes,
166 ..
167 }: &Arguments,
168 head: Span,
169) -> Value {
170 match input {
171 Value::String { val: s, .. } => {
172 let (search_str, start_index) = if let Some(spanned_range) = range {
173 let range_span = spanned_range.span;
174 let range = &spanned_range.item;
175
176 let (start, end) = range.absolute_bounds(s.len());
177 let s = match end {
178 Bound::Excluded(end) => s.get(start..end),
179 Bound::Included(end) => s.get(start..=end),
180 Bound::Unbounded => s.get(start..),
181 };
182
183 let s = match s {
184 Some(s) => s,
185 None => {
186 return Value::error(
187 ShellError::OutOfBounds {
188 left_flank: start.to_string(),
189 right_flank: match range.end() {
190 Bound::Unbounded => "".to_string(),
191 Bound::Included(end) => format!("={end}"),
192 Bound::Excluded(end) => format!("<{end}"),
193 },
194 span: range_span,
195 },
196 head,
197 );
198 }
199 };
200 (s, start)
201 } else {
202 (s.as_str(), 0)
203 };
204
205 if let Some(result) = if *end {
207 search_str.rfind(&**substring)
208 } else {
209 search_str.find(&**substring)
210 } {
211 let result = result + start_index;
212 Value::int(
213 if *graphemes {
214 s.grapheme_indices(true)
218 .enumerate()
219 .find(|e| e.1.0 >= result)
220 .expect("No grapheme index for substring")
221 .0
222 } else {
223 result
224 } as i64,
225 head,
226 )
227 } else {
228 Value::int(-1, head)
229 }
230 }
231 Value::Error { .. } => input.clone(),
232 _ => Value::error(
233 ShellError::OnlySupportsThisInputType {
234 exp_input_type: "string".into(),
235 wrong_type: input.get_type().to_string(),
236 dst_span: head,
237 src_span: input.span(),
238 },
239 head,
240 ),
241 }
242}
243
244#[cfg(test)]
245mod tests {
246 use nu_protocol::ast::RangeInclusion;
247
248 use super::*;
249 use super::{Arguments, StrIndexOf, action};
250
251 #[test]
252 fn test_examples() {
253 use crate::test_examples;
254
255 test_examples(StrIndexOf {})
256 }
257
258 #[test]
259 fn returns_index_of_substring() {
260 let word = Value::test_string("Cargo.tomL");
261
262 let options = Arguments {
263 substring: String::from(".tomL"),
264 range: None,
265 cell_paths: None,
266 end: false,
267 graphemes: false,
268 };
269
270 let actual = action(&word, &options, Span::test_data());
271
272 assert_eq!(actual, Value::test_int(5));
273 }
274 #[test]
275 fn index_of_does_not_exist_in_string() {
276 let word = Value::test_string("Cargo.tomL");
277
278 let options = Arguments {
279 substring: String::from("Lm"),
280 range: None,
281 cell_paths: None,
282 end: false,
283 graphemes: false,
284 };
285
286 let actual = action(&word, &options, Span::test_data());
287
288 assert_eq!(actual, Value::test_int(-1));
289 }
290
291 #[test]
292 fn returns_index_of_next_substring() {
293 let word = Value::test_string("Cargo.Cargo");
294 let range = IntRange::new(
295 Value::int(1, Span::test_data()),
296 Value::nothing(Span::test_data()),
297 Value::nothing(Span::test_data()),
298 RangeInclusion::Inclusive,
299 Span::test_data(),
300 )
301 .expect("valid range");
302
303 let spanned_range = Spanned {
304 item: range,
305 span: Span::test_data(),
306 };
307
308 let options = Arguments {
309 substring: String::from("Cargo"),
310
311 range: Some(spanned_range),
312 cell_paths: None,
313 end: false,
314 graphemes: false,
315 };
316
317 let actual = action(&word, &options, Span::test_data());
318 assert_eq!(actual, Value::test_int(6));
319 }
320
321 #[test]
322 fn index_does_not_exist_due_to_end_index() {
323 let word = Value::test_string("Cargo.Banana");
324 let range = IntRange::new(
325 Value::nothing(Span::test_data()),
326 Value::nothing(Span::test_data()),
327 Value::int(5, Span::test_data()),
328 RangeInclusion::Inclusive,
329 Span::test_data(),
330 )
331 .expect("valid range");
332
333 let spanned_range = Spanned {
334 item: range,
335 span: Span::test_data(),
336 };
337
338 let options = Arguments {
339 substring: String::from("Banana"),
340
341 range: Some(spanned_range),
342 cell_paths: None,
343 end: false,
344 graphemes: false,
345 };
346
347 let actual = action(&word, &options, Span::test_data());
348 assert_eq!(actual, Value::test_int(-1));
349 }
350
351 #[test]
352 fn returns_index_of_nums_in_middle_due_to_index_limit_from_both_ends() {
353 let word = Value::test_string("123123123");
354 let range = IntRange::new(
355 Value::int(2, Span::test_data()),
356 Value::nothing(Span::test_data()),
357 Value::int(6, Span::test_data()),
358 RangeInclusion::Inclusive,
359 Span::test_data(),
360 )
361 .expect("valid range");
362
363 let spanned_range = Spanned {
364 item: range,
365 span: Span::test_data(),
366 };
367
368 let options = Arguments {
369 substring: String::from("123"),
370
371 range: Some(spanned_range),
372 cell_paths: None,
373 end: false,
374 graphemes: false,
375 };
376
377 let actual = action(&word, &options, Span::test_data());
378 assert_eq!(actual, Value::test_int(3));
379 }
380
381 #[test]
382 fn index_does_not_exists_due_to_strict_bounds() {
383 let word = Value::test_string("123456");
384 let range = IntRange::new(
385 Value::int(2, Span::test_data()),
386 Value::nothing(Span::test_data()),
387 Value::int(5, Span::test_data()),
388 RangeInclusion::RightExclusive,
389 Span::test_data(),
390 )
391 .expect("valid range");
392
393 let spanned_range = Spanned {
394 item: range,
395 span: Span::test_data(),
396 };
397
398 let options = Arguments {
399 substring: String::from("1"),
400
401 range: Some(spanned_range),
402 cell_paths: None,
403 end: false,
404 graphemes: false,
405 };
406
407 let actual = action(&word, &options, Span::test_data());
408 assert_eq!(actual, Value::test_int(-1));
409 }
410
411 #[test]
412 fn use_utf8_bytes() {
413 let word = Value::string(String::from("๐ฏ๐ตใปใ ใตใ ใดใ"), Span::test_data());
414
415 let options = Arguments {
416 substring: String::from("ใตใ"),
417 range: None,
418 cell_paths: None,
419 end: false,
420 graphemes: false,
421 };
422
423 let actual = action(&word, &options, Span::test_data());
424 assert_eq!(actual, Value::test_int(15));
425 }
426
427 #[test]
428 fn index_is_not_a_char_boundary() {
429 let word = Value::string(String::from("๐"), Span::test_data());
430
431 let range = IntRange::new(
432 Value::int(0, Span::test_data()),
433 Value::int(1, Span::test_data()),
434 Value::int(2, Span::test_data()),
435 RangeInclusion::Inclusive,
436 Span::test_data(),
437 )
438 .expect("valid range");
439
440 let spanned_range = Spanned {
441 item: range,
442 span: Span::test_data(),
443 };
444
445 let options = Arguments {
446 substring: String::new(),
447
448 range: Some(spanned_range),
449 cell_paths: None,
450 end: false,
451 graphemes: false,
452 };
453
454 let actual = action(&word, &options, Span::test_data());
455 assert!(actual.is_error());
456 }
457
458 #[test]
459 fn index_is_out_of_bounds() {
460 let word = Value::string(String::from("hello"), Span::test_data());
461
462 let range = IntRange::new(
463 Value::int(-1, Span::test_data()),
464 Value::int(1, Span::test_data()),
465 Value::int(3, Span::test_data()),
466 RangeInclusion::Inclusive,
467 Span::test_data(),
468 )
469 .expect("valid range");
470
471 let spanned_range = Spanned {
472 item: range,
473 span: Span::test_data(),
474 };
475
476 let options = Arguments {
477 substring: String::from("h"),
478
479 range: Some(spanned_range),
480 cell_paths: None,
481 end: false,
482 graphemes: false,
483 };
484
485 let actual = action(&word, &options, Span::test_data());
486 assert_eq!(actual, Value::test_int(-1));
487 }
488}