mod.rs 12.5 KB
Newer Older
1
//! Contains the binary logic of Ion.
AdminXVII's avatar
AdminXVII committed
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 13
    expansion::Expander,
    parser::Terminator,
14
    types, IonError, PipelineError, Shell, Signal,
15
};
16 17
use itertools::Itertools;
use liner::{Buffer, Context, KeyBindings};
AdminXVII's avatar
AdminXVII committed
18
use std::{
19
    cell::{Cell, RefCell},
AdminXVII's avatar
AdminXVII committed
20 21 22 23 24
    fs::{self, OpenOptions},
    io,
    path::Path,
    rc::Rc,
};
25
use xdg::BaseDirectories;
Sag0Sag0's avatar
Sag0Sag0 committed
26

27 28 29 30
#[cfg(not(feature = "advanced_arg_parsing"))]
pub const MAN_ION: &str = r#"Ion - The Ion Shell 1.0.0-alpha
Ion is a commandline shell created to be a faster and easier to use alternative to the currently available shells. It is
not POSIX compliant.
Sag0Sag0's avatar
Sag0Sag0 committed
31

32 33
USAGE:
    ion [FLAGS] [OPTIONS] [args]...
Sag0Sag0's avatar
Sag0Sag0 committed
34

35 36 37 38 39 40
FLAGS:
    -h, --help           Prints help information
    -i, --interactive    Force interactive mode
    -n, --no-execute     Do not execute any commands, perform only syntax checking
    -x                   Print commands before execution
    -v, --version        Print the version, platform and revision of Ion then exit
Sag0Sag0's avatar
Sag0Sag0 committed
41

42
OPTIONS:
43 44
    -c <command>             Evaluate given commands instead of reading from the commandline
    -o <key_bindings>        Shortcut layout. Valid options: "vi", "emacs"
45 46

ARGS:
47 48
    <args>...    Script arguments (@args). If the -c option is not specified, the first parameter is taken as a
                 filename to execute"#;
49

50 51 52 53 54 55 56 57 58
pub(crate) const MAN_HISTORY: &str = r#"NAME
    history - print command history

SYNOPSIS
    history

DESCRIPTION
    Prints the command history."#;

59
pub struct InteractiveShell<'a> {
60 61 62 63
    context:    Rc<RefCell<Context>>,
    shell:      RefCell<Shell<'a>>,
    terminated: Cell<bool>,
    huponexit:  Rc<Cell<bool>>,
64 65
}

66
impl<'a> InteractiveShell<'a> {
67 68
    const CONFIG_FILE_NAME: &'static str = "initrc";

69 70 71
    pub fn new(shell: Shell<'a>) -> Self {
        let mut context = Context::new();
        context.word_divider_fn = Box::new(word_divide);
AdminXVII's avatar
AdminXVII committed
72
        if shell.variables().get_str("HISTFILE_ENABLED").ok() == Some("1".into()) {
73
            let path = shell.variables().get_str("HISTFILE").expect("shell didn't set HISTFILE");
74 75 76 77 78
            if !Path::new(path.as_str()).exists() {
                eprintln!("ion: creating history file at \"{}\"", path);
            }
            let _ = context.history.set_file_name_and_load_history(path.as_str());
        }
79 80 81 82 83 84
        InteractiveShell {
            context:    Rc::new(RefCell::new(context)),
            shell:      RefCell::new(shell),
            terminated: Cell::new(true),
            huponexit:  Rc::new(Cell::new(false)),
        }
85 86 87 88 89
    }

    /// Handles commands given by the REPL, and saves them to history.
    pub fn save_command(&self, cmd: &str) {
        if !cmd.ends_with('/')
90 91 92 93 94 95
            && self
                .shell
                .borrow()
                .tilde(cmd)
                .ok()
                .map_or(false, |path| Path::new(&path.as_str()).is_dir())
96
        {
97 98 99
            self.save_command_in_history(&[cmd, "/"].concat());
        } else {
            self.save_command_in_history(cmd);
Michael Aaron Murphy's avatar
Michael Aaron Murphy committed
100
        }
101 102
    }

103 104
    pub fn add_callbacks(&self) {
        let context = self.context.clone();
AdminXVII's avatar
AdminXVII committed
105
        self.shell.borrow_mut().set_on_command(Some(Box::new(move |shell, elapsed| {
106 107 108
            // If `RECORD_SUMMARY` is set to "1" (True, Yes), then write a summary of the
            // pipline just executed to the the file and context histories. At the
            // moment, this means record how long it took.
AdminXVII's avatar
AdminXVII committed
109
            if Some("1".into()) == shell.variables().get_str("RECORD_SUMMARY").ok() {
110 111 112 113 114 115 116 117 118 119 120 121
                let summary = format!(
                    "#summary# elapsed real time: {}.{:09} seconds",
                    elapsed.as_secs(),
                    elapsed.subsec_nanos()
                );
                println!("{:?}", summary);
                context.borrow_mut().history.push(summary.into()).unwrap_or_else(|err| {
                    eprintln!("ion: history append: {}", err);
                });
            }
        })));
    }
122

123 124 125 126 127 128
    fn create_config_file(base_dirs: BaseDirectories, file_name: &str) -> Result<(), io::Error> {
        let path = base_dirs.place_config_file(file_name)?;
        OpenOptions::new().write(true).create_new(true).open(path)?;
        Ok(())
    }

129 130 131
    /// Creates an interactive session that reads from a prompt provided by
    /// Liner.
    pub fn execute_interactive(self) -> ! {
AdminXVII's avatar
AdminXVII committed
132
        let context_bis = self.context.clone();
133
        let huponexit = self.huponexit.clone();
AdminXVII's avatar
AdminXVII committed
134 135 136
        let prep_for_exit = &move |shell: &mut Shell<'_>| {
            // context will be sent a signal to commit all changes to the history file,
            // and waiting for the history thread in the background to finish.
137
            if huponexit.get() {
AdminXVII's avatar
AdminXVII committed
138
                shell.resume_stopped();
139
                shell.background_send(Signal::SIGHUP).expect("Failed to prepare for exit");
AdminXVII's avatar
AdminXVII committed
140 141 142 143 144
            }
            context_bis.borrow_mut().history.commit_to_file();
        };

        let exit = self.shell.borrow().builtins().get("exit").unwrap();
145
        let exit = &|args: &[types::Str], shell: &mut Shell<'_>| -> Status {
AdminXVII's avatar
AdminXVII committed
146 147 148 149 150
            prep_for_exit(shell);
            exit(args, shell)
        };

        let exec = self.shell.borrow().builtins().get("exec").unwrap();
151
        let exec = &|args: &[types::Str], shell: &mut Shell<'_>| -> Status {
AdminXVII's avatar
AdminXVII committed
152 153 154 155
            prep_for_exit(shell);
            exec(args, shell)
        };

156
        let context_bis = self.context.clone();
157
        let history = &move |args: &[types::Str], _shell: &mut Shell<'_>| -> Status {
158
            if man_pages::check_help(args, MAN_HISTORY) {
159
                return Status::SUCCESS;
160 161 162
            }

            print!("{}", context_bis.borrow().history.buffers.iter().format("\n"));
163
            Status::SUCCESS
164 165
        };

166 167 168 169 170 171 172 173 174
        let huponexit = self.huponexit.clone();
        let set_huponexit: BuiltinFunction = &move |args, _shell| {
            huponexit.set(match args.get(1).map(AsRef::as_ref) {
                Some("false") | Some("off") => false,
                _ => true,
            });
            Status::SUCCESS
        };

175
        let context_bis = self.context.clone();
176
        let keybindings = &move |args: &[types::Str], _shell: &mut Shell<'_>| -> Status {
177 178 179
            match args.get(1).map(|s| s.as_str()) {
                Some("vi") => {
                    context_bis.borrow_mut().key_bindings = KeyBindings::Vi;
180
                    Status::SUCCESS
181 182 183
                }
                Some("emacs") => {
                    context_bis.borrow_mut().key_bindings = KeyBindings::Emacs;
184
                    Status::SUCCESS
185
                }
186 187
                Some(_) => Status::error("Invalid keybindings. Choices are vi and emacs"),
                None => Status::error("keybindings need an argument"),
188 189 190
            }
        };

191
        // change the lifetime to allow adding local builtins
192
        let InteractiveShell { context, shell, terminated, huponexit } = self;
AdminXVII's avatar
AdminXVII committed
193
        let mut shell = shell.into_inner();
194 195 196 197 198
        shell
            .builtins_mut()
            .add("history", history, "Display a log of all commands previously executed")
            .add("keybindings", keybindings, "Change the keybindings")
            .add("exit", exit, "Exits the current session")
199 200
            .add("exec", exec, "Replace the shell with the given command.")
            .add("huponexit", set_huponexit, "Hangup the shell's background jobs on exit");
201

AdminXVII's avatar
AdminXVII committed
202
        Self::exec_init_file(&mut shell);
203

204 205
        InteractiveShell { context, shell: RefCell::new(shell), terminated, huponexit }
            .exec(prep_for_exit)
AdminXVII's avatar
AdminXVII committed
206 207 208
    }

    fn exec_init_file(shell: &mut Shell) {
209 210
        match BaseDirectories::with_prefix("ion") {
            Ok(base_dirs) => match base_dirs.find_config_file(Self::CONFIG_FILE_NAME) {
AdminXVII's avatar
AdminXVII committed
211 212 213 214 215
                Some(initrc) => match fs::File::open(initrc) {
                    Ok(script) => {
                        if let Err(err) = shell.execute_command(std::io::BufReader::new(script)) {
                            eprintln!("ion: {}", err);
                        }
216
                    }
AdminXVII's avatar
AdminXVII committed
217 218
                    Err(cause) => println!("ion: init file was not found: {}", cause),
                },
219 220 221 222 223 224 225 226 227 228
                None => {
                    if let Err(err) = Self::create_config_file(base_dirs, Self::CONFIG_FILE_NAME) {
                        eprintln!("ion: could not create config file: {}", err);
                    }
                }
            },
            Err(err) => {
                eprintln!("ion: unable to get base directory: {}", err);
            }
        }
AdminXVII's avatar
AdminXVII committed
229
    }
230

AdminXVII's avatar
AdminXVII committed
231
    fn exec<T: Fn(&mut Shell<'_>)>(self, prep_for_exit: &T) -> ! {
232
        loop {
AdminXVII's avatar
AdminXVII committed
233
            let mut lines = std::iter::repeat_with(|| self.readln(prep_for_exit))
234 235
                .filter_map(|cmd| cmd)
                .flat_map(|s| s.into_bytes().into_iter().chain(Some(b'\n')));
236
            match Terminator::new(&mut lines).terminate() {
237
                Some(command) => {
238
                    let cmd: &str = &designators::expand_designators(
AdminXVII's avatar
AdminXVII committed
239
                        &self.context.borrow(),
240 241
                        command.trim_end(),
                    );
242
                    self.terminated.set(true);
243 244
                    {
                        let mut shell = self.shell.borrow_mut();
245 246 247 248 249
                        match shell.on_command(&cmd) {
                            Ok(_) => (),
                            Err(IonError::PipelineExecutionError(
                                PipelineError::CommandNotFound(command),
                            )) => {
250 251
                                if let Some(func) =
                                    shell.variables().get_func("COMMAND_NOT_FOUND").cloned()
252
                                {
253 254 255 256 257 258
                                    if let Err(why) =
                                        shell.execute_function(&func, &["ion", &command])
                                    {
                                        eprintln!("ion: command not found handler: {}", why);
                                    }
                                } else {
259 260
                                    eprintln!("ion: command not found: {}", command);
                                }
261 262 263 264 265 266
                                // Status::COULD_NOT_EXEC
                            }
                            Err(err) => {
                                eprintln!("ion: {}", err);
                                shell.reset_flow();
                            }
267
                        }
AdminXVII's avatar
AdminXVII committed
268
                    }
AdminXVII's avatar
AdminXVII committed
269
                    self.save_command(&cmd);
270
                }
271
                None => self.terminated.set(false),
272 273 274 275
            }
        }
    }

276 277 278
    /// Set the keybindings of the underlying liner context
    pub fn set_keybindings(&mut self, key_bindings: KeyBindings) {
        self.context.borrow_mut().key_bindings = key_bindings;
279
    }
280 281
}

282
#[derive(Debug)]
283
struct WordDivide<I>
284 285
where
    I: Iterator<Item = (usize, char)>,
286
{
AdminXVII's avatar
AdminXVII committed
287 288
    iter:       I,
    count:      usize,
289
    word_start: Option<usize>,
290 291
}
impl<I> WordDivide<I>
292 293
where
    I: Iterator<Item = (usize, char)>,
294
{
295
    #[inline]
296 297
    fn check_boundary(&mut self, c: char, index: usize, escaped: bool) -> Option<(usize, usize)> {
        if let Some(start) = self.word_start {
298
            if c == ' ' && !escaped {
299 300
                self.word_start = None;
                Some((start, index))
301
            } else {
302
                self.next()
303 304 305
            }
        } else {
            if c != ' ' {
306
                self.word_start = Some(index);
307
            }
308
            self.next()
309
        }
310
    }
311 312
}
impl<I> Iterator for WordDivide<I>
313 314
where
    I: Iterator<Item = (usize, char)>,
315 316
{
    type Item = (usize, usize);
317

318 319 320
    fn next(&mut self) -> Option<Self::Item> {
        self.count += 1;
        match self.iter.next() {
321
            Some((i, '\\')) => {
322
                if let Some((_, cnext)) = self.iter.next() {
323
                    self.count += 1;
324
                    // We use `i` in order to include the backslash as part of the word
325
                    self.check_boundary(cnext, i, true)
326
                } else {
327
                    self.next()
328 329
                }
            }
330
            Some((i, c)) => self.check_boundary(c, i, false),
331 332
            None => {
                // When start has been set, that means we have encountered a full word.
333
                self.word_start.take().map(|start| (start, self.count - 1))
334
            }
335
        }
336 337 338
    }
}

339
fn word_divide(buf: &Buffer) -> Vec<(usize, usize)> {
340
    // -> impl Iterator<Item = (usize, usize)> + 'a
AdminXVII's avatar
AdminXVII committed
341
    WordDivide { iter: buf.chars().cloned().enumerate(), count: 0, word_start: None }.collect() // TODO: return iterator directly :D
342
}