Differences between revisions 4 and 13 (spanning 9 versions)
Revision 4 as of 2008-05-31 06:37:22
Size: 2166
Editor: pgas
Comment: add a note about quoting, still not sure about how to explain concisely and simply word splitting...
Revision 13 as of 2009-03-30 04:33:11
Size: 3391
Editor: ip72-219-235-71
Comment: Add a "for" loop that skips filenames with control characters
Deletions are marked like this. Additions are marked like this.
Line 1: Line 1:
[[Anchor(faq20)]] <<Anchor(faq20)>>
Line 3: Line 3:
The preferred method is still to use [:UsingFind:find(1)]: The preferred method is still to use [[UsingFind|find(1)]]:
Line 23: Line 23:
Another way to deal with files with spaces in their names is to use the shell's filename expansion (["globbing"]). This has the disadvantage of not working recursively (except with zsh's extensions), but if you just need to process all the files in a single directory, it works fantastically well, but you need to '''quote all your [:BashFAQ/073:Parameter Expansions] using double
quotes''' "$file" "${file%
.mp3}" ...If you don't use the quotes the expansion will be split into several words, which means
that the command will think you gave it several arguments instead of just one.
Another way to deal with files with spaces in their names is to use the shell's filename expansion ([[globbing]]). This has the disadvantage of not working recursively (except with zsh's extensions), but if you just need to process all the files in a single directory, it works fantastically well.
Line 27: Line 25:
This example changes all the *.mp3 files in the current directory to use underscores in their names instead of spaces. It uses [:BashFAQ/073:Parameter Expansions] that will not work in the original BourneShell, but should be good in Korn and Bash. This example changes all the *.mp3 files in the current directory to use underscores in their names instead of spaces. It uses [[BashFAQ/073|Parameter Expansions]] that will not work in the original BourneShell or POSIX shell, but should be good in KornShell and [[BASH]].
Line 30: Line 28:
for file in *.mp3; do for file in ./*.mp3; do
Line 34: Line 32:
Remember, you need to '''quote all your [[BashFAQ/073|Parameter Expansions]] using double
quotes'''. If you don't, the expansion will undergo WordSplitting (see also BashGuide/TheBasics/ArgumentSplitting and BashPitfalls). Also, always prefix globs with "./"; otherwise, if there's a file with "-" as the first character, the expansions might be misinterpreted as options.
Line 35: Line 35:
You could do the same thing for all files (regardless of extension) by using You could do the same thing for all files with spaces in their names (regardless of extension) by using
Line 38: Line 38:
for file in *\ *; do for file in ./*\ *; do
Line 43: Line 43:
Another way to handle filenames recursively involes using the {{{-print0}}} option of {{{find}}} (a GNU/BSD extension), together with bash's {{{-d}}} option for read: Another way to handle filenames recursively involves using the {{{-print0}}} option of {{{find}}} (a GNU/BSD extension), together with bash's {{{-d}}} option for read:
Line 46: Line 46:
# Bash
Line 47: Line 48:
while read -d $'\0' file; do while IFS= read -r -d $'\0' file; do
Line 52: Line 53:
The preceding example reads all the files under /tmp (recursively) into an array, even if they have newlines or other whitespace in their names, by forcing {{{read}}} to use the NUL byte (\0) as its word delimiter. Since NUL is not a valid byte in Unix filenames, this is the safest approach besides using {{{find -exec}}}. The preceding example reads all the files under `/tmp` (recursively) into an array, even if they have newlines or other whitespace in their names, by forcing {{{read}}} to use the NUL byte (\0) as its line delimiter. Since NUL is not a valid byte in Unix filenames, this is the safest approach besides using {{{find -exec}}}. `IFS=` is required to avoid trimming leading/trailing whitespace, and `-r` is needed to avoid backslash processing. In fact, `$'\0'` is equivalent to `''` so we could also write it like this:

{{{
# Bash
unset a i
while IFS= read -r -d '' file; do
  a[i++]="$file"
done < <(find /tmp -type f -print0)
}}}

Filenames with control characters (including newline, tab, and escape) are a pain to deal with, and can also be somewhat dangerous to display (since the control characters can end up controlling terminal emulators). To skip filenames with control characters, but process correctly other filenames (such as those with embedded spaces), you can use this portable approach (from [[http://www.dwheeler.com/essays/fixing-unix-linux-filenames.html|Fixing Unix/Linux/POSIX Filenames]]):
{{{
  IFS=`printf '\n\t'` # Must remove space so spaces-in-filenames still work
  controlchars=`printf '*[\001-\037\177]*'`
  for file in `find . ! -name "$controlchars"'` ; do
    echo "$file" # etc, be sure to quote "$file" or it'll be globbed
  done
}}}

How can I find and deal with file names containing newlines, spaces or both?

The preferred method is still to use find(1):

    find ... -exec command {} \;

or, if you need to handle filenames en masse, with GNU and recent BSD tools:

    find ... -print0 | xargs -0 command

or with POSIX find:

    find ... -exec command {} +

Use that unless you really can't.

Another way to deal with files with spaces in their names is to use the shell's filename expansion (globbing). This has the disadvantage of not working recursively (except with zsh's extensions), but if you just need to process all the files in a single directory, it works fantastically well.

This example changes all the *.mp3 files in the current directory to use underscores in their names instead of spaces. It uses Parameter Expansions that will not work in the original BourneShell or POSIX shell, but should be good in KornShell and BASH.

for file in ./*.mp3; do
    mv "$file" "${file// /_}"
done

Remember, you need to quote all your Parameter Expansions using double quotes. If you don't, the expansion will undergo WordSplitting (see also BashGuide/TheBasics/ArgumentSplitting and BashPitfalls). Also, always prefix globs with "./"; otherwise, if there's a file with "-" as the first character, the expansions might be misinterpreted as options.

You could do the same thing for all files with spaces in their names (regardless of extension) by using

for file in ./*\ *; do

instead of *.mp3.

Another way to handle filenames recursively involves using the -print0 option of find (a GNU/BSD extension), together with bash's -d option for read:

# Bash
unset a i
while IFS= read -r -d $'\0' file; do
  a[i++]="$file"        # or however you want to process each file
done < <(find /tmp -type f -print0)

The preceding example reads all the files under /tmp (recursively) into an array, even if they have newlines or other whitespace in their names, by forcing read to use the NUL byte (\0) as its line delimiter. Since NUL is not a valid byte in Unix filenames, this is the safest approach besides using find -exec. IFS= is required to avoid trimming leading/trailing whitespace, and -r is needed to avoid backslash processing. In fact, $'\0' is equivalent to '' so we could also write it like this:

# Bash
unset a i
while IFS= read -r -d '' file; do
  a[i++]="$file"
done < <(find /tmp -type f -print0)

Filenames with control characters (including newline, tab, and escape) are a pain to deal with, and can also be somewhat dangerous to display (since the control characters can end up controlling terminal emulators). To skip filenames with control characters, but process correctly other filenames (such as those with embedded spaces), you can use this portable approach (from Fixing Unix/Linux/POSIX Filenames):

  IFS=`printf '\n\t'` # Must remove space so spaces-in-filenames still work
  controlchars=`printf '*[\001-\037\177]*'`
  for file in `find . ! -name "$controlchars"'` ; do
    echo "$file" # etc, be sure to quote "$file" or it'll be globbed
  done

BashFAQ/020 (last edited 2024-05-06 09:19:34 by StephaneChazelas)