nu_command/platform/input/
input_.rs1use 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 let use_reedline = [
80 call.has_flag(engine_state, stack, "reedline")?,
82 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 let history = match (history_entries.is_some(), history_file_val.is_some()) {
118 (false, false) => None, _ => {
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 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}