bash 루프 목록에서 공백을 어떻게 이스케이프 할 수 있습니까?


121

특정 디렉토리의 모든 하위 디렉토리 (파일 제외)를 반복하는 bash 쉘 스크립트가 있습니다. 문제는 일부 디렉토리 이름에 공백이 있다는 것입니다.

내 테스트 디렉토리의 내용은 다음과 같습니다.

$ls -F test
Baltimore/  Cherry Hill/  Edison/  New York City/  Philadelphia/  cities.txt

그리고 디렉토리를 반복하는 코드 :

for f in `find test/* -type d`; do
  echo $f
done

출력은 다음과 같습니다.

테스트 / 볼티모어
테스트 / 체리
언덕
테스트 / 에디슨 
테스트 / 신규
요크
시티
test / 필라델피아

Cherry Hill과 New York City는 2 ~ 3 개의 개별 항목으로 처리됩니다.

다음과 같이 파일 이름을 인용 해 보았습니다.

for f in `find test/* -type d | sed -e 's/^/\"/' | sed -e 's/$/\"/'`; do
  echo $f
done

그러나 소용이 없습니다.

이것을하기위한 간단한 방법이 있어야합니다.


아래 답변은 훌륭합니다. 그러나 이것을 더 복잡하게 만들기 위해-나는 항상 내 테스트 디렉토리에 나열된 디렉토리를 사용하고 싶지는 않습니다. 때로는 디렉토리 이름을 명령 줄 매개 변수로 대신 전달하고 싶습니다.

IFS 설정에 대한 Charles의 제안을 받아 들여 다음을 생각해 냈습니다.

dirlist="${@}"
(
  [[ -z "$dirlist" ]] && dirlist=`find test -mindepth 1 -type d` && IFS=$'\n'
  for d in $dirlist; do
    echo $d
  done
)

그리고 이것은 명령 줄 인수에 공백이 없으면 잘 작동합니다 (해당 인수가 인용 된 경우에도). 예를 들어 test.sh "Cherry Hill" "New York City"다음과 같은 스크립트를 호출 하면 다음과 같은 출력 이 생성됩니다.

체리
언덕
새로운
요크
시티

re : edit, list="$@"원래 값의 목록 성을 완전히 버리고 문자열로 축소합니다. 내 대답의 관행 을 주어진대로 정확하게 따르십시오. 그러한 임무는 그 안에있는 어느 곳에서도 권장되지 않습니다. 프로그램에 명령 줄 인수 목록을 전달하려면 해당 인수를 배열로 수집하고 해당 배열을 직접 확장해야합니다.
Charles Duffy

답변:


105

첫째, 그렇게하지 마십시오. 가장 좋은 방법은 find -exec올바르게 사용하는 것입니다.

# this is safe
find test -type d -exec echo '{}' +

다른 안전한 접근 방식은 NUL로 끝나는 목록을 사용하는 것입니다 -print0.

# this is safe
while IFS= read -r -d '' n; do
  printf '%q\n' "$n"
done < <(find test -mindepth 1 -type d -print0)

find에서 배열을 채우고 나중에 해당 배열을 전달할 수도 있습니다.

# this is safe
declare -a myarray
while IFS= read -r -d '' n; do
  myarray+=( "$n" )
done < <(find test -mindepth 1 -type d -print0)
printf '%q\n' "${myarray[@]}" # printf is an example; use it however you want

찾기가를 지원하지 않으면 -print0결과는 안전하지 않습니다. 파일 이름에 줄 바꿈이 포함 된 파일이 있으면 아래 내용이 원하는대로 작동하지 않습니다 (예, 합법적 임).

# this is unsafe
while IFS= read -r n; do
  printf '%q\n' "$n"
done < <(find test -mindepth 1 -type d)

위의 방법 중 하나를 사용하지 않을 경우 세 번째 방법 (워드 분할을 수행하기 전에 하위 프로세스의 전체 출력을 읽으므로 시간과 메모리 사용 측면에서 덜 효율적입니다)은 다음과 같은 IFS변수 를 사용하는 것 입니다. 공백 문자를 포함하지 않습니다. , 또는 set -f과 같은 glob 문자를 포함하는 문자열 이 확장 되지 않도록하려면 globbing ( )을 끄십시오 .[]*?

# this is unsafe (but less unsafe than it would be without the following precautions)
(
 IFS=$'\n' # split only on newlines
 set -f    # disable globbing
 for n in $(find test -mindepth 1 -type d); do
   printf '%q\n' "$n"
 done
)

마지막으로, 명령 줄 매개 변수의 경우 쉘에서 지원하는 경우 배열을 사용해야합니다 (예 : ksh, bash 또는 zsh).

# this is safe
for d in "$@"; do
  printf '%s\n' "$d"
done

분리를 유지할 것입니다. 인용 (및 $@대신 사용 $*)이 중요합니다. 배열은 glob 표현식과 같은 다른 방법으로도 채울 수 있습니다.

# this is safe
entries=( test/* )
for d in "${entries[@]}"; do
  printf '%s\n' "$d"
done

1
-exec의 '+'풍미에 대해 몰랐습니다. sweet
Johannes Schaub-litb

1
외모 그래도 그것은 또한, xargs를 같이 만 주어진 명령의 끝에 인수를 넣을 수 있습니다처럼 :의 날 때로는 도청 / 있음
요하네스 SCHAUB을 - litb

나는 -exec [name] {} +가 GNU와 4.4-BSD 확장이라고 생각합니다. (적어도 Solaris 8에는 나타나지 않으며 AIX 4.3에도있는 것 같지 않습니다.) 나머지는 xargs에 대한 배관에 갇혀있을 수 있습니다.
Michael Ratanapintha

2
전에 $ '\ n'구문을 본 적이 없습니다. 어떻게 작동합니까? (IFS = '\ n'또는 IFS = "\ n"가 작동한다고 생각했지만 둘 다 작동하지 않습니다.)
MCS

1
@crosstalk 그것은 확실히 Solaris 10에 있으며 방금 사용했습니다.
Nick

26
find . -type d | while read file; do echo $file; done

그러나 파일 이름에 줄 바꿈이 포함되어 있으면 작동하지 않습니다. 위는 실제로 변수에 디렉토리 이름을 원할 때 내가 아는 유일한 솔루션입니다. 일부 명령을 실행하려면 xargs를 사용하십시오.

find . -type d -print0 | xargs -0 echo 'The directory is: '

xargs가 필요 없습니다. find -exec 참조 ... {} +
Charles Duffy

4
@Charles : 많은 파일의 경우 xargs가 훨씬 더 효율적입니다. 하나의 프로세스 만 생성합니다. -exec 옵션은 각 파일에 대해 새로운 프로세스를 포크하므로 훨씬 느릴 수 있습니다.
Adam Rosenfield

1
나는 xargs를 더 좋아합니다. 이 두 가지는 본질적으로 두 가지를 모두 수행하는 것처럼 보이지만 xargs에는 병렬 실행과 같은 더 많은 옵션이 있습니다.
Johannes Schaub-litb

2
Adam, '+'는 가능한 한 많은 파일 이름을 집계 한 다음 실행합니다. 하지만 병렬로 실행되는 것과 같은 깔끔한 기능은 없습니다. :)
Johannes Schaub-litb

2
파일 이름으로 무언가를하고 싶다면, 인용해야합니다. 예 :find . -type d | while read file; do ls "$file"; done
David Moles

23

다음은 파일 이름의 탭 및 / 또는 공백을 처리하는 간단한 솔루션입니다. 줄 바꿈과 같은 파일 이름의 다른 이상한 문자를 처리해야하는 경우 다른 답을 선택하십시오.

테스트 디렉토리

ls -F test
Baltimore/  Cherry Hill/  Edison/  New York City/  Philadelphia/  cities.txt

디렉토리로 이동하는 코드

find test -type d | while read f ; do
  echo "$f"
done

"$f"인수로 사용되는 경우 파일 이름은 따옴표 ( ) 로 묶어야합니다 . 따옴표가 없으면 공백이 인수 구분 기호로 작동하고 호출 된 명령에 여러 인수가 제공됩니다.

그리고 출력 :

test/Baltimore
test/Cherry Hill
test/Edison
test/New York City
test/Philadelphia

감사합니다, 이것은 현재 폴더의 각 디렉토리가 사용하는 공간을 나열하기 위해 만든 별칭에서 작동했으며 이전 화신에서 공백이있는 일부 디렉토리에서 질식했습니다. 이 zsh을에서 작동하지만 다른 답변 중 일부는하지 않았다 :alias duc='ls -d * | while read D; do du -sh "$D"; done;'
테드 Naleid

2
zsh를 사용하는 경우 다음을 수행 할 수도 있습니다.alias duc='du -sh *(/)'
cbliard

@cbliard 이것은 여전히 ​​버그가 있습니다. 탭 시퀀스 또는 여러 공백이있는 파일 이름으로 실행 해보십시오. 에코에서 인용하지 않기 때문에 하나의 공백으로 변경된다는 것을 알 수 있습니다. 그리고 줄 바꿈을 포함하는 파일 이름의 경우는 ... 거기
찰스 더피

@CharlesDuffy 탭 시퀀스와 여러 공백으로 시도했습니다. 따옴표와 함께 작동합니다. 나는 또한 개행으로 시도했지만 전혀 작동하지 않습니다. 그에 따라 답변을 업데이트했습니다. 지적 해주셔서 감사합니다.
cbliard

1
@cbliard Right-echo 명령에 따옴표를 추가하는 것이 제가 얻은 것입니다. 개행의 경우 find -print0및 을 사용하여 작업을 수행 할 수 있습니다 IFS='' read -r -d '' f.
Charles Duffy

7

이것은 표준 유닉스에서 매우 까다 롭고 대부분의 솔루션은 줄 바꿈이나 다른 문자를 잘못 실행합니다. 그러나 GNU 도구 세트를 사용하는 경우 find옵션 -print0을 활용 xargs하고 해당 옵션 -0(-0) 과 함께 사용할 수 있습니다 . 단순한 파일 이름에 나타날 수없는 두 문자가 있습니다. 슬래시와 NUL '\ 0'입니다. 분명히 슬래시가 경로명에 나타나기 때문에 NUL '\ 0'을 사용하여 이름의 끝을 표시하는 GNU 솔루션은 독창적이고 완벽합니다.


4

그냥 두지 않는 이유

IFS='\n'

for 명령 앞에? 이렇게하면 필드 구분 기호가 <Space> <Tab> <Newline>에서 <Newline>으로 변경됩니다.


4
find . -print0|while read -d $'\0' file; do echo "$file"; done

1
-d $'\0'이고 정확하게 동일 -d ''- 배시 NUL 종료 문자열을 사용하고 있기 때문에, 빈 문자열의 첫 번째 문자가 NUL, 그리고 동일한 이유로, NUL을 전혀 C 문자열의 내부를 표시 할 수 없다.
Charles Duffy

4

나는 사용한다

SAVEIFS=$IFS
IFS=$(echo -en "\n\b")
for f in $( find "$1" -type d ! -path "$1" )
do
  echo $f
done
IFS=$SAVEIFS

충분하지 않습니까? http://www.cyberciti.biz/tips/handling-filenames-with-spaces-in-bash.html
에서 가져온 아이디어


유용한 팁 : 이는 명령 줄 osascript (OS X AppleScript) 옵션에 매우 유용합니다. 여기서 공백은 인수를 하나만 의도 된 여러 매개 변수로 분할합니다
tim

아니, 충분하지 않습니다. 비효율적이며 (의 불필요한 사용으로 인해 $(echo ...)) glob 표현식으로 파일 이름을 올바르게 처리하지 못하며 $'\b'또는 $ '\ n'문자 를 포함하는 파일 이름을 올바르게 처리하지 않으며 또한 여러 번의 공백을 단일 공백 ​​문자로 변환합니다. 잘못된 인용으로 인해 출력측.
Charles Duffy 2013

4

목록을 문자열로 저장하지 마십시오. 이 모든 구분 기호 혼동을 피하기 위해 배열로 저장하십시오. 다음은 test의 모든 하위 디렉토리 또는 명령 줄에 제공된 목록에서 작동하는 예제 스크립트입니다.

#!/bin/bash
if [ $# -eq 0 ]; then
        # if no args supplies, build a list of subdirs of test/
        dirlist=() # start with empty list
        for f in test/*; do # for each item in test/ ...
                if [ -d "$f" ]; then # if it's a subdir...
                        dirlist=("${dirlist[@]}" "$f") # add it to the list
                fi
        done
else
        # if args were supplied, copy the list of args into dirlist
        dirlist=("$@")
fi
# now loop through dirlist, operating on each one
for dir in "${dirlist[@]}"; do
        printf "Directory: %s\n" "$dir"
done

이제 한두 개의 곡선이있는 테스트 디렉토리에서 이것을 시도해 봅시다.

$ ls -F test
Baltimore/
Cherry Hill/
Edison/
New York City/
Philadelphia/
this is a dirname with quotes, lfs, escapes: "\''?'?\e\n\d/
this is a file, not a directory
$ ./test.sh 
Directory: test/Baltimore
Directory: test/Cherry Hill
Directory: test/Edison
Directory: test/New York City
Directory: test/Philadelphia
Directory: test/this is a dirname with quotes, lfs, escapes: "\''
'
\e\n\d
$ ./test.sh "Cherry Hill" "New York City"
Directory: Cherry Hill
Directory: New York City

1
이 다시 찾고 - 실제로 존재 했다 POSIX 쉬와 솔루션 : 당신은 다시 사용할 수 "$@"와 함께에 추가, 배열을 set -- "$@" "$f".
Charles Duffy

4

다음을 사용하여 IFS (내부 필드 구분 기호)를 일시적으로 사용할 수 있습니다.

OLD_IFS=$IFS     # Stores Default IFS
IFS=$'\n'        # Set it to line break
for f in `find test/* -type d`; do
    echo $f
done

$IFS=$OLD_IFS


설명을 입력하십시오.
Steve K

IFS가 구분 기호를 지정하면 공백이있는 파일 이름이 잘리지 않습니다.
amazingthere

끝에 $ IFS = $ OLD_IFS : IFS = $ OLD_IFS
Michel Donais

3

ps 입력의 공백에 관한 것이라면 일부 큰 따옴표가 원활하게 작동했습니다 ...

read artist;

find "/mnt/2tb_USB_hard_disc/p_music/$artist" -type f -name *.mp3 -exec mpg123 '{}' \;

2

무엇을 추가하려면 조나단은 말했다 : 사용 -print0에 대한 옵션 find과 함께 xargs다음과 같이 :

find test/* -type d -print0 | xargs -0 command

그러면 command적절한 인수로 명령이 실행됩니다 . 공백이있는 디렉토리는 올바르게 인용됩니다 (즉, 하나의 인수로 전달됨).


1
#!/bin/bash

dirtys=()

for folder in *
do    
 if [ -d "$folder" ]; then    
    dirtys=("${dirtys[@]}" "$folder")    
 fi    
done    

for dir in "${dirtys[@]}"    
do    
   for file in "$dir"/\*.mov   # <== *.mov
   do    
       #dir_e=`echo "$dir" | sed 's/[[:space:]]/\\\ /g'`   -- This line will replace each space into '\ '   
       out=`echo "$file" | sed 's/\(.*\)\/\(.*\)/\2/'`     # These two line code can be written in one line using multiple sed commands.    
       out=`echo "$out" | sed 's/[[:space:]]/_/g'`    
       #echo "ffmpeg -i $out_e -sameq -vcodec msmpeg4v2 -acodec pcm_u8 $dir_e/${out/%mov/avi}"    
       `ffmpeg -i "$file" -sameq -vcodec msmpeg4v2 -acodec pcm_u8 "$dir"/${out/%mov/avi}`    
   done    
done

위의 코드는 .mov 파일을 .avi로 변환합니다. .mov 파일은 다른 폴더에 있으며 폴더 이름에는 공백이 있습니다. 있습니다. 위의 스크립트는 .mov 파일을 동일한 폴더 자체의 .avi 파일로 변환합니다. 나는 그것이 당신들에게 도움이되는지 모르겠습니다.

케이스:

[sony@localhost shell_tutorial]$ ls
Chapter 01 - Introduction  Chapter 02 - Your First Shell Script
[sony@localhost shell_tutorial]$ cd Chapter\ 01\ -\ Introduction/
[sony@localhost Chapter 01 - Introduction]$ ls
0101 - About this Course.mov   0102 - Course Structure.mov
[sony@localhost Chapter 01 - Introduction]$ ./above_script
 ... successfully executed.
[sony@localhost Chapter 01 - Introduction]$ ls
0101_-_About_this_Course.avi  0102_-_Course_Structure.avi
0101 - About this Course.mov  0102 - Course Structure.mov
[sony@localhost Chapter 01 - Introduction]$ CHEERS!

건배!


echo "$name" | ...경우 일을하지 않는 name것입니다 -n, 그것은 백 슬래시 - 이스케이프 시퀀스 이름으로 동작하는 방법을 구현에 따라 달라집니다 - POSIX이의 행동을하게 echoXSI - 확장 된 반면,이 경우에 명시 적으로 정의되지 않은를 (POSIX는 백 슬래시 - 이스케이프 시퀀스 표준 정의 행동의 확장한다 , GNU 시스템 (bash 포함) POSIXLY_CORRECT=1은 구현하여 POSIX 표준을 위반 하지 않고 -e(사양이 출력 echo -e에 인쇄 해야 함 -e) printf '%s\n' "$name" | ...더 안전합니다.
Charles Duffy

1

경로 이름의 공백도 처리해야했습니다. 마침내 내가 한 것은 재귀를 사용하는 것이 었습니다 for item in /path/*.

function recursedir {
    local item
    for item in "${1%/}"/*
    do
        if [ -d "$item" ]
        then
            recursedir "$item"
        else
            command
        fi
    done
}

1
function키워드를 사용하지 마십시오. 코드가 POSIX sh와 호환되지 않게되지만 다른 유용한 목적은 없습니다. 를 사용하여 함수를 정의하고 recursedir() {괄호 두 개를 추가하고 함수 키워드를 제거하면 모든 POSIX 호환 쉘과 호환됩니다.
Charles Duffy 2013

1

파일 목록을 Bash 배열로 변환하십시오. 이것은 Bash 함수에서 배열을 반환하기 위해 Matt McClure의 접근 방식을 사용합니다 : http://notes-matthewlmcclure.blogspot.com/2009/12/return-array-from-bash-function-v-2.html 결과는 방법입니다 여러 줄 입력을 Bash 배열로 변환합니다.

#!/bin/bash

# This is the command where we want to convert the output to an array.
# Output is: fileSize fileNameIncludingPath
multiLineCommand="find . -mindepth 1 -printf '%s %p\\n'"

# This eval converts the multi-line output of multiLineCommand to a
# Bash array. To convert stdin, remove: < <(eval "$multiLineCommand" )
eval "declare -a myArray=`( arr=(); while read -r line; do arr[${#arr[@]}]="$line"; done; declare -p arr | sed -e 's/^declare -a arr=//' ) < <(eval "$multiLineCommand" )`"

for f in "${myArray[@]}"
do
   echo "Element: $f"
done

이 접근 방식은 잘못된 문자가있는 경우에도 작동하는 것으로 보이며 입력을 Bash 배열로 변환하는 일반적인 방법입니다. 단점은 입력이 길면 Bash의 명령 줄 크기 제한을 초과하거나 많은 양의 메모리를 사용할 수 있다는 것입니다.

목록에서 최종적으로 작동하는 루프가 목록을 파이프로 연결하는 방식은 stdin을 읽는 것이 쉽지 않다는 단점이 있으며 (예 : 사용자에게 입력 요청) 루프가 새로운 프로세스이므로 왜 변수가 궁금 할 수 있습니다. 루프 내부에 설정 한 사용자는 루프가 완료된 후에는 사용할 수 없습니다.

나는 또한 IFS 설정을 싫어하며 다른 코드를 엉망으로 만들 수 있습니다.


당신이 사용하는 경우 IFS='' read, 같은 라인에서 IFS의 설정은 읽기 명령에 대한 존재하고 탈출하지 않습니다. 이런 식으로 IFS를 설정하는 것을 싫어할 이유가 없습니다.
Charles Duffy 2013

1

글쎄요, 복잡한 답이 너무 많습니다. find 유틸리티에 "exec"옵션이 있기 때문에 find 유틸리티의 출력을 전달하거나 루프를 작성하고 싶지 않습니다.

내 문제는 dbf 확장자를 가진 모든 파일을 현재 폴더로 옮기고 싶었고 그중 일부에는 공백이 포함되어 있다는 것입니다.

나는 그것을 그렇게 다뤘다.

 find . -name \*.dbf -print0 -exec mv '{}'  . ';'

나를 위해 훨씬 간단 해 보인다


0

질문 과 당신의 질문 사이에 몇 가지 유사점이 있다는 것을 알았습니다 . 명령에 인수를 전달하려는 경우

test.sh "Cherry Hill" "New York City"

순서대로 인쇄하려면

for SOME_ARG in "$@"
do
    echo "$SOME_ARG";
done;

$ @가 큰 따옴표로 둘러싸여 있습니다. 여기에 몇 가지 메모가 있습니다.


0

특정 폴더에서 여러 디렉토리 또는 파일을 순차적으로 압축하려면 동일한 개념이 필요했습니다. ls에서 목록을 구문 분석하고 이름의 공백 문제를 피하기 위해 awk를 사용하여 해결했습니다.

source="/xxx/xxx"
dest="/yyy/yyy"

n_max=`ls . | wc -l`

echo "Loop over items..."
i=1
while [ $i -le $n_max ];do
item=`ls . | awk 'NR=='$i'' `
echo "File selected for compression: $item"
tar -cvzf $dest/"$item".tar.gz "$item"
i=$(( i + 1 ))
done
echo "Done!!!"

어떻게 생각해?


파일 이름에 줄 바꿈이 있으면 올바르게 작동하지 않을 것이라고 생각합니다. 아마도 당신은 그것을 시도해야합니다.
user000001 2013-08-24


-3

나를 위해 이것은 작동하고 거의 "깨끗"합니다.

for f in "$(find ./test -type d)" ; do
  echo "$f"
done

4
그러나 이것은 더 나쁩니다. 찾기를 큰 따옴표로 묶으면 모든 경로 이름이 단일 문자열로 연결됩니다. 문제를 확인 하려면 에코ls 로 변경 하십시오.
NVRAM

-4

간단한 변형 문제가있었습니다 ... .flv 형식의 파일을 .mp3 (yawn)로 변환합니다.

for file in read `find . *.flv`; do ffmpeg -i ${file} -acodec copy ${file}.mp3;done

모든 매킨토시 사용자 플래시 파일을 재귀 적으로 찾아서 오디오로 변환합니다 (복사, 트랜스 코딩 없음) ... 위와 비슷합니다. 'for file in ' 대신 읽기 가 이스케이프 된다는 점에 유의하십시오 .


2
read후에는 in당신이 이상 반복하고 목록에 또 하나 개의 단어입니다. 당신이 게시 한 것은 질문자가 가지고있는 것의 약간 깨진 버전인데, 작동하지 않습니다. 다른 것을 게시하려고 할 수도 있지만 어쨌든 여기에서 다른 답변으로 다룰 수 있습니다.
Gilles 'SO- 사악한 중지'
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.