conch-runtime 0.1.2

A library for evaluating/executing programs written in the shell programming language.
Documentation
use {EXIT_ERROR, EXIT_SUCCESS, HOME, POLLED_TWICE};
use clap::{App, AppSettings, Arg, ArgMatches, Result as ClapResult};
use env::{AsyncIoEnvironment, ChangeWorkingDirectoryEnvironment, FileDescEnvironment,
          StringWrapper, ReportErrorEnvironment, VariableEnvironment, WorkingDirectoryEnvironment};
use io::FileDesc;
use future::{Async, EnvFuture, Poll};
use path::NormalizedPath;
use spawn::{ExitResult, Spawn};
use std::borrow::{Borrow, Cow};
use std::error::Error;
use std::fmt;
use std::io;
use std::path::{Component, Path, PathBuf};
use void::Void;

const CD: &str = "cd";
const ARG_LOGICAL: &str = "L";
const ARG_PHYSICAL: &str = "P";
const ARG_DIR: &str = "dir";

const LONG_ABOUT: &str = "Changes the current working directory to the specified
argument, provided the argument points to a valid directory. If the operation is
successful, $PWD will be updated with the new working directory, and $OLDPWD
will be set to the previous working directory.

If no argument is specified, the value of $HOME will be used as the new working
directory. If `-` is specified as an argument, the value of $OLDPWD will be used
instead, and the new working directory will be printed to standard output.

If the specified argument is neither an absolute path, nor begins with ./ or
../, the value of $CDPATH will be searched for alternative directory names
(seprated by `:`) to use as a prefix for the argument. If a valid directory is
discovered using an alternative directory name from $CDPATH, the new working
directory will be printed to standard output.";

lazy_static! {
    static ref CDPATH: String = { String::from("CDPATH") };
    static ref OLDPWD: String = { String::from("OLDPWD") };
}

#[derive(Debug)]
enum VarNotDefinedError {
    Home,
    OldPwd,
}

impl Error for VarNotDefinedError {
    fn description(&self) -> &str {
        match *self {
            VarNotDefinedError::Home => "HOME not set",
            VarNotDefinedError::OldPwd => "OLDPWD not set",
        }
    }
}

impl fmt::Display for VarNotDefinedError {
    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
        write!(fmt, "{}", self.description())
    }
}

impl_generic_builtin_cmd_no_spawn! {
    /// Represents a `cd` builtin command which will
    /// change the current working directory.
    pub struct Cd;

    /// Creates a new `cd` builtin command with the provided arguments.
    pub fn cd();

    /// A future representing a fully spawned `cd` builtin command.
    pub struct SpawnedCd;

    /// A future representing a fully spawned `cd` builtin command
    /// which no longer requires an environment to run.
    pub struct CdFuture;
}

impl<T, I, E: ?Sized> Spawn<E> for Cd<I>
    where T: StringWrapper,
          I: Iterator<Item = T>,
          E: AsyncIoEnvironment
              + ChangeWorkingDirectoryEnvironment
              + FileDescEnvironment
              + ReportErrorEnvironment
              + VariableEnvironment
              + WorkingDirectoryEnvironment,
          E::FileHandle: Borrow<FileDesc>,
          E::VarName: Borrow<String> + From<String>,
          E::Var: Borrow<String> + From<String>,
{
    type EnvFuture = SpawnedCd<I>;
    type Future = ExitResult<CdFuture<E::WriteAll>>;
    type Error = Void;

    fn spawn(self, _env: &E) -> Self::EnvFuture {
        SpawnedCd {
            args: Some(self.args)
        }
    }
}

impl<I> SpawnedCd<I> {
    fn get_matches(&mut self) -> ClapResult<ArgMatches<'static>>
        where I: Iterator,
              I::Item: StringWrapper,
    {
        let app = App::new(CD)
            .setting(AppSettings::NoBinaryName)
            .setting(AppSettings::DisableVersion)
            .about("Changes the current working directory of the shell")
            .long_about(LONG_ABOUT)
            .arg(Arg::with_name(ARG_LOGICAL)
                 .short(ARG_LOGICAL)
                 .multiple(true)
                 .overrides_with(ARG_PHYSICAL)
                 .help("Handle paths logically (symbolic links will not be resolved)")
            )
            .arg(Arg::with_name(ARG_PHYSICAL)
                 .short(ARG_PHYSICAL)
                 .multiple(true)
                 .overrides_with(ARG_LOGICAL)
                 .help("Handle paths physically (all symbolic links resolved)")
            )
            .arg(Arg::with_name(ARG_DIR)
                 .help("An absolute or relative path for the what shall become the new working directory")
            );

        let app_args = self.args.take()
            .expect(POLLED_TWICE)
            .into_iter()
            .map(StringWrapper::into_owned);

        app.get_matches_from_safe(app_args)
    }
}

#[derive(Debug)]
struct Flags<'a> {
    resolve_symlinks: bool,
    dir: Option<&'a str>,
}

fn get_flags<'a>(matches: &'a ArgMatches<'a>) -> Flags<'a> {
    Flags {
        resolve_symlinks: matches.is_present(ARG_PHYSICAL),
        dir: matches.value_of(ARG_DIR),
    }
}

impl<T, I, E: ?Sized> EnvFuture<E> for SpawnedCd<I>
    where T: StringWrapper,
          I: Iterator<Item = T>,
          E: AsyncIoEnvironment
              + ChangeWorkingDirectoryEnvironment
              + FileDescEnvironment
              + ReportErrorEnvironment
              + VariableEnvironment
              + WorkingDirectoryEnvironment,
          E::FileHandle: Borrow<FileDesc>,
          E::VarName: Borrow<String> + From<String>,
          E::Var: Borrow<String> + From<String>,
{
    type Item = ExitResult<CdFuture<E::WriteAll>>;
    type Error = Void;

    fn poll(&mut self, env: &mut E) -> Poll<Self::Item, Self::Error> {
        let matches = try_and_report!(CD, self.get_matches(), env);
        let flags = get_flags(&matches);

        let should_print_pwd;
        let new_working_dir = {
            let (new_working_dir, spp) = try_and_report!(CD, get_dir_arg(flags.dir, env), env);
            should_print_pwd = spp;

            if flags.resolve_symlinks {
                match new_working_dir {
                    Cow::Borrowed(dir) => {
                        let mut normalized_path = NormalizedPath::new();
                        try_and_report!(CD, normalized_path.join_normalized_physical(dir), env);
                        normalized_path
                    },
                    Cow::Owned(b) => try_and_report!(CD, NormalizedPath::new_normalized_physical(b), env),
                }
            } else {
                match new_working_dir {
                    Cow::Borrowed(dir) => {
                        let mut normalized_path = NormalizedPath::new();
                        normalized_path.join_normalized_logial(dir);
                        normalized_path
                    },
                    Cow::Owned(buf) => NormalizedPath::new_normalized_logical(buf),
                }
            }
        };

        let new_working_dir = Cow::Owned(new_working_dir.into_inner());
        match try_and_report!(CD, perform_cd_change(should_print_pwd, new_working_dir, env), env) {
            Some(pwd) => generate_and_print_output!(CD, env, |_| -> Result<_, Void> {
                Ok(pwd.into_bytes())
            }),
            None => Ok(Async::Ready(ExitResult::from(EXIT_SUCCESS))),
        }
    }

    fn cancel(&mut self, _env: &mut E) {
        self.args.take();
    }
}

fn get_dir_arg<'a, E: ?Sized>(dir: Option<&'a str>, env: &'a E)
    -> Result<(Cow<'a, Path>, bool), VarNotDefinedError>
    where E: VariableEnvironment + WorkingDirectoryEnvironment,
          E::VarName: Borrow<String>,
          E::Var: Borrow<String>,
{
    let mut should_print_pwd = false;
    let dir = match dir {
        None => match env.var(&HOME) {
            Some(home) => Path::new((*home).borrow()),
            None => return Err(VarNotDefinedError::Home),
        },
        Some("-") => match env.var(&OLDPWD) {
            Some(oldpwd) => {
                should_print_pwd = true;
                Path::new((*oldpwd).borrow())
            },
            None => return Err(VarNotDefinedError::OldPwd),
        },
        Some(d) => Path::new(d),
    };

    let candidate = if is_cdpath_candidate(dir) {
        env.var(&CDPATH).and_then(|cdpath| cdpath_candidate(dir, cdpath.borrow().as_str(), env))
    } else {
        None
    };

    let dir = match candidate {
        Some(c) => {
            should_print_pwd = true;
            c
        },
        None => env.path_relative_to_working_dir(Cow::Borrowed(dir)),
    };

    Ok((dir, should_print_pwd))
}

fn is_cdpath_candidate(path: &Path) -> bool {
    if path.is_absolute() {
        return false;
    }

    match path.components().next() {
        Some(Component::CurDir) |
        Some(Component::ParentDir) => false,
        _ => true,
    }
}

fn cdpath_candidate<'a, E: ?Sized>(dir: &'a Path, cdpaths: &'a str, env: &'a E)
    -> Option<Cow<'a, Path>>
    where E: WorkingDirectoryEnvironment
{
    cdpaths.split(':')
        .map(PathBuf::from)
        .map(|buf| buf.join(dir))
        .map(|buf| env.path_relative_to_working_dir(Cow::Owned(buf)))
        .find(|path| path.is_dir())
}

fn perform_cd_change<E: ?Sized>(should_print_pwd: bool, new_working_dir: Cow<Path>, env: &mut E)
    -> io::Result<Option<String>>
    where E: ChangeWorkingDirectoryEnvironment
              + VariableEnvironment
              + WorkingDirectoryEnvironment,
          E::VarName: From<String>,
          E::Var: From<String>,
{
    let old_pwd = env.current_working_dir()
        .to_string_lossy()
        .into_owned();

    env.change_working_dir(new_working_dir)?;

    let pwd = env.current_working_dir()
        .to_string_lossy()
        .into_owned();

    let ret = if should_print_pwd {
        Some(format!("{}\n", pwd))
    } else {
        None
    };

    env.set_var(OLDPWD.clone().into(), old_pwd.into());
    env.set_var("PWD".to_owned().into(), pwd.into());

    Ok(ret)
}