Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
32 changes: 16 additions & 16 deletions src/dir_entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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<FileType> {
match &self.inner {
DirEntryInner::Normal(e) => e.file_type(),
Expand All @@ -94,6 +78,22 @@ impl DirEntry {
}
}

pub fn ino(&self) -> Option<u64> {
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())
Expand Down
13 changes: 4 additions & 9 deletions src/exec/job.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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());
Expand All @@ -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)
}
61 changes: 38 additions & 23 deletions src/exec/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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<I>(&self, paths: I, limit: usize, path_separator: Option<&str>) -> ExitCode
pub fn execute_batch<I>(
&self,
entries: I,
limit: usize,
config: &crate::config::Config,
) -> ExitCode
where
I: Iterator<Item = PathBuf>,
I: Iterator<Item = crate::dir_entry::DirEntry>,
{
let builders: io::Result<Vec<_>> = self
.commands
Expand All @@ -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);
}
}
Expand Down Expand Up @@ -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());
}
}

Expand All @@ -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))
Expand Down Expand Up @@ -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<Command> {
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<Command> {
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)
}
Expand All @@ -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()
}

Expand Down Expand Up @@ -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)
);
};
}

Expand All @@ -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)
);
};
}

Expand Down
80 changes: 73 additions & 7 deletions src/fmt/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
///
Expand All @@ -21,6 +23,8 @@ pub enum Token {
Parent,
NoExt,
BasenameNoExt,
Inode,
FileSize,
Text(String),
}

Expand All @@ -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(())
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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>, 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>,
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) => {
Expand All @@ -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),
}
}
Expand Down Expand Up @@ -206,6 +267,8 @@ fn token_from_pattern_id(id: u32) -> Token {
4 => Parent,
5 => NoExt,
6 => BasenameNoExt,
7 => Inode,
8 => FileSize,
_ => unreachable!(),
}
}
Expand Down Expand Up @@ -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,
Expand Down
5 changes: 1 addition & 4 deletions src/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,7 @@ fn print_entry_format<W: Write>(
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())
}
Expand Down