use std::env;
use std::path::{Path, PathBuf};
use std::fs::File;
use std::io::Read;
use std::cmp::max;
use std::process::Command;
use clap::App;
use toml;
use num_cpus;
use types::{TagsExe, TagsKind, TagsSpec};
use rt_result::RtResult;
use dirs;
pub struct Config {
pub tags_spec: TagsSpec,
pub start_dir: PathBuf,
pub omit_deps: bool,
pub force_recreate: bool,
pub verbose: bool,
pub quiet: bool,
pub num_threads: u32
}
impl Config {
pub fn from_command_args() -> RtResult<Config> {
let matches = App::new("rusty-tags")
.about("Create ctags/etags for a cargo project and all of its dependencies")
.version(crate_version!())
.author("Daniel Trstenjak <daniel.trstenjak@gmail.com>")
.arg_from_usage("<TAGS_KIND> 'The kind of the created tags (vi, emacs)'")
.arg_from_usage("-s --start-dir [DIR] 'Start directory for the search of the Cargo.toml (default: current working directory)'")
.arg_from_usage("-o --omit-deps 'Do not generate tags for dependencies'")
.arg_from_usage("-f --force-recreate 'Forces the recreation of the tags of all dependencies and the Rust standard library'")
.arg_from_usage("-v --verbose 'Verbose output about all operations'")
.arg_from_usage("-q --quiet 'Don't output anything but errors'")
.arg_from_usage("-n --num-threads [NUM] 'Num threads used for the tags creation (default: num available physical cpus)'")
.get_matches();
let start_dir = matches.value_of("start-dir")
.map(PathBuf::from)
.unwrap_or(env::current_dir()?);
if ! start_dir.is_dir() {
return Err(format!("Invalid directory given to '--start-dir': '{}'!", start_dir.display()).into());
}
let (vi_tags, emacs_tags, ctags_exe, ctags_options) = {
let mut vt = "rusty-tags.vi".to_string();
let mut et = "rusty-tags.emacs".to_string();
let mut cte = None;
let mut cto = "".to_string();
if let Some(file_config) = ConfigFromFile::load()? {
if let Some(fcvt) = file_config.vi_tags { vt = fcvt; }
if let Some(fcet) = file_config.emacs_tags { et = fcet; }
cte = file_config.ctags_exe;
if let Some(fccto) = file_config.ctags_options { cto = fccto; }
}
(vt, et, cte, cto)
};
let kind = value_t_or_exit!(matches.value_of("TAGS_KIND"), TagsKind);
let omit_deps = matches.is_present("omit-deps");
let force_recreate = matches.is_present("force-recreate");
let quiet = matches.is_present("quiet");
let verbose = if quiet { false } else { matches.is_present("verbose") };
let num_threads = if verbose {
println!("Switching to single threaded for verbose output");
1
} else {
value_t!(matches.value_of("num-threads"), u32)
.map(|n| max(1, n))
.unwrap_or(num_cpus::get_physical() as u32)
};
if verbose {
println!("Using configuration: vi_tags='{}', emacs_tags='{}', ctags_exe='{:?}', ctags_options='{}'",
vi_tags, emacs_tags, ctags_exe, ctags_options);
}
let ctags_exe = detect_tags_exe(&ctags_exe)?;
if verbose {
println!("Found ctags executable: {:?}", ctags_exe);
}
Ok(Config {
tags_spec: TagsSpec::new(kind, ctags_exe, vi_tags, emacs_tags, ctags_options)?,
start_dir: start_dir,
omit_deps: omit_deps,
force_recreate: force_recreate,
verbose: verbose,
quiet: quiet,
num_threads: num_threads
})
}
}
#[derive(Deserialize, Debug, Default)]
struct ConfigFromFile {
vi_tags: Option<String>,
emacs_tags: Option<String>,
ctags_exe: Option<String>,
ctags_options: Option<String>
}
impl ConfigFromFile {
fn load() -> RtResult<Option<ConfigFromFile>> {
let config_file = dirs::rusty_tags_dir().map(|p| p.join("config.toml"))?;
if ! config_file.is_file() {
return Ok(None);
}
let config = map_file(&config_file, |contents| {
let config = toml::from_str(&contents)?;
Ok(config)
})?;
Ok(Some(config))
}
}
fn map_file<R, F>(file: &Path, f: F) -> RtResult<R>
where F: FnOnce(String) -> RtResult<R>
{
let mut file = File::open(file)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
let r = f(contents)?;
Ok(r)
}
fn detect_tags_exe(ctags_exe: &Option<String>) -> RtResult<TagsExe> {
let exes = if let &Some(ref exe) = ctags_exe {
vec![exe.as_str()]
} else {
vec!["ctags", "exuberant-ctags", "exctags", "universal-ctags", "uctags"]
};
for exe in &exes {
let mut cmd = Command::new(exe);
cmd.arg("--version");
if let Ok(output) = cmd.output() {
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
if stdout.contains("Universal Ctags") {
return Ok(TagsExe::UniversalCtags(exe.to_string()));
}
return Ok(TagsExe::ExuberantCtags(exe.to_string()));
}
}
}
Err(format!("Couldn't find 'ctags' executable! Searched for executables with names: {:?}. Is 'ctags' correctly installed?", &exes).into())
}