diff --git a/src/cli.rs b/src/cli.rs index d5174689d..01b2fcc4a 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -459,6 +459,8 @@ pub struct Opts { /// '{//}': parent directory /// '{.}': path without file extension /// '{/.}': basename without file extension + /// '{inode}': inode number (Unix only) + /// '{filesize}': file size in bytes #[arg( long, value_name = "fmt", diff --git a/src/dir_entry.rs b/src/dir_entry.rs index 19d2101e5..78cc2f899 100644 --- a/src/dir_entry.rs +++ b/src/dir_entry.rs @@ -46,13 +46,6 @@ impl DirEntry { } } - pub fn into_path(self) -> PathBuf { - match self.inner { - DirEntryInner::Normal(e) => e.into_path(), - DirEntryInner::BrokenSymlink(p) => p, - } - } - /// Returns the path as it should be presented to the user. pub fn stripped_path(&self, config: &Config) -> &Path { if config.strip_cwd_prefix { @@ -62,15 +55,6 @@ impl DirEntry { } } - /// Returns the path as it should be presented to the user. - pub fn into_stripped_path(self, config: &Config) -> PathBuf { - if config.strip_cwd_prefix { - self.stripped_path(config).to_path_buf() - } else { - self.into_path() - } - } - pub fn file_type(&self) -> Option { match &self.inner { DirEntryInner::Normal(e) => e.file_type(), @@ -94,6 +78,22 @@ impl DirEntry { } } + pub fn ino(&self) -> Option { + match &self.inner { + DirEntryInner::Normal(e) => { + #[cfg(unix)] + { + e.ino() + } + #[cfg(not(unix))] + { + None + } + } + DirEntryInner::BrokenSymlink(_) => None, + } + } + pub fn style(&self, ls_colors: &LsColors) -> Option<&Style> { self.style .get_or_init(|| ls_colors.style_for(self).cloned()) diff --git a/src/exec/job.rs b/src/exec/job.rs index 55f1e88d0..e8cd65fe8 100644 --- a/src/exec/job.rs +++ b/src/exec/job.rs @@ -31,12 +31,7 @@ pub fn job( }; // Generate a command, execute it and store its exit code. - let code = cmd.execute( - dir_entry.stripped_path(config), - config.path_separator.as_deref(), - config.null_separator, - buffer_output, - ); + let code = cmd.execute(&dir_entry, config, config.null_separator, buffer_output); ret = merge_exitcodes([ret, code]); } // Returns error in case of any error. @@ -48,10 +43,10 @@ pub fn batch( cmd: &CommandSet, config: &Config, ) -> ExitCode { - let paths = results + let entries = results .into_iter() .filter_map(|worker_result| match worker_result { - WorkerResult::Entry(dir_entry) => Some(dir_entry.into_stripped_path(config)), + WorkerResult::Entry(dir_entry) => Some(dir_entry), WorkerResult::Error(err) => { if config.show_filesystem_errors { print_error(err.to_string()); @@ -60,5 +55,5 @@ pub fn batch( } }); - cmd.execute_batch(paths, config.batch_size, config.path_separator.as_deref()) + cmd.execute_batch(entries, config.batch_size, config) } diff --git a/src/exec/mod.rs b/src/exec/mod.rs index c22e0bd2a..1f3fbeb4c 100644 --- a/src/exec/mod.rs +++ b/src/exec/mod.rs @@ -4,7 +4,6 @@ mod job; use std::ffi::OsString; use std::io; use std::iter; -use std::path::{Path, PathBuf}; use std::process::Stdio; use anyhow::{Result, bail}; @@ -78,21 +77,23 @@ impl CommandSet { pub fn execute( &self, - input: &Path, - path_separator: Option<&str>, + entry: &crate::dir_entry::DirEntry, + config: &crate::config::Config, null_separator: bool, buffer_output: bool, ) -> ExitCode { - let commands = self - .commands - .iter() - .map(|c| c.generate(input, path_separator)); + let commands = self.commands.iter().map(|c| c.generate(entry, config)); execute_commands(commands, OutputBuffer::new(null_separator), buffer_output) } - pub fn execute_batch(&self, paths: I, limit: usize, path_separator: Option<&str>) -> ExitCode + pub fn execute_batch( + &self, + entries: I, + limit: usize, + config: &crate::config::Config, + ) -> ExitCode where - I: Iterator, + I: Iterator, { let builders: io::Result> = self .commands @@ -102,9 +103,9 @@ impl CommandSet { match builders { Ok(mut builders) => { - for path in paths { + for entry in entries { for builder in &mut builders { - if let Err(e) = builder.push(&path, path_separator) { + if let Err(e) = builder.push(&entry, config) { return handle_cmd_error(Some(&builder.cmd), e); } } @@ -145,9 +146,9 @@ impl CommandBuilder { if arg.has_tokens() { path_arg = Some(arg.clone()); } else if path_arg.is_none() { - pre_args.push(arg.generate("", None)); + pre_args.push(arg.as_text()); } else { - post_args.push(arg.generate("", None)); + post_args.push(arg.as_text()); } } @@ -173,12 +174,16 @@ impl CommandBuilder { Ok(cmd) } - fn push(&mut self, path: &Path, separator: Option<&str>) -> io::Result<()> { + fn push( + &mut self, + entry: &crate::dir_entry::DirEntry, + config: &crate::config::Config, + ) -> io::Result<()> { if self.limit > 0 && self.count >= self.limit { self.finish()?; } - let arg = self.path_arg.generate(path, separator); + let arg = self.path_arg.generate(entry, config); if !self .cmd .args_would_fit(iter::once(&arg).chain(&self.post_args)) @@ -259,12 +264,16 @@ impl CommandTemplate { /// Generates and executes a command. /// - /// Using the internal `args` field, and a supplied `input` variable, a `Command` will be - /// build. - fn generate(&self, input: &Path, path_separator: Option<&str>) -> io::Result { - let mut cmd = Command::new(self.args[0].generate(input, path_separator)); + /// Using the internal `args` field, and a supplied `entry` variable, a `Command` will be + /// built. + fn generate( + &self, + entry: &crate::dir_entry::DirEntry, + config: &crate::config::Config, + ) -> io::Result { + let mut cmd = Command::new(self.args[0].generate(entry, config)); for arg in &self.args[1..] { - cmd.try_arg(arg.generate(input, path_separator))?; + cmd.try_arg(arg.generate(entry, config))?; } Ok(cmd) } @@ -278,7 +287,7 @@ mod tests { template .args .iter() - .map(|arg| arg.generate(input, None).into_string().unwrap()) + .map(|arg| arg.generate_from_path(input, None).into_string().unwrap()) .collect() } @@ -434,7 +443,10 @@ mod tests { let arg = FormatTemplate::Tokens(vec![Token::Placeholder]); macro_rules! check { ($input:expr, $expected:expr) => { - assert_eq!(arg.generate($input, Some("#")), OsString::from($expected)); + assert_eq!( + arg.generate_from_path($input, Some("#")), + OsString::from($expected) + ); }; } @@ -449,7 +461,10 @@ mod tests { let arg = FormatTemplate::Tokens(vec![Token::Placeholder]); macro_rules! check { ($input:expr, $expected:expr) => { - assert_eq!(arg.generate($input, Some("#")), OsString::from($expected)); + assert_eq!( + arg.generate_from_path($input, Some("#")), + OsString::from($expected) + ); }; } diff --git a/src/fmt/mod.rs b/src/fmt/mod.rs index 87ee41923..92b21bba2 100644 --- a/src/fmt/mod.rs +++ b/src/fmt/mod.rs @@ -9,6 +9,8 @@ use std::sync::OnceLock; use aho_corasick::AhoCorasick; use self::input::{basename, dirname, remove_extension}; +use crate::config::Config; +use crate::dir_entry::DirEntry; /// Designates what should be written to a buffer /// @@ -21,6 +23,8 @@ pub enum Token { Parent, NoExt, BasenameNoExt, + Inode, + FileSize, Text(String), } @@ -32,6 +36,8 @@ impl Display for Token { Token::Parent => f.write_str("{//}")?, Token::NoExt => f.write_str("{.}")?, Token::BasenameNoExt => f.write_str("{/.}")?, + Token::Inode => f.write_str("{inode}")?, + Token::FileSize => f.write_str("{filesize}")?, Token::Text(ref string) => f.write_str(string)?, } Ok(()) @@ -62,7 +68,18 @@ impl FormatTemplate { let mut remaining = fmt; let mut buf = String::new(); let placeholders = PLACEHOLDERS.get_or_init(|| { - AhoCorasick::new(["{{", "}}", "{}", "{/}", "{//}", "{.}", "{/.}"]).unwrap() + AhoCorasick::new([ + "{{", + "}}", + "{}", + "{/}", + "{//}", + "{.}", + "{/.}", + "{inode}", + "{filesize}", + ]) + .unwrap() }); while let Some(m) = placeholders.find(remaining) { match m.pattern().as_u32() { @@ -106,12 +123,42 @@ impl FormatTemplate { FormatTemplate::Tokens(tokens) } - /// Generate a result string from this template. If path_separator is Some, then it will replace - /// the path separator in all placeholder tokens. Fixed text and tokens are not affected by - /// path separator substitution. - pub fn generate(&self, path: impl AsRef, path_separator: Option<&str>) -> OsString { + /// Generate a result string from this template using a DirEntry and Config. + /// This method supports metadata-based tokens like {inode}. + pub fn generate(&self, entry: &DirEntry, config: &Config) -> OsString { + self.generate_impl( + entry.stripped_path(config), + config.path_separator.as_deref(), + Some(entry), + ) + } + + /// Extract the text content for Text templates. Panics if this is a Tokens template. + pub fn as_text(&self) -> OsString { + match self { + Self::Text(text) => OsString::from(text), + Self::Tokens(_) => panic!("as_text() called on Tokens template"), + } + } + + /// Test-only helper to generate from a path without DirEntry. + /// Metadata-based tokens like {inode} will be ignored. + #[cfg(test)] + pub fn generate_from_path( + &self, + path: impl AsRef, + path_separator: Option<&str>, + ) -> OsString { + self.generate_impl(path.as_ref(), path_separator, None) + } + + fn generate_impl( + &self, + path: &Path, + path_separator: Option<&str>, + dir_entry: Option<&DirEntry>, + ) -> OsString { use Token::*; - let path = path.as_ref(); match *self { Self::Tokens(ref tokens) => { @@ -131,6 +178,20 @@ impl FormatTemplate { Placeholder => { s.push(Self::replace_separator(path.as_ref(), path_separator)) } + Inode => { + if let Some(entry) = dir_entry + && let Some(ino) = entry.ino() + { + s.push(ino.to_string()); + } + } + FileSize => { + if let Some(entry) = dir_entry + && let Some(metadata) = entry.metadata() + { + s.push(metadata.len().to_string()); + } + } Text(string) => s.push(string), } } @@ -206,6 +267,8 @@ fn token_from_pattern_id(id: u32) -> Token { 4 => Parent, 5 => NoExt, 6 => BasenameNoExt, + 7 => Inode, + 8 => FileSize, _ => unreachable!(), } } @@ -267,7 +330,10 @@ mod fmt_tests { path.push("folder"); path.push("file.txt"); - let expanded = templ.generate(&path, Some("/")).into_string().unwrap(); + let expanded = templ + .generate_from_path(&path, Some("/")) + .into_string() + .unwrap(); assert_eq!( expanded, diff --git a/src/output.rs b/src/output.rs index 91e9f0216..c14d4a849 100644 --- a/src/output.rs +++ b/src/output.rs @@ -71,10 +71,7 @@ fn print_entry_format( config: &Config, format: &FormatTemplate, ) -> io::Result<()> { - let output = format.generate( - entry.stripped_path(config), - config.path_separator.as_deref(), - ); + let output = format.generate(entry, config); // TODO: support writing raw bytes on unix? write!(stdout, "{}", output.to_string_lossy()) }