Ubuntu – How to find and copy an arbitrary list of files in terminal

command linecopyfind

I'm trying to get my head around finding files in terminal at the moment. I've found a lot of answers about how to find files to a specific pattern, such as files ending in a certain extension, or from a certain timeframe, etc.

What I've been trying to figure out, though, is how to find and copy multiple arbitrary files, like a list of specific files.

Say, for example, I have the following folders and files

/Documents/Folder_1/ with contents:

file1.txt file2.txt file3.txt file4.txt

/Documents/Folder_2/ with contents:

file5.txt file6.txt file7.txt

/Documents/Folder_3/ with contents:

file8.txt, file9.txt

and what I want to do is to make a new folder called Folder_4 and copy file2.txt, file5.txt and file9.txt to that new folder.

Is there any way to do this through terminal?

What I've tried to do is to make a text list of the file names and call it something like list.txt, e.g.

file2.txt
file5.txt
file9.txt

and loading that text file into the Find command, for example like this:

find /home/usr/Documents -name $cat list.txt

with the thinking that I would then feed the output to either a text list or a copy command, but this doesn't seem to work.

I'm wondering if there is any way to find multiple arbitrary files through the Find command, or am I thinking about this in the wrong way?

EDIT: Just wanted to say thanks for the answers and summarise the way that I ended up approaching this problem. I especially appreciate that someone explained to me that the find command is not designed to accept more than one input without the -o flag, it made me feel a bit less stupid for not being able to figure it out!

What I ended up doing was to make a little shell script and ask for user input and use an array and a for loop to iterate through the list.

Basically the script that I ended up with is more or less like this:

#!/bin/bash

#take user input
echo "Please enter list of filenames to search"
read list
echo "Please enter the folder path to search"
read path
echo "Please enter the destination folder path"
read destination

#use inputted list as input for array, and then use a for loop to find each item on list one by one
array=($list)
for i in "${array[@]}"
do
    find "$path" -iname "i" -exec cp '{}' "$destination" \;
done

Best Answer

Unfortunately the find command's -name predicate only accepts a single pattern. If you want to search for multiple files by name you'd need to chain them with the -o (logical OR) operator - something like:

find Documents/ \( -name "file2.txt" -o -name "file5.txt" -o -name "file9.txt" \) -print

This makes it tricky to construct the search programmatically from a list; the closest I can come to your attempted command is:

  1. read the list into a shell array

    mapfile -t files < list.txt
    
  2. use the bash shell's printf to construct the predicate list

    printf -- '-name "%s" -o ' "${files[@]}"
    
  3. use eval to evaluate the resulting command string

There's a wrinkle here inasmuch as if we use printf's format re-use feature to construct the list in this way, we're left with a 'dangling' -o; we can work around this by terminating the list with a -false test (since -o -false is a Boolean no-op) so that our final predicate string becomes

"\( $(printf -- '-name "%s" -o ' "${files[@]}") -false \)"

Putting it all together - given

$ tree dir
dir
├── Folder_1
│   ├── file1.txt
│   ├── file2.txt
│   ├── file3.txt
│   └── file4.txt
├── Folder_2
│   ├── file5.txt
│   ├── file6.txt
│   └── file7.txt
└── Folder_3
    ├── file8.txt
    └── file9.txt

3 directories, 9 files

and

$ cat list.txt
file2.txt
file5.txt
file9.txt

then

$ mapfile -t files < list.txt

$ eval find dir/ "\( $(printf -- '-name "%s" -o ' "${files[@]}") -false \)" -print
dir/Folder_1/file2.txt
dir/Folder_2/file5.txt
dir/Folder_3/file9.txt

To copy files instead of just listing them, you could then do

$ mkdir newdir
$ eval find dir/ "\( $(printf -- '-name "%s" -o ' "${files[@]}") -false \)" -exec cp -t newdir/ -- {} +

resulting in

$ tree newdir
newdir
├── file2.txt
├── file5.txt
└── file9.txt

0 directories, 3 files

Note: the eval command is powerful and potentially open to abuse: use with care.


In practice, given that you appear to want to find only a small number of files, the KISS approach would be to accept the performance hit of multiple find calls and just use a loop:

while read -r f; do
  find dir/ -name "$f" -exec cp -v -- {} newdir/ \;
done < list.txt

or even using xargs

xargs -a list.txt -n1 -I@ find dir/ -name @ -exec cp -v -- {} newdir/ \;
Related Question