From f065a071f32f6189fd36a06f4799f97b72b90272 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy <mmstickman@gmail.com> Date: Sun, 16 Jul 2017 15:29:37 -0400 Subject: [PATCH] Migrate Readln Prompt Into Binary Logic --- src/shell/binary.rs | 159 ++++++++++++++++++++++++++++++++++++++++++-- src/shell/mod.rs | 155 ++---------------------------------------- 2 files changed, 159 insertions(+), 155 deletions(-) diff --git a/src/shell/binary.rs b/src/shell/binary.rs index a33fce0a..c667a530 100644 --- a/src/shell/binary.rs +++ b/src/shell/binary.rs @@ -1,16 +1,21 @@ //! Contains the binary logic of Ion. -use liner::{Buffer, Context}; +use liner::{BasicCompleter, Buffer, Context, Event, EventKind, CursorPosition}; +use parser::*; +use parser::QuoteTerminator; +use smallstring::SmallString; use smallvec::SmallVec; use std::env; use std::fs::File; use std::io::{self, Write, Read, ErrorKind}; use std::iter::{self, FromIterator}; -use std::path::Path; +use std::mem; +use std::path::{Path, PathBuf}; +use super::completer::*; use super::flow_control::Statement; use super::status::*; -use super::{Shell, FlowLogic, JobControl, ShellHistory}; -use parser::QuoteTerminator; +use super::{Shell, FlowLogic, JobControl, ShellHistory, Variables, DirectoryStack}; +use types::*; pub trait Binary { /// Launches the shell, parses arguments, and then diverges into one of the `execution` paths. @@ -25,9 +30,134 @@ pub trait Binary { fn terminate_script_quotes<I: Iterator<Item = String>>(&mut self, lines: I); /// Ensures that read statements from the interactive prompt is terminated. fn terminate_quotes(&mut self, command: String) -> Result<String, ()>; + /// Ion's interface to Liner's `read_line` method, which handles everything related to + /// rendering, controlling, and getting input from the prompt. + fn readln(&mut self) -> Option<String>; + /// Generates the prompt that will be used by Liner. + fn prompt(&self) -> String; } impl<'a> Binary for Shell<'a> { + fn prompt(&self) -> String { + if self.flow_control.level == 0 { + let prompt_var = self.variables.get_var_or_empty("PROMPT"); + expand_string(&prompt_var, &get_expanders!(&self.variables, &self.directory_stack), false).join(" ") + } else { + " ".repeat(self.flow_control.level as usize) + } + } + + fn readln(&mut self) -> Option<String> { + { + let vars_ptr = &self.variables as *const Variables; + let dirs_ptr = &self.directory_stack as *const DirectoryStack; + let funcs = &self.functions; + let vars = &self.variables; + let builtins = self.builtins; + + // Collects the current list of values from history for completion. + let history = &self.context.as_ref().unwrap().history.buffers.iter() + // Map each underlying `liner::Buffer` into a `String`. + .map(|x| x.chars().cloned().collect()) + // Collect each result into a vector to avoid borrowing issues. + .collect::<Vec<SmallString>>(); + + loop { + let prompt = self.prompt(); + let line = self.context.as_mut().unwrap().read_line(prompt, &mut move |Event { editor, kind }| { + if let EventKind::BeforeComplete = kind { + let (words, pos) = editor.get_words_and_cursor_position(); + + let filename = match pos { + CursorPosition::InWord(index) => index > 0, + CursorPosition::InSpace(Some(_), _) => true, + CursorPosition::InSpace(None, _) => false, + CursorPosition::OnWordLeftEdge(index) => index >= 1, + CursorPosition::OnWordRightEdge(index) => { + match (words.into_iter().nth(index), env::current_dir()) { + (Some((start, end)), Ok(file)) => { + let filename = editor.current_buffer().range(start, end); + complete_as_file(file, filename, index) + }, + _ => false, + } + } + }; + + if filename { + if let Ok(current_dir) = env::current_dir() { + if let Some(url) = current_dir.to_str() { + let completer = IonFileCompleter::new(Some(url), dirs_ptr, vars_ptr); + mem::replace(&mut editor.context().completer, Some(Box::new(completer))); + } + } + } else { + // Creates a list of definitions from the shell environment that will be used + // in the creation of a custom completer. + let words = builtins.iter() + // Add built-in commands to the completer's definitions. + .map(|(&s, _)| Identifier::from(s)) + // Add the history list to the completer's definitions. + .chain(history.iter().cloned()) + // Add the aliases to the completer's definitions. + .chain(vars.aliases.keys().cloned()) + // Add the list of available functions to the completer's definitions. + .chain(funcs.keys().cloned()) + // Add the list of available variables to the completer's definitions. + // TODO: We should make it free to do String->SmallString + // and mostly free to go back (free if allocated) + .chain(vars.get_vars().into_iter().map(|s| ["$", &s].concat().into())) + .collect(); + + // Initialize a new completer from the definitions collected. + let custom_completer = BasicCompleter::new(words); + + // Creates completers containing definitions from all directories listed + // in the environment's **$PATH** variable. + let mut file_completers = if let Ok(val) = env::var("PATH") { + val.split(if cfg!(unix) { ':' } else { ';' }) + .map(|s| IonFileCompleter::new(Some(s), dirs_ptr, vars_ptr)) + .collect() + } else { + vec![IonFileCompleter::new(Some("/bin/"), dirs_ptr, vars_ptr)] + }; + + // Also add files/directories in the current directory to the completion list. + if let Ok(current_dir) = env::current_dir() { + if let Some(url) = current_dir.to_str() { + file_completers.push(IonFileCompleter::new(Some(url), dirs_ptr, vars_ptr)); + } + } + + // Merge the collected definitions with the file path definitions. + let completer = MultiCompleter::new(file_completers, custom_completer); + + // Replace the shell's current completer with the newly-created completer. + mem::replace(&mut editor.context().completer, Some(Box::new(completer))); + } + } + }); + + match line { + Ok(line) => return Some(line), + // Handles Ctrl + C + Err(ref err) if err.kind() == ErrorKind::Interrupted => return None, + // Handles Ctrl + D + Err(ref err) if err.kind() == ErrorKind::UnexpectedEof => break, + Err(err) => { + let stderr = io::stderr(); + let mut stderr = stderr.lock(); + let _ = writeln!(stderr, "ion: liner: {}", err); + return None + } + } + } + } + + let previous_status = self.previous_status; + self.exit(previous_status); + } + fn terminate_script_quotes<I: Iterator<Item = String>>(&mut self, mut lines: I) { while let Some(command) = lines.next() { let mut buffer = QuoteTerminator::new(command); @@ -237,3 +367,24 @@ fn word_divide(buf: &Buffer) -> Vec<(usize, usize)> { } res } + +/// Infer if the given filename is actually a partial filename +fn complete_as_file(current_dir : PathBuf, filename : String, index : usize) -> bool { + let filename = filename.trim(); + let mut file = current_dir.clone(); + file.push(&filename); + // If the user explicitly requests a file through this syntax then complete as a file + if filename.trim().starts_with(".") { return true; } + // If the file starts with a dollar sign, it's a variable, not a file + if filename.trim().starts_with("$") { return false; } + // Once we are beyond the first string, assume its a file + if index > 0 { return true; } + // If we are referencing a file that exists then just complete to that file + if file.exists() { return true; } + // If we have a partial file inside an existing directory, e.g. /foo/b when /foo/bar + // exists, then treat it as file as long as `foo` isn't the current directory, otherwise + // this would apply to any string `foo` + if let Some(parent) = file.parent() { return parent.exists() && parent != current_dir; } + // By default assume its not a file + false +} diff --git a/src/shell/mod.rs b/src/shell/mod.rs index b2d471b5..d1f889a9 100644 --- a/src/shell/mod.rs +++ b/src/shell/mod.rs @@ -20,14 +20,12 @@ pub use self::job::{Job, JobKind}; pub use self::flow::FlowLogic; pub use self::binary::Binary; - use app_dirs::{AppDataType, AppInfo, app_root}; use builtins::*; use fnv::FnvHashMap; -use liner::{Context, CursorPosition, Event, EventKind, BasicCompleter}; -use parser::*; +use liner::Context; +use parser::ArgumentSplitter; use parser::peg::Pipeline; -use self::completer::{MultiCompleter, IonFileCompleter}; use self::directory_stack::DirectoryStack; use self::flags::*; use self::flow_control::{FlowControl, Function, FunctionArgument, Type}; @@ -36,13 +34,11 @@ use self::job_control::{JobControl, BackgroundProcess}; use self::pipe::PipelineExecution; use self::status::*; use self::variables::Variables; -use smallstring::SmallString; use smallvec::SmallVec; use std::env; use std::fs::File; -use std::io::{self, ErrorKind, Write}; -use std::mem; -use std::path::{PathBuf, Path}; +use std::io::{self, Write}; +use std::path::Path; use std::process; use std::sync::mpsc::Receiver; use std::sync::{Arc, Mutex}; @@ -108,140 +104,6 @@ impl<'a> Shell<'a> { } } - /// Infer if the given filename is actually a partial filename - fn complete_as_file(current_dir : PathBuf, filename : String, index : usize) -> bool { - let filename = filename.trim(); - let mut file = current_dir.clone(); - file.push(&filename); - // If the user explicitly requests a file through this syntax then complete as a file - if filename.trim().starts_with(".") { return true; } - // If the file starts with a dollar sign, it's a variable, not a file - if filename.trim().starts_with("$") { return false; } - // Once we are beyond the first string, assume its a file - if index > 0 { return true; } - // If we are referencing a file that exists then just complete to that file - if file.exists() { return true; } - // If we have a partial file inside an existing directory, e.g. /foo/b when /foo/bar - // exists, then treat it as file as long as `foo` isn't the current directory, otherwise - // this would apply to any string `foo` - if let Some(parent) = file.parent() { return parent.exists() && parent != current_dir; } - // By default assume its not a file - false - } - - /// Ion's interface to Liner's `read_line` method, which handles everything related to - /// rendering, controlling, and getting input from the prompt. - fn readln(&mut self) -> Option<String> { - { - let vars_ptr = &self.variables as *const Variables; - let dirs_ptr = &self.directory_stack as *const DirectoryStack; - let funcs = &self.functions; - let vars = &self.variables; - let builtins = self.builtins; - - // Collects the current list of values from history for completion. - let history = &self.context.as_ref().unwrap().history.buffers.iter() - // Map each underlying `liner::Buffer` into a `String`. - .map(|x| x.chars().cloned().collect()) - // Collect each result into a vector to avoid borrowing issues. - .collect::<Vec<SmallString>>(); - - loop { - let prompt = self.prompt(); - let line = self.context.as_mut().unwrap().read_line(prompt, &mut move |Event { editor, kind }| { - if let EventKind::BeforeComplete = kind { - let (words, pos) = editor.get_words_and_cursor_position(); - - let filename = match pos { - CursorPosition::InWord(index) => index > 0, - CursorPosition::InSpace(Some(_), _) => true, - CursorPosition::InSpace(None, _) => false, - CursorPosition::OnWordLeftEdge(index) => index >= 1, - CursorPosition::OnWordRightEdge(index) => { - match (words.into_iter().nth(index), env::current_dir()) { - (Some((start, end)), Ok(file)) => { - let filename = editor.current_buffer().range(start, end); - Shell::complete_as_file(file, filename, index) - }, - _ => false, - } - } - }; - - if filename { - if let Ok(current_dir) = env::current_dir() { - if let Some(url) = current_dir.to_str() { - let completer = IonFileCompleter::new(Some(url), dirs_ptr, vars_ptr); - mem::replace(&mut editor.context().completer, Some(Box::new(completer))); - } - } - } else { - // Creates a list of definitions from the shell environment that will be used - // in the creation of a custom completer. - let words = builtins.iter() - // Add built-in commands to the completer's definitions. - .map(|(&s, _)| Identifier::from(s)) - // Add the history list to the completer's definitions. - .chain(history.iter().cloned()) - // Add the aliases to the completer's definitions. - .chain(vars.aliases.keys().cloned()) - // Add the list of available functions to the completer's definitions. - .chain(funcs.keys().cloned()) - // Add the list of available variables to the completer's definitions. - // TODO: We should make it free to do String->SmallString - // and mostly free to go back (free if allocated) - .chain(vars.get_vars().into_iter().map(|s| ["$", &s].concat().into())) - .collect(); - - // Initialize a new completer from the definitions collected. - let custom_completer = BasicCompleter::new(words); - - // Creates completers containing definitions from all directories listed - // in the environment's **$PATH** variable. - let mut file_completers = if let Ok(val) = env::var("PATH") { - val.split(if cfg!(unix) { ':' } else { ';' }) - .map(|s| IonFileCompleter::new(Some(s), dirs_ptr, vars_ptr)) - .collect() - } else { - vec![IonFileCompleter::new(Some("/bin/"), dirs_ptr, vars_ptr)] - }; - - // Also add files/directories in the current directory to the completion list. - if let Ok(current_dir) = env::current_dir() { - if let Some(url) = current_dir.to_str() { - file_completers.push(IonFileCompleter::new(Some(url), dirs_ptr, vars_ptr)); - } - } - - // Merge the collected definitions with the file path definitions. - let completer = MultiCompleter::new(file_completers, custom_completer); - - // Replace the shell's current completer with the newly-created completer. - mem::replace(&mut editor.context().completer, Some(Box::new(completer))); - } - } - }); - - match line { - Ok(line) => return Some(line), - // Handles Ctrl + C - Err(ref err) if err.kind() == ErrorKind::Interrupted => return None, - // Handles Ctrl + D - Err(ref err) if err.kind() == ErrorKind::UnexpectedEof => break, - Err(err) => { - let stderr = io::stderr(); - let mut stderr = stderr.lock(); - let _ = writeln!(stderr, "ion: liner: {}", err); - return None - } - } - } - } - - let previous_status = self.previous_status; - self.exit(previous_status); - } - pub fn exit(&mut self, status: i32) -> ! { if let Some(context) = self.context.as_mut() { context.history.commit_history(); @@ -286,15 +148,6 @@ impl<'a> Shell<'a> { } } - pub fn prompt(&self) -> String { - if self.flow_control.level == 0 { - let prompt_var = self.variables.get_var_or_empty("PROMPT"); - expand_string(&prompt_var, &get_expanders!(&self.variables, &self.directory_stack), false).join(" ") - } else { - " ".repeat(self.flow_control.level as usize) - } - } - /// Executes a pipeline and returns the final exit status of the pipeline. /// To avoid infinite recursion when using aliases, the noalias boolean will be set the true /// if an alias branch was executed. -- GitLab