You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
441 lines
10 KiB
441 lines
10 KiB
//! Arg parsing
|
|
use super::*;
|
|
use std::{
|
|
ffi::OsStr,
|
|
path::{
|
|
Path, PathBuf,
|
|
},
|
|
borrow::Cow,
|
|
fmt,
|
|
};
|
|
use tokio::{
|
|
sync::{
|
|
mpsc,
|
|
},
|
|
};
|
|
use futures::{
|
|
stream::{self, Stream, BoxStream, StreamExt,},
|
|
};
|
|
|
|
/// Parsed command-line args
|
|
#[derive(Debug, Clone)]
|
|
pub struct Args
|
|
{
|
|
pub delim: u8,
|
|
pub reverse: bool,
|
|
pub walker: walk::Config,
|
|
pub worker: work::Config,
|
|
paths: Option<Vec<PathBuf>>,
|
|
}
|
|
|
|
impl Default for Args
|
|
{
|
|
#[inline]
|
|
fn default() -> Self
|
|
{
|
|
Self {
|
|
delim: b'\n',
|
|
reverse: false,
|
|
walker: Default::default(),
|
|
worker: Default::default(),
|
|
paths: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
impl Args
|
|
{
|
|
/// Paths as an async stream
|
|
///
|
|
/// # Non-immediate
|
|
/// When input paths come from `stdin`, the output stream will be non-immediate.
|
|
pub fn paths(&self) -> BoxStream<'_, Cow<'_, Path>>
|
|
{
|
|
if let Some(paths) = self.paths.as_ref() {
|
|
stream::iter(paths.iter().map(|x| Cow::Borrowed(Path::new(x)))).boxed()
|
|
} else {
|
|
let (tx, rx) = mpsc::channel(128);
|
|
let read_chr = self.delim;
|
|
tokio::spawn(async move {
|
|
use tokio::io::{
|
|
self,
|
|
AsyncReadExt, AsyncBufReadExt
|
|
};
|
|
let mut stdin = {
|
|
tokio::io::BufReader::new(io::stdin())
|
|
};
|
|
let mut buf = Vec::with_capacity(1024);
|
|
loop
|
|
{
|
|
buf.clear();
|
|
use std::os::unix::prelude::*;
|
|
let n = match stdin.read_until(read_chr, &mut buf).await {
|
|
Ok(n) => n,
|
|
Err(e) => {
|
|
error!("paths: Failed to read input line: {}", e);
|
|
break;
|
|
},
|
|
};
|
|
trace!("paths: buffer: {:?}", &buf[..]);
|
|
if n == 0 {
|
|
trace!("paths: Stdin exhausted. Exiting.");
|
|
break;
|
|
}
|
|
let path_bytes = &buf[..n];
|
|
let path_bytes = if path_bytes.len() == 1 {
|
|
trace!("paths: Ignoring empty line. Yielding then continuing.");
|
|
tokio::task::yield_now().await;
|
|
continue;
|
|
} else if path_bytes[n-1] == read_chr {
|
|
&path_bytes[.. (path_bytes.len()-1)]
|
|
} else {
|
|
path_bytes
|
|
};
|
|
let path = Path::new(OsStr::from_bytes(path_bytes));
|
|
trace!("Read path {:?}", path);
|
|
if path.exists() {
|
|
if tx.send(path.to_owned()).await.is_err() {
|
|
trace!("paths: Stream dropped, cancelling stdin read.");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
tokio_stream::wrappers::ReceiverStream::new(rx).map(|x| Cow::Owned(x)).boxed()
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum Mode
|
|
{
|
|
Normal(Args),
|
|
Help,
|
|
}
|
|
|
|
#[inline]
|
|
pub fn parse_args() -> eyre::Result<Mode>
|
|
{
|
|
//return Ok(Args { paths: None });
|
|
parse(std::env::args().skip(1))
|
|
.with_context(|| format!("{:?}", std::env::args().collect::<Vec<_>>()).header("ARGV was"))
|
|
}
|
|
|
|
/// The executable name, if readable from argv as a valid UTF8 string.
|
|
///
|
|
/// If not readable, the project name will be returned.
|
|
#[inline]
|
|
pub fn prog_name() -> &'static str
|
|
{
|
|
lazy_static! {
|
|
static ref PROG_NAME: &'static str = std::env::args().next().map(|x| &*Box::leak(x.into_boxed_str())).unwrap_or(env!("CARGO_PKG_NAME"));
|
|
}
|
|
*PROG_NAME
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Copy)]
|
|
enum Arg<'a>
|
|
{
|
|
Long(&'a str),
|
|
Short(&'a [u8]),
|
|
ShortSingle(u8),
|
|
}
|
|
|
|
impl<'a> Arg<'a>
|
|
{
|
|
#[inline]
|
|
pub fn as_long(&self) -> Option<&'a str>
|
|
{
|
|
match self {
|
|
Self::Long(l) => Some(l),
|
|
_ => None,
|
|
}
|
|
}
|
|
#[inline]
|
|
pub fn as_short_ascii(&self) -> Option<&'a [u8]>
|
|
{
|
|
match self {
|
|
Self::Short(l) => Some(l),
|
|
//Self::ShortSingle(s) => Some(&[*s]),
|
|
_ => None,
|
|
}
|
|
}
|
|
#[inline]
|
|
pub fn split_short(&self) -> Option<impl IntoIterator<Item = char> + 'a>
|
|
{
|
|
self.as_short_ascii().map(|x| std::str::from_utf8(x).ok() /* XXX: Silent failure is not a good idea.. We should return an error (or maybe just panic? if there's invalid utf8 here, it shouldn't happen)*/).flatten().map(|s| s.chars())
|
|
}
|
|
|
|
#[inline]
|
|
pub fn split_short_ascii(&self) -> Option<impl Iterator<Item = u8> + 'a>
|
|
{
|
|
self.as_short_ascii().map(|opt| opt.into_iter().copied())
|
|
}
|
|
|
|
#[inline]
|
|
pub fn explode(self) -> impl Iterator<Item = Arg<'a>> + 'a
|
|
{
|
|
std::iter::once(self)
|
|
.chain(std::iter::once(if let Self::Short(short) = self { Some(short.into_iter().copied().map(|x| Arg::ShortSingle(x))) } else { None })
|
|
.flat_map(std::convert::identity).flatten())
|
|
}
|
|
|
|
#[inline]
|
|
pub fn is_any<'b: 'a, I: 'b, A>(&self, these: I) -> bool
|
|
where I: IntoIterator<Item = A>,
|
|
A: Into<Arg<'b>> + 'b
|
|
{
|
|
let iter: Vec<_> = these.into_iter().map(Into::into).map(|x| x.explode()).flatten().collect();
|
|
for split in self.explode() {
|
|
if iter.iter().any(|arg| arg == &split) {
|
|
return true;
|
|
}
|
|
}
|
|
false
|
|
}
|
|
|
|
#[inline(always)]
|
|
pub fn is_long(&self) -> bool
|
|
{
|
|
self.as_long().is_some()
|
|
}
|
|
|
|
#[inline(always)]
|
|
pub fn is_short(&self) -> bool
|
|
{
|
|
!self.is_long()
|
|
}
|
|
}
|
|
|
|
impl<'a> fmt::Display for Arg<'a>
|
|
{
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result
|
|
{
|
|
match self {
|
|
Self::Long(s) => write!(f, "--{s}"),
|
|
Self::Short(short) => write!(f, "-{}", std::str::from_utf8(short).unwrap()),
|
|
Self::ShortSingle(one) => write!(f, "-{}", *one as char),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'a> From<&'a [u8]> for Arg<'a>
|
|
{
|
|
#[inline]
|
|
fn from(from: &'a [u8]) -> Self
|
|
{
|
|
Self::Short(from)
|
|
}
|
|
}
|
|
|
|
impl<'a, const N: usize> From<&'a [u8; N]> for Arg<'a>
|
|
{
|
|
#[inline]
|
|
fn from(from: &'a [u8; N]) -> Self
|
|
{
|
|
Self::Short(&from[..])
|
|
}
|
|
}
|
|
|
|
|
|
impl<'a> From<&'a str> for Arg<'a>
|
|
{
|
|
#[inline]
|
|
fn from(from: &'a str) -> Self
|
|
{
|
|
Self::Long(from)
|
|
}
|
|
}
|
|
|
|
impl From<u8> for Arg<'static>
|
|
{
|
|
#[inline]
|
|
fn from(from: u8) -> Self
|
|
{
|
|
Self::ShortSingle(from)
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
fn parse_single<'a, I: ?Sized + 'a>(input: Arg<'a>, args: &mut I, output: &mut Args) -> eyre::Result<Option<Mode>>
|
|
where I: Iterator<Item = String>
|
|
{
|
|
macro_rules! take {
|
|
($fmt:literal $(, $ag:expr)*) => {
|
|
match args.next() {
|
|
Some(n) => n,
|
|
None => return Err(eyre!($fmt $(, $ag)*)),
|
|
}
|
|
};
|
|
() => {
|
|
take!("`{}` expects an argument", &input)
|
|
}
|
|
}
|
|
|
|
macro_rules! args {
|
|
($($arg:expr),*) => {
|
|
[$(Arg::from($arg)),*]
|
|
};
|
|
}
|
|
|
|
// Modes //
|
|
|
|
// --help
|
|
if input == Arg::Long("help") {
|
|
return Ok(Some(Mode::Help));
|
|
}
|
|
|
|
// Normal //
|
|
|
|
// -r, --recursive <limit>
|
|
if input.is_any(args![b'r', "recursive"]) {
|
|
output.walker.recursion_depth = if input.is_long() {
|
|
let limit = take!();
|
|
let limit: usize = (&limit).parse().wrap_err("`--recursive` expects a positive integer")
|
|
.with_section(move || limit.header("Invalid parameter was"))?;
|
|
match limit {
|
|
0 => None,
|
|
1 => {
|
|
warn!("`--recursive 1` is a no-op, did you mean `--recursive 2`?");
|
|
Some(1)
|
|
},
|
|
n => Some(n),
|
|
}
|
|
} else {
|
|
None
|
|
};
|
|
}
|
|
|
|
// -a, -m, -c, -b, --{a,m,c,b}time
|
|
if input.is_any(args![b'a', "atime"]) {
|
|
output.worker.by = work::OrderBy::AccessTime;
|
|
} else if input.is_any(args![b'c', "ctime"]) {
|
|
output.worker.by = work::OrderBy::ChangeTime;
|
|
} else if input.is_any(args![b'm', "mtime"]) {
|
|
output.worker.by = work::OrderBy::ModifiedTime;
|
|
} else if input.is_any(args![b'b', "btime"]) {
|
|
output.worker.by = work::OrderBy::BirthTime;
|
|
}
|
|
|
|
// -z, -0, --nul,
|
|
// -I, --delim <char>|ifs
|
|
'delim: {
|
|
output.delim = if input.is_any(args![b"z0", "nul"]) {
|
|
0u8
|
|
} else if input.is_any(args![b'I', "delim"]) {
|
|
fn read_ifs_as_byte() -> eyre::Result<u8>
|
|
{
|
|
let Some(ifs) = std::env::var_os("IFS") else {
|
|
return Err(eyre!("IFS env-var not set"));
|
|
};
|
|
use std::os::unix::prelude::*;
|
|
ifs.as_bytes().first().copied().ok_or(eyre!("IFS env-var empty"))
|
|
}
|
|
|
|
if input.is_long() {
|
|
let val = take!();
|
|
match val.as_bytes() {
|
|
[] => return Err(eyre!("Line seperator cannot be empty").with_suggestion(|| "--delim ifs|<byte>".header("Usage is"))),
|
|
b"ifs" | b"IFS" => {
|
|
read_ifs_as_byte().wrap_err(eyre!("Failed to read line seperator from IFS env var"))?
|
|
}
|
|
[de] => *de,
|
|
[de, rest @ ..] => {
|
|
warn!("Specified more than one byte for line seperator. Ignoring other {} bytes", rest.len());
|
|
*de
|
|
},
|
|
}
|
|
} else {
|
|
// Read IFS
|
|
read_ifs_as_byte().wrap_err(eyre!("Failed to read line seperator from IFS env var"))?
|
|
}
|
|
} else {
|
|
// No change
|
|
break 'delim;
|
|
};
|
|
};
|
|
|
|
// -n, --reverse
|
|
output.reverse = input.is_any(args![b'n', "reverse"]);
|
|
|
|
// -P, -p, --parallel cpus|<max>
|
|
// -1
|
|
if input.is_any(args![b'P', b'p', "parallel"]) {
|
|
if input.is_long() {
|
|
let mut num = take!();
|
|
if let Ok(n) = num.parse() {
|
|
output.walker.max_walkers = std::num::NonZeroUsize::new(n);
|
|
} else {
|
|
num.make_ascii_lowercase();
|
|
match &num[..] {
|
|
"cpus" => output.walker.max_walkers = std::num::NonZeroUsize::new(*walk::NUM_CPUS),
|
|
_ => return Err(eyre!("`--parallel` expects a positive integer or the string 'cpus'")).with_context(move || num.header("Invalid parameter was")),
|
|
}
|
|
}
|
|
} else {
|
|
output.walker.max_walkers = if input.is_any(*b"P") {
|
|
None
|
|
} else {
|
|
std::num::NonZeroUsize::new(*walk::NUM_CPUS)
|
|
};
|
|
}
|
|
} else if input.is_any(args![b'1']) {
|
|
output.walker.max_walkers = std::num::NonZeroUsize::new(1);
|
|
}
|
|
|
|
Ok(None)
|
|
}
|
|
|
|
fn parse(args: impl IntoIterator<Item=String>) -> eyre::Result<Mode>
|
|
{
|
|
let mut output = Args::default();
|
|
let mut args = args.into_iter().fuse();
|
|
|
|
let mut rest = Vec::new();
|
|
while let Some(current) = args.next()
|
|
{
|
|
|
|
macro_rules! single {
|
|
($input:expr) => {
|
|
{
|
|
let input = Arg::from($input);
|
|
if let Some(mode) = parse_single($input, &mut args, &mut output)
|
|
.wrap_err(eyre!("Parsing error for argument '{}'", &input))
|
|
.with_section(|| current.clone().header("Current arg was"))?
|
|
{
|
|
return Ok(mode);
|
|
}
|
|
}
|
|
};
|
|
}
|
|
match current.as_bytes() {
|
|
b"-" |
|
|
b"--" => break,
|
|
[b'-', b'-', ..] => {
|
|
// Long opt
|
|
single!(Arg::Long(¤t[2..]));
|
|
},
|
|
[b'-', short @ ..] => {
|
|
// Short opts
|
|
single!(Arg::Short(short))
|
|
},
|
|
_ => {
|
|
// Not an opt, a path.
|
|
rest.push(PathBuf::from(current));
|
|
break;
|
|
},
|
|
}
|
|
}
|
|
rest.extend(args.map(Into::into));
|
|
output.paths = match rest {
|
|
empty if empty.is_empty() => None,
|
|
rest => Some(rest),
|
|
};
|
|
|
|
Ok(Mode::Normal(output))
|
|
}
|
|
|
|
//TODO: fn parse(args: impl IntoIterator<Item=String>) -> eyre::Result<Args>
|
|
|