cargo-script 0.2.5

A Cargo subcommand designed to let people quickly and easily run Rust "scripts" which can make use of Cargo's package ecosystem.
/*
Copyright ⓒ 2017 cargo-script contributors.

Licensed under the MIT license (see LICENSE or <http://opensource.org
/licenses/MIT>) or the Apache License, Version 2.0 (see LICENSE of
<http://www.apache.org/licenses/LICENSE-2.0>), at your option. All
files in the project carrying such notice may not be copied, modified,
or distributed except according to those terms.
*/
/*!

This module contains code related to template support.
*/
use std::borrow::Cow;
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use clap;
use open;
use regex::Regex;
use consts;
use error::{Blame, MainError, Result, ResultExt};
use platform;

lazy_static! {
    static ref RE_SUB: Regex = Regex::new(r#"#\{([A-Za-z_][A-Za-z0-9_]*)}"#).unwrap();
}

#[derive(Debug)]
pub enum Args {
    Dump { name: String },
    List,
    Show { path: bool },
}

impl Args {
    pub fn subcommand() -> clap::App<'static, 'static> {
        use clap::{AppSettings, Arg, SubCommand};

        SubCommand::with_name("templates")
            .about("Manage Cargo Script expression templates.")
            .setting(AppSettings::SubcommandRequiredElseHelp)

            .subcommand(SubCommand::with_name("dump")
                .about("Outputs the contents of a template to standard output.")

                .arg(Arg::with_name("template")
                    .help("Name of template to dump.")
                    .index(1)
                    .required(true)
                )
            )

            .subcommand(SubCommand::with_name("list")
                .about("List the available templates.")
            )

            .subcommand(SubCommand::with_name("show")
                .about("Open the template folder in a file browser.")

                .arg(Arg::with_name("show_path")
                    .help("Output the path to the template folder to standard output instead.")
                    .long("path")
                )
            )
    }

    pub fn parse(m: &clap::ArgMatches) -> Self {
        match m.subcommand() {
            ("dump", Some(m)) => {
                Args::Dump {
                    name: m.value_of("template").unwrap().into(),
                }
            },
            ("list", _) => Args::List,
            ("show", Some(m)) => {
                Args::Show {
                    path: m.is_present("show_path"),
                }
            },
            (name, _) => panic!("bad subcommand: {:?}", name)
        }
    }
}

pub fn try_main(args: Args) -> Result<i32> {
    match args {
        Args::Dump { name } => try!(dump(&name)),
        Args::List => try!(list()),
        Args::Show { path } => try!(show(path)),
    }

    Ok(0)
}

pub fn expand(src: &str, subs: &HashMap<&str, &str>) -> Result<String> {
    // The estimate of final size is the sum of the size of all the input.
    let sub_size = subs
        .iter()
        .map(|(_, v)| v.len())
        .sum::<usize>();
    let est_size = src.len() + sub_size;

    let mut anchor = 0;
    let mut result = String::with_capacity(est_size);

    for m in RE_SUB.captures_iter(src) {
        // Concatenate the static bit just before the match.
        let (m_start, m_end) = {
            let m_0 = m.get(0).unwrap();
            (m_0.start(), m_0.end())
        };
        let prior_slice = anchor..m_start;
        anchor = m_end;
        result.push_str(&src[prior_slice]);

        // Concat the substitution.
        let sub_name = m.get(1).unwrap().as_str();
        match subs.get(sub_name) {
            Some(s) => result.push_str(s),
            None => return Err(MainError::OtherOwned(Blame::Human, format!("substitution `{}` in template is unknown", sub_name))),
        }
    }
    result.push_str(&src[anchor..]);
    Ok(result)
}

/**

Returns the path to the template directory.
*/
pub fn get_template_path() -> Result<PathBuf> {
    if cfg!(debug_assertions) {
        use std::env;
        if let Ok(path) = env::var("CARGO_SCRIPT_DEBUG_TEMPLATE_PATH") {
            return Ok(path.into());
        }
    }

    let cache_path = try!(platform::get_config_dir());
    Ok(cache_path.join("script-templates"))
}

/**

Attempts to locate and load the contents of the specified template.
*/
pub fn get_template(name: &str) -> Result<Cow<'static, str>> {
    use std::io::Read;

    let base = try!(get_template_path());

    let file = fs::File::open(base.join(format!("{}.rs", name)))
        .map_err(MainError::from)
        .err_tag(format!("template file `{}.rs` does not exist in {}",
            name,
            base.display()))
        .shift_blame(Blame::Human);

    // If the template is one of the built-in ones, do fallback if it wasn't found on disk.
    if file.is_err() {
        if let Some(text) = builtin_template(name) {
            return Ok(text.into())
        }
    }

    let mut file = try!(file);

    let mut text = String::new();
    try!(file.read_to_string(&mut text));
    Ok(text.into())
}

fn builtin_template(name: &str) -> Option<&'static str> {
    Some(match name {
        "expr" => consts::EXPR_TEMPLATE,
        "file" => consts::FILE_TEMPLATE,
        "loop" => consts::LOOP_TEMPLATE,
        "loop-count" => consts::LOOP_COUNT_TEMPLATE,
        _ => return None,
    })
}

fn dump(name: &str) -> Result<()> {
    let text = try!(get_template(name));
    print!("{}", text);
    Ok(())
}

fn list() -> Result<()> {
    use std::ffi::OsStr;

    let t_path = try!(get_template_path());

    if !t_path.exists() {
        return Err(format!("cannot list template directory `{}`: it does not exist", t_path.display()).into());
    }

    if !t_path.is_dir() {
        return Err(format!("cannot list template directory `{}`: it is not a directory", t_path.display()).into());
    }

    for entry in try!(fs::read_dir(&t_path)) {
        let entry = try!(entry);
        if !try!(entry.file_type()).is_file() {
            continue;
        }
        let f_path = entry.path();
        if f_path.extension() != Some(OsStr::new("rs")) {
            continue;
        }
        if let Some(stem) = f_path.file_stem() {
            println!("{}", stem.to_string_lossy());
        }
    }
    Ok(())
}

fn show(path: bool) -> Result<()> {
    let t_path = try!(get_template_path());

    if path {
        println!("{}", t_path.display());
        Ok(())
    } else {
        if !t_path.exists() {
            try!(fs::create_dir_all(&t_path));
        }
        if t_path.is_dir() {
            try!(open::that(&t_path));
        } else {
            return Err(format!("cannot open directory `{}`; it isn't a directory", t_path.display()).into());
        }
        Ok(())
    }
}