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.
enumerate-ordered/src/args.rs

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(&current[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>