nu_command/platform/input/
input_.rs

1use crate::platform::input::legacy_input::LegacyInput;
2use crate::platform::input::reedline_prompt::ReedlinePrompt;
3use nu_engine::command_prelude::*;
4use nu_protocol::shell_error::{self, io::IoError};
5use reedline::{FileBackedHistory, HISTORY_SIZE, History, HistoryItem, Reedline, Signal};
6
7#[derive(Clone)]
8pub struct Input;
9
10impl LegacyInput for Input {}
11
12impl Command for Input {
13    fn name(&self) -> &str {
14        "input"
15    }
16
17    fn description(&self) -> &str {
18        "Get input from the user."
19    }
20
21    fn search_terms(&self) -> Vec<&str> {
22        vec!["prompt", "interactive"]
23    }
24
25    fn signature(&self) -> Signature {
26        Signature::build("input")
27            .input_output_types(vec![
28                (Type::Nothing, Type::Any),
29                (Type::List(Box::new(Type::String)), Type::Any)])
30            .allow_variants_without_examples(true)
31            .optional("prompt", SyntaxShape::String, "Prompt to show the user.")
32            .named(
33                "bytes-until-any",
34                SyntaxShape::String,
35                "read bytes (not text) until any of the given stop bytes is seen",
36                Some('u'),
37            )
38            .named(
39                "numchar",
40                SyntaxShape::Int,
41                "number of characters to read; suppresses output",
42                Some('n'),
43            )
44            .named(
45                "default",
46                SyntaxShape::String,
47                "default value if no input is provided",
48                Some('d'),
49            )
50            .switch(
51                "reedline",
52                "use the reedline library, defaults to false",
53                None
54            )
55            .named(
56                "history-file",
57                SyntaxShape::Filepath,
58                "Path to a file to read and write command history. This is a text file and will be created if it doesn't exist. Will be used as the selection list. Implies `--reedline`.",
59                None,
60            )
61            .named(
62                "max-history",
63                SyntaxShape::Int,
64                "The maximum number of entries to keep in the history, defaults to $env.config.history.max_size. Implies `--reedline`.",
65                None,
66            )
67            .switch("suppress-output", "don't print keystroke values", Some('s'))
68            .category(Category::Platform)
69    }
70
71    fn run(
72        &self,
73        engine_state: &EngineState,
74        stack: &mut Stack,
75        call: &Call,
76        input: PipelineData,
77    ) -> Result<PipelineData, ShellError> {
78        // Check if we should use the legacy implementation or the reedline implementation
79        let use_reedline = [
80            // reedline is not set - use legacy implementation
81            call.has_flag(engine_state, stack, "reedline")?,
82            // We have the history-file or max-history flags set to None
83            call.get_flag::<String>(engine_state, stack, "history-file")?
84                .is_some(),
85            call.get_flag::<i64>(engine_state, stack, "max-history")?
86                .is_some(),
87        ]
88        .iter()
89        .any(|x| *x);
90
91        if !use_reedline {
92            return self.legacy_input(engine_state, stack, call, input);
93        }
94
95        let prompt_str: Option<String> = call.opt(engine_state, stack, 0)?;
96        let default_val: Option<String> = call.get_flag(engine_state, stack, "default")?;
97        let history_file_val: Option<String> =
98            call.get_flag(engine_state, stack, "history-file")?;
99        let max_history: usize = call
100            .get_flag::<i64>(engine_state, stack, "max-history")?
101            .map(|l| if l < 0 { 0 } else { l as usize })
102            .unwrap_or(HISTORY_SIZE);
103        let max_history_span = call.get_flag_span(stack, "max-history");
104        let history_file_span = call.get_flag_span(stack, "history-file");
105
106        let default_str = match (&prompt_str, &default_val) {
107            (Some(_prompt), Some(val)) => format!("(default: {val}) "),
108            _ => "".to_string(),
109        };
110
111        let history_entries = match input {
112            PipelineData::Value(Value::List { vals, .. }, ..) => Some(vals),
113            _ => None,
114        };
115
116        // If we either have history entries or history file, we create an history
117        let history = match (history_entries.is_some(), history_file_val.is_some()) {
118            (false, false) => None, // Neither are set, no need for history support
119            _ => {
120                let file_history = match history_file_val {
121                    Some(file) => FileBackedHistory::with_file(max_history, file.into()),
122                    None => FileBackedHistory::new(max_history),
123                };
124                let mut history = match file_history {
125                    Ok(h) => h,
126                    Err(e) => match e.0 {
127                        reedline::ReedlineErrorVariants::IOError(err) => {
128                            return Err(ShellError::IncorrectValue {
129                                msg: err.to_string(),
130                                val_span: history_file_span.expect("history-file should be set"),
131                                call_span: call.head,
132                            });
133                        }
134                        reedline::ReedlineErrorVariants::OtherHistoryError(msg) => {
135                            return Err(ShellError::IncorrectValue {
136                                msg: msg.to_string(),
137                                val_span: max_history_span.expect("max-history should be set"),
138                                call_span: call.head,
139                            });
140                        }
141                        _ => {
142                            return Err(ShellError::IncorrectValue {
143                                msg: "unable to create history".to_string(),
144                                val_span: call.head,
145                                call_span: call.head,
146                            });
147                        }
148                    },
149                };
150
151                if let Some(vals) = history_entries {
152                    vals.iter().for_each(|val| {
153                        if let Value::String { val, .. } = val {
154                            let _ = history.save(HistoryItem::from_command_line(val.clone()));
155                        }
156                    });
157                }
158                Some(history)
159            }
160        };
161
162        let prompt = ReedlinePrompt {
163            indicator: default_str,
164            left_prompt: prompt_str.unwrap_or("".to_string()),
165            right_prompt: "".to_string(),
166        };
167
168        let mut line_editor = Reedline::create();
169        line_editor = line_editor.with_ansi_colors(false);
170        line_editor = match history {
171            Some(h) => line_editor.with_history(Box::new(h)),
172            None => line_editor,
173        };
174
175        let mut buf = String::new();
176
177        match line_editor.read_line(&prompt) {
178            Ok(Signal::Success(buffer)) => {
179                buf.push_str(&buffer);
180            }
181            Ok(Signal::CtrlC) => {
182                return Err(IoError::new(
183                    shell_error::io::ErrorKind::from_std(std::io::ErrorKind::Interrupted),
184                    call.head,
185                    None,
186                )
187                .into());
188            }
189            Ok(Signal::CtrlD) => {
190                // Do nothing on ctrl-d
191                return Ok(Value::nothing(call.head).into_pipeline_data());
192            }
193            Err(event_error) => {
194                let from_io_error = IoError::factory(call.head, None);
195                return Err(from_io_error(event_error).into());
196            }
197        }
198        match default_val {
199            Some(val) if buf.is_empty() => Ok(Value::string(val, call.head).into_pipeline_data()),
200            _ => Ok(Value::string(buf, call.head).into_pipeline_data()),
201        }
202    }
203
204    fn examples(&self) -> Vec<Example<'_>> {
205        vec![
206            Example {
207                description: "Get input from the user, and assign to a variable",
208                example: "let user_input = (input)",
209                result: None,
210            },
211            Example {
212                description: "Get two characters from the user, and assign to a variable",
213                example: "let user_input = (input --numchar 2)",
214                result: None,
215            },
216            Example {
217                description: "Get input from the user with default value, and assign to a variable",
218                example: "let user_input = (input --default 10)",
219                result: None,
220            },
221            Example {
222                description: "Get multiple lines of input from the user (newlines can be entered using `Alt` + `Enter` or `Ctrl` + `Enter`), and assign to a variable",
223                example: "let multiline_input = (input --reedline)",
224                result: None,
225            },
226            Example {
227                description: "Get input from the user with history, and assign to a variable",
228                example: "let user_input = ([past,command,entries] | input --reedline)",
229                result: None,
230            },
231            Example {
232                description: "Get input from the user with history backed by a file, and assign to a variable",
233                example: "let user_input = (input --reedline --history-file ./history.txt)",
234                result: None,
235            },
236        ]
237    }
238}
239
240#[cfg(test)]
241mod tests {
242    use super::Input;
243
244    #[test]
245    fn examples_work_as_expected() {
246        use crate::test_examples;
247        test_examples(Input {})
248    }
249}