Search code examples
linuxbashfor-loopwhile-loopfind

How to read out a file line by line and for every line do a search with find and copy the search result to destination?


I hope you can help me with the following problem:

The Situation

  • I need to find files in various folders and copy them to another folder. The files and folders can contain white spaces and umlauts.
  • The filenames contain an ID and a string like: "2022-01-11-02 super important file"
  • The filenames I need to find are collected in a textfile named ids.txt. This file only contains the IDs but not the whole filename as a string.

What I want to achieve:

  • I want to read out ids.txt line by line.
  • For every line in ids.txt I want to do a find search and copy cp the result to destination.

So far I tried:

  • for n in $(cat ids.txt); do find /home/alex/testzone/ -name "$n" -exec cp {} /home/alex/testzone/output \; ;
  • while read -r ids; do find /home/alex/testzone -name "$ids" -exec cp {} /home/alex/testzone/output \; ; done < ids.txt

The output folder remains empty. Not using -exec also gives no (search)results.

I was thinking that -name "$ids" is the root cause here. My files contain the ID + a String so I should search for names containing the ID plus a variable string (star)

  • As argument for -name I also tried "$ids *" "$ids"" *" and so on with no luck.

Is there an argument that I can use in conjunction with find instead of using the star in the -name argument?


Do you have any solution for me to automate this process in a bash script to read out ids.txt file, search the filenames and copy them over to specified folder?

In the end I would like to create a bash script that takes ids.txt and the search-folder and the output-folder as arguments like:

my-id-search.sh /home/alex/testzone/ids.txt /home/alex/testzone/ /home/alex/testzone/output 

EDIT: This is some example content of the ids.txt file where only ids are listed (not the whole filename):

2022-01-11-01
2022-01-11-02
2020-12-01-62

EDIT II: Going on with the solution from tripleee:

#!/bin/bash

grep . $1 | while read -r id; do
echo "Der Suchbegriff lautet:"$id; echo;
   find /home/alex/testzone -name "$id*" -exec cp {} /home/alex/testzone/ausgabe \;
done

In case my ids.txt file contains empty lines the -name "$id*" will be -name * which in turn finds all files and copies all files.

Trying to prevent empty line to be read does not seem to work. They should be filtered by the expression grep . $1 |. What am I doing wrong?


Solution

  • If your destination folder is always the same, the quickest and absolutely most elegant solution is to run a single find command to look for all of the files.

    sed 's/.*/-o\n—name\n&*/' ids.txt |
    xargs -I {} find -false {} -exec cp {} /home/alex/testzone/output +
    

    The -false predicate is a bit of a hack to allow the list of actual predicates to start with -o (as in "or").

    This could fail if ids.txt is too large to fit into a single xargs invocation, or if your sed does not understand \n to mean a literal newline.

    (Here's a fix for the latter case:

    xargs printf '-o\n-name\n%s*\n' <ids.txt |
    ...
    

    Still the inherent problem with using xargs find like this is that xargs could split the list between -o and -name or between -name and the actual file name pattern if it needs to run more than one find command to process all the arguments.

    A slightly hackish solution to that is to ensure that each pair is a single string, and then separately split them back out again:

    xargs printf '-o_-name_%s*\n' <ids.txt |
    xargs bash -c 'arr=("$@"); find -false ${arr[@]/-o_-name_/-o -name } -exec cp {} "$0"' /home/alex/testzone/ausgabe
    

    where we temporarily hold the arguments in an array where each file name and its flags is a single item, and then replace the flags into separate tokens. This still won't work correctly if the file names you operate on contain literal shell metacharacters like * etc.)

    A more mundane solution fixes your while read attempt by adding the missing wildcard in the -name argument. (I also took the liberty to rename the variable, since read will only read one argument at a time, so the variable name should be singular.)

    while read -r id; do
       find /home/alex/testzone -name "$id*" -exec cp {} /home/alex/testzone/output \;
    done < ids.txt