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() -> nu_test_support::Result {
253 nu_test_support::test().examples(StrIndexOf)
254 }
255
256 #[test]
257 fn returns_index_of_substring() {
258 let word = Value::test_string("Cargo.tomL");
259
260 let options = Arguments {
261 substring: String::from(".tomL"),
262 range: None,
263 cell_paths: None,
264 end: false,
265 graphemes: false,
266 };
267
268 let actual = action(&word, &options, Span::test_data());
269
270 assert_eq!(actual, Value::test_int(5));
271 }
272 #[test]
273 fn index_of_does_not_exist_in_string() {
274 let word = Value::test_string("Cargo.tomL");
275
276 let options = Arguments {
277 substring: String::from("Lm"),
278 range: None,
279 cell_paths: None,
280 end: false,
281 graphemes: false,
282 };
283
284 let actual = action(&word, &options, Span::test_data());
285
286 assert_eq!(actual, Value::test_int(-1));
287 }
288
289 #[test]
290 fn returns_index_of_next_substring() {
291 let word = Value::test_string("Cargo.Cargo");
292 let range = IntRange::new(
293 Value::int(1, Span::test_data()),
294 Value::nothing(Span::test_data()),
295 Value::nothing(Span::test_data()),
296 RangeInclusion::Inclusive,
297 Span::test_data(),
298 )
299 .expect("valid range");
300
301 let spanned_range = Spanned {
302 item: range,
303 span: Span::test_data(),
304 };
305
306 let options = Arguments {
307 substring: String::from("Cargo"),
308
309 range: Some(spanned_range),
310 cell_paths: None,
311 end: false,
312 graphemes: false,
313 };
314
315 let actual = action(&word, &options, Span::test_data());
316 assert_eq!(actual, Value::test_int(6));
317 }
318
319 #[test]
320 fn index_does_not_exist_due_to_end_index() {
321 let word = Value::test_string("Cargo.Banana");
322 let range = IntRange::new(
323 Value::nothing(Span::test_data()),
324 Value::nothing(Span::test_data()),
325 Value::int(5, Span::test_data()),
326 RangeInclusion::Inclusive,
327 Span::test_data(),
328 )
329 .expect("valid range");
330
331 let spanned_range = Spanned {
332 item: range,
333 span: Span::test_data(),
334 };
335
336 let options = Arguments {
337 substring: String::from("Banana"),
338
339 range: Some(spanned_range),
340 cell_paths: None,
341 end: false,
342 graphemes: false,
343 };
344
345 let actual = action(&word, &options, Span::test_data());
346 assert_eq!(actual, Value::test_int(-1));
347 }
348
349 #[test]
350 fn returns_index_of_nums_in_middle_due_to_index_limit_from_both_ends() {
351 let word = Value::test_string("123123123");
352 let range = IntRange::new(
353 Value::int(2, Span::test_data()),
354 Value::nothing(Span::test_data()),
355 Value::int(6, Span::test_data()),
356 RangeInclusion::Inclusive,
357 Span::test_data(),
358 )
359 .expect("valid range");
360
361 let spanned_range = Spanned {
362 item: range,
363 span: Span::test_data(),
364 };
365
366 let options = Arguments {
367 substring: String::from("123"),
368
369 range: Some(spanned_range),
370 cell_paths: None,
371 end: false,
372 graphemes: false,
373 };
374
375 let actual = action(&word, &options, Span::test_data());
376 assert_eq!(actual, Value::test_int(3));
377 }
378
379 #[test]
380 fn index_does_not_exists_due_to_strict_bounds() {
381 let word = Value::test_string("123456");
382 let range = IntRange::new(
383 Value::int(2, Span::test_data()),
384 Value::nothing(Span::test_data()),
385 Value::int(5, Span::test_data()),
386 RangeInclusion::RightExclusive,
387 Span::test_data(),
388 )
389 .expect("valid range");
390
391 let spanned_range = Spanned {
392 item: range,
393 span: Span::test_data(),
394 };
395
396 let options = Arguments {
397 substring: String::from("1"),
398
399 range: Some(spanned_range),
400 cell_paths: None,
401 end: false,
402 graphemes: false,
403 };
404
405 let actual = action(&word, &options, Span::test_data());
406 assert_eq!(actual, Value::test_int(-1));
407 }
408
409 #[test]
410 fn use_utf8_bytes() {
411 let word = Value::string(String::from("๐ฏ๐ตใปใ ใตใ ใดใ"), Span::test_data());
412
413 let options = Arguments {
414 substring: String::from("ใตใ"),
415 range: None,
416 cell_paths: None,
417 end: false,
418 graphemes: false,
419 };
420
421 let actual = action(&word, &options, Span::test_data());
422 assert_eq!(actual, Value::test_int(15));
423 }
424
425 #[test]
426 fn index_is_not_a_char_boundary() {
427 let word = Value::string(String::from("๐"), Span::test_data());
428
429 let range = IntRange::new(
430 Value::int(0, Span::test_data()),
431 Value::int(1, Span::test_data()),
432 Value::int(2, Span::test_data()),
433 RangeInclusion::Inclusive,
434 Span::test_data(),
435 )
436 .expect("valid range");
437
438 let spanned_range = Spanned {
439 item: range,
440 span: Span::test_data(),
441 };
442
443 let options = Arguments {
444 substring: String::new(),
445
446 range: Some(spanned_range),
447 cell_paths: None,
448 end: false,
449 graphemes: false,
450 };
451
452 let actual = action(&word, &options, Span::test_data());
453 assert!(actual.is_error());
454 }
455
456 #[test]
457 fn index_is_out_of_bounds() {
458 let word = Value::string(String::from("hello"), Span::test_data());
459
460 let range = IntRange::new(
461 Value::int(-1, Span::test_data()),
462 Value::int(1, Span::test_data()),
463 Value::int(3, Span::test_data()),
464 RangeInclusion::Inclusive,
465 Span::test_data(),
466 )
467 .expect("valid range");
468
469 let spanned_range = Spanned {
470 item: range,
471 span: Span::test_data(),
472 };
473
474 let options = Arguments {
475 substring: String::from("h"),
476
477 range: Some(spanned_range),
478 cell_paths: None,
479 end: false,
480 graphemes: false,
481 };
482
483 let actual = action(&word, &options, Span::test_data());
484 assert_eq!(actual, Value::test_int(-1));
485 }
486}