1use nu_cmd_base::input_handler::{CmdArgument, operate};
2use nu_engine::command_prelude::*;
3
4use nu_utils::IgnoreCaseExt;
5
6struct Arguments {
7 substring: String,
8 cell_paths: Option<Vec<CellPath>>,
9 case_insensitive: bool,
10}
11
12impl CmdArgument for Arguments {
13 fn take_cell_paths(&mut self) -> Option<Vec<CellPath>> {
14 self.cell_paths.take()
15 }
16}
17
18#[derive(Clone)]
19pub struct StrEndswith;
20
21impl Command for StrEndswith {
22 fn name(&self) -> &str {
23 "str ends-with"
24 }
25
26 fn signature(&self) -> Signature {
27 Signature::build("str ends-with")
28 .input_output_types(vec![
29 (Type::String, Type::Bool),
30 (Type::List(Box::new(Type::String)), Type::List(Box::new(Type::Bool))),
31 (Type::table(), Type::table()),
32 (Type::record(), Type::record()),
33 ])
34 .allow_variants_without_examples(true)
35 .required("string", SyntaxShape::String, "The string to match.")
36 .rest(
37 "rest",
38 SyntaxShape::CellPath,
39 "For a data structure input, check strings at the given cell paths, and replace with result.",
40 )
41 .switch("ignore-case", "search is case insensitive", Some('i'))
42 .category(Category::Strings)
43 }
44
45 fn description(&self) -> &str {
46 "Check if an input ends with a string."
47 }
48
49 fn search_terms(&self) -> Vec<&str> {
50 vec!["suffix", "match", "find", "search"]
51 }
52
53 fn is_const(&self) -> bool {
54 true
55 }
56
57 fn run(
58 &self,
59 engine_state: &EngineState,
60 stack: &mut Stack,
61 call: &Call,
62 input: PipelineData,
63 ) -> Result<PipelineData, ShellError> {
64 let cell_paths: Vec<CellPath> = call.rest(engine_state, stack, 1)?;
65 let cell_paths = (!cell_paths.is_empty()).then_some(cell_paths);
66 let args = Arguments {
67 substring: call.req::<String>(engine_state, stack, 0)?,
68 cell_paths,
69 case_insensitive: call.has_flag(engine_state, stack, "ignore-case")?,
70 };
71 operate(action, args, input, call.head, engine_state.signals())
72 }
73
74 fn run_const(
75 &self,
76 working_set: &StateWorkingSet,
77 call: &Call,
78 input: PipelineData,
79 ) -> Result<PipelineData, ShellError> {
80 let cell_paths: Vec<CellPath> = call.rest_const(working_set, 1)?;
81 let cell_paths = (!cell_paths.is_empty()).then_some(cell_paths);
82 let args = Arguments {
83 substring: call.req_const::<String>(working_set, 0)?,
84 cell_paths,
85 case_insensitive: call.has_flag_const(working_set, "ignore-case")?,
86 };
87 operate(
88 action,
89 args,
90 input,
91 call.head,
92 working_set.permanent().signals(),
93 )
94 }
95
96 fn examples(&self) -> Vec<Example<'_>> {
97 vec![
98 Example {
99 description: "Checks if string ends with '.rb'",
100 example: "'my_library.rb' | str ends-with '.rb'",
101 result: Some(Value::test_bool(true)),
102 },
103 Example {
104 description: "Checks if strings end with '.txt'",
105 example: "['my_library.rb', 'README.txt'] | str ends-with '.txt'",
106 result: Some(Value::test_list(vec![
107 Value::test_bool(false),
108 Value::test_bool(true),
109 ])),
110 },
111 Example {
112 description: "Checks if string ends with '.RB', case-insensitive",
113 example: "'my_library.rb' | str ends-with --ignore-case '.RB'",
114 result: Some(Value::test_bool(true)),
115 },
116 ]
117 }
118}
119
120fn action(input: &Value, args: &Arguments, head: Span) -> Value {
121 match input {
122 Value::String { val: s, .. } => {
123 let ends_with = if args.case_insensitive {
124 s.to_folded_case()
125 .ends_with(&args.substring.to_folded_case())
126 } else {
127 s.ends_with(&args.substring)
128 };
129 Value::bool(ends_with, head)
130 }
131 Value::Error { .. } => input.clone(),
132 _ => Value::error(
133 ShellError::OnlySupportsThisInputType {
134 exp_input_type: "string".into(),
135 wrong_type: input.get_type().to_string(),
136 dst_span: head,
137 src_span: input.span(),
138 },
139 head,
140 ),
141 }
142}
143
144#[cfg(test)]
145mod test {
146 use super::*;
147
148 #[test]
149 fn test_examples() {
150 use crate::test_examples;
151
152 test_examples(StrEndswith {})
153 }
154}