diff --git a/Cargo.lock b/Cargo.lock index a5da4477..ccdbbf0e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -766,8 +766,7 @@ dependencies = [ [[package]] name = "walkdir" version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +source = "git+https://github.com/refi64/walkdir?branch=symlinks#f799a1c6c008ecd9810f124237470435707ea253" dependencies = [ "same-file", "winapi", diff --git a/Cargo.toml b/Cargo.toml index 237facd6..b565b793 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,9 @@ predicates = "2" serial_test = "0.5" tempfile = "3" +[patch.crates-io] +walkdir = { git = "https://github.com/refi64/walkdir", branch = "symlinks" } + [[bin]] name = "find" path = "src/find/main.rs" diff --git a/src/find/matchers/printf.rs b/src/find/matchers/printf.rs index 62f7dff1..11641392 100644 --- a/src/find/matchers/printf.rs +++ b/src/find/matchers/printf.rs @@ -349,11 +349,13 @@ fn get_starting_point(file_info: &walkdir::DirEntry) -> &Path { .unwrap() } -fn format_non_link_file_type(file_type: fs::FileType) -> char { +fn format_file_type(file_type: fs::FileType) -> char { if file_type.is_file() { 'f' } else if file_type.is_dir() { 'd' + } else if file_type.is_symlink() { + 'l' } else { #[cfg(unix)] if file_type.is_block_device() { @@ -377,19 +379,7 @@ fn format_directive<'entry>( directive: &FormatDirective, meta_cell: &OnceCell, ) -> Result, Box> { - let meta = || { - meta_cell.get_or_try_init(|| { - if file_info.path_is_symlink() && !file_info.file_type().is_symlink() { - // The file_info already followed the symlink, meaning that the - // metadata will be for the target file, which isn't the - // behavior we want, so manually re-compute the metadata for the - // symlink itself instead. - file_info.path().symlink_metadata() - } else { - file_info.metadata().map_err(|e| e.into()) - } - }) - }; + let meta = || meta_cell.get_or_try_init(|| file_info.metadata()); // NOTE ON QUOTING: // GNU find's man page claims that several directives that print names (like @@ -542,7 +532,7 @@ fn format_directive<'entry>( FormatDirective::StartingPoint => get_starting_point(file_info).to_string_lossy(), FormatDirective::SymlinkTarget => { - if file_info.path_is_symlink() { + if meta()?.is_symlink() { fs::read_link(file_info.path())? .to_string_lossy() .into_owned() @@ -552,10 +542,10 @@ fn format_directive<'entry>( } } - FormatDirective::Type { follow_links } => if file_info.path_is_symlink() { - if *follow_links { + FormatDirective::Type { follow_links } => { + if file_info.file_type().is_symlink() && *follow_links { match file_info.path().metadata() { - Ok(meta) => format_non_link_file_type(meta.file_type()), + Ok(meta) => format_file_type(meta.file_type()), Err(e) if e.kind() == std::io::ErrorKind::NotFound => 'N', // The ErrorKinds corresponding to ELOOP and ENOTDIR are // nightly-only: @@ -568,13 +558,11 @@ fn format_directive<'entry>( Err(_) => '?', } } else { - 'l' + format_file_type(file_info.file_type()) } - } else { - format_non_link_file_type(file_info.file_type()) + .to_string() + .into() } - .to_string() - .into(), #[cfg(not(unix))] FormatDirective::User { .. } => "0".into(), diff --git a/src/find/mod.rs b/src/find/mod.rs index 383b0022..a3e02a19 100644 --- a/src/find/mod.rs +++ b/src/find/mod.rs @@ -14,6 +14,7 @@ use std::time::SystemTime; use walkdir::WalkDir; pub struct Config { + follow_links: bool, depth_first: bool, min_depth: usize, max_depth: usize, @@ -25,6 +26,7 @@ pub struct Config { impl Default for Config { fn default() -> Config { Config { + follow_links: false, depth_first: false, min_depth: 0, max_depth: usize::max_value(), @@ -86,6 +88,16 @@ fn parse_args(args: &[&str]) -> Result> { let mut i = 0; let mut config = Config::default(); + while i < args.len() { + if args[i] == "-L" { + config.follow_links = true; + } else { + break; + } + + i += 1; + } + while i < args.len() && (args[i] == "-" || !args[i].starts_with('-')) && args[i] != "!" @@ -115,7 +127,10 @@ fn process_dir<'a>( let mut walkdir = WalkDir::new(dir) .contents_first(config.depth_first) .max_depth(config.max_depth) - .min_depth(config.min_depth); + .min_depth(config.min_depth) + .follow_links(config.follow_links) + .follow_root_link(config.follow_links) + .yield_link_on_error(true); if config.sorted_output { walkdir = walkdir.sort_by(|a, b| a.file_name().cmp(b.file_name())); } @@ -171,6 +186,7 @@ fn print_help() { If no path is supplied then the current working directory is used by default. Early alpha implementation. Currently the only expressions supported are + -L -print -print0 -printf diff --git a/tests/common/test_helpers.rs b/tests/common/test_helpers.rs index 6c723d5f..9ffc460b 100644 --- a/tests/common/test_helpers.rs +++ b/tests/common/test_helpers.rs @@ -93,3 +93,65 @@ pub fn get_dir_entry_for(directory: &str, filename: &str) -> DirEntry { } panic!("Couldn't find {} in {}", directory, filename); } + +pub fn create_testing_symlinks() { + use std::io::ErrorKind; + + #[cfg(unix)] + use std::os::unix::fs::symlink; + + #[cfg(windows)] + use std::os::windows::fs::{symlink_dir, symlink_file}; + + #[cfg(unix)] + { + if let Err(e) = symlink("abbbc", "test_data/links/link-f") { + if e.kind() != ErrorKind::AlreadyExists { + panic!("Failed to create sym link: {:?}", e); + } + } + if let Err(e) = symlink("subdir", "test_data/links/link-d") { + if e.kind() != ErrorKind::AlreadyExists { + panic!("Failed to create sym link: {:?}", e); + } + } + if let Err(e) = symlink("missing", "test_data/links/link-missing") { + if e.kind() != ErrorKind::AlreadyExists { + panic!("Failed to create sym link: {:?}", e); + } + } + if let Err(e) = symlink("abbbc/x", "test_data/links/link-notdir") { + if e.kind() != ErrorKind::AlreadyExists { + panic!("Failed to create sym link: {:?}", e); + } + } + if let Err(e) = symlink("link-loop", "test_data/links/link-loop") { + if e.kind() != ErrorKind::AlreadyExists { + panic!("Failed to create sym link: {:?}", e); + } + } + } + #[cfg(windows)] + { + if let Err(e) = symlink_file("abbbc", "test_data/links/link-f") { + if e.kind() != ErrorKind::AlreadyExists { + panic!("Failed to create sym link: {:?}", e); + } + } + if let Err(e) = symlink_dir("subdir", "test_data/links/link-d") { + if e.kind() != ErrorKind::AlreadyExists { + panic!("Failed to create sym link: {:?}", e); + } + } + if let Err(e) = symlink_file("missing", "test_data/links/link-missing") { + if e.kind() != ErrorKind::AlreadyExists { + panic!("Failed to create sym link: {:?}", e); + } + } + if let Err(e) = symlink_file("abbbc/x", "test_data/links/link-notdir") { + if e.kind() != ErrorKind::AlreadyExists { + panic!("Failed to create sym link: {:?}", e); + } + } + } +} diff --git a/tests/find_cmd_tests.rs b/tests/find_cmd_tests.rs index 3be2217a..8b9c04e8 100644 --- a/tests/find_cmd_tests.rs +++ b/tests/find_cmd_tests.rs @@ -11,16 +11,10 @@ use assert_cmd::Command; use predicates::prelude::*; use serial_test::serial; +use std::env; use std::fs::File; -use std::{env, io::ErrorKind}; use tempfile::Builder; -#[cfg(unix)] -use std::os::unix::fs::symlink; - -#[cfg(windows)] -use std::os::windows::fs::{symlink_dir, symlink_file}; - use common::test_helpers::*; mod common; @@ -191,60 +185,46 @@ fn regex_types() { .stdout(predicate::str::contains("teeest")); } +#[serial(working_dir)] +#[test] +fn follow_links() { + create_testing_symlinks(); + + // By default, we should not follow the root if it's a link. + Command::cargo_bin("find") + .expect("found binary") + .args(&[ + &fix_up_slashes("./test_data/links/link-d"), + "-printf", + "%p %y\n", + ]) + .assert() + .success() + .stderr(predicate::str::is_empty()) + .stdout(predicate::str::diff(fix_up_slashes( + "./test_data/links/link-d l\n", + ))); + + Command::cargo_bin("find") + .expect("found binary") + .args(&[ + "-L", + &fix_up_slashes("./test_data/links/link-d"), + "-printf", + "%p %y\n", + ]) + .assert() + .success() + .stderr(predicate::str::is_empty()) + .stdout(predicate::str::diff(fix_up_slashes( + "./test_data/links/link-d d\n./test_data/links/link-d/test f\n", + ))); +} + #[serial(working_dir)] #[test] fn find_printf() { - #[cfg(unix)] - { - if let Err(e) = symlink("abbbc", "test_data/links/link-f") { - if e.kind() != ErrorKind::AlreadyExists { - panic!("Failed to create sym link: {:?}", e); - } - } - if let Err(e) = symlink("subdir", "test_data/links/link-d") { - if e.kind() != ErrorKind::AlreadyExists { - panic!("Failed to create sym link: {:?}", e); - } - } - if let Err(e) = symlink("missing", "test_data/links/link-missing") { - if e.kind() != ErrorKind::AlreadyExists { - panic!("Failed to create sym link: {:?}", e); - } - } - if let Err(e) = symlink("abbbc/x", "test_data/links/link-notdir") { - if e.kind() != ErrorKind::AlreadyExists { - panic!("Failed to create sym link: {:?}", e); - } - } - if let Err(e) = symlink("link-loop", "test_data/links/link-loop") { - if e.kind() != ErrorKind::AlreadyExists { - panic!("Failed to create sym link: {:?}", e); - } - } - } - #[cfg(windows)] - { - if let Err(e) = symlink_file("abbbc", "test_data/links/link-f") { - if e.kind() != ErrorKind::AlreadyExists { - panic!("Failed to create sym link: {:?}", e); - } - } - if let Err(e) = symlink_dir("subdir", "test_data/links/link-d") { - if e.kind() != ErrorKind::AlreadyExists { - panic!("Failed to create sym link: {:?}", e); - } - } - if let Err(e) = symlink_file("missing", "test_data/links/link-missing") { - if e.kind() != ErrorKind::AlreadyExists { - panic!("Failed to create sym link: {:?}", e); - } - } - if let Err(e) = symlink_file("abbbc/x", "test_data/links/link-notdir") { - if e.kind() != ErrorKind::AlreadyExists { - panic!("Failed to create sym link: {:?}", e); - } - } - } + create_testing_symlinks(); Command::cargo_bin("find") .expect("found binary")