diff --git a/Cargo.lock b/Cargo.lock index b4b42ca..2d88da5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,17 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + [[package]] name = "base64" version = "0.20.0" @@ -98,6 +109,15 @@ dependencies = [ "libc", ] +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + [[package]] name = "hermit-abi" version = "0.2.6" @@ -134,7 +154,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "927609f78c2913a6f6ac3c27a4fe87f43e2a35367c0c4b0f8265e8f49a104330" dependencies = [ - "hermit-abi", + "hermit-abi 0.2.6", "io-lifetimes", "rustix", "windows-sys", @@ -194,6 +214,7 @@ dependencies = [ name = "shitbox" version = "0.1.0" dependencies = [ + "atty", "base64", "clap", "clap_complete", @@ -202,6 +223,7 @@ dependencies = [ "data-encoding", "hostname", "once_cell", + "termcolor", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index be3299c..85e3186 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +atty = "0.2.14" base64 = "0.20.0" clap = "4.0.29" clap_complete = "4.0.6" @@ -14,6 +15,7 @@ clap_mangen = "0.2.5" data-encoding = "2.3.3" hostname = { version = "0.3", features = ["set"] } once_cell = "1.16.0" +termcolor = "1.1.3" [profile.release] codegen-units = 1 diff --git a/src/cmd/base32/mod.rs b/src/cmd/base32/mod.rs index 1d0b770..4b02c42 100644 --- a/src/cmd/base32/mod.rs +++ b/src/cmd/base32/mod.rs @@ -3,9 +3,10 @@ use clap::{value_parser, Arg, ArgAction, Command}; use data_encoding::BASE32; use std::{ fs, - io::{self, Read}, + io::{self, Read, Write}, process, }; +use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor}; #[derive(Debug)] pub struct Base32 { @@ -47,6 +48,10 @@ impl Cmd for Base32 { .long("wrap") .value_parser(value_parser!(usize)) .default_value("76"), + Arg::new("color") + .short('c') + .long("color") + .value_parser(["always", "ansi", "auto", "never"]), Arg::new("VERBOSE") .help("Display a header naming each file") .short('v') @@ -69,12 +74,27 @@ impl Cmd for Base32 { None => vec![String::from("-")], }; let len = files.len(); + let color = match matches.get_one::("color").map(|x| x.as_str()) { + Some("always") => ColorChoice::Always, + Some("ansi") => ColorChoice::AlwaysAnsi, + Some("auto") => { + if atty::is(atty::Stream::Stdout) { + ColorChoice::Auto + } else { + ColorChoice::Never + } + } + _ => ColorChoice::Never, + }; for (index, file) in files.into_iter().enumerate() { if { len > 1 || matches.get_flag("VERBOSE") } && !matches.get_flag("QUIET") { + let mut stdout = StandardStream::stdout(color); + stdout.set_color(ColorSpec::new().set_fg(Some(Color::Green)))?; match index { - 0 => println!("===> {file} <==="), - _ => println!("\n===> {file} <==="), - }; + 0 => writeln!(stdout, "===> {file} <==="), + _ => writeln!(stdout, "\n===> {file} <==="), + }?; + stdout.reset()?; } else if index > 0 { println!(); } diff --git a/src/cmd/factor/mod.rs b/src/cmd/factor/mod.rs index 8c6792c..1a5e8ae 100644 --- a/src/cmd/factor/mod.rs +++ b/src/cmd/factor/mod.rs @@ -60,10 +60,10 @@ impl Cmd for Factor { } fn first_factor(num: u64) -> u64 { - if crate::math::is_prime(num) { - return num; + if num % 2 == 0 { + return 2; } - for n in 2..=num { + for n in (3..=num).step_by(1) { if num % n == 0 { return n; } diff --git a/src/cmd/head/mod.rs b/src/cmd/head/mod.rs index 94aafee..8075bff 100644 --- a/src/cmd/head/mod.rs +++ b/src/cmd/head/mod.rs @@ -5,9 +5,10 @@ use std::{ env, error::Error, fs, - io::{self, stdin, Read}, + io::{self, stdin, Read, Write}, process, }; +use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor}; #[derive(Debug)] pub struct Head { @@ -27,45 +28,49 @@ impl Cmd for Head { fn cli(&self) -> Command { Command::new(self.name) - .author("Nathan Fisher") - .about("Display first lines of a file") - .long_about( - "Print the first 10 lines of each FILE to standard output.\n\ - With more than one FILE, precede each with a header giving the file name.\n\n\ - With no FILE, or when FILE is -, read standard input." - ) - .args([ - Arg::new("FILES") - .help("The input file to use") - .num_args(1..), - Arg::new("BYTES") - .help("Count bytes instead of lines") - .short('c') - .long("bytes") - .action(ArgAction::SetTrue), - Arg::new("QUIET") - .help("Disable printing a header. Overrides -c") - .short('q') - .long("quiet") - .action(ArgAction::SetTrue), - Arg::new("HEADER") - .help("Each file is preceded by a header consisting of the string \"==> XXX <==\" where \"XXX\" is the name of the file.") - .short('v') - .long("verbose") - .action(ArgAction::SetTrue), - Arg::new("LINES") - .help("Count n number of lines (or bytes if -c is specified).") - .short('n') - .long("lines") - .allow_negative_numbers(false) - .conflicts_with("num") - .value_parser(value_parser!(usize)), - Arg::new("num") - .short('1') - .short_aliases(['2', '3', '4', '5', '6', '7', '8', '9']) - .hide(true) - .action(ArgAction::Append) - ]) + .author("Nathan Fisher") + .about("Display first lines of a file") + .long_about( + "Print the first 10 lines of each FILE to standard output.\n\ + With more than one FILE, precede each with a header giving the file name.\n\n\ + With no FILE, or when FILE is -, read standard input." + ) + .args([ + Arg::new("FILES") + .help("The input file to use") + .num_args(1..), + Arg::new("BYTES") + .help("Count bytes instead of lines") + .short('c') + .long("bytes") + .action(ArgAction::SetTrue), + Arg::new("QUIET") + .help("Disable printing a header. Overrides -c") + .short('q') + .long("quiet") + .action(ArgAction::SetTrue), + Arg::new("HEADER") + .help("Each file is preceded by a header consisting of the string \"==> XXX <==\" where \"XXX\" is the name of the file.") + .short('v') + .long("verbose") + .action(ArgAction::SetTrue), + Arg::new("LINES") + .help("Count n number of lines (or bytes if -c is specified).") + .short('n') + .long("lines") + .allow_negative_numbers(false) + .conflicts_with("num") + .value_parser(value_parser!(usize)), + Arg::new("color") + .short('C') + .long("color") + .value_parser(["always", "ansi", "auto", "never"]), + Arg::new("num") + .short('1') + .short_aliases(['2', '3', '4', '5', '6', '7', '8', '9']) + .hide(true) + .action(ArgAction::Append) + ]) } fn run(&self, matches: Option<&ArgMatches>) -> Result<(), Box> { @@ -78,16 +83,17 @@ impl Cmd for Head { if args.len() > idx { let arg1 = &args[idx]; if arg1.starts_with('-') && arg1.len() > 1 { - let lines: usize = arg1[1..].parse()?; - let files: Vec<_> = if args.len() > idx + 1 { - args[idx + 1..].iter().map(String::as_str).collect() - } else { - vec!["-"] - }; - for file in &files { - head(file, lines, false, false); + if let Ok(lines) = arg1[1..].parse::() { + let files: Vec<_> = if args.len() > idx + 1 { + args[idx + 1..].iter().map(String::as_str).collect() + } else { + vec!["-"] + }; + for file in &files { + head(file, lines, false, false, ColorChoice::Never)?; + } + return Ok(()); } - return Ok(()); } } let Some(matches) = matches else { @@ -102,11 +108,23 @@ impl Cmd for Head { }; let header = !matches.get_flag("QUIET") && { files.len() > 1 || matches.get_flag("HEADER") }; + let color = match matches.get_one::("color").map(|x| x.as_str()) { + Some("always") => ColorChoice::Always, + Some("ansi") => ColorChoice::AlwaysAnsi, + Some("auto") => { + if atty::is(atty::Stream::Stdout) { + ColorChoice::Auto + } else { + ColorChoice::Never + } + } + _ => ColorChoice::Never, + }; for (index, file) in files.into_iter().enumerate() { if index == 1 && header { println!(); } - head(&file, lines, header, matches.get_flag("BYTES")); + head(&file, lines, header, matches.get_flag("BYTES"), color)?; } Ok(()) } @@ -116,7 +134,13 @@ impl Cmd for Head { } } -fn head(file: &str, count: usize, header: bool, bytes: bool) { +fn head( + file: &str, + count: usize, + header: bool, + bytes: bool, + color: ColorChoice, +) -> Result<(), Box> { let mut contents = String::new(); if file == "-" { match stdin().read_to_string(&mut contents) { @@ -137,7 +161,10 @@ fn head(file: &str, count: usize, header: bool, bytes: bool) { }; } if header { - println!("==> {file} <=="); + let mut stdout = StandardStream::stdout(color); + stdout.set_color(ColorSpec::new().set_fg(Some(Color::Green)))?; + writeln!(stdout, "==> {file} <==")?; + stdout.reset()?; } if bytes { for (index, char) in contents.chars().into_iter().enumerate() { @@ -145,7 +172,7 @@ fn head(file: &str, count: usize, header: bool, bytes: bool) { print!("{char}"); } else { println!(); - return; + return Ok(()); } } println!(); @@ -154,8 +181,9 @@ fn head(file: &str, count: usize, header: bool, bytes: bool) { if index < count { println!("{line}"); } else { - return; + return Ok(()); } } } + Ok(()) } diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs index 1ee07d6..57aab0d 100644 --- a/src/cmd/mod.rs +++ b/src/cmd/mod.rs @@ -26,7 +26,7 @@ pub mod head; pub mod hostname; mod ln; mod ls; -mod mountpoint; +pub mod mountpoint; mod mv; pub mod nologin; mod pwd; @@ -38,9 +38,9 @@ mod sync; pub mod r#true; pub use { + self::base64::{Base64, BASE_64}, self::hostname::{Hostname, HOSTNAME}, base32::{Base32, BASE_32}, - self::base64::{Base64, BASE_64}, bootstrap::{Bootstrap, BOOTSTRAP}, dirname::{Dirname, DIRNAME}, echo::{Echo, ECHO}, diff --git a/src/cmd/mountpoint/mod.rs b/src/cmd/mountpoint/mod.rs index 8b13789..5a15116 100644 --- a/src/cmd/mountpoint/mod.rs +++ b/src/cmd/mountpoint/mod.rs @@ -1 +1,50 @@ +use super::Cmd; +use clap::{Arg, ArgAction, Command}; +#[derive(Debug)] +pub struct Mountpoint { + name: &'static str, + path: Option, +} + +pub const MOUNTPOINT: Mountpoint = Mountpoint { + name: "mountpoint", + path: Some(crate::Path::Bin), +}; + +impl Cmd for Mountpoint { + fn name(&self) -> &str { + self.name + } + + fn cli(&self) -> clap::Command { + Command::new(self.name) + .about("see if a directory or file is a mountpoint") + .author("Nathan Fisher") + .args([ + Arg::new("fs-devno") + .help("Show the major/minor numbers of the device that is mounted on the given directory.") + .short('d') + .long("fs-devno") + .action(ArgAction::SetTrue), + Arg::new("devno") + .help("Show the major/minor numbers of the given blockdevice on standard output.") + .short('x') + .long("devno") + .action(ArgAction::SetTrue), + Arg::new("quiet") + .help("Be quiet - don’t print anything.") + .short('q') + .long("quiet") + .action(ArgAction::SetTrue), + ]) + } + + fn run(&self, matches: Option<&clap::ArgMatches>) -> Result<(), Box> { + unimplemented!(); + } + + fn path(&self) -> Option { + self.path + } +} diff --git a/src/cmd/shitbox/mod.rs b/src/cmd/shitbox/mod.rs index 1e59f19..4bcef2a 100644 --- a/src/cmd/shitbox/mod.rs +++ b/src/cmd/shitbox/mod.rs @@ -1,5 +1,6 @@ use super::{ - Cmd, BASE_32, BASE_64, BOOTSTRAP, DIRNAME, ECHO, FACTOR, FALSE, HEAD, HOSTNAME, NOLOGIN, SLEEP, TRUE, + Cmd, BASE_32, BASE_64, BOOTSTRAP, DIRNAME, ECHO, FACTOR, FALSE, HEAD, HOSTNAME, NOLOGIN, SLEEP, + TRUE, }; use clap::Command; use std::{ diff --git a/src/lib.rs b/src/lib.rs index 3ddba2a..953ebb3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,8 +3,8 @@ use std::{env, error::Error, path::PathBuf, string::ToString}; pub mod cmd; pub use cmd::{ - Cmd, Commands, BASE_32, BASE_64, BOOTSTRAP, DIRNAME, ECHO, FACTOR, FALSE, HEAD, HOSTNAME, NOLOGIN, - SHITBOX, SLEEP, TRUE, + Cmd, Commands, BASE_32, BASE_64, BOOTSTRAP, DIRNAME, ECHO, FACTOR, FALSE, HEAD, HOSTNAME, + NOLOGIN, SHITBOX, SLEEP, TRUE, }; pub mod math; @@ -41,8 +41,8 @@ pub fn run() -> Result<(), Box> { cmd::COMMANDS .set(Commands { items: vec![ - &BASE_32, &BASE_64, &BOOTSTRAP, &DIRNAME, &ECHO, &FALSE, &FACTOR, &HEAD, &HOSTNAME, - &NOLOGIN, &TRUE, &SLEEP, &SHITBOX, + &BASE_32, &BASE_64, &BOOTSTRAP, &DIRNAME, &ECHO, &FALSE, &FACTOR, &HEAD, + &HOSTNAME, &NOLOGIN, &TRUE, &SLEEP, &SHITBOX, ], }) .expect("Cannot register commands");