How to loop through file names returned by find?

前端 未结 13 1105
野性不改
野性不改 2020-11-22 04:20
x=$(find . -name \"*.txt\")
echo $x

if I run the above piece of code in Bash shell, what I get is a string containing several file names separated

相关标签:
13条回答
  • 2020-11-22 04:56

    Filenames can include spaces and even control characters. Spaces are (default) delimiters for shell expansion in bash and as a result of that x=$(find . -name "*.txt") from the question is not recommended at all. If find gets a filename with spaces e.g. "the file.txt" you will get 2 separated strings for processing, if you process x in a loop. You can improve this by changing delimiter (bash IFS Variable) e.g. to \r\n, but filenames can include control characters - so this is not a (completely) safe method.

    From my point of view, there are 2 recommended (and safe) patterns for processing files:

    1. Use for loop & filename expansion:

    for file in ./*.txt; do
        [[ ! -e $file ]] && continue  # continue, if file does not exist
        # single filename is in $file
        echo "$file"
        # your code here
    done
    

    2. Use find-read-while & process substitution

    while IFS= read -r -d '' file; do
        # single filename is in $file
        echo "$file"
        # your code here
    done < <(find . -name "*.txt" -print0)
    

    Remarks

    on Pattern 1:

    1. bash returns the search pattern ("*.txt") if no matching file is found - so the extra line "continue, if file does not exist" is needed. see Bash Manual, Filename Expansion
    2. shell option nullglob can be used to avoid this extra line.
    3. "If the failglob shell option is set, and no matches are found, an error message is printed and the command is not executed." (from Bash Manual above)
    4. shell option globstar: "If set, the pattern ‘**’ used in a filename expansion context will match all files and zero or more directories and subdirectories. If the pattern is followed by a ‘/’, only directories and subdirectories match." see Bash Manual, Shopt Builtin
    5. other options for filename expansion: extglob, nocaseglob, dotglob & shell variable GLOBIGNORE

    on Pattern 2:

    1. filenames can contain blanks, tabs, spaces, newlines, ... to process filenames in a safe way, find with -print0 is used: filename is printed with all control characters & terminated with NUL. see also Gnu Findutils Manpage, Unsafe File Name Handling, safe File Name Handling, unusual characters in filenames. See David A. Wheeler below for detailed discussion of this topic.

    2. There are some possible patterns to process find results in a while loop. Others (kevin, David W.) have shown how to do this using pipes:

      files_found=1
      find . -name "*.txt" -print0 | 
         while IFS= read -r -d '' file; do
             # single filename in $file
             echo "$file"
             files_found=0   # not working example
             # your code here
         done
      [[ $files_found -eq 0 ]] && echo "files found" || echo "no files found"
      

      When you try this piece of code, you will see, that it does not work: files_found is always "true" & the code will always echo "no files found". Reason is: each command of a pipeline is executed in a separate subshell, so the changed variable inside the loop (separate subshell) does not change the variable in the main shell script. This is why I recommend using process substitution as the "better", more useful, more general pattern.
      See I set variables in a loop that's in a pipeline. Why do they disappear... (from Greg's Bash FAQ) for a detailed discussion on this topic.

    Additional References & Sources:

    • Gnu Bash Manual, Pattern Matching

    • Filenames and Pathnames in Shell: How to do it Correctly, David A. Wheeler

    • Why you don't read lines with "for", Greg's Wiki

    • Why you shouldn't parse the output of ls(1), Greg's Wiki

    • Gnu Bash Manual, Process Substitution

    0 讨论(0)
提交回复
热议问题