Iterate over a list of files with spaces

前端 未结 11 1426
-上瘾入骨i
-上瘾入骨i 2020-11-22 05:16

I want to iterate over a list of files. This list is the result of a find command, so I came up with:

getlist() {
  for f in $(find . -iname \"f         


        
11条回答
  •  一向
    一向 (楼主)
    2020-11-22 06:03

    There are several workable ways to accomplish this.

    If you wanted to stick closely to your original version it could be done this way:

    getlist() {
            IFS=$'\n'
            for file in $(find . -iname 'foo*') ; do
                    printf 'File found: %s\n' "$file"
            done
    }
    

    This will still fail if file names have literal newlines in them, but spaces will not break it.

    However, messing with IFS isn't necessary. Here's my preferred way to do this:

    getlist() {
        while IFS= read -d $'\0' -r file ; do
                printf 'File found: %s\n' "$file"
        done < <(find . -iname 'foo*' -print0)
    }
    

    If you find the < <(command) syntax unfamiliar you should read about process substitution. The advantage of this over for file in $(find ...) is that files with spaces, newlines and other characters are correctly handled. This works because find with -print0 will use a null (aka \0) as the terminator for each file name and, unlike newline, null is not a legal character in a file name.

    The advantage to this over the nearly-equivalent version

    getlist() {
            find . -iname 'foo*' -print0 | while read -d $'\0' -r file ; do
                    printf 'File found: %s\n' "$file"
            done
    }
    

    Is that any variable assignment in the body of the while loop is preserved. That is, if you pipe to while as above then the body of the while is in a subshell which may not be what you want.

    The advantage of the process substitution version over find ... -print0 | xargs -0 is minimal: The xargs version is fine if all you need is to print a line or perform a single operation on the file, but if you need to perform multiple steps the loop version is easier.

    EDIT: Here's a nice test script so you can get an idea of the difference between different attempts at solving this problem

    #!/usr/bin/env bash
    
    dir=/tmp/getlist.test/
    mkdir -p "$dir"
    cd "$dir"
    
    touch       'file not starting foo' foo foobar barfoo 'foo with spaces'\
        'foo with'$'\n'newline 'foo with trailing whitespace      '
    
    # while with process substitution, null terminated, empty IFS
    getlist0() {
        while IFS= read -d $'\0' -r file ; do
                printf 'File found: '"'%s'"'\n' "$file"
        done < <(find . -iname 'foo*' -print0)
    }
    
    # while with process substitution, null terminated, default IFS
    getlist1() {
        while read -d $'\0' -r file ; do
                printf 'File found: '"'%s'"'\n' "$file"
        done < <(find . -iname 'foo*' -print0)
    }
    
    # pipe to while, newline terminated
    getlist2() {
        find . -iname 'foo*' | while read -r file ; do
                printf 'File found: '"'%s'"'\n' "$file"
        done
    }
    
    # pipe to while, null terminated
    getlist3() {
        find . -iname 'foo*' -print0 | while read -d $'\0' -r file ; do
                printf 'File found: '"'%s'"'\n' "$file"
        done
    }
    
    # for loop over subshell results, newline terminated, default IFS
    getlist4() {
        for file in "$(find . -iname 'foo*')" ; do
                printf 'File found: '"'%s'"'\n' "$file"
        done
    }
    
    # for loop over subshell results, newline terminated, newline IFS
    getlist5() {
        IFS=$'\n'
        for file in $(find . -iname 'foo*') ; do
                printf 'File found: '"'%s'"'\n' "$file"
        done
    }
    
    
    # see how they run
    for n in {0..5} ; do
        printf '\n\ngetlist%d:\n' $n
        eval getlist$n
    done
    
    rm -rf "$dir"
    

提交回复
热议问题