//! 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" ] ) {
0 u8
} 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>