1 //! Contains the binary logic of Ion.
2 pub mod builtins;
3 mod completer;
4 mod designators;
5 mod history;
6 mod lexer;
7 mod prompt;
8 mod readln;
9
10 use ion_shell::{
11 builtins::{man_pages, BuiltinFunction, Status},
12 expansion::Expander,
13 parser::Terminator,
14 types::{self, array},
15 IonError, PipelineError, Shell, Signal, Value,
16 };
17 use itertools::Itertools;
18 use liner::{Buffer, Context, KeyBindings};
19 use std::{
20 cell::{Cell, RefCell},
21 fs::{self, OpenOptions},
22 io::{self, Write},
23 path::Path,
24 rc::Rc,
25 };
26 use xdg::BaseDirectories;
27
28 #[cfg(not(feature = "advanced_arg_parsing"))]
29 pub const MAN_ION: &str = r#"Ion - The Ion Shell 1.0.0-alpha
30 Ion is a commandline shell created to be a faster and easier to use alternative to the currently available shells. It is
31 not POSIX compliant.
32
33 USAGE:
34 ion [FLAGS] [OPTIONS] [args]...
35
36 FLAGS:
37 -f, --fake-interactive Use a fake interactive mode, where errors don't exit the shell
38 -h, --help Prints help information
39 -i, --interactive Force interactive mode
40 -n, --no-execute Do not execute any commands, perform only syntax checking
41 -x Print commands before execution
42 -v, --version Print the version, platform and revision of Ion then exit
43
44 OPTIONS:
45 -c <command> Evaluate given commands instead of reading from the commandline
46 -o <key_bindings> Shortcut layout. Valid options: "vi", "emacs"
47
48 ARGS:
49 <args>... Script arguments (@args). If the -c option is not specified, the first parameter is taken as a
50 filename to execute"#;
51
52 pub(crate) const MAN_HISTORY: &str = r#"NAME
53 history - print command history
54
55 SYNOPSIS
56 history [option]
57
58 DESCRIPTION
59 Prints or manupulate the command history.
60
61 OPTIONS:
62 +inc_append: Append each command to history as entered.
63 -inc_append: Default, do not append each command to history as entered.
64 +shared: Share history between shells using the same history file, implies inc_append.
65 -shared: Default, do not share shell history.
66 +duplicates: Default, allow duplicates in history.
67 -duplicates: Do not allow duplicates in history.
68 "#;
69
70 pub struct InteractiveShell<'a> {
71 context: Rc<RefCell<Context>>,
72 shell: RefCell<Shell<'a>>,
73 terminated: Cell<bool>,
74 huponexit: Rc<Cell<bool>>,
75 }
76
77 impl<'a> InteractiveShell<'a> {
78 const CONFIG_FILE_NAME: &'static str = "initrc";
79
new(shell: Shell<'a>) -> Self80 pub fn new(shell: Shell<'a>) -> Self {
81 let mut context = Context::new();
82 context.word_divider_fn = Box::new(word_divide);
83 InteractiveShell {
84 context: Rc::new(RefCell::new(context)),
85 shell: RefCell::new(shell),
86 terminated: Cell::new(true),
87 huponexit: Rc::new(Cell::new(false)),
88 }
89 }
90
91 /// Handles commands given by the REPL, and saves them to history.
save_command(&self, cmd: &str)92 pub fn save_command(&self, cmd: &str) {
93 if !cmd.ends_with('/')
94 && self
95 .shell
96 .borrow()
97 .tilde(cmd)
98 .ok()
99 .map_or(false, |path| Path::new(&path.as_str()).is_dir())
100 {
101 self.save_command_in_history(&[cmd, "/"].concat());
102 } else {
103 self.save_command_in_history(cmd);
104 }
105 }
106
add_callbacks(&self)107 pub fn add_callbacks(&self) {
108 let context = self.context.clone();
109 self.shell.borrow_mut().set_on_command(Some(Box::new(move |shell, elapsed| {
110 // If `RECORD_SUMMARY` is set to "1" (True, Yes), then write a summary of the
111 // pipline just executed to the the file and context histories. At the
112 // moment, this means record how long it took.
113 if Some("1".into()) == shell.variables().get_str("RECORD_SUMMARY").ok() {
114 let summary = format!(
115 "#summary# elapsed real time: {}.{:09} seconds",
116 elapsed.as_secs(),
117 elapsed.subsec_nanos()
118 );
119 println!("{:?}", summary);
120 context.borrow_mut().history.push(summary.into()).unwrap_or_else(|err| {
121 eprintln!("ion: history append: {}", err);
122 });
123 }
124 })));
125 }
126
create_config_file(base_dirs: &BaseDirectories) -> Result<(), io::Error>127 fn create_config_file(base_dirs: &BaseDirectories) -> Result<(), io::Error> {
128 let path = base_dirs.place_config_file(Self::CONFIG_FILE_NAME)?;
129 OpenOptions::new().write(true).create_new(true).open(path).map(|_| ())
130 }
131
132 /// Creates an interactive session that reads from a prompt provided by
133 /// Liner.
execute_interactive(self) -> !134 pub fn execute_interactive(self) -> ! {
135 let context_bis = self.context.clone();
136 let huponexit = self.huponexit.clone();
137 let prep_for_exit = &move |shell: &mut Shell<'_>| {
138 // context will be sent a signal to commit all changes to the history file,
139 // and waiting for the history thread in the background to finish.
140 if huponexit.get() {
141 shell.resume_stopped();
142 shell.background_send(Signal::SIGHUP).expect("Failed to prepare for exit");
143 }
144 context_bis.borrow_mut().history.commit_to_file();
145 };
146
147 let exit = self.shell.borrow().builtins().get("exit").unwrap();
148 let exit = &|args: &[types::Str], shell: &mut Shell<'_>| -> Status {
149 prep_for_exit(shell);
150 exit(args, shell)
151 };
152
153 let exec = self.shell.borrow().builtins().get("exec").unwrap();
154 let exec = &|args: &[types::Str], shell: &mut Shell<'_>| -> Status {
155 prep_for_exit(shell);
156 exec(args, shell)
157 };
158
159 let context_bis = self.context.clone();
160 let history = &move |args: &[types::Str], _shell: &mut Shell<'_>| -> Status {
161 if man_pages::check_help(args, MAN_HISTORY) {
162 return Status::SUCCESS;
163 }
164
165 match args.get(1).map(|s| s.as_str()) {
166 Some("+inc_append") => {
167 context_bis.borrow_mut().history.inc_append = true;
168 }
169 Some("-inc_append") => {
170 context_bis.borrow_mut().history.inc_append = false;
171 }
172 Some("+share") => {
173 context_bis.borrow_mut().history.inc_append = true;
174 context_bis.borrow_mut().history.share = true;
175 }
176 Some("-share") => {
177 context_bis.borrow_mut().history.inc_append = false;
178 context_bis.borrow_mut().history.share = false;
179 }
180 Some("+duplicates") => {
181 context_bis.borrow_mut().history.load_duplicates = true;
182 }
183 Some("-duplicates") => {
184 context_bis.borrow_mut().history.load_duplicates = false;
185 }
186 Some(_) => {
187 Status::error(
188 "Invalid history option. Choices are [+|-] inc_append, duplicates and \
189 share (implies inc_append).",
190 );
191 }
192 None => {
193 print!("{}", context_bis.borrow().history.buffers.iter().format("\n"));
194 }
195 }
196 Status::SUCCESS
197 };
198
199 let huponexit = self.huponexit.clone();
200 let set_huponexit: BuiltinFunction = &move |args, _shell| {
201 huponexit.set(match args.get(1).map(AsRef::as_ref) {
202 Some("false") | Some("off") => false,
203 _ => true,
204 });
205 Status::SUCCESS
206 };
207
208 let context_bis = self.context.clone();
209 let keybindings = &move |args: &[types::Str], _shell: &mut Shell<'_>| -> Status {
210 match args.get(1).map(|s| s.as_str()) {
211 Some("vi") => {
212 context_bis.borrow_mut().key_bindings = KeyBindings::Vi;
213 Status::SUCCESS
214 }
215 Some("emacs") => {
216 context_bis.borrow_mut().key_bindings = KeyBindings::Emacs;
217 Status::SUCCESS
218 }
219 Some(_) => Status::error("Invalid keybindings. Choices are vi and emacs"),
220 None => Status::error("keybindings need an argument"),
221 }
222 };
223
224 // change the lifetime to allow adding local builtins
225 let InteractiveShell { context, shell, terminated, huponexit } = self;
226 let mut shell = shell.into_inner();
227 shell
228 .builtins_mut()
229 .add("history", history, "Display a log of all commands previously executed")
230 .add("keybindings", keybindings, "Change the keybindings")
231 .add("exit", exit, "Exits the current session")
232 .add("exec", exec, "Replace the shell with the given command.")
233 .add("huponexit", set_huponexit, "Hangup the shell's background jobs on exit");
234
235 match BaseDirectories::with_prefix("ion") {
236 Ok(project_dir) => {
237 Self::exec_init_file(&project_dir, &mut shell);
238 Self::load_history(&project_dir, &mut shell, &mut context.borrow_mut());
239 }
240 Err(err) => eprintln!("ion: unable to get xdg base directory: {}", err),
241 }
242
243 InteractiveShell { context, shell: RefCell::new(shell), terminated, huponexit }
244 .exec(prep_for_exit)
245 }
246
load_history(project_dir: &BaseDirectories, shell: &mut Shell, context: &mut Context)247 fn load_history(project_dir: &BaseDirectories, shell: &mut Shell, context: &mut Context) {
248 shell.variables_mut().set("HISTFILE_ENABLED", "1");
249
250 // History Timestamps enabled variable, disabled by default
251 shell.variables_mut().set("HISTORY_TIMESTAMP", "0");
252 shell
253 .variables_mut()
254 .set("HISTORY_IGNORE", array!["no_such_command", "whitespace", "duplicates"]);
255 // Initialize the HISTFILE variable
256 if let Some(histfile) = project_dir.find_data_file("history") {
257 shell.variables_mut().set("HISTFILE", histfile.to_string_lossy().as_ref());
258 let _ = context.history.set_file_name_and_load_history(&histfile);
259 } else {
260 match project_dir.place_data_file("history") {
261 Ok(histfile) => {
262 eprintln!("ion: creating history file at \"{}\"", histfile.display());
263 shell.variables_mut().set("HISTFILE", histfile.to_string_lossy().as_ref());
264 let _ = context.history.set_file_name_and_load_history(&histfile);
265 }
266 Err(err) => println!("ion: could not create history file: {}", err),
267 }
268 }
269 }
270
exec_init_file(project_dir: &BaseDirectories, shell: &mut Shell)271 fn exec_init_file(project_dir: &BaseDirectories, shell: &mut Shell) {
272 let initrc = project_dir.find_config_file(Self::CONFIG_FILE_NAME);
273 match initrc.and_then(|initrc| fs::File::open(&initrc).ok()) {
274 Some(script) => {
275 if let Err(err) = shell.execute_command(std::io::BufReader::new(script)) {
276 eprintln!("ion: could not exec initrc: {}", err);
277 }
278 }
279 None => {
280 if let Err(err) = Self::create_config_file(project_dir) {
281 eprintln!("ion: could not create config file: {}", err);
282 }
283 }
284 }
285 }
286
exec<T: Fn(&mut Shell<'_>)>(self, prep_for_exit: &T) -> !287 fn exec<T: Fn(&mut Shell<'_>)>(self, prep_for_exit: &T) -> ! {
288 loop {
289 if let Err(err) = io::stdout().flush() {
290 eprintln!("ion: failed to flush stdio: {}", err);
291 }
292 if let Err(err) = io::stderr().flush() {
293 println!("ion: failed to flush stderr: {}", err);
294 }
295 let mut lines = std::iter::from_fn(|| self.readln(prep_for_exit))
296 .flat_map(|s| s.into_bytes().into_iter().chain(Some(b'\n')));
297 match Terminator::new(&mut lines).terminate() {
298 Some(command) => {
299 let cmd: &str = &designators::expand_designators(
300 &self.context.borrow(),
301 command.trim_end(),
302 );
303 self.terminated.set(true);
304 {
305 let mut shell = self.shell.borrow_mut();
306 match shell.on_command(&cmd) {
307 Ok(_) => (),
308 Err(IonError::PipelineExecutionError(
309 PipelineError::CommandNotFound(command),
310 )) => {
311 if let Some(Value::Function(func)) =
312 shell.variables().get("COMMAND_NOT_FOUND").cloned()
313 {
314 if let Err(why) =
315 shell.execute_function(&func, &["ion", &command])
316 {
317 eprintln!("ion: command not found handler: {}", why);
318 }
319 } else {
320 eprintln!("ion: command not found: {}", command);
321 }
322 // Status::COULD_NOT_EXEC
323 }
324 Err(err) => {
325 eprintln!("ion: {}", err);
326 shell.reset_flow();
327 }
328 }
329 }
330 self.save_command(&cmd);
331 }
332 None => self.terminated.set(false),
333 }
334 }
335 }
336
337 /// Set the keybindings of the underlying liner context
set_keybindings(&mut self, key_bindings: KeyBindings)338 pub fn set_keybindings(&mut self, key_bindings: KeyBindings) {
339 self.context.borrow_mut().key_bindings = key_bindings;
340 }
341 }
342
343 #[derive(Debug)]
344 struct WordDivide<I>
345 where
346 I: Iterator<Item = (usize, char)>,
347 {
348 iter: I,
349 count: usize,
350 word_start: Option<usize>,
351 }
352 impl<I> WordDivide<I>
353 where
354 I: Iterator<Item = (usize, char)>,
355 {
356 #[inline]
check_boundary(&mut self, c: char, index: usize, escaped: bool) -> Option<(usize, usize)>357 fn check_boundary(&mut self, c: char, index: usize, escaped: bool) -> Option<(usize, usize)> {
358 if let Some(start) = self.word_start {
359 if c == ' ' && !escaped {
360 self.word_start = None;
361 Some((start, index))
362 } else {
363 self.next()
364 }
365 } else {
366 if c != ' ' {
367 self.word_start = Some(index);
368 }
369 self.next()
370 }
371 }
372 }
373 impl<I> Iterator for WordDivide<I>
374 where
375 I: Iterator<Item = (usize, char)>,
376 {
377 type Item = (usize, usize);
378
next(&mut self) -> Option<Self::Item>379 fn next(&mut self) -> Option<Self::Item> {
380 self.count += 1;
381 match self.iter.next() {
382 Some((i, '\\')) => {
383 if let Some((_, cnext)) = self.iter.next() {
384 self.count += 1;
385 // We use `i` in order to include the backslash as part of the word
386 self.check_boundary(cnext, i, true)
387 } else {
388 self.next()
389 }
390 }
391 Some((i, c)) => self.check_boundary(c, i, false),
392 None => {
393 // When start has been set, that means we have encountered a full word.
394 self.word_start.take().map(|start| (start, self.count - 1))
395 }
396 }
397 }
398 }
399
word_divide(buf: &Buffer) -> Vec<(usize, usize)>400 fn word_divide(buf: &Buffer) -> Vec<(usize, usize)> {
401 // -> impl Iterator<Item = (usize, usize)> + 'a
402 WordDivide { iter: buf.chars().cloned().enumerate(), count: 0, word_start: None }.collect() // TODO: return iterator directly :D
403 }
404