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