2878
Comment: Helping the user to avoid remembering the '< <(' syntax and putting first "find" as it's executed first.
|
3300
spam
|
Deletions are marked like this. | Additions are marked like this. |
Line 2: | Line 2: |
== 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. |
|
Line 3: | Line 5: |
== How can I find and deal with file names containing newlines, spaces or both? == The preferred method is still to use [[UsingFind|find(1)]]: |
The preferred method to deal with arbitrary filenames is still to use [[UsingFind|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`: {{{ # Requires GNU/BSD find and xargs find ... -print0 | xargs -0 command # Never use xargs without -0 or similar extensions! }}} 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 ([[glob|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. For example, this code renames all the `*.mp3` files in the current directory to use underscores in their names instead of spaces: |
Line 7: | Line 30: |
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 [[BashFAQ/073|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// /_}" |
# Bash/ksh for file in ./*\ *.mp3; do mv "$file" "${file// /_}" |
Line 30: | Line 35: |
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 32: | Line 36: |
You could do the same thing for all files with spaces in their names (regardless of extension) by using | For more examples of renaming files, see [[BashFAQ/030|FAQ #30]]. |
Line 34: | Line 38: |
{{{ for file in ./*\ *; do }}} instead of *.mp3. |
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. |
Line 39: | Line 40: |
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: | 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 48: | Line 49: |
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: | 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 57: | Line 59: |
We can also write it in another form, without having to remember the '< <(' syntax and seeing first what is executed first: | So, why doesn't this work? |
Line 60: | Line 63: |
# Bash | # DOES NOT WORK |
Line 66: | Line 69: |
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.
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:
# Requires GNU/BSD find and xargs find ... -print0 | xargs -0 command # Never use xargs without -0 or similar extensions!
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.
For example, this code renames all the *.mp3 files in the current directory to use underscores in their names instead of spaces:
# Bash/ksh for file in ./*\ *.mp3; do mv "$file" "${file// /_}" done
For more examples of renaming files, see FAQ #30.
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.
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.)