diff --git a/Cargo.lock b/Cargo.lock index b4e2919..bd5ed05 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -78,6 +78,7 @@ version = "0.1.0" dependencies = [ "color-eyre", "futures", + "lazy_static", "num_cpus", "pin-project", "tokio", diff --git a/Cargo.toml b/Cargo.toml index c549ce6..6eb69ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,9 +6,16 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +default = ["splash"] + +# Show splash screen +splash = [] + [dependencies] color-eyre = {version = "0.5.10", default-features=false} futures = "0.3.12" +lazy_static = "1.4.0" num_cpus = "1.13.0" pin-project = "1.0.5" tokio = {version = "0.2", features=["full"]} diff --git a/src/arg.rs b/src/arg.rs new file mode 100644 index 0000000..58c6d49 --- /dev/null +++ b/src/arg.rs @@ -0,0 +1,124 @@ +//! Argument parsing and handling +use super::*; +use std::num::NonZeroUsize; + +use config::Config; + +/// Executable name +pub fn program_name() -> &'static str +{ + lazy_static! { + static ref NAME: String = std::env::args().next().unwrap(); + } + &NAME[..] +} + +#[cfg(feature="splash")] +/// Print splash screen +#[inline] pub fn splash() +{ + eprintln!("dirstat version {}", env!("CARGO_PKG_VERSION")); + eprintln!("Made by {} with <3.\n Licensed with GPL v3.0 +", env!("CARGO_PKG_AUTHORS")); +} + +/// Print usage message +pub fn usage() +{ + #[cfg(feature="splash")] + { + splash(); println!(); + } + + println!("{} [OPTIONS] [-] ", program_name()); + println!("{} --help", program_name()); + println!(r#" +OPTIONS: + --recursive Set max directory recursion depth limit (1 = No recursion (default), 0 = Unlimited recursion). + -r Set unlimited directory recursion depth. (same as `--recursive 0`). + --threads Limit the maximum number of tasks allowed to process concurrently (Set to 0 for unlimited.) + -M Set number of parallel running tasks to unlimited. (Same as `--threads 0`). + -m Limit number of parallel tasks to the number of active CPU processors. (default). + - Stop parsing arguments, treat all the rest as paths. + + --help Print this message and exit. + +NOTES: + The first time a non-option argument is encountered, the program stops parsing arguments and assumes the rest of the arguments are paths. + If parallelism is set to unlimited, there can be a huge syscall overhead. It is recommended to use `-m` (which is default anyway). +"#); +} + +/// Print usage message then exit with code 1. +pub fn help() -> ! +{ + usage(); + + std::process::exit(1) +} + +/// Which mode to run in +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Mode +{ + Normal(Config), + Help, +} + +/// Parse command-line arguments +#[inline] pub fn parse_args() -> eyre::Result +{ + parse(std::env::args().skip(1)) +} + +fn parse>(args: I) -> eyre::Result +{ + let suggestion_intended_arg = || "If this was intended as a path instead of an option, use option `-` before it."; + + let mut args = args.into_iter(); + let mut cfg = Config::default(); + + let mut reading = true; + while let Some(opt) = args.next() + { + if reading { + match opt.trim() + { + "--help" => return Ok(Mode::Help), + "-" => reading = false, + + "--threads" => { + let max = args.next().ok_or(eyre!("`--threads` expects a parameter")) + .with_suggestion(suggestion_intended_arg.clone())?; + cfg.max_tasks = NonZeroUsize::new(max.parse::() + .wrap_err(eyre!("`--threads` expects a non-negative number")) + .with_suggestion(suggestion_intended_arg.clone()) + .with_section(move || max.header("Parameter given was"))?); + }, + "-M" => cfg.max_tasks = None, + "-m" => { + cfg.max_tasks = config::max_tasks_cpus(); // this is the default, but it is possible an earlier command mutated it, so doing nothing here would be a bug for that corner case + }, + + "--recursive" => { + let max = args.next().ok_or(eyre!("`--recursive` expects a parameter")) + .with_suggestion(suggestion_intended_arg.clone())?; + + cfg.recursive = max.parse::() + .wrap_err(eyre!("`--recursive` expects a non-negative number")) + .with_suggestion(suggestion_intended_arg.clone()) + .with_section(move || max.header("Parameter given was"))?.into(); + }, + "-r" => cfg.recursive = config::Recursion::Unlimited, + + _ => { + cfg.paths.push(opt.into()); + reading = false; + } + } + continue; + } else { + cfg.paths.push(opt.into()); + } + } + Ok(Mode::Normal(cfg)) +} diff --git a/src/config.rs b/src/config.rs index ec6c001..fd977af 100644 --- a/src/config.rs +++ b/src/config.rs @@ -15,7 +15,7 @@ impl Default for Recursion #[inline] fn default() -> Self { - Self::Unlimited + Self::None } } @@ -31,9 +31,21 @@ impl Recursion Self::Unlimited => true } } +} +impl From for Recursion +{ + fn from(from: usize) -> Self + { + match from { + 0 => Self::Unlimited, + 1 => Self::None, + x => Self::Limited(unsafe {NonZeroUsize::new_unchecked(x)}), + } + } } + /// Configuration for this run #[derive(Debug, Clone, PartialEq, Eq)] pub struct Config @@ -43,6 +55,15 @@ pub struct Config pub max_tasks: Option, } +/// The default `max_tasks` +#[inline(always)] pub fn max_tasks_cpus() -> Option +{ + lazy_static! { + static ref CPUS: usize = num_cpus::get(); + } + NonZeroUsize::new(*CPUS) +} + impl Default for Config { #[inline] @@ -51,7 +72,7 @@ impl Default for Config Self { paths: Vec::new(), recursive: Default::default(), - max_tasks: NonZeroUsize::new(num_cpus::get()), + max_tasks: max_tasks_cpus(), } } } diff --git a/src/main.rs b/src/main.rs index 01ef70a..392514d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ #![allow(dead_code)] #[macro_use] extern crate pin_project; +#[macro_use] extern crate lazy_static; use color_eyre::{ eyre::{ @@ -12,6 +13,7 @@ use color_eyre::{ }, Help as _, + SectionExt as _, }; #[macro_use] mod ext; @@ -20,17 +22,25 @@ pub use ext::prelude::*; mod data; mod config; mod state; +mod arg; mod work; async fn read_config() -> eyre::Result { - Ok(config::Config::default()) //TODO: read config + match arg::parse_args().wrap_err(eyre!("Failed to parse args"))? + { + arg::Mode::Normal(cfg) => { + #[cfg(debug_assertions)] eprintln!("Parsed config: {:#?}\n", cfg); + Ok(cfg) + }, + arg::Mode::Help => arg::help(), + } } #[tokio::main] async fn main() -> eyre::Result<()> { color_eyre::install()?; - + let state = state::State::new(read_config().await .wrap_err(eyre!("Failed to load config"))? .validate() diff --git a/src/work.rs b/src/work.rs index e5ddafa..a5ebe22 100644 --- a/src/work.rs +++ b/src/work.rs @@ -76,7 +76,7 @@ pub async fn work_on_all(state: State) -> INodeInfoGraph // If this inode is not in the map, this is a top-level path. //if let Some(_) = ino_map.get(&ino) { - output.insert(path, ino); + output.insert(path, ino); //XXX: Why does `output` always end up empty??? //} else { // eprintln!("No ino entry for {:?} ({:?})", path, ino); //}