2

(N.B. There are many similar questions (e.g. here, here, here, and here) but they either assume that the directory structure is one-deep, or the answers are more complex multi-line scripts.)

This is my situation:

.
├── to_keep
│   ├── a
│   │   └── duplicate1.txt
│   └── b
│       ├── duplicate2.txt
│       └── unique1.txt
└── to_purge
    ├── c
    │   └── duplicate1.txt
    └── d
        ├── duplicate2.txt
        └── unique2.txt

Is there a simple one line script that will use the basenames found in to_keep (and its sub-directories) and remove files with the same name from to_purge (and its sub-directories)?

The two attempts I have made both fail.

(In both I have used find -print to test the command, with the intention of swapping it to find -delete when it is working.)

The first uses $():

find ./to_purge/ -print -name $(find ./to_keep/ -type f -printf "%f\n")
find: paths must precede expression: `duplicate2.txt'

The second uses xargs:

find ./to_keep/ -type f -printf "%f\n" | xargs --max-args=1 find ./to_purge/ -print -name
./to_purge/
./to_purge/c
./to_purge/c/duplicate1.txt
./to_purge/d
./to_purge/d/duplicate2.txt
./to_purge/d/unique2.txt
./to_purge/
./to_purge/c
./to_purge/c/duplicate1.txt
./to_purge/d
./to_purge/d/duplicate2.txt
./to_purge/d/unique2.txt
./to_purge/
./to_purge/c
./to_purge/c/duplicate1.txt
./to_purge/d
./to_purge/d/duplicate2.txt
./to_purge/d/unique2.txt

Neither attempt works. What have I got wrong?

4
  • 1
    so you want to remove every duplicated files based on filename from any directories if there is one in to_keep directory? Commented Jan 9, 2021 at 8:53
  • @αғsнιη exactly, as you say I "want to remove every duplicated files based on filename from any directories if there is one in the to_keep directory"
    – dumbledad
    Commented Jan 9, 2021 at 10:23
  • 1
    Is there a reason you want a "one-line script" and not something more rubust that you could re-use later?
    – Kusalananda
    Commented Jan 9, 2021 at 12:18
  • @Kusalananda You ask "Is there a reason you want a 'one-line script'"? Yes, if I was to write a multi-line script I would write it in a programming language I am more familiar with. I find bash useful for short one-line tasks, but for longer scripts I'd swap to Python (or some-such)
    – dumbledad
    Commented Jan 11, 2021 at 15:20

2 Answers 2

1

The following will find all regular files in or under ./to_keep and will call an in-line sh -c script with these in batches. For each batch of pathnames, the in-line script will call find once to find the regular files under ./to_purge that have the same names. The pathnames of these files under ./to_purge will be printed (to delete them, add -delete after -print).

find to_keep -type f -exec sh -c '
    for pathname do
        set -- "$@" -o -name "${pathname##*/}"
        shift
    done; shift
    find to_purge \( "$@" \) -type f -print' sh {} +

or, as requested, on one line:

find to_keep -type f -exec sh -c 'for pathname do set -- "$@" -o -name "${pathname##*/}"; shift; done; shift; find to_purge \( "$@" \) -type f -print' sh {} +

The in-line script constructs an OR-list of -name tests for the find command that it uses on its last line. The loop constructs this list in the positional parameters from the filename component of each pathname that the outer find has passed to it.

This deals with all allowed filenames, including filenames containing spaces, tabs and newlines. Again, to delete files, add -delete (or -exec rm {} +) after -print in the code.

As a short script that takes the "keep directory" and "purge directory" as command line arguments:

#!/bin/sh

keepdir=$1
purgedir=$2

find "$keepdir" -type f -exec sh -c '
    dir=$1; shift
    for pathname do
        set -- "$@" -o -name "${pathname##*/}"
        shift
    done; shift
    find "$dir" \( "$@" \) -type f -print' sh "$purgedir" {} +

The only issue with this code is that it will use the names in one directory as patterns for finding the names of files in the other directory. This means that if a file in the first directory is called *, all files in the second directory are removed. You can fix that protecting the filenames in the inner find:

for pathname do
    sane=$( printf "%s\n" "${pathname##*/}" | sed "s/[[*?]/\\&/g" )
    set -- "$@" -o -name "$sane"
    shift
done; shift

This modification to the loop in the in-line sh -c script escapes the [, * and ? characters (otherwise used as filename globbing patterns). The script would now not deal with filenames that end in a newline (due to the command substitution used), but that might arguably be something that one could live with.

3
  • The question asks for a simple one-line script. Have you opted for a longer script because there is no simpler alternative? If so it would be great if the answer mentioned that.
    – dumbledad
    Commented Jan 11, 2021 at 15:22
  • @dumbledad It's a single find command. You can easily write it all out on one line if you wanted to, but I didn't do so here as it would be difficult to read here.
    – Kusalananda
    Commented Jan 11, 2021 at 15:42
  • @dumbledad See updated answer.
    – Kusalananda
    Commented Jan 11, 2021 at 15:43
0

Typical, no sooner posted than I find an answer!

find ./to_keep/ -type f -exec basename '{}' \; | xargs --max-args=1 find ./to_purge/ -name | xargs --max-args=1 rm

I will not accept this as the answer since I do not yet know what is wrong with my previous attempts.

2
  • This appears to try to remove directories in to_purge that has the same names as files under to_keep. It will also delete the two separate files hello and world under to_purge if there is a file called hello world under to_keep.
    – Kusalananda
    Commented Jan 9, 2021 at 12:22
  • That will break horribly if your file names contain spaces. (A name of apple sauce would delete files named apple or sauce but not apple sauce itself) Commented Jan 9, 2021 at 12:39

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .