use std::path::PathBuf;
use std::process::Command;
use std::env;
use std::process::exit;
use std::io::Stdin;
use std::io::StdoutLock;
use std::borrow::Borrow;
pub use clap::App;
use clap::AppSettings;
use toml::Value;
use toml_query::read::TomlValueReadExt;
use clap::{Arg, ArgMatches};
use failure::ResultExt;
use failure::Fallible as Result;
use failure::Error;
use failure::err_msg;
use crate::configuration::{fetch_config, override_config, InternalConfiguration};
use crate::logger::ImagLogger;
use crate::io::OutputProxy;
use libimagerror::errors::ErrorMsg as EM;
use libimagerror::trace::*;
use libimagstore::store::Store;
use libimagstore::storeid::StoreId;
use crate::spec::CliSpec;
use atty;
#[derive(Debug)]
pub struct Runtime<'a> {
rtp: PathBuf,
configuration: Option<Value>,
cli_matches: ArgMatches<'a>,
store: Store,
has_output_pipe: bool,
has_input_pipe: bool,
ignore_ids: bool
}
impl<'a> Runtime<'a> {
pub fn new<C>(cli_app: C) -> Result<Runtime<'a>>
where C: Clone + CliSpec<'a> + InternalConfiguration
{
let matches = cli_app.clone().matches();
let rtp = get_rtp_match(&matches)?;
let configpath = matches.value_of("config")
.map_or_else(|| rtp.clone(), PathBuf::from);
debug!("Config path = {:?}", configpath);
let config = match fetch_config(&configpath)? {
None => {
return Err(err_msg("No configuration file found"))
.context(err_msg("Maybe try to use 'imag-init' to initialize imag?"))
.context(err_msg("Continuing without configuration file"))
.context(err_msg("Cannot instantiate runtime"))
.map_err(Error::from);
},
Some(mut config) => {
if let Err(e) = override_config(&mut config, get_override_specs(&matches)) {
error!("Could not apply config overrides");
trace_error(&e);
}
Some(config)
}
};
Runtime::_new(cli_app, matches, config)
}
pub fn with_configuration<C>(cli_app: C, config: Option<Value>) -> Result<Runtime<'a>>
where C: Clone + CliSpec<'a> + InternalConfiguration
{
let matches = cli_app.clone().matches();
Runtime::_new(cli_app, matches, config)
}
fn _new<C>(cli_app: C, matches: ArgMatches<'a>, config: Option<Value>) -> Result<Runtime<'a>>
where C: Clone + CliSpec<'a> + InternalConfiguration
{
if cli_app.enable_logging() {
Runtime::init_logger(&matches, config.as_ref())
}
let rtp = get_rtp_match(&matches)?;
let storepath = matches.value_of("storepath")
.map_or_else(|| {
let mut spath = rtp.clone();
spath.push("store");
spath
}, PathBuf::from);
debug!("RTP path = {:?}", rtp);
debug!("Store path = {:?}", storepath);
debug!("CLI = {:?}", matches);
trace!("Config = {:#?}", config);
let store_result = if cli_app.use_inmemory_fs() {
Store::new_inmemory(storepath, &config)
} else {
Store::new(storepath, &config)
};
let has_output_pipe = !atty::is(atty::Stream::Stdout);
let has_input_pipe = !atty::is(atty::Stream::Stdin);
let ignore_ids = matches.is_present("ignore-ids");
debug!("has output pipe = {}", has_output_pipe);
debug!("has input pipe = {}", has_input_pipe);
debug!("ignore ids = {}", ignore_ids);
store_result.map(|store| Runtime {
cli_matches: matches,
configuration: config,
rtp,
store,
has_output_pipe,
has_input_pipe,
ignore_ids,
})
.context(err_msg("Cannot instantiate runtime"))
.map_err(Error::from)
}
pub fn get_default_cli_builder(appname: &'a str,
version: &'a str,
about: &'a str)
-> App<'a, 'a>
{
App::new(appname)
.version(version)
.author("Matthias Beyer <mail@beyermatthias.de>")
.about(about)
.settings(&[AppSettings::AllowExternalSubcommands])
.arg(Arg::with_name("verbosity")
.short("v")
.long("verbose")
.help("Set log level")
.required(false)
.takes_value(true)
.possible_values(&["trace", "debug", "info", "warn", "error"])
.value_name("LOGLEVEL"))
.arg(Arg::with_name("debugging")
.long("debug")
.help("Enables debugging output. Shortcut for '--verbose debug'")
.required(false)
.takes_value(false))
.arg(Arg::with_name("no-color-output")
.long("no-color")
.help("Disable color output")
.required(false)
.takes_value(false))
.arg(Arg::with_name("config")
.long("config")
.help("Path to alternative config file")
.required(false)
.validator(::libimagutil::cli_validators::is_existing_path)
.takes_value(true))
.arg(Arg::with_name("config-override")
.long("override-config")
.help("Override a configuration settings. Use 'key=value' pairs, where the key is a path in the TOML configuration. The value must be present in the configuration and be convertible to the type of the configuration setting. If the argument does not contain a '=', it gets ignored. Setting Arrays and Tables is not yet supported.")
.required(false)
.multiple(true)
.takes_value(true))
.arg(Arg::with_name("runtimepath")
.long("rtp")
.help("Alternative runtimepath")
.required(false)
.validator(::libimagutil::cli_validators::is_directory)
.takes_value(true))
.arg(Arg::with_name("storepath")
.long("store")
.help("Alternative storepath. Must be specified as full path, can be outside of the RTP")
.required(false)
.validator(::libimagutil::cli_validators::is_directory)
.takes_value(true))
.arg(Arg::with_name("editor")
.long("editor")
.help("Set editor")
.required(false)
.takes_value(true))
.arg(Arg::with_name("ignore-ids")
.long("ignore-ids")
.help("Do not assume that the output is a pipe to another imag command. This overrides the default behaviour where imag only prints the IDs of the touched entries to stdout if stdout is a pipe.")
.long_help("Without this flag, imag assumes that if stdout is a pipe, the command imag pipes to is also an imag command. Thus, it prints the IDs of the processed entries to stdout and automatically redirects the command output to stderr. By providing this flag, this behaviour gets overridden: The IDs are not printed at all and the normal output is printed to stdout.")
.required(false)
.takes_value(false))
}
#[cfg(feature = "testing")]
pub fn extract_store(self) -> Store {
self.store
}
#[cfg(feature = "testing")]
pub fn with_store(mut self, s: Store) -> Self {
self.store = s;
self
}
#[cfg(feature = "pub_logging_initialization")]
pub fn init_logger(matches: &ArgMatches, config: Option<&Value>) {
Self::_init_logger(matches, config)
}
#[cfg(not(feature = "pub_logging_initialization"))]
fn init_logger(matches: &ArgMatches, config: Option<&Value>) {
Self::_init_logger(matches, config)
}
fn _init_logger(matches: &ArgMatches, config: Option<&Value>) {
use log::set_max_level;
use log::set_boxed_logger;
use std::env::var as env_var;
if env_var("IMAG_LOG_ENV").is_ok() {
let _ = env_logger::try_init();
} else {
let logger = ImagLogger::new(matches, config)
.map_err_trace()
.unwrap_or_else(|_| exit(1));
set_max_level(logger.global_loglevel().to_level_filter());
#[allow(unused)]
let logger_expl = format!("{:?}", logger);
set_boxed_logger(Box::new(logger))
.map_err(|e| panic!("Could not setup logger: {:?}", e))
.ok();
trace!("Imag Logger = {}", logger_expl);
}
}
pub fn is_verbose(&self) -> bool {
self.cli_matches.is_present("verbosity")
}
pub fn is_debugging(&self) -> bool {
self.cli_matches.is_present("debugging")
}
pub fn rtp(&self) -> &PathBuf {
&self.rtp
}
pub fn cli(&self) -> &ArgMatches {
&self.cli_matches
}
pub fn ids<T: IdPathProvider>(&self) -> Result<Option<Vec<StoreId>>> {
use std::io::Read;
if self.has_input_pipe {
trace!("Getting IDs from stdin...");
let stdin = ::std::io::stdin();
let mut lock = stdin.lock();
let mut buf = String::new();
lock.read_to_string(&mut buf)
.context("Failed to read stdin to buffer")
.map_err(Error::from)
.and_then(|_| {
trace!("Got IDs = {}", buf);
buf.lines()
.map(PathBuf::from)
.map(|id| StoreId::new(id).map_err(Error::from))
.collect()
})
.map(Some)
} else {
Ok(T::get_ids(self.cli())?)
}
}
pub fn config(&self) -> Option<&Value> {
self.configuration.as_ref()
}
pub fn store(&self) -> &Store {
&self.store
}
pub fn editor(&self) -> Result<Option<Command>> {
self.cli()
.value_of("editor")
.map(String::from)
.ok_or_else(|| {
self.config()
.ok_or_else(|| err_msg("No Configuration!"))
.and_then(|v| match v.read("rt.editor")? {
Some(&Value::String(ref s)) => Ok(Some(s.clone())),
Some(_) => Err(err_msg("Type error at 'rt.editor', expected 'String'")),
None => Ok(None),
})
})
.or_else(|_| env::var("EDITOR"))
.map_err(|_| Error::from(EM::IO))
.and_then(|s| {
debug!("Editing with '{}'", s);
let mut split = s.split_whitespace();
let command = split.next();
if command.is_none() {
return Ok(None)
}
let mut c = Command::new(command.unwrap()); c.args(split);
c.stdin(::std::fs::File::open("/dev/tty").context(EM::IO)?);
c.stderr(::std::process::Stdio::inherit());
Ok(Some(c))
})
}
pub fn output_is_pipe(&self) -> bool {
self.has_output_pipe
}
pub fn input_is_pipe(&self) -> bool {
self.has_input_pipe
}
pub fn ids_from_stdin(&self) -> bool {
self.input_is_pipe()
}
pub fn ignore_ids(&self) -> bool {
self.ignore_ids
}
pub fn stdout(&self) -> OutputProxy {
if self.output_is_pipe() && !self.ignore_ids {
OutputProxy::Err(::std::io::stderr())
} else {
OutputProxy::Out(::std::io::stdout())
}
}
pub fn stderr(&self) -> OutputProxy {
OutputProxy::Err(::std::io::stderr())
}
pub fn stdin(&self) -> Option<Stdin> {
if self.has_input_pipe {
None
} else {
Some(::std::io::stdin())
}
}
pub fn handle_unknown_subcommand<S: AsRef<str>>(&self,
command: S,
subcommand: S,
args: &ArgMatches)
-> Result<::std::process::ExitStatus>
{
use std::io::Write;
use std::io::ErrorKind;
let rtp_str = self.rtp()
.to_str()
.map(String::from)
.ok_or_else(|| Error::from(EM::IO))?;
let command = format!("{}-{}", command.as_ref(), subcommand.as_ref());
let subcommand_args = args.values_of("")
.map(|sx| sx.map(String::from).collect())
.unwrap_or_else(|| vec![]);
Command::new(&command)
.stdin(::std::process::Stdio::inherit())
.stdout(::std::process::Stdio::inherit())
.stderr(::std::process::Stdio::inherit())
.args(&subcommand_args[..])
.env("IMAG_RTP", rtp_str)
.spawn()
.and_then(|mut c| c.wait())
.map_err(|e| match e.kind() {
ErrorKind::NotFound => {
let mut out = self.stdout();
if let Err(e) = writeln!(out, "No such command: '{}'", command) {
return e;
}
if let Err(e) = writeln!(out, "See 'imag --help' for available subcommands") {
return e;
}
::std::process::exit(1)
},
_ => e,
})
.context(EM::IO)
.map_err(Error::from)
}
pub fn report_touched(&self, id: &StoreId) -> Result<()> {
let out = ::std::io::stdout();
let mut lock = out.lock();
self.report_touched_id(id, &mut lock).map(|_| ())
}
pub fn report_all_touched<ID, I>(&self, ids: I) -> Result<()>
where ID: Borrow<StoreId> + Sized,
I: Iterator<Item = ID>
{
let out = ::std::io::stdout();
let mut lock = out.lock();
for id in ids {
if !self.report_touched_id(id.borrow(), &mut lock)? {
break
}
}
Ok(())
}
#[inline]
fn report_touched_id(&self, id: &StoreId, output: &mut StdoutLock) -> Result<bool> {
use std::io::Write;
if self.output_is_pipe() && !self.ignore_ids {
trace!("Reporting: {} to {:?}", id, output);
if let Err(e) = writeln!(output, "{}", id) {
return if e.kind() == std::io::ErrorKind::BrokenPipe {
Ok(false)
} else {
Err(failure::Error::from(e))
}
}
}
Ok(true)
}
}
pub trait IdPathProvider {
fn get_ids(matches: &ArgMatches) -> Result<Option<Vec<StoreId>>>;
}
pub fn get_rtp_match<'a>(matches: &ArgMatches<'a>) -> Result<PathBuf> {
if let Some(p) = matches
.value_of("runtimepath")
.map(PathBuf::from)
{
return Ok(p)
}
match env::var("IMAG_RTP").map(PathBuf::from) {
Ok(p) => return Ok(p),
Err(env::VarError::NotUnicode(_)) => {
return Err(err_msg("Environment variable 'IMAG_RTP' does not contain valid Unicode"))
},
Err(env::VarError::NotPresent) => { }
}
env::var("HOME")
.map(PathBuf::from)
.map(|mut p| { p.push(".imag"); p })
.map_err(|e| match e {
env::VarError::NotUnicode(_) => {
err_msg("Environment variable 'HOME' does not contain valid Unicode")
},
env::VarError::NotPresent => {
err_msg("You seem to be $HOME-less. Please get a $HOME before using this \
software. We are sorry for you and hope you have some \
accommodation anyways.")
}
})
}
fn get_override_specs(matches: &ArgMatches) -> Vec<String> {
matches
.values_of("config-override")
.map(|values| {
values
.filter(|s| {
let b = s.contains('=');
if !b { warn!("override '{}' does not contain '=' - will be ignored!", s); }
b
})
.map(String::from)
.collect()
})
.unwrap_or_else(|| vec![])
}