Differences between revisions 1 and 25 (spanning 24 versions)
Revision 1 as of 2007-05-02 23:06:25
Size: 1868
Editor: redondos
Comment:
Revision 25 as of 2012-02-24 14:05:15
Size: 4138
Editor: nrbg-4dbe20ab
Comment: Forget mv, use rename from perl!
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)]: First and foremost, to understand why you're having trouble, read [[Arguments]] to get a grasp on how the shell understands the statements you give it. It is vital that you grasp this matter well if you're going to be doing anything with the shell.
Line 5: Line 5:
See also David Wheeler's essay (external link) [[http://www.dwheeler.com/essays/filenames-in-shell.html|Filenames and Pathnames in Shell: How to do it correctly]] for more discussion on this topic, including pointing out weaknesses of some proposed solutions.

The preferred method to deal with arbitrary filenames is still to use [[UsingFind|find(1)]]:
Line 9: Line 12:
or, if you need to handle filenames ''en masse'', with GNU and recent BSD tools: or, if you need to handle filenames ''en masse'':
{{{
   find ... -exec command {} +
}}}
Line 11: Line 17:
`xargs` is rarely ever more useful than the above, but if you ''really'' insist, remember to use `-0`:
Line 15: Line 22:
or with POSIX {{{find}}}: Use one of these 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 or bash 4's globstar), but if you just need to process all the files in a single directory, it works fantastically well.

This example [[BashFAQ/030|renames]] 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 18: Line 29:
    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 [#faq73 Parameter Expansions] that will not work in the original BourneShell, but should be good in Korn and Bash.

{{{
for file in *.mp3; do
for file in ./*.mp3; do
Line 33: Line 34:
You could do the same thing for all files (regardless of extension) by using However, for this task, there is yet another approach with the excellent ''rename'' tool from Perl distribution, which should be installed on most systems. You can even do without the for loop when using ''rename'' (and even remember the familiar 's/../..' syntax from ''sed'')
Line 36: Line 37:
for file in *\ *; do    rename 's// /' *.mp3
Line 38: Line 39:
You could even do this recursively by combining ''rename'' with ''find(1)''.
Line 39: Line 41:
instead of *.mp3.
Line 41: Line 42:
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: 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/CommandsAndArguments#Argument_Splitting|argument splitting]] 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
Line 44: Line 47:
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
Line 45: Line 56:
while read -d $'\0' file; do while IFS= read -r -d $'\0' file; do
Line 49: Line 60:
The preceding example reads all the files under `/tmp` (recursively) into an [[BashGuide/Arrays|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|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:
Line 50: Line 62:
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}}}. {{{
# Bash
unset a i
while IFS= read -r -d '' file; do
  a[i++]="$file"
done < <(find /tmp -type f -print0)
}}}

So, why doesn't this work?

{{{
# DOES NOT WORK
unset a i
find /tmp -type f -print0 | while IFS= read -r -d '' file; do
  a[i++]="$file"
done
}}}

Because of the pipeline, the entire `while` loop is executed in a SubShell and therefore the array assignments will be lost after the loop terminates. (For more details about this, see [[BashFAQ/024|FAQ #24]].)

----
CategoryShell

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

First and foremost, to understand why you're having trouble, read Arguments to get a grasp on how the shell understands the statements you give it. It is vital that you grasp this matter well if you're going to be doing anything with the shell.

See also David Wheeler's essay (external link) Filenames and Pathnames in Shell: How to do it correctly for more discussion on this topic, including pointing out weaknesses of some proposed solutions.

The preferred method to deal with arbitrary filenames is still to use find(1):

    find ... -exec command {} \;

or, if you need to handle filenames en masse:

    find ... -exec command {} +

xargs is rarely ever more useful than the above, but if you really insist, remember to use -0:

    find ... -print0 | xargs -0 command

Use one of these 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 or bash 4's globstar), but if you just need to process all the files in a single directory, it works fantastically well.

This example renames 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

However, for this task, there is yet another approach with the excellent rename tool from Perl distribution, which should be installed on most systems. You can even do without the for loop when using rename (and even remember the familiar 's/../..' syntax from sed)

   rename 's// /' *.mp3

You could even do this recursively by combining rename with find(1).

Remember, you need to quote all your Parameter Expansions using double quotes. If you don't, the expansion will undergo WordSplitting (see also argument splitting 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)

So, why doesn't this work?

# DOES NOT WORK
unset a i
find /tmp -type f -print0 | while IFS= read -r -d '' file; do
  a[i++]="$file"
done

Because of the pipeline, the entire while loop is executed in a SubShell and therefore the array assignments will be lost after the loop terminates. (For more details about this, see FAQ #24.)


CategoryShell

BashFAQ/020 (last edited 2016-03-06 16:39:47 by GreyCat)