Skip to content

Commit

Permalink
Implement option for printing custom formats
Browse files Browse the repository at this point in the history
  • Loading branch information
tmccombs committed Jun 16, 2022
1 parent f227bb2 commit 794865b
Show file tree
Hide file tree
Showing 8 changed files with 252 additions and 200 deletions.
18 changes: 17 additions & 1 deletion src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,22 @@ pub fn build_app() -> Command<'static> {
you can use the regex '^[^.]+$' as a normal search pattern.",
),
)
.arg(
Arg::new("format")
.long("format")
.takes_value(true)
.value_name("fmt")
.help("Print results using according to template")
.conflicts_with("list-details")
.long_help(
"Instead of printing the file normally, print the format string with the following placeholders replaced:\n \
'{}': path (of the current search result)\n \
'{/}': basename\n \
'{//}': parent directory\n \
'{.}': path without file extension\n \
'{/.}': basename without file extension\n\n",
),
)
.arg(
Arg::new("exec")
.long("exec")
Expand All @@ -390,7 +406,7 @@ pub fn build_app() -> Command<'static> {
.allow_hyphen_values(true)
.value_terminator(";")
.value_name("cmd")
.conflicts_with("list-details")
.conflicts_with_all(&["list-details", "format"])
.help("Execute a command for each search result")
.long_help(
"Execute a command for each search result in parallel (use --threads=1 for sequential command execution). \
Expand Down
4 changes: 4 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use crate::filetypes::FileTypes;
#[cfg(unix)]
use crate::filter::OwnerFilter;
use crate::filter::{SizeFilter, TimeFilter};
use crate::fmt::FormatTemplate;

/// Configuration options for *fd*.
pub struct Config {
Expand Down Expand Up @@ -82,6 +83,9 @@ pub struct Config {
/// The value (if present) will be a lowercase string without leading dots.
pub extensions: Option<RegexSet>,

/// A format string to use to format results, similarly to exec
pub format: Option<FormatTemplate>,

/// If a value is supplied, each item found will be used to generate and execute commands.
pub command: Option<Arc<CommandSet>>,

Expand Down
196 changes: 27 additions & 169 deletions src/exec/mod.rs
Original file line number Diff line number Diff line change
@@ -1,27 +1,21 @@
mod command;
mod input;
mod job;
mod token;

use std::borrow::Cow;
use std::ffi::{OsStr, OsString};
use std::ffi::OsString;
use std::io;
use std::iter;
use std::path::{Component, Path, PathBuf, Prefix};
use std::path::{Path, PathBuf};
use std::process::Stdio;
use std::sync::{Arc, Mutex};

use anyhow::{bail, Result};
use argmax::Command;
use once_cell::sync::Lazy;
use regex::Regex;

use crate::exit_codes::ExitCode;
use crate::fmt::{FormatTemplate, Token};

use self::command::{execute_commands, handle_cmd_error};
use self::input::{basename, dirname, remove_extension};
pub use self::job::{batch, job};
use self::token::Token;

/// Execution mode of the command
#[derive(Debug, Clone, Copy, PartialEq)]
Expand Down Expand Up @@ -131,7 +125,7 @@ impl CommandSet {
#[derive(Debug)]
struct CommandBuilder {
pre_args: Vec<OsString>,
path_arg: ArgumentTemplate,
path_arg: FormatTemplate,
post_args: Vec<OsString>,
cmd: Command,
count: usize,
Expand Down Expand Up @@ -212,7 +206,7 @@ impl CommandBuilder {
/// `generate_and_execute()` method will be used to generate a command and execute it.
#[derive(Debug, Clone, PartialEq)]
struct CommandTemplate {
args: Vec<ArgumentTemplate>,
args: Vec<FormatTemplate>,
}

impl CommandTemplate {
Expand All @@ -221,50 +215,19 @@ impl CommandTemplate {
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
static PLACEHOLDER_PATTERN: Lazy<Regex> =
Lazy::new(|| Regex::new(r"\{(/?\.?|//)\}").unwrap());

let mut args = Vec::new();
let mut has_placeholder = false;

for arg in input {
let arg = arg.as_ref();

let mut tokens = Vec::new();
let mut start = 0;

for placeholder in PLACEHOLDER_PATTERN.find_iter(arg) {
// Leading text before the placeholder.
if placeholder.start() > start {
tokens.push(Token::Text(arg[start..placeholder.start()].to_owned()));
}

start = placeholder.end();

match placeholder.as_str() {
"{}" => tokens.push(Token::Placeholder),
"{.}" => tokens.push(Token::NoExt),
"{/}" => tokens.push(Token::Basename),
"{//}" => tokens.push(Token::Parent),
"{/.}" => tokens.push(Token::BasenameNoExt),
_ => unreachable!("Unhandled placeholder"),
}
let template = FormatTemplate::parse(arg);

if template.has_tokens() {
has_placeholder = true;
}

// Without a placeholder, the argument is just fixed text.
if tokens.is_empty() {
args.push(ArgumentTemplate::Text(arg.to_owned()));
continue;
}

if start < arg.len() {
// Trailing text after last placeholder.
tokens.push(Token::Text(arg[start..].to_owned()));
}

args.push(ArgumentTemplate::Tokens(tokens));
args.push(template);
}

// We need to check that we have at least one argument, because if not
Expand All @@ -278,7 +241,7 @@ impl CommandTemplate {

// If a placeholder token was not supplied, append one at the end of the command.
if !has_placeholder {
args.push(ArgumentTemplate::Tokens(vec![Token::Placeholder]));
args.push(FormatTemplate::Tokens(vec![Token::Placeholder]));
}

Ok(CommandTemplate { args })
Expand All @@ -301,111 +264,6 @@ impl CommandTemplate {
}
}

/// Represents a template for a single command argument.
///
/// The argument is either a collection of `Token`s including at least one placeholder variant, or
/// a fixed text.
#[derive(Clone, Debug, PartialEq)]
enum ArgumentTemplate {
Tokens(Vec<Token>),
Text(String),
}

impl ArgumentTemplate {
pub fn has_tokens(&self) -> bool {
matches!(self, ArgumentTemplate::Tokens(_))
}

/// Generate an argument from this template. If path_separator is Some, then it will replace
/// the path separator in all placeholder tokens. Text arguments and tokens are not affected by
/// path separator substitution.
pub fn generate(&self, path: impl AsRef<Path>, path_separator: Option<&str>) -> OsString {
use self::Token::*;
let path = path.as_ref();

match *self {
ArgumentTemplate::Tokens(ref tokens) => {
let mut s = OsString::new();
for token in tokens {
match *token {
Basename => s.push(Self::replace_separator(basename(path), path_separator)),
BasenameNoExt => s.push(Self::replace_separator(
&remove_extension(basename(path).as_ref()),
path_separator,
)),
NoExt => s.push(Self::replace_separator(
&remove_extension(path),
path_separator,
)),
Parent => s.push(Self::replace_separator(&dirname(path), path_separator)),
Placeholder => {
s.push(Self::replace_separator(path.as_ref(), path_separator))
}
Text(ref string) => s.push(string),
}
}
s
}
ArgumentTemplate::Text(ref text) => OsString::from(text),
}
}

/// Replace the path separator in the input with the custom separator string. If path_separator
/// is None, simply return a borrowed Cow<OsStr> of the input. Otherwise, the input is
/// interpreted as a Path and its components are iterated through and re-joined into a new
/// OsString.
fn replace_separator<'a>(path: &'a OsStr, path_separator: Option<&str>) -> Cow<'a, OsStr> {
// fast-path - no replacement necessary
if path_separator.is_none() {
return Cow::Borrowed(path);
}

let path_separator = path_separator.unwrap();
let mut out = OsString::with_capacity(path.len());
let mut components = Path::new(path).components().peekable();

while let Some(comp) = components.next() {
match comp {
// Absolute paths on Windows are tricky. A Prefix component is usually a drive
// letter or UNC path, and is usually followed by RootDir. There are also
// "verbatim" prefixes beginning with "\\?\" that skip normalization. We choose to
// ignore verbatim path prefixes here because they're very rare, might be
// impossible to reach here, and there's no good way to deal with them. If users
// are doing something advanced involving verbatim windows paths, they can do their
// own output filtering with a tool like sed.
Component::Prefix(prefix) => {
if let Prefix::UNC(server, share) = prefix.kind() {
// Prefix::UNC is a parsed version of '\\server\share'
out.push(path_separator);
out.push(path_separator);
out.push(server);
out.push(path_separator);
out.push(share);
} else {
// All other Windows prefix types are rendered as-is. This results in e.g. "C:" for
// drive letters. DeviceNS and Verbatim* prefixes won't have backslashes converted,
// but they're not returned by directories fd can search anyway so we don't worry
// about them.
out.push(comp.as_os_str());
}
}

// Root directory is always replaced with the custom separator.
Component::RootDir => out.push(path_separator),

// Everything else is joined normally, with a trailing separator if we're not last
_ => {
out.push(comp.as_os_str());
if components.peek().is_some() {
out.push(path_separator);
}
}
}
}
Cow::Owned(out)
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand All @@ -417,9 +275,9 @@ mod tests {
CommandSet {
commands: vec![CommandTemplate {
args: vec![
ArgumentTemplate::Text("echo".into()),
ArgumentTemplate::Text("${SHELL}:".into()),
ArgumentTemplate::Tokens(vec![Token::Placeholder]),
FormatTemplate::Text("echo".into()),
FormatTemplate::Text("${SHELL}:".into()),
FormatTemplate::Tokens(vec![Token::Placeholder]),
]
}],
mode: ExecutionMode::OneByOne,
Expand All @@ -435,8 +293,8 @@ mod tests {
CommandSet {
commands: vec![CommandTemplate {
args: vec![
ArgumentTemplate::Text("echo".into()),
ArgumentTemplate::Tokens(vec![Token::NoExt]),
FormatTemplate::Text("echo".into()),
FormatTemplate::Tokens(vec![Token::NoExt]),
],
}],
mode: ExecutionMode::OneByOne,
Expand All @@ -452,8 +310,8 @@ mod tests {
CommandSet {
commands: vec![CommandTemplate {
args: vec![
ArgumentTemplate::Text("echo".into()),
ArgumentTemplate::Tokens(vec![Token::Basename]),
FormatTemplate::Text("echo".into()),
FormatTemplate::Tokens(vec![Token::Basename]),
],
}],
mode: ExecutionMode::OneByOne,
Expand All @@ -469,8 +327,8 @@ mod tests {
CommandSet {
commands: vec![CommandTemplate {
args: vec![
ArgumentTemplate::Text("echo".into()),
ArgumentTemplate::Tokens(vec![Token::Parent]),
FormatTemplate::Text("echo".into()),
FormatTemplate::Tokens(vec![Token::Parent]),
],
}],
mode: ExecutionMode::OneByOne,
Expand All @@ -486,8 +344,8 @@ mod tests {
CommandSet {
commands: vec![CommandTemplate {
args: vec![
ArgumentTemplate::Text("echo".into()),
ArgumentTemplate::Tokens(vec![Token::BasenameNoExt]),
FormatTemplate::Text("echo".into()),
FormatTemplate::Tokens(vec![Token::BasenameNoExt]),
],
}],
mode: ExecutionMode::OneByOne,
Expand All @@ -503,9 +361,9 @@ mod tests {
CommandSet {
commands: vec![CommandTemplate {
args: vec![
ArgumentTemplate::Text("cp".into()),
ArgumentTemplate::Tokens(vec![Token::Placeholder]),
ArgumentTemplate::Tokens(vec![
FormatTemplate::Text("cp".into()),
FormatTemplate::Tokens(vec![Token::Placeholder]),
FormatTemplate::Tokens(vec![
Token::BasenameNoExt,
Token::Text(".ext".into())
]),
Expand All @@ -524,8 +382,8 @@ mod tests {
CommandSet {
commands: vec![CommandTemplate {
args: vec![
ArgumentTemplate::Text("echo".into()),
ArgumentTemplate::Tokens(vec![Token::NoExt]),
FormatTemplate::Text("echo".into()),
FormatTemplate::Tokens(vec![Token::NoExt]),
],
}],
mode: ExecutionMode::Batch,
Expand All @@ -551,7 +409,7 @@ mod tests {

#[test]
fn generate_custom_path_separator() {
let arg = ArgumentTemplate::Tokens(vec![Token::Placeholder]);
let arg = FormatTemplate::Tokens(vec![Token::Placeholder]);
macro_rules! check {
($input:expr, $expected:expr) => {
assert_eq!(arg.generate($input, Some("#")), OsString::from($expected));
Expand All @@ -566,7 +424,7 @@ mod tests {
#[cfg(windows)]
#[test]
fn generate_custom_path_separator_windows() {
let arg = ArgumentTemplate::Tokens(vec![Token::Placeholder]);
let arg = FormatTemplate::Tokens(vec![Token::Placeholder]);
macro_rules! check {
($input:expr, $expected:expr) => {
assert_eq!(arg.generate($input, Some("#")), OsString::from($expected));
Expand Down

0 comments on commit 794865b

Please sign in to comment.