4

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
  • This is very useful. Although, I don't seem to make it work. In the end, the terminal says: logout Saving session... ...copying shared history... ...saving history...truncating history files... ...completed. But no file was actually copied in the selected folder. – RobertP. Mar 20 at 13:48
4

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/ \;
  • thanks very much for the detailed reply. I did a bit of digging after posting the question and came upon a solution fairly similar to what you described! – david Oct 16 '16 at 6:19
  • (didn't mean to hit enter on the previous comment)... Basically I made a little script which prompts the user to enter the list of filenames separated by spaces, and then used that as the input to an array. basically the script was to the effect of: echo "Please enter your list" read list echo "please enter the path to search in" read path echo "please enter the path to the destination folder read destination array=($list) for i in "${array[@]}" do find "$path" -iname "$i" -exec cp '{}' "$destination" \; done – david Oct 16 '16 at 6:24
  • @david I have added a couple of KISS suggestions - if using multiple find calls is acceptable – steeldriver Oct 16 '16 at 13:42
0

Still trying to figure out your question in the clearest way.

Are you trying to find some specific files in a directory ? If yes and there is a pattern that you could use regular expressions to find them then use find <directory> -regex "your_regex" and if there is no pattern you need the specify the filenames which you're looking for.You can use the python script below to copy your files.

import shutil
import os

list = [file1.txt, file2.txt] # replace with your file list.
destination = "your_destination" # replace with your destination.
directory = os.getcwd() # replace os.getcwd() with your directory.

for dirpath, dirname, filenames in os.walk(directory, topdown='true'):
    for file in filenames :
        if file in list :
            shutil.copy(os.path.join(dirpath, file), destination) 

If the above isn't your case then I suggest using this :

find <somewhere> -name "something" | xargs cp -t <your_destination>
0

The bash script below, if executed from within the top level directory ( say from your ~/Documents folder ) , will create new directory and copy/move the files you want. By default it only echoes found files, so that the user can check if this is the result they'd like. Note: replace echo with cp to copy or mv to move.

List of files is assigned to regex variable with \| separator (which for grep means logical or statement)

copy_files.sh:

#!/bin/bash
new_dir="./dir4"
[ -d "$new_dir" ] ||  mkdir "$new_dir"
regex="file1\|file3\|file6"
find . -print0 | while IFS= read -d $'\0' line;
do
    if grep -q "$regex" <<< $line
    then
       filename=$(basename "$line")
       printf "%s\n" "Found $filename"
       echo "$line" "$new_dir"/"$filename"
       # Replace echo with cp for copying,
       # or mv for moving
    fi   
done
bash-4.3$ ./copy_files.sh 
Found file6
./dir3/file6 ./dir4/file6
Found file3
./dir1/file3 ./dir4/file3
Found file1
./dir1/file1 ./dir4/file1

As you can see above, the script reports what file was found and where it would copy it or move , for instance in last line it would copy/move ./dir1/file1 to ./dir4/file1 if this was actually using cp or mv and not just echo

  • Suggestion: Stick the grep between find and the loop: find … | grep -z "$regex" | while …. – muru Oct 16 '16 at 6:37

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.