0

I've got a huge tree of folders, each with multiple subdirectories going down around 3 levels. Here's an example with just one level:

$ tree
.
|-- AB.txt
|-- CD.txt
|-- destination_folder
|-- spreadsheet.txt
`-- subdirectory
    `-- EF.txt

2 directories, 4 files

I've got a list of filenames I'm interested in, called spreadsheet.txt:

$ cat spreadsheet.txt 
AB.txt
CD.txt
EF.txt

I'd like to copy all the files which appear in spreadsheet.txt into a single folder, e.g. destination_folder. Any help very gratefully received! I imagine it will involve find and cp, but can't seem to work it out,

0

Running find once per filename, and assuming that no filename contain embedded newlines:

#!/bin/sh

mkdir -p destination_folder || exit 1
while IFS= read -r name; do
    find . -path ./destination_folder -prune -o \
        -type f -name "$name" -exec cp {} destination_folder \;
done <spreadsheet.txt

This first creates the destination directory in the current directory (and terminates if that fails). It then reads the input file, line by line, and calls find to locate any regular file that has that name. The destination directory is explicitly avoided by find using -prune whenever we come across that in our search.

Whenever a file with the correct name is found, it is copied to the destination directory. If multiple files has the same name, the copy in in destination_folder will be overwritten.

If the current directory is huge, or if the list of filenames is long (but not many thousands of lines), then this will be a slow operation. We may therefore choose to do a single call to find. The following code assumes that it's executed with e.g. bash as it uses arrays:

#!/bin/bash

mkdir -p destination_folder || exit 1

names=()
while IFS= read -r name; do
    names+=( -o -name "$name" )
done <spreadsheet.txt

find . -path ./destination_folder -prune -o \
    \( "${names[@]:1}" \) -type f -exec cp -t destination_folder {} +

Here, I've also chosen to use cp -t (a GNU cp extension) to be able to call cp as few times as possible and not once per found file.

The code above builds an array, names, that eventually will be on the correct format for use with find. The command that is actually executed at the end of the above code, given the example in your question, will be

find . -path ./destination_folder -prune -o '(' -name AB.txt -o -name CD.txt -o -name EF.txt ')' -type f -exec cp -t destination_folder '{}' +

To avoid the issue of filename collisions in the destination directory, if you're using GNU cp (e.g. on a Linux system), is to use cp with its -b or --backup option.

On non-Linux systems, GNU cp may often be available as gcp after installing GNU coreutils via a package manager.


That last script, but for /bin/sh (no arrays):

#!/bin/sh

mkdir -p destination_folder || exit 1

set --
while IFS= read -r name; do
    set -- -o -name "$name" "$@"
done <spreadsheet.txt

shift

find . -path ./destination_folder -prune -o \
    \( "$@" \) -type f -exec cp -t destination_folder {} +
3
  • Thank you, I'm not sure it I'm running it correctly, but all each of the scripts seem to do is create a destination_folder, but not copy any files into it. I'm just copying your code out and pasting into terminal from the directory containing AB.txt and CD.txt, so not sure if there's something else I should be doing? – Mike Apr 14 '20 at 15:22
  • @Mike Thanks, you discovered a bug. I will update! (done now) – Kusalananda Apr 14 '20 at 15:26
  • Perfect, that's working well now, thanks! – Mike Apr 14 '20 at 15:58
0

A simple for loop :

for filename in $(cat spreadsheet.txt)
do
find . -name "$filename" -exec cp {} /destination/folder \;
done

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.