6

For moving all files in sub folders into the current folder I use this script

while read f
do
    mv "$f" .
done < file_list

this works great but i have to generate the file_list with

find . -name *.avi > file_list

what i want is to add the command directly to my while loop

while read f
do
    mv "$f" .
done < find . -name *.avi

but bash tells me -bash: syntax error near unexpected token `.'

What is an easy solution to pipe the find command into my while loop?

5
  • 3
    Hi there! This is the wrong, error-prone (see what you happen if you have files with leading spaces or newline characters) and inefficient approach. You should use -exec option of find to have a sound way of doing what you want. Are you open to that?
    – Quasímodo
    Commented Jun 12, 2020 at 13:44
  • 3
    *.avi will expand to multiple file names and passing as arguments to find (which will interpet as options) glob must be escaped or quoted (see answer)
    – alecxs
    Commented Jun 12, 2020 at 15:58
  • 2
    See also Why is looping over find's output bad practice? Commented Jun 13, 2020 at 7:05
  • 2
    And Understanding "IFS= read -r line" Commented Jun 13, 2020 at 7:06
  • 1
    @StéphaneChazelas interesting read thx Commented Jun 13, 2020 at 10:15

4 Answers 4

21

You don't need any loop here, find can do it for you:

find . -name '*.avi' -exec mv {} . \;
6
  • 1
    @kevinSpaceyIsKeyserSöze -iname will match '*.AVI' and '*.avi'
    – alecxs
    Commented Jun 12, 2020 at 16:02
  • and *.Avi, and *.aVI... and possibly *.avı, *.AVİ in some locales Commented Jun 13, 2020 at 7:18
  • My version of find requires that {} is the last argument of -exec command. -exec mv -t . {} should work. It is also ok to use + (many arguments at once) instead of \; (one command per file).
    – aragaer
    Commented Jun 13, 2020 at 11:51
  • @aragaer what find is that? What operating system? And yes, the + can also work, sure.
    – terdon
    Commented Jun 13, 2020 at 16:01
  • It appears on my current computer exec works fine with {} in the middle and ; in the end and only complains about {} when using +. Probably that was some older version of find.
    – aragaer
    Commented Jun 13, 2020 at 20:04
10

You can also use process substitution.

while IFS= read -r f
do
    mv -- "$f" .
done < <(find . -name '*.avi' )

In shells like bash, compared to the pipe approach, it has the advantage of not running the loop in a subshell, so the variable assignments you would do in the loop for instance are not lost afterwards. In bash (or zsh), you'd rather do:

while IFS= read <&3 -rd '' f 
do
    mv -- "$f" .
done 3< <(find . -name '*.avi' -print0)

using NUL delimiters to be able to work with arbitrary file names, using fd 3 instead of 0 as mv may prompt the user and would read the answer on stdin which would be the output of find if you used 0.

Or using the find command itself

find . -name '*.avi' -exec mv -t . {} +

Using + means we pass many arguments at a time to mv which saves having to run one mv invocation per file. -t is a GNU extension. With other mv implementations, you can change it to:

find . -name '*.avi' -exec sh -c 'exec mv "$@" .' sh {} +
8

proper way to do this is pipe

find . -name '*.avi' |
while read f
do
    mv "$f" .
done 

the result from first command "find . -name *.avi" will feed the second.

This is call a pipe (from the symbol | ). You can think of pipe as a temporary file like

find . -name '*.avi' > file1.tmp
while read f
do
    mv "$f" .
done < file1.tmp
rm file1.tmp

As pointed out filename with space or newline are likely to cause error.

If the only purpose is to move file, use @Terdon's commands.

Also you must quote *.avi or it will break on second run.

find: paths must precede expression: `foo.avi' 
find: possible unquoted pattern after predicate `-name'?
7
  • 4
    Well, proper way would be to use -print0, and while IFS= read -r -d ''; do... Without these, the loop will fail on file names with newlines.
    – terdon
    Commented Jun 12, 2020 at 13:48
  • 1
    I always forgot about newline files, given that list was generate with find at first place it would have error before and piping would not change that.
    – Archemar
    Commented Jun 12, 2020 at 13:51
  • 4
    True. But if you're calling it the proper way, it's better to be on the safe side :)
    – terdon
    Commented Jun 12, 2020 at 13:54
  • 1
    I will accept this answer as it is the specific answer my question. For future readers please check out @terdon answer as it is the proper way to do it:). Thx for all the insights! Commented Jun 12, 2020 at 14:10
  • @terdon -print0 is a non-standard solution for a problem that did no longer exist when print0 has been introduced, since there is find -exec + since 1988.
    – schily
    Commented Jun 12, 2020 at 14:19
-4

A halfway position between the two existing answers is to "inline" the find and use a for loop.

for f in `find . -name '*.avi' `
do
    mv "$f" .
done 

The back-ticks are what make the find command execute in place and then its output is substituted.

Downside is spaces and unexpected weird characters in filenames can cause issues. The answer by @Terdon is superior.

2
  • 5
    This is Bash pitfall number 1. If you didn't say the method is flawed, I would vote down. I guess it makes some sense to have this answer here as a warning. Commented Jun 13, 2020 at 4:10
  • @KamilMaciorowski exactly - showing why something is sub-optimal is just as important as showing the best answers.
    – Criggie
    Commented Jun 13, 2020 at 6:41

You must log in to answer this question.

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