Something went wrong on our end
Forked from
redox-os / ion
1214 commits behind the upstream repository.
-
Michael Aaron Murphy authored
Closes #577
Michael Aaron Murphy authoredCloses #577
mod.rs 20.84 KiB
mod assignments;
mod binary;
mod completer;
mod flow;
mod history;
mod job;
mod pipe_exec;
pub(crate) mod colors;
pub(crate) mod directory_stack;
pub mod flags;
pub(crate) mod plugins;
pub(crate) mod flow_control;
pub(crate) mod signals;
pub mod status;
pub mod variables;
pub(crate) use self::binary::Binary;
pub(crate) use self::flow::FlowLogic;
pub(crate) use self::history::{IgnoreSetting, ShellHistory};
pub(crate) use self::job::{Job, JobKind};
pub(crate) use self::pipe_exec::{foreground, job_control};
use self::directory_stack::DirectoryStack;
use self::flags::*;
use self::flow_control::{FlowControl, Function, FunctionError};
use self::foreground::ForegroundSignals;
use self::job_control::{BackgroundProcess, JobControl};
use self::pipe_exec::PipelineExecution;
use self::status::*;
use self::variables::Variables;
use app_dirs::{app_root, AppDataType, AppInfo};
use builtins::{BuiltinMap, BUILTINS};
use fnv::FnvHashMap;
use liner::Context;
use parser::{ArgumentSplitter, Expander, Select};
use parser::Terminator;
use parser::pipelines::Pipeline;
use smallvec::SmallVec;
use std::env;
use std::fmt::{self, Display, Formatter};
use std::fs::File;
use std::io::{self, Read, Write};
use std::iter::FromIterator;
use std::ops::Deref;
use std::path::Path;
use std::process;
use std::sync::{Arc, Mutex};
use std::sync::atomic::Ordering;
use std::time::SystemTime;
use sys;
use types::*;
#[allow(dead_code)]
#[derive(Debug)]
pub enum IonError {
Fork(io::Error),
DoesNotExist,
Unterminated,
Function(FunctionError),
}
impl Display for IonError {
fn fmt(&self, fmt: &mut Formatter) -> fmt::Result {
match *self {
IonError::Fork(ref why) => writeln!(fmt, "failed to fork: {}", why),
IonError::DoesNotExist => writeln!(fmt, "element does not exist"),
IonError::Function(ref why) => writeln!(fmt, "function error: {}", why),
IonError::Unterminated => writeln!(fmt, "input was not terminated"),
}
}
}
#[allow(dead_code)]
pub struct IonResult {
pub pid: u32,
pub stdout: File,
pub stderr: File,
}
/// The shell structure is a megastructure that manages all of the state of the shell throughout
/// the entirety of the
/// program. It is initialized at the beginning of the program, and lives until the end of the
/// program.
pub struct Shell {
/// Contains a list of built-in commands that were created when the program started.
pub(crate) builtins: &'static BuiltinMap,
/// Contains the history, completions, and manages writes to the history file.
/// Note that the context is only available in an interactive session.
pub(crate) context: Option<Context>,
/// Contains the aliases, strings, and array variable maps.
pub(crate) variables: Variables,
/// Contains the current state of flow control parameters.
flow_control: FlowControl,
/// Contains the directory stack parameters.
pub(crate) directory_stack: DirectoryStack,
/// Contains all of the user-defined functions that have been created.
pub(crate) functions: FnvHashMap<Identifier, Function>,
/// When a command is executed, the final result of that command is stored here.
pub previous_status: i32,
/// The job ID of the previous command sent to the background.
pub(crate) previous_job: u32,
/// Contains all the boolean flags that control shell behavior.
pub flags: u8,
/// A temporary field for storing foreground PIDs used by the pipeline execution.
foreground: Vec<u32>,
/// Contains information on all of the active background processes that are being managed
/// by the shell.
pub(crate) background: Arc<Mutex<Vec<BackgroundProcess>>>,
/// If set, denotes that this shell is running as a background job.
pub(crate) is_background_shell: bool,
/// Set when a signal is received, this will tell the flow control logic to abort.
pub(crate) break_flow: bool,
// Useful for disabling the execution of the `tcsetpgrp` call.
pub(crate) is_library: bool,
/// When the `fg` command is run, this will be used to communicate with the specified
/// background process.
foreground_signals: Arc<ForegroundSignals>,
/// Stores the patterns used to determine whether a command should be saved in the history
/// or not
ignore_setting: IgnoreSetting,
}
impl<'a> Shell {
#[allow(dead_code)]
/// Panics if DirectoryStack construction fails
pub(crate) fn new_bin() -> Shell {
Shell {
builtins: BUILTINS,
context: None,
variables: Variables::default(),
flow_control: FlowControl::default(),
directory_stack: DirectoryStack::new(),
functions: FnvHashMap::default(),
previous_job: !0,
previous_status: 0,
flags: 0,
foreground: Vec::new(),
background: Arc::new(Mutex::new(Vec::new())),
is_background_shell: false,
is_library: false,
break_flow: false,
foreground_signals: Arc::new(ForegroundSignals::new()),
ignore_setting: IgnoreSetting::default(),
}
}
#[allow(dead_code)]
pub fn new() -> Shell {
Shell {
builtins: BUILTINS,
context: None,
variables: Variables::default(),
flow_control: FlowControl::default(),
directory_stack: DirectoryStack::new(),
functions: FnvHashMap::default(),
previous_job: !0,
previous_status: 0,
flags: 0,
foreground: Vec::new(),
background: Arc::new(Mutex::new(Vec::new())),
is_background_shell: false,
is_library: true,
break_flow: false,
foreground_signals: Arc::new(ForegroundSignals::new()),
ignore_setting: IgnoreSetting::default(),
}
}
pub(crate) fn next_signal(&self) -> Option<i32> {
match signals::PENDING.swap(0, Ordering::SeqCst) {
0 => None,
signals::SIGINT => Some(sys::SIGINT),
signals::SIGHUP => Some(sys::SIGHUP),
signals::SIGTERM => Some(sys::SIGTERM),
_ => unreachable!(),
}
}
pub(crate) fn exit(&mut self, status: i32) -> ! {
if let Some(context) = self.context.as_mut() {
context.history.commit_history();
}
process::exit(status);
}
/// This function updates variables that need to be kept consistent with each iteration
/// of the prompt. For example, the PWD variable needs to be updated to reflect changes to
/// the
/// the current working directory.
fn update_variables(&mut self) {
// Update the PWD (Present Working Directory) variable if the current working directory has
// been updated.
env::current_dir().ok().map_or_else(
|| env::set_var("PWD", "?"),
|path| {
let pwd = self.get_var_or_empty("PWD");
let pwd: &str = &pwd;
let current_dir = path.to_str().unwrap_or("?");
if pwd != current_dir {
env::set_var("OLDPWD", pwd);
env::set_var("PWD", current_dir);
}
},
)
}
/// Evaluates the source init file in the user's home directory.
pub fn evaluate_init_file(&mut self) {
match app_root(
AppDataType::UserConfig,
&AppInfo {
name: "ion",
author: "Redox OS Developers",
},
) {
Ok(mut initrc) => {
initrc.push("initrc");
if initrc.exists() {
if let Err(err) = self.execute_script(&initrc) {
eprintln!("ion: {}", err);
}
} else {
eprintln!("ion: creating initrc file at {:?}", initrc);
if let Err(why) = File::create(initrc) {
eprintln!("ion: could not create initrc file: {}", why);
}
}
}
Err(why) => {
eprintln!("ion: unable to get config root: {}", why);
}
}
}
/// 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.
fn run_pipeline(&mut self, pipeline: &mut Pipeline) -> Option<i32> {
let command_start_time = SystemTime::now();
let builtins = self.builtins;
// Expand any aliases found
for job_no in 0..pipeline.items.len() {
if let Some(alias) = {
let key: &str = pipeline.items[job_no].job.command.as_ref();
self.variables.aliases.get(key)
} {
let new_args = ArgumentSplitter::new(alias)
.map(String::from)
.chain(pipeline.items[job_no].job.args.drain().skip(1))
.collect::<SmallVec<[String; 4]>>();
pipeline.items[job_no].job.command = new_args[0].clone().into();
pipeline.items[job_no].job.args = new_args;
}
}
// Branch if -> input == shell command i.e. echo
let exit_status = if let Some(command) = {
let key: &str = pipeline.items[0].job.command.as_ref();
builtins.get(key)
} {
pipeline.expand(self);
// Run the 'main' of the command and set exit_status
if !pipeline.requires_piping() {
if self.flags & PRINT_COMMS != 0 {
eprintln!("> {}", pipeline.to_string());
}
let borrowed = &pipeline.items[0].job.args;
let small: SmallVec<[&str; 4]> = borrowed.iter().map(|x| x as &str).collect();
if self.flags & NO_EXEC != 0 {
Some(SUCCESS)
} else {
Some((command.main)(&small, self))
}
} else {
Some(self.execute_pipeline(pipeline))
}
// Branch else if -> input == shell function and set the exit_status
} else if let Some(function) = self.functions.get(&pipeline.items[0].job.command).cloned() {
if !pipeline.requires_piping() {
let args: &[String] = pipeline.items[0].job.args.deref();
let args: Vec<&str> = args.iter().map(AsRef::as_ref).collect();
match function.execute(self, &args) {
Ok(()) => None,
Err(FunctionError::InvalidArgumentCount) => {
eprintln!("ion: invalid number of function arguments supplied");
Some(FAILURE)
}
Err(FunctionError::InvalidArgumentType(expected_type, value)) => {
eprintln!(
"ion: function argument has invalid type: expected {}, found value \
\'{}\'",
expected_type,
value
);
Some(FAILURE)
}
}
} else {
Some(self.execute_pipeline(pipeline))
}
} else {
pipeline.expand(self);
Some(self.execute_pipeline(pipeline))
};
// 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.
if let Some(context) = self.context.as_mut() {
if "1" == self.variables.get_var_or_empty("RECORD_SUMMARY") {
if let Ok(elapsed_time) = command_start_time.elapsed() {
let summary = format!(
"#summary# elapsed real time: {}.{:09} seconds",
elapsed_time.as_secs(),
elapsed_time.subsec_nanos()
);
context.history.push(summary.into()).unwrap_or_else(|err| {
let stderr = io::stderr();
let mut stderr = stderr.lock();
let _ = writeln!(stderr, "ion: {}\n", err);
});
}
}
}
// Retrieve the exit_status and set the $? variable and history.previous_status
if let Some(code) = exit_status {
self.set_var("?", &code.to_string());
self.previous_status = code;
}
exit_status
}
/// Sets a variable.
pub fn set_var(&mut self, name: &str, value: &str) { self.variables.set_var(name, value); }
/// Gets a variable, if it exists.
pub fn get_var(&self, name: &str) -> Option<String> { self.variables.get_var(name) }
/// Obtains a variable, returning an empty string if it does not exist.
pub(crate) fn get_var_or_empty(&self, name: &str) -> String {
self.variables.get_var_or_empty(name)
}
/// Gets an array, if it exists.
pub fn get_array(&self, name: &str) -> Option<&[String]> {
self.variables.get_array(name).map(SmallVec::as_ref)
}
#[allow(dead_code)]
/// A method for executing commands in the Ion shell without capturing.
///
/// Takes command(s) as a string argument, parses them, and executes them the same as it
/// would if you had executed the command(s) in the command line REPL interface for Ion.
/// If the supplied command is not terminated, then an error will be returned.
pub fn execute_command<CMD>(&mut self, command: CMD) -> Result<i32, IonError>
where CMD: Into<Terminator>
{
let mut terminator = command.into();
if terminator.is_terminated() {
self.on_command(&terminator.consume());
Ok(self.previous_status)
} else {
Err(IonError::Unterminated)
}
}
#[allow(dead_code)]
/// A method for executing scripts in the Ion shell without capturing.
///
/// Given a `Path`, this method will attempt to execute that file as a script, and then
/// returns the final exit status of the evaluated script.
pub fn execute_script<SCRIPT: AsRef<Path>>(&mut self, script: SCRIPT) -> io::Result<i32> {
let mut script = File::open(script.as_ref())?;
let capacity = script.metadata().ok().map_or(0, |x| x.len());
let mut command_list = String::with_capacity(capacity as usize);
let _ = script.read_to_string(&mut command_list)?;
if FAILURE == self.terminate_script_quotes(command_list.lines().map(|x| x.to_owned())) {
self.previous_status = FAILURE;
}
Ok(self.previous_status)
}
#[allow(dead_code)]
/// A method for executing a function with the given `name`, using `args` as the input.
/// If the function does not exist, an `IonError::DoesNotExist` is returned.
pub fn execute_function(&mut self, name: &str, args: &[&str]) -> Result<i32, IonError> {
self.functions
.get_mut(name.into())
.ok_or(IonError::DoesNotExist)
.map(|fnc| fnc.clone())
.and_then(|function| {
function
.execute(self, args)
.map(|_| self.previous_status)
.map_err(IonError::Function)
})
}
/// A method for capturing the output of the shell, and performing actions without modifying
/// the state of the original shell. This performs a fork, taking a closure that controls
/// the
/// shell in the child of the fork.
///
/// The method is non-blocking, and therefore will immediately return file handles to the
/// stdout and stderr of the child. The PID of the child is returned, which may be used to
/// wait for and obtain the exit status.
pub fn fork<F: FnMut(&mut Shell)>(&self, mut child_func: F) -> Result<IonResult, IonError> {
use std::os::unix::io::{AsRawFd, FromRawFd};
use std::process::exit;
use sys;
sys::signals::block();
let (stdout_read, stdout_write) = sys::pipe2(sys::O_CLOEXEC)
.map(|fds| unsafe { (File::from_raw_fd(fds.0), File::from_raw_fd(fds.1)) })
.map_err(IonError::Fork)?;
let (stderr_read, stderr_write) = sys::pipe2(sys::O_CLOEXEC)
.map(|fds| unsafe { (File::from_raw_fd(fds.0), File::from_raw_fd(fds.1)) })
.map_err(IonError::Fork)?;
match unsafe { sys::fork() } {
Ok(0) => {
sys::signals::unblock();
let _ = sys::dup2(stdout_write.as_raw_fd(), sys::STDOUT_FILENO);
let _ = sys::dup2(stderr_write.as_raw_fd(), sys::STDERR_FILENO);
drop(stdout_write);
drop(stdout_read);
drop(stderr_write);
drop(stderr_read);
let mut shell: Shell = unsafe { (self as *const Shell).read() };
child_func(&mut shell);
// Reap the child, enabling the parent to get EOF from the read end of the pipe.
exit(shell.previous_status);
}
Ok(pid) => {
// Drop the write end of the pipe, because the parent will not use it.
drop(stdout_write);
drop(stderr_write);
Ok(IonResult {
pid,
stdout: stdout_read,
stderr: stderr_read,
})
}
Err(why) => Err(IonError::Fork(why)),
}
}
}
impl<'a> Expander for Shell {
fn tilde(&self, input: &str) -> Option<String> {
self.variables.tilde_expansion(input, &self.directory_stack)
}
/// Expand an array variable with some selection
fn array(&self, array: &str, selection: Select) -> Option<Array> {
let mut found = match self.variables.get_array(array) {
Some(array) => match selection {
Select::None => None,
Select::All => Some(array.clone()),
Select::Index(id) => id.resolve(array.len())
.and_then(|n| array.get(n))
.map(|x| Array::from_iter(Some(x.to_owned()))),
Select::Range(range) => if let Some((start, length)) = range.bounds(array.len()) {
if array.len() <= start {
None
} else {
Some(
array
.iter()
.skip(start)
.take(length)
.map(|x| x.to_owned())
.collect::<Array>(),
)
}
} else {
None
},
Select::Key(_) => None,
},
None => None,
};
if found.is_none() {
found = match self.variables.get_map(array) {
Some(map) => match selection {
Select::All => {
let mut arr = Array::new();
for (_, value) in map {
arr.push(value.clone());
}
Some(arr)
}
Select::Key(ref key) => {
Some(array![map.get(key.get()).unwrap_or(&"".into()).clone()])
}
_ => None,
},
None => None,
}
}
found
}
/// Expand a string variable given if its quoted / unquoted
fn variable(&self, variable: &str, quoted: bool) -> Option<Value> {
use ascii_helpers::AsciiReplace;
if quoted {
self.get_var(variable)
} else {
self.get_var(variable).map(|x| x.ascii_replace('\n', ' ').into())
}
}
/// Uses a subshell to expand a given command.
fn command(&self, command: &str) -> Option<Value> {
let mut output = None;
match self.fork(move |shell| shell.on_command(command)) {
Ok(mut result) => {
let mut string = String::new();
match result.stdout.read_to_string(&mut string) {
Ok(_) => output = Some(string),
Err(why) => {
eprintln!("ion: error reading stdout of child: {}", why);
}
}
}
Err(why) => {
eprintln!("ion: fork error: {}", why);
}
}
// Ensure that the parent retains ownership of the terminal before exiting.
let _ = sys::tcsetpgrp(sys::STDIN_FILENO, process::id());
output
}
}