From 1d8ad3d13d25576210fa71a59b19d8705ef43703 Mon Sep 17 00:00:00 2001 From: Thayne McCombs Date: Mon, 29 Jan 2024 00:38:15 -0700 Subject: [PATCH] Add docs and tests for --format Also fix bug where we didn't strip leading cwd. --- doc/fd.1 | 63 ++++++++++++++++++++++----------------------- src/fmt/mod.rs | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/output.rs | 5 +++- tests/tests.rs | 60 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 164 insertions(+), 34 deletions(-) diff --git a/doc/fd.1 b/doc/fd.1 index 8877317..1692831 100644 --- a/doc/fd.1 +++ b/doc/fd.1 @@ -363,6 +363,30 @@ Set the path separator to use when printing file paths. The default is the OS-sp Provide paths to search as an alternative to the positional \fIpath\fR argument. Changes the usage to \'fd [FLAGS/OPTIONS] \-\-search\-path PATH \-\-search\-path PATH2 [PATTERN]\' .TP +.BI "\-\-format " fmt +Specify a template string that is used for printing a line for each file found. + +The following placeholders are substituted into the string for each file before printing: +.RS +.IP {} +path (of the current search result) +.IP {/} +basename +.IP {//} +parent directory +.IP {.} +path without file extension +.IP {/.} +basename without file extension +.IP {{ +literal '{' (an escape sequence) +.IP }} +literal '}' (an escape sequence) +.P +Notice that you can use "{{" and "}}" to escape "{" and "}" respectively, which is especially +useful if you need to include the literal text of one of the above placeholders. +.RE +.TP .BI "\-x, \-\-exec " command .RS Execute @@ -383,29 +407,12 @@ If parallelism is enabled, the order commands will be executed in is non-determi --threads=1, the order is determined by the operating system and may not be what you expect. Thus, it is recommended that you don't rely on any ordering of the results. -The following placeholders are substituted before the command is executed: -.RS -.IP {} -path (of the current search result) -.IP {/} -basename -.IP {//} -parent directory -.IP {.} -path without file extension -.IP {/.} -basename without file extension -.IP {{ -literal '{' (an escape sequence) -.IP }} -literal '}' (an escape sequence) -.RE +Before executing the command, any placeholder patterns in the command are replaced with the +corresponding values for the current file. The same placeholders are used as in the "\-\-format" +option. If no placeholder is present, an implicit "{}" at the end is assumed. -Notice that you can use "{{" and "}}" to escape "{" and "}" respectively, which is especially -useful if you need to include the literal text of one of the above placeholders. - Examples: - find all *.zip files and unzip them: @@ -429,19 +436,9 @@ once, with all search results as arguments. The order of the arguments is non-deterministic and should not be relied upon. -One of the following placeholders is substituted before the command is executed: -.RS -.IP {} -path (of all search results) -.IP {/} -basename -.IP {//} -parent directory -.IP {.} -path without file extension -.IP {/.} -basename without file extension -.RE +This uses the same placeholders as "\-\-format" and "\-\-exec", but instead of expanding +once per command invocation each argument containing a placeholder is expanding for every +file in a batch and passed as separate arguments. If no placeholder is present, an implicit "{}" at the end is assumed. diff --git a/src/fmt/mod.rs b/src/fmt/mod.rs index 423b49b..6c9a05f 100644 --- a/src/fmt/mod.rs +++ b/src/fmt/mod.rs @@ -209,3 +209,73 @@ fn token_from_pattern_id(id: u32) -> Token { _ => unreachable!(), } } + +#[cfg(test)] +mod fmt_tests { + use super::*; + use std::path::PathBuf; + + #[test] + fn parse_no_placeholders() { + let templ = FormatTemplate::parse("This string has no placeholders"); + assert_eq!( + templ, + FormatTemplate::Text("This string has no placeholders".into()) + ); + } + + #[test] + fn parse_only_brace_escapes() { + let templ = FormatTemplate::parse("This string only has escapes like {{ and }}"); + assert_eq!( + templ, + FormatTemplate::Text("This string only has escapes like { and }".into()) + ); + } + + #[test] + fn all_placeholders() { + use Token::*; + + let templ = FormatTemplate::parse( + "{{path={} \ + basename={/} \ + parent={//} \ + noExt={.} \ + basenameNoExt={/.} \ + }}", + ); + assert_eq!( + templ, + FormatTemplate::Tokens(vec![ + Text("{path=".into()), + Placeholder, + Text(" basename=".into()), + Basename, + Text(" parent=".into()), + Parent, + Text(" noExt=".into()), + NoExt, + Text(" basenameNoExt=".into()), + BasenameNoExt, + Text(" }".into()), + ]) + ); + + 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, + "{path=a/folder/file.txt \ + basename=file.txt \ + parent=a/folder \ + noExt=a/folder/file \ + basenameNoExt=file }" + ); + } +} diff --git a/src/output.rs b/src/output.rs index a58b12b..08ad22b 100644 --- a/src/output.rs +++ b/src/output.rs @@ -66,7 +66,10 @@ fn print_entry_format( format: &FormatTemplate, ) -> io::Result<()> { let separator = if config.null_separator { "\0" } else { "\n" }; - let output = format.generate(entry.path(), config.path_separator.as_deref()); + let output = format.generate( + entry.stripped_path(&config), + config.path_separator.as_deref(), + ); // TODO: support writing raw bytes on unix? write!(stdout, "{}{}", output.to_string_lossy(), separator) } diff --git a/tests/tests.rs b/tests/tests.rs index 1edbeae..6fccdb7 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -1622,6 +1622,66 @@ fn test_excludes() { ); } +#[test] +fn format() { + let te = TestEnv::new(DEFAULT_DIRS, DEFAULT_FILES); + + te.assert_output( + &["--format", "path={}", "--path-separator=/"], + "path=a.foo + path=e1 e2 + path=one + path=one/b.foo + path=one/two + path=one/two/C.Foo2 + path=one/two/c.foo + path=one/two/three + path=one/two/three/d.foo + path=one/two/three/directory_foo + path=symlink", + ); + + te.assert_output( + &["foo", "--format", "noExt={.}", "--path-separator=/"], + "noExt=a + noExt=one/b + noExt=one/two/C + noExt=one/two/c + noExt=one/two/three/d + noExt=one/two/three/directory_foo", + ); + + te.assert_output( + &["foo", "--format", "basename={/}", "--path-separator=/"], + "basename=a.foo + basename=b.foo + basename=C.Foo2 + basename=c.foo + basename=d.foo + basename=directory_foo", + ); + + te.assert_output( + &["foo", "--format", "name={/.}", "--path-separator=/"], + "name=a + name=b + name=C + name=c + name=d + name=directory_foo", + ); + + te.assert_output( + &["foo", "--format", "parent={//}", "--path-separator=/"], + "parent=. + parent=one + parent=one/two + parent=one/two + parent=one/two/three + parent=one/two/three", + ); +} + /// Shell script execution (--exec) #[test] fn test_exec() {