diff --git a/.gitignore b/.gitignore index f8dd5124..8d3118ef 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,6 @@ tests/tmp ## Dynamically generated tests/test_dir + +# Miscenallous +.idea diff --git a/README.md b/README.md index 74f5c4f9..9f813b8d 100644 --- a/README.md +++ b/README.md @@ -142,6 +142,7 @@ These options are available when running with `--long` (`-l`): - **--no-filesize**: suppress the filesize field - **--no-user**: suppress the user field - **--no-time**: suppress the time field +- **--stdin**: read file names from stdin Some of the options accept parameters: diff --git a/completions/fish/eza.fish b/completions/fish/eza.fish index 63242ed0..3bccb110 100644 --- a/completions/fish/eza.fish +++ b/completions/fish/eza.fish @@ -110,6 +110,7 @@ complete -c eza -l no-filesize -d "Suppress the filesize field" complete -c eza -l no-user -d "Suppress the user field" complete -c eza -l no-time -d "Suppress the time field" complete -c eza -s M -l mounts -d "Show mount details" +complete -c eza -l stdin -d "Read file names from standard input" # Optional extras complete -c eza -l git -d "List each file's Git status, if tracked" diff --git a/completions/nush/eza.nu b/completions/nush/eza.nu index e2fecf72..b6fbe40b 100644 --- a/completions/nush/eza.nu +++ b/completions/nush/eza.nu @@ -58,4 +58,5 @@ export extern "eza" [ --extended(-@) # List each file's extended attributes and sizes --context(-Z) # List each file's security context --smart-group # Only show group if it has a different name from owner + --stdin # Read file paths from stdin ] diff --git a/completions/zsh/_eza b/completions/zsh/_eza index 53c379c8..958c4fcd 100644 --- a/completions/zsh/_eza +++ b/completions/zsh/_eza @@ -67,7 +67,8 @@ __eza() { {-Z,--context}"[List each file's security context]" \ {-M,--mounts}"[Show mount details (long mode only)]" \ '*:filename:_files' \ - --smart-group"[Only show group if it has a different name from owner]" + --smart-group"[Only show group if it has a different name from owner]" \ + --stdin"[Read file names from stdin]" } __eza diff --git a/man/eza.1.md b/man/eza.1.md index 400d24cb..f0ebe04b 100644 --- a/man/eza.1.md +++ b/man/eza.1.md @@ -237,6 +237,9 @@ These options are available when running with `--long` (`-l`): `--no-time` : Suppress the time field. +`--stdin` +: read file names from stdin, one per line or other separator specified in environment + `-@`, `--extended` : List each file’s extended attributes and sizes. @@ -323,6 +326,9 @@ If set, automates the same behavior as using `--icons` or `--icons=auto`. Useful Any explicit use of the `--icons=WHEN` flag overrides this behavior. +## `EZA_STDIN_SEPARATOR` + +Specifies the separator to use when reading file names from stdin. Defaults to newline. EXIT STATUSES ============= diff --git a/src/main.rs b/src/main.rs index 97418ffe..e55e3cbd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,20 +23,20 @@ use std::env; use std::ffi::{OsStr, OsString}; -use std::io::{self, ErrorKind, IsTerminal, Write}; +use std::io::{self, stdin, ErrorKind, IsTerminal, Read, Write}; use std::path::{Component, PathBuf}; use std::process::exit; use ansiterm::{ANSIStrings, Style}; -use log::*; - use crate::fs::feature::git::GitCache; use crate::fs::filter::GitIgnore; use crate::fs::{Dir, File}; +use crate::options::stdin::FilesInput; use crate::options::{vars, Options, OptionsResult, Vars}; use crate::output::{details, escape, file_name, grid, grid_details, lines, Mode, View}; use crate::theme::Theme; +use log::*; mod fs; mod info; @@ -60,13 +60,29 @@ fn main() { let stdout_istty = io::stdout().is_terminal(); + let mut input = String::new(); let args: Vec<_> = env::args_os().skip(1).collect(); match Options::parse(args.iter().map(std::convert::AsRef::as_ref), &LiveVars) { OptionsResult::Ok(options, mut input_paths) => { // List the current directory by default. // (This has to be done here, otherwise git_options won’t see it.) if input_paths.is_empty() { - input_paths = vec![OsStr::new(".")]; + match &options.stdin { + FilesInput::Args => { + input_paths = vec![OsStr::new(".")]; + } + FilesInput::Stdin(separator) => { + stdin() + .read_to_string(&mut input) + .expect("Failed to read from stdin"); + input_paths.extend( + input + .split(&separator.clone().into_string().unwrap_or("\n".to_string())) + .map(std::ffi::OsStr::new).filter(|s| !s.is_empty()) + .collect::>(), + ); + } + } } let git = git_options(&options, &input_paths); diff --git a/src/options/flags.rs b/src/options/flags.rs index b821074f..96e035b0 100644 --- a/src/options/flags.rs +++ b/src/options/flags.rs @@ -80,6 +80,7 @@ pub static GIT_REPOS_NO_STAT: Arg = Arg { short: None, long: "git-repos-no pub static EXTENDED: Arg = Arg { short: Some(b'@'), long: "extended", takes_value: TakesValue::Forbidden }; pub static OCTAL: Arg = Arg { short: Some(b'o'), long: "octal-permissions", takes_value: TakesValue::Forbidden }; pub static SECURITY_CONTEXT: Arg = Arg { short: Some(b'Z'), long: "context", takes_value: TakesValue::Forbidden }; +pub static STDIN: Arg = Arg { short: None, long: "stdin", takes_value: TakesValue::Forbidden }; pub static ALL_ARGS: Args = Args(&[ &VERSION, &HELP, @@ -96,5 +97,5 @@ pub static ALL_ARGS: Args = Args(&[ &NO_PERMISSIONS, &NO_FILESIZE, &NO_USER, &NO_TIME, &SMART_GROUP, &GIT, &NO_GIT, &GIT_REPOS, &GIT_REPOS_NO_STAT, - &EXTENDED, &OCTAL, &SECURITY_CONTEXT + &EXTENDED, &OCTAL, &SECURITY_CONTEXT, &STDIN, ]); diff --git a/src/options/help.rs b/src/options/help.rs index f6283348..281e6ca5 100644 --- a/src/options/help.rs +++ b/src/options/help.rs @@ -75,7 +75,8 @@ LONG VIEW OPTIONS -o, --octal-permissions list each file's permission in octal format --no-filesize suppress the filesize field --no-user suppress the user field - --no-time suppress the time field"; + --no-time suppress the time field + --stdin read file names from stdin, one per line or other separator specified in environment"; static GIT_VIEW_HELP: &str = " \ --git list each file's Git status, if tracked or ignored diff --git a/src/options/mod.rs b/src/options/mod.rs index 80eb2222..c6f98577 100644 --- a/src/options/mod.rs +++ b/src/options/mod.rs @@ -72,6 +72,7 @@ use std::ffi::OsStr; use crate::fs::dir_action::DirAction; use crate::fs::filter::{FileFilter, GitIgnore}; +use crate::options::stdin::FilesInput; use crate::output::{details, grid_details, Mode, View}; use crate::theme::Options as ThemeOptions; @@ -95,7 +96,9 @@ use self::parser::MatchedFlags; pub mod vars; pub use self::vars::Vars; +pub mod stdin; mod version; + use self::version::VersionString; /// These **options** represent a parsed, error-checked versions of the @@ -117,14 +120,17 @@ pub struct Options { /// The options to make up the styles of the UI and file names. pub theme: ThemeOptions, + + /// Whether to read file names from stdin instead of the command-line + pub stdin: FilesInput, } -impl Options { +impl<'args> Options { /// Parse the given iterator of command-line strings into an Options /// struct and a list of free filenames, using the environment variables /// for extra options. #[allow(unused_results)] - pub fn parse<'args, I, V>(args: I, vars: &V) -> OptionsResult<'args> + pub fn parse(args: I, vars: &V) -> OptionsResult<'args> where I: IntoIterator, V: Vars, @@ -199,12 +205,14 @@ impl Options { let dir_action = DirAction::deduce(matches, matches!(view.mode, Mode::Details(_)))?; let filter = FileFilter::deduce(matches)?; let theme = ThemeOptions::deduce(matches, vars)?; + let stdin = FilesInput::deduce(matches, vars)?; Ok(Self { dir_action, filter, view, theme, + stdin, }) } } diff --git a/src/options/stdin.rs b/src/options/stdin.rs new file mode 100644 index 00000000..d2cbb3d8 --- /dev/null +++ b/src/options/stdin.rs @@ -0,0 +1,29 @@ +use crate::options::parser::MatchedFlags; +use crate::options::vars::EZA_STDIN_SEPARATOR; +use crate::options::{flags, OptionsError, Vars}; +use std::ffi::OsString; +use std::io; +use std::io::IsTerminal; + +#[derive(Debug, PartialEq)] +pub enum FilesInput { + Stdin(OsString), + Args, +} + +impl FilesInput { + pub fn deduce(matches: &MatchedFlags<'_>, vars: &V) -> Result { + Ok( + if io::stdin().is_terminal() || !matches.has(&flags::STDIN)? { + FilesInput::Args + } else if matches.has(&flags::STDIN)? && !io::stdin().is_terminal() { + let separator = vars + .get(EZA_STDIN_SEPARATOR) + .unwrap_or(OsString::from("\n")); + FilesInput::Stdin(separator) + } else { + FilesInput::Args + }, + ) + } +} diff --git a/src/options/vars.rs b/src/options/vars.rs index e49dbab9..f3a1653a 100644 --- a/src/options/vars.rs +++ b/src/options/vars.rs @@ -64,6 +64,8 @@ pub static EZA_MIN_LUMINANCE: &str = "EZA_MIN_LUMINANCE"; /// Any explicit use of `--icons=WHEN` overrides this behavior. pub static EZA_ICONS_AUTO: &str = "EZA_ICONS_AUTO"; +pub static EZA_STDIN_SEPARATOR: &str = "EZA_STDIN_SEPARATOR"; + /// Mockable wrapper for `std::env::var_os`. pub trait Vars { fn get(&self, name: &'static str) -> Option;