diff --git a/topiary-cli/src/cli.rs b/topiary-cli/src/cli.rs index 0798a070..9a2343fe 100644 --- a/topiary-cli/src/cli.rs +++ b/topiary-cli/src/cli.rs @@ -58,7 +58,7 @@ pub struct GlobalArgs { // NOTE This abstraction is largely to workaround clap-rs/clap#4707 #[derive(Args, Debug)] pub struct FromStdin { - /// Topiary language identifier (for formatting stdin) + /// Topiary language identifier (when formatting stdin) #[arg(short, long)] pub language: String, @@ -109,6 +109,10 @@ pub struct AtLeastOneInput { /// Language detection and query selection is automatic, mapped from file extensions defined in /// the Topiary configuration. pub files: Vec, + + /// Follow symlinks (when formatting files) + #[arg(short = 'L', long)] + pub follow_symlinks: bool, } // NOTE When changing the subcommands, please update verify-documented-usage.sh respectively. @@ -172,16 +176,34 @@ pub enum Commands { } /// Given a vector of paths, recursively expand those that identify as directories, in place -fn traverse_fs(files: &mut Vec) -> CLIResult<()> { +fn traverse_fs(files: &mut Vec, follow_symlinks: bool) -> CLIResult<()> { let mut expanded = vec![]; for file in &mut *files { - if file.is_dir() { + let is_dir = if follow_symlinks { + file.is_dir() + } else { + file.is_dir() && !file.is_symlink() + }; + + if is_dir { + // Descend into directory, symlink-aware as required let mut subfiles = file.read_dir()?.flatten().map(|f| f.path()).collect(); - traverse_fs(&mut subfiles)?; + traverse_fs(&mut subfiles, follow_symlinks)?; expanded.append(&mut subfiles); } else { - expanded.push(file.to_path_buf()); + if file.is_symlink() && !follow_symlinks { + log::debug!( + "{} is a symlink, which we're not following", + file.to_string_lossy() + ); + continue; + } + + // Only push the file if the canonicalisation succeeds (i.e., skip broken symlinks) + if let Ok(candidate) = file.canonicalize() { + expanded.push(candidate.to_path_buf()); + } } } @@ -217,17 +239,23 @@ pub fn get_args() -> CLIResult { match &mut args.command { Commands::Format { - inputs: AtLeastOneInput { files, .. }, + inputs: + AtLeastOneInput { + files, + follow_symlinks, + .. + }, .. } => { // If we're given a list of FILES... then we assume them to all be on disk, even if "-" // is passed as an argument (i.e., interpret this as a valid filename, rather than as - // stdin). We deduplicate this list to avoid formatting the same file multiple times - // and recursively expand directories until we're left with a list of unique - // (potential) files as input sources. + // stdin). We recursively expand directories until we're left with a list of + // (potential) files, as input sources. This is finally deduplicated to avoid + // formatting the same file multiple times (e.g., in the case that a symlink points to + // a file within the set, or if the same file is specified twice at the command line). + traverse_fs(files, *follow_symlinks)?; files.sort_unstable(); files.dedup(); - traverse_fs(files)?; } Commands::Visualise {