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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## Features
- Add `--ignore-parent` option to override `--no-ignore-parent`, see #1958 (@tmchow)
- Add `--exact` option to match the entire filename exactly (literal, non-substring).
- Add `{.ext}` placeholder for file extensions in `--format`, `--exec`, and `--exec-batch`, see #1818 (@puneetdixit200).

## Bugfixes
- Handle invalid working directories gracefully when using `--full-path`, see #1900 (@Xavrir).
Expand Down
3 changes: 3 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,7 @@ pub struct Opts {
/// '{/}': basename
/// '{//}': parent directory
/// '{.}': path without file extension
/// '{.ext}': file extension
/// '{/.}': basename without file extension
#[arg(
long,
Expand Down Expand Up @@ -901,6 +902,7 @@ impl clap::Args for Exec {
'{/}': basename\n \
'{//}': parent directory\n \
'{.}': path without file extension\n \
'{.ext}': file extension\n \
'{/.}': basename without file extension\n \
'{{': literal '{' (for escaping)\n \
'}}': literal '}' (for escaping)\n\n\
Expand Down Expand Up @@ -936,6 +938,7 @@ impl clap::Args for Exec {
'{/}': basename\n \
'{//}': parent directory\n \
'{.}': path without file extension\n \
'{.ext}': file extension\n \
'{/.}': basename without file extension\n \
'{{': literal '{' (for escaping)\n \
'}}': literal '}' (for escaping)\n\n\
Expand Down
16 changes: 16 additions & 0 deletions src/exec/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,22 @@ mod tests {
);
}

#[test]
fn tokens_with_extension() {
assert_eq!(
CommandSet::new(vec![vec!["echo", "{.ext}"]]).unwrap(),
CommandSet {
commands: vec![CommandTemplate {
args: vec![
FormatTemplate::Text("echo".into()),
FormatTemplate::Tokens(vec![Token::Extension]),
],
}],
mode: ExecutionMode::OneByOne,
}
);
}

#[test]
fn tokens_with_basename() {
assert_eq!(
Expand Down
11 changes: 11 additions & 0 deletions src/fmt/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ pub fn remove_extension(path: &Path) -> OsString {
strip_current_dir(&path).to_owned().into_os_string()
}

/// Returns the extension of the path.
pub fn extension(path: &Path) -> &OsStr {
path.extension().unwrap_or_default()
}

/// Removes the basename from the path.
pub fn dirname(path: &Path) -> OsString {
path.parent()
Expand Down Expand Up @@ -60,6 +65,12 @@ mod path_tests {
remove_ext_utf8: remove_extension for "💖.txt" => "💖"
remove_ext_empty: remove_extension for "" => ""

extension_simple: extension for "foo.txt" => "txt"
extension_dir: extension for "dir/foo.txt" => "txt"
extension_hidden: extension for ".foo" => ""
extension_no_ext: extension for "foo" => ""
extension_utf8: extension for "dir/foo.💖" => "💖"

basename_simple: basename for "foo.txt" => "foo.txt"
basename_dir: basename for "dir/foo.txt" => "foo.txt"
basename_empty: basename for "" => ""
Expand Down
30 changes: 27 additions & 3 deletions src/fmt/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use std::sync::OnceLock;

use aho_corasick::AhoCorasick;

use self::input::{basename, dirname, remove_extension};
use self::input::{basename, dirname, extension, remove_extension};

/// Designates what should be written to a buffer
///
Expand All @@ -20,6 +20,7 @@ pub enum Token {
Basename,
Parent,
NoExt,
Extension,
BasenameNoExt,
Text(String),
}
Expand All @@ -31,6 +32,7 @@ impl Display for Token {
Token::Basename => f.write_str("{/}")?,
Token::Parent => f.write_str("{//}")?,
Token::NoExt => f.write_str("{.}")?,
Token::Extension => f.write_str("{.ext}")?,
Token::BasenameNoExt => f.write_str("{/.}")?,
Token::Text(ref string) => f.write_str(string)?,
}
Expand Down Expand Up @@ -62,7 +64,7 @@ impl FormatTemplate {
let mut remaining = fmt;
let mut buf = String::new();
let placeholders = PLACEHOLDERS.get_or_init(|| {
AhoCorasick::new(["{{", "}}", "{}", "{/}", "{//}", "{.}", "{/.}"]).unwrap()
AhoCorasick::new(["{{", "}}", "{}", "{/}", "{//}", "{.}", "{.ext}", "{/.}"]).unwrap()
});
while let Some(m) = placeholders.find(remaining) {
match m.pattern().as_u32() {
Expand Down Expand Up @@ -127,6 +129,9 @@ impl FormatTemplate {
&remove_extension(path),
path_separator,
)),
Extension => {
s.push(Self::replace_separator(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))
Expand Down Expand Up @@ -205,7 +210,8 @@ fn token_from_pattern_id(id: u32) -> Token {
3 => Basename,
4 => Parent,
5 => NoExt,
6 => BasenameNoExt,
6 => Extension,
7 => BasenameNoExt,
_ => unreachable!(),
}
}
Expand Down Expand Up @@ -242,6 +248,7 @@ mod fmt_tests {
basename={/} \
parent={//} \
noExt={.} \
ext={.ext} \
basenameNoExt={/.} \
}}",
);
Expand All @@ -256,6 +263,8 @@ mod fmt_tests {
Parent,
Text(" noExt=".into()),
NoExt,
Text(" ext=".into()),
Extension,
Text(" basenameNoExt=".into()),
BasenameNoExt,
Text(" }".into()),
Expand All @@ -275,7 +284,22 @@ mod fmt_tests {
basename=file.txt \
parent=a/folder \
noExt=a/folder/file \
ext=txt \
basenameNoExt=file }"
);
}

#[test]
fn extension_placeholder() {
let templ = FormatTemplate::parse("extension={.ext}");

let mut path = PathBuf::new();
path.push("a");
path.push("folder");
path.push("file.txt");

let expanded = templ.generate(&path, Some("/")).into_string().unwrap();

assert_eq!(expanded, "extension=txt");
}
}
20 changes: 20 additions & 0 deletions tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1701,6 +1701,16 @@ fn format() {
noExt=one/two/three/directory_foo",
);

te.assert_output(
&["foo", "--format", "extension={.ext}", "--path-separator=/"],
"extension=foo
extension=foo
extension=Foo2
extension=foo
extension=foo
extension=",
);

te.assert_output(
&["foo", "--format", "basename={/}", "--path-separator=/"],
"basename=a.foo
Expand Down Expand Up @@ -1781,6 +1791,16 @@ fn test_exec() {
one/two/three/directory_foo",
);

te.assert_output(
&["foo", "--exec", "echo", "{.}_encoded.{.ext}"],
"a_encoded.foo
one/b_encoded.foo
one/two/C_encoded.Foo2
one/two/c_encoded.foo
one/two/three/d_encoded.foo
one/two/three/directory_foo_encoded.",
);

te.assert_output(
&["foo", "--exec", "echo", "{/}"],
"a.foo
Expand Down
Loading