Programing

공백이있는 파일 목록을 반복합니다.

lottogame 2020. 5. 16. 10:01
반응형

공백이있는 파일 목록을 반복합니다.


파일 목록을 반복하고 싶습니다. 이 목록은 find명령 의 결과 이므로 다음을 생각해 냈습니다.

getlist() {
  for f in $(find . -iname "foo*")
  do
    echo "File found: $f"
    # do something useful
  done
}

파일 이름에 공백이있는 경우를 제외하고는 괜찮습니다.

$ ls
foo_bar_baz.txt
foo bar baz.txt

$ getlist
File found: foo_bar_baz.txt
File found: foo
File found: bar
File found: baz.txt

공간 분할을 피하려면 어떻게해야합니까?


단어 기반 반복을 라인 기반 반복으로 바꿀 수 있습니다.

find . -iname "foo*" | while read f
do
    # ... loop body
done

이를 달성하기위한 몇 가지 실행 가능한 방법이 있습니다.

원래 버전에 가깝게 고정하려면 다음과 같이하십시오.

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

파일 이름에 리터럴 개행 문자가 있으면 여전히 실패하지만 공백은 끊지 않습니다.

그러나 IFS를 망칠 필요는 없습니다. 여기에 내가 선호하는 방법이 있습니다.

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

< <(command)익숙하지 않은 구문 을 찾으면 프로세스 대체 에 대해 읽어야 합니다. 이 방법의 장점은 for file in $(find ...)공백, 줄 바꿈 및 기타 문자가 포함 된 파일이 올바르게 처리된다는 것입니다. 때문에이 작품 find-print0용도 것 null(일명를 \0줄 바꿈과는 달리, 각 파일 이름에 대한 터미네이터로)과, 널 (null) 파일 이름에 법적 문자가 아닙니다.

거의 동등한 버전에 비해 이점

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

while 루프의 본문에 변수 할당이 유지된다는 것입니다. 즉, while위와 같이 파이프 하면 몸체가 while서브 쉘에 있으며 원하는 것이 아닐 수도 있습니다.

프로세스 대체 버전의 장점 find ... -print0 | xargs -0은 최소입니다. xargs파일을 한 줄로 인쇄하거나 단일 작업을 수행하는 것만으로도 버전이 양호하지만 여러 단계를 수행해야하는 경우 루프 버전이 더 쉽습니다.

편집 : 여기 에이 문제를 해결하기위한 다른 시도의 차이점에 대한 아이디어를 얻을 수있는 좋은 테스트 스크립트가 있습니다.

#!/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"

bash globbing에 의존하는 매우 간단한 해결책도 있습니다.

$ mkdir test
$ cd test
$ touch "stupid file1"
$ touch "stupid file2"
$ touch "stupid   file 3"
$ ls
stupid   file 3  stupid file1     stupid file2
$ for file in *; do echo "file: '${file}'"; done
file: 'stupid   file 3'
file: 'stupid file1'
file: 'stupid file2'

이 동작이 기본 동작인지 확신 할 수 없지만 내 shopt에 특별한 설정이 표시되지 않으므로 "안전"해야합니다 (osx 및 ubuntu에서 테스트 됨).


find . -iname "foo*" -print0 | xargs -L1 -0 echo "File found:"

find . -name "fo*" -print0 | xargs -0 ls -l

참조하십시오 man xargs.


로 다른 유형의 필터링을 수행하지 않기 때문에 4.0 find에서 다음을 사용할 수 있습니다 bash.

shopt -s globstar
getlist() {
    for f in **/foo*
    do
        echo "File found: $f"
        # do something useful
    done
}

The **/ will match zero or more directories, so the full pattern will match foo* in the current directory or any subdirectories.


I really like for loops and array iteration, so I figure I will add this answer to the mix...

I also liked marchelbling's stupid file example. :)

$ mkdir test
$ cd test
$ touch "stupid file1"
$ touch "stupid file2"
$ touch "stupid   file 3"

Inside the test directory:

readarray -t arr <<< "`ls -A1`"

This adds each file listing line into a bash array named arr with any trailing newline removed.

Let's say we want to give these files better names...

for i in ${!arr[@]}
do 
    newname=`echo "${arr[$i]}" | sed 's/stupid/smarter/; s/  */_/g'`; 
    mv "${arr[$i]}" "$newname"
done

${!arr[@]} expands to 0 1 2 so "${arr[$i]}" is the ith element of the array. The quotes around the variables are important to preserve the spaces.

The result is three renamed files:

$ ls -1
smarter_file1
smarter_file2
smarter_file_3

find has an -exec argument that loops over the find results and executes an arbitrary command. For example:

find . -iname "foo*" -exec echo "File found: {}" \;

Here {} represents the found files, and wrapping it in "" allows for the resultant shell command to deal with spaces in the file name.

In many cases you can replace that last \; (which starts a new command) with a \+, which will put multiple files in the one command (not necessarily all of them at once though, see man find for more details).


In some cases, here if you just need to copy or move a list of files, you could pipe that list to awk as well.
Important the \"" "\" around the field $0 (in short your files, one line-list = one file).

find . -iname "foo*" | awk '{print "mv \""$0"\" ./MyDir2" | "sh" }'

Ok - my first post on Stack Overflow!

Though my problems with this have always been in csh not bash the solution I present will, I'm sure, work in both. The issue is with the shell's interpretation of the "ls" returns. We can remove "ls" from the problem by simply using the shell expansion of the * wildcard - but this gives a "no match" error if there are no files in the current (or specified folder) - to get around this we simply extend the expansion to include dot-files thus: * .* - this will always yield results since the files . and .. will always be present. So in csh we can use this construct ...

foreach file (* .*)
   echo $file
end

if you want to filter out the standard dot-files then that is easy enough ...

foreach file (* .*)
   if ("$file" == .) continue
   if ("file" == ..) continue
   echo $file
end

The code in the first post on this thread would be written thus:-

getlist() {
  for f in $(* .*)
  do
    echo "File found: $f"
    # do something useful
  done
}

Hope this helps!


Another soluce doing the job...

Goal was :

  • select/filter filenames recursively in directories
  • handle each names (whatever space in path...)
#!/bin/bash  -e
## @Trick in order handle File with space in their path...
OLD_IFS=${IFS}
IFS=$'\n'
files=($(find ${INPUT_DIR} -type f -name "*.md"))
for filename in ${files[*]}
do
      # do your stuff
      #  ....
done
IFS=${OLD_IFS}


참고URL : https://stackoverflow.com/questions/7039130/iterate-over-a-list-of-files-with-spaces

반응형