Take the 2-minute tour ×
Unix & Linux Stack Exchange is a question and answer site for users of Linux, FreeBSD and other Un*x-like operating systems.. It's 100% free, no registration required.

I'm working on some scripting:

for x in `find ./ -name *.pdf`
do
  echo pathname $x
done

My filenames are Test1 ( Volume II), Test2 ( Volume II). I'm getting a return of

pathname Test1
pathname (
pathname Volume
pathname II
…

How do I get it to stay as one variable?

share|improve this question

4 Answers 4

As said many time on this site, leaving a variable expansion (as in $var) or command substitution (as in `cmd` or $(cmd)) (or arithmetic expansion (as in $((11 * 11))) in most shells) unquoted in Bourne/POSIX shells is the split+glob operator.

The content of $var or the output of cmd (without the trailing newline characters) is split according to the current value of the $IFS special variable (which by default contains the SPC, TAB, and NL characters (and NUL in zsh)) and each word resulting of that splitting is subject to filename generation also known as globbing.

For instance, if find ouputs ./foo bar.pdf\n./*foo*\tbar.pdf\n (\t meaning TAB and \n NL), with the default value of $IFS, the command substitution will expand to ./foo bar.pdf\n./*foo*\tbar.pdf (trailing newline removed), and then be split into ./foo, bar.pdf, ./*foo*, and foo.pdf and ./*foo* which is a wildcard pattern will be expanded into as many arguments as there are non-hidden files in the current directory whose name contains foo.

If you want to split on newline characters only, you need to set $IFS to newline only:

IFS='
'

If you don't want the wildcard patterns to be expanded, you need to disable it with

set -f

However note that newline is as valid a character as any in a file name, so more generally, the find -print output cannot be post-processed reliably.

An output like:

./a.pdf
./b.pdf

either means the a.pdf and b.pdf files in the current directory, or the file called b.pdf in the a.pdf\n. directory.

Some find implementations like GNU find (where it originated from) have a -print0 predicate to output the filename followed by a NUL character instead of a NL character. With standard find, you can use -exec printf '%s\0' {} + with the same result. NUL is the only character that cannot occur in a filename.

However, zsh is the only shell that can store a NUL character in its variables (like the $IFS character), so:

IFS=$'\0'
for i in `find ... -print0`; do
  ...
done

(no need for set -f in zsh since zsh doesn't do globbing upon command substitution) will work in zsh but not in other shells.

Best, and portably, is to have find call the commands you want to run on those files. As @Gnouc suggests:

find . -name '*.pdf' -exec the command {} \;

If you need anything more complex involving shell statements, you can still do things like:

find . -name '*.pdf' -exec sh -c '
  for i do
    something complex with "$i"
  done' sh {} +

With zsh or bash, you can also do:

find . -name '*.pdf' -print0 |
  while IFS= read -r -d '' file; do
    whatever with "$file"
  done

However note that the stdin within the loop is affected.

zsh (since 1990) has most of the functionality of find included in its globbing capabilities through a syntax where you can specify any level of subdirectories ((*/)# syntax or its simpler form **/) and globbing qualifiers (which are the pendant of the -type f, -mtime, -perm... in find).

The **/ part of that was copied by ksh93 in 2003, fish in 2005, bash in 2009 and tcsh in 2010 (though tcsh also copied the ***/ part). And all of them do not enable it by default. Unfortunately, note that both bash and fish ** do follow symlinks to directories (like -L/-follow in find, or *** in zsh or tcsh).

In those shells, you can find pdf files in any level of subdirectories without having to rely on find, but note that caveat above about fish and bash, and only zsh has the globbing qualifiers.

So, for instance, the zsh equivalent of:

find . -name '*.pdf' -type f -exec ls -ld {} +

would be:

ls -ld ./**/*.pdf(D.)

While with bash, you'd have to do something like:

shopt -s failglob
shopt -s globstart
files=(./**/*.pdf) &&
  for i do
    [ -f "$i" ] && ! [ -L "$i" ] && set -- "$i" "$@"
    shift
  done && ls -ld "$@"
share|improve this answer

The safest way is to use globbing:

for file in *pdf; do echo pathname "$file"; done

If you need to find all pdf recursively, do this:

shopt -s globstar
for file in **/*pdf; do echo pathname "$file"; done
share|improve this answer
1  
Unfortunately, bash's **/ follows symlinks when descending the directory tree, which is generally not what you want, you may want to use ksh93's one (which bash copied it from and doesn't have the issue) or even better zsh's one (which ksh93 copied it from) where you can have most of the functionality of find through its globbing qualifiers. –  Stéphane Chazelas Jun 13 '13 at 19:14

Try this in one-liner:

find ./ -name "*.pdf" -exec echo pathname {} \;
share|improve this answer
    
Do you try my solution? Do you know the meaning of {} in find command? –  Gnouc Jun 13 '13 at 19:10
    
@terdon, the point of Gnouc's is that find will take care of the looping (using loops in shell is generally bad practice) and will call any command you want it to run, here echo like in the OP's question. Granted, echo is a bad choice of commands, but that's the command the OP was using. –  Stéphane Chazelas Jun 13 '13 at 19:12
    
@StephaneChazelas I took the question to be asking how to safely store a file name with spaces in a bash variable. I imagine the OP is doing something more complex then echo pathname $foo. This solution is identical to what the OP is doing and its output can still not be saved in a variable. –  terdon Jun 13 '13 at 19:18
    
@Gnouc I get what you mean to do with -exec but you still can't use this to save find's output in a variable. Granted if all the OP wants is to print the string pathname before the actual path, then this is all you need. But not if the intention is more complex than what -exec can handle. And yes, I know what {} is :). –  terdon Jun 13 '13 at 19:22

There are a few options. Sticking with a looping solution, you can use read to read a line at a time into a variable:

find . -name "*.pdf" | while IFS= read -r x
do
  echo pathname "$x"
done

You can also use xargs -0 and find -print0 to handle any sort of special characters:

find . -name "*.pdf" -print0 | xargs -0 -L1 echo pathname
share|improve this answer

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

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