projclean 0.1.0

Project Cleaner
Documentation
use std::{
    env,
    fs::{canonicalize, read_to_string},
    path::{Path, PathBuf},
    process,
    sync::mpsc::channel,
    thread,
};

use anyhow::{anyhow, Context, Result};
use clap::{Arg, Command};
use projclean::{ls, run, search, Config};

fn main() {
    if let Err(err) = start() {
        eprintln!("{}", err);
        process::exit(1);
    }
}

fn start() -> Result<()> {
    let matches = command().get_matches();

    let config = init_config(&matches)?;

    if matches.is_present("list_projects") {
        config.list_projects()?;
        return Ok(());
    }

    let entry = set_working_dir(&matches)?;

    let (tx, rx) = channel();
    let tx2 = tx.clone();
    let handle = thread::spawn(move || search(entry, config, tx2));

    if matches.is_present("list_targets") {
        ls(rx)?;
    } else {
        run(rx, tx)?;
    }
    handle.join().unwrap()?;
    Ok(())
}

fn command() -> Command<'static> {
    Command::new(env!("CARGO_CRATE_NAME"))
        .version(env!("CARGO_PKG_VERSION"))
        .author(env!("CARGO_PKG_AUTHORS"))
        .about(concat!(
            env!("CARGO_PKG_DESCRIPTION"),
            " - ",
            env!("CARGO_PKG_REPOSITORY")
        ))
        .arg(
            Arg::new("list_targets")
                .short('t')
                .long("list-targets")
                .help("List found targets"),
        )
        .arg(
            Arg::new("list_projects")
                .short('l')
                .long("list-projects")
                .help("List current project rules"),
        )
        .arg(
            Arg::new("project")
                .short('p')
                .long("project")
                .value_name("PROJECT")
                .help("Append a project rule")
                .takes_value(true)
                .multiple_values(true),
        )
        .arg(
            Arg::new("file")
                .short('f')
                .long("file")
                .value_name("FILE")
                .help("Load project rules from <FILE>")
                .allow_invalid_utf8(true)
                .takes_value(true),
        )
        .arg(
            Arg::new("entry")
                .allow_invalid_utf8(true)
                .value_name("PATH")
                .help("Start searching from"),
        )
}

fn init_config(matches: &clap::ArgMatches) -> Result<Config> {
    let mut config = if let Some(config_file) = matches.value_of_os("file") {
        let config_file = Path::new(config_file);
        let content = read_to_string(config_file).map_err(|err| {
            anyhow!(
                "Cannot read config file '{}', {}",
                config_file.display(),
                err
            )
        })?;
        let mut config = Config::default();
        config.add_projects_from_file(&content)?;
        config
    } else {
        let mut config = Config::default();
        config.add_default_projects();
        config
    };

    if let Some(values) = matches.values_of("project") {
        for value in values {
            config.add_project(value)?;
        }
    }

    Ok(config)
}

fn set_working_dir(matches: &clap::ArgMatches) -> Result<PathBuf> {
    if let Some(base_directory) = matches.value_of_os("entry") {
        let base_directory = Path::new(base_directory);

        if !is_existing_directory(base_directory) {
            return Err(anyhow!(
                "The '--file' path '{}' is not a directory.",
                base_directory.to_string_lossy()
            ));
        }
        let base_directory = canonicalize(base_directory).unwrap();
        env::set_current_dir(&base_directory).with_context(|| {
            format!(
                "Cannot set '{}' as the current working directory",
                base_directory.to_string_lossy()
            )
        })?;
        Ok(base_directory)
    } else {
        let current_dir = env::current_dir()?;
        Ok(current_dir)
    }
}

fn is_existing_directory(path: &Path) -> bool {
    path.is_dir() && path.exists()
}