bash : 선택 영역에서 찾기의 공백 안전 절차 적 사용


12

다음과 같은 파일 이름이 주어집니다.

$ ls -1
file
file name
otherfile

bash 공백 문자가 있으면 완벽하게 잘 수행됩니다.

$ for file in *; do echo "$file"; done
file
file name
otherfile
$ select file in *; do echo "$file"; done
1) file
2) file name
3) otherfile
#?

그러나, 때로는 심지어 엄격에있는 모든 파일 또는 작업 할되지 않을 수도 있습니다 $PWD어디에, find들어오는 또한 핸들 공백 명목상. :

$ find -type f -name file\*
./file
./file name
./directory/file
./directory/file name

나는 스크립틀릿 의 whispace-safe 버전을 생성 하여 출력을 가져 와서 find제시하려고합니다 select.

$ select file in $(find -type f -name file); do echo $file; break; done
1) ./file
2) ./directory/file

그러나 이것은 파일 이름에서 공백으로 폭발합니다.

$ select file in $(find -type f -name file\*); do echo $file; break; done
1) ./file        3) name          5) ./directory/file
2) ./file        4) ./directory/file  6) name

보통, 나는 엉망으로 주위를 어지럽 힐 것이다 IFS. 하나:

$ IFS=$'\n' select file in $(find -type f -name file\*); do echo $file; break; done
-bash: syntax error near unexpected token `do'
$ IFS='\n' select file in $(find -type f -name file\*); do echo $file; break; done
-bash: syntax error near unexpected token `do'

이것에 대한 해결책은 무엇입니까?



1
특정 파일 이름과 일치하는 기능으로 사용 find하는 경우 4를 select file in **/file*설정 한 후 (설정 후 shopt -s globstar) 간단히 사용할 수 있습니다 bash.
chepner

답변:


14

공백과 탭 (포함 된 개행 문자는 아님) 만 처리해야하는 경우 mapfile(또는 동의어 readarray)를 사용하여 배열을 읽을 수 있습니다.

$ ls -1
file
other file
somefile

그때

$ IFS= mapfile -t files < <(find . -type f)
$ select f in "${files[@]}"; do ls "$f"; break; done
1) ./file
2) ./somefile
3) ./other file
#? 3
./other file

당신이 경우 어떻게 핸들 줄 바꿈에 필요하고 bash버전은이 널 (null)로 구분 제공 mapfile 다음, 당신이 그것을 수정할 수 있습니다 IFS= mapfile -t -d '' files < <(find . -type f -print0). 그렇지 않으면 루프를 find사용하여 널로 구분 된 출력 에서 동등한 배열을 어셈블하십시오 read.

$ touch $'filename\nwith\nnewlines'
$ 
$ files=()
$ while IFS= read -r -d '' f; do files+=("$f"); done < <(find . -type f -print0)
$ 
$ select f in "${files[@]}"; do ls "$f"; break; done
1) ./file
2) ./somefile
3) ./other file
4) ./filename
with
newlines
#? 4
./filename?with?newlines

1-d 옵션이 추가되었습니다 mapfilebash버전 4.4 IIRC


2
내가 전에 사용하지 않은 다른 동사에 대해 +1
roaima

실제로 mapfile나에게도 새로운 것입니다. 명성.
DopeGhoti

while IFS= read버전 (맥 OS를 사용하는 우리의 사람들을 위해 중요하다) bash는 v3에서 다시 작동합니다.
Gordon Davisson

3
find -print0변형의 경우 +1 ; 불평 을 퍼팅 알려진-잘못된 버전, 하나가있는 경우에만 사용할 수 있도록 기술 을 알고 그들이 줄 바꿈을 처리 할 필요가있다. 예상치 못한 장소에서만 예상치 못한 상황을 처리하면 전혀 예상치 못한 상황을 처리하지 않습니다.
찰스 더피

8

이 답변에는 모든 유형의 파일에 대한 솔루션 이 있습니다. 줄 바꿈 또는 공백으로.
최근 배쉬뿐만 아니라 고대 배쉬 및 오래된 posix 포탄에 대한 솔루션이 있습니다.

이 답변 [1] 에서 아래에 나열된 트리 가 테스트에 사용됩니다.

고르다

select배열 로 쉽게 작업 할 수 있습니다 .

$ dir='deep/inside/a/dir'
$ arr=( "$dir"/* )
$ select var in "${arr[@]}"; do echo "$var"; break; done

또는 위치 매개 변수를 사용하여 :

$ set -- "$dir"/*
$ select var; do echo "$var"; break; done

따라서 유일한 실제 문제는 배열 내부 또는 위치 매개 변수 내부에 "파일 목록"(정확하게 구분 된)을 얻는 것입니다. 계속 읽으세요.

세게 때리다

bash 로보 고 한 문제가 보이지 않습니다. Bash는 주어진 디렉토리 안에서 검색 할 수 있습니다 :

$ dir='deep/inside/a/dir'
$ printf '<%s>\n' "$dir"/*
<deep/inside/a/dir/directory>
<deep/inside/a/dir/file>
<deep/inside/a/dir/file name>
<deep/inside/a/dir/file with a
newline>
<deep/inside/a/dir/zz last file>

또는 루프가 마음에 들면

$ set -- "$dir"/*
$ for f; do printf '<%s>\n' "$f"; done
<deep/inside/a/dir/directory>
<deep/inside/a/dir/file>
<deep/inside/a/dir/file name>
<deep/inside/a/dir/file with a
newline>
<deep/inside/a/dir/zz last file>

위의 구문은 (합리적) 셸 (적어도 csh는 아님)에서 올바르게 작동합니다.

위 구문의 유일한 한계는 다른 디렉토리로 내려가는 것입니다.
그러나 bash는 그렇게 할 수 있습니다.

$ shopt -s globstar
$ set -- "$dir"/**/*
$ for f; do printf '<%s>\n' "$f"; done
<deep/inside/a/dir/directory>
<deep/inside/a/dir/directory/file>
<deep/inside/a/dir/directory/file name>
<deep/inside/a/dir/directory/file with a
newline>
<deep/inside/a/dir/directory/zz last file>
<deep/inside/a/dir/file>
<deep/inside/a/dir/file name>
<deep/inside/a/dir/file with a
newline>
<deep/inside/a/dir/zz last file>

파일로 끝나는 파일 과 같은 일부 파일 만 선택하려면 *를 바꾸십시오.

$ set -- "$dir"/**/*file
$ printf '<%s>\n' "$@"
<deep/inside/a/dir/directory/file>
<deep/inside/a/dir/directory/zz last file>
<deep/inside/a/dir/file>
<deep/inside/a/dir/zz last file>

건장한

제목에 "공간 안전 " 을 배치 하면 의미가 " 강력한 " 것으로 가정합니다 .

공백 (또는 개행)을 견고하게하는 가장 간단한 방법은 공백 (또는 개행)이있는 입력 처리를 거부하는 것입니다. 쉘에서이를 수행하는 매우 간단한 방법은 파일 이름이 공백으로 확장되면 오류로 종료하는 것입니다. 이를 수행하는 방법은 여러 가지가 있지만 가장 컴팩트 한 (및 posix) (서드 디렉토리 이름 및 도트 파일 방지를 포함하여 하나의 디렉토리 내용으로 제한됨)은 다음과 같습니다.

$ set -- "$dir"/file*                            # read the directory
$ a="$(printf '%s' "$@" x)"                      # make it a long string
$ [ "$a" = "${a%% *}" ] || echo "exit on space"  # if $a has an space.
$ nl='
'                    # define a new line in the usual posix way.  

$ [ "$a" = "${a%%"$nl"*}" ] || echo "exit on newline"  # if $a has a newline.

사용 된 솔루션이 해당 항목 중 하나라도 견고하면 테스트를 제거하십시오.

bash에서 하위 디렉토리는 위에서 설명한 **를 사용하여 한 번에 테스트 할 수 있습니다.

도트 파일을 포함하는 몇 가지 방법이 있습니다. Posix 솔루션은 다음과 같습니다.

set -- "$dir"/* "$dir"/.[!.]* "$dir"/..?*

찾기

어떤 이유로 찾기를 사용해야하는 경우 구분 기호를 NUL (0x00)로 바꾸십시오.

배쉬 4.4+

$ readarray -t -d '' arr < <(find "$dir" -type f -name file\* -print0)
$ printf '<%s>\n' "${arr[@]}"
<deep/inside/a/dir/file name>
<deep/inside/a/dir/file with a
newline>
<deep/inside/a/dir/directory/file name>
<deep/inside/a/dir/directory/file with a
newline>
<deep/inside/a/dir/directory/file>
<deep/inside/a/dir/file>

배쉬 2.05+

i=1  # lets start on 1 so it works also in zsh.
while IFS='' read -d '' val; do 
    arr[i++]="$val";
done < <(find "$dir" -type f -name \*file -print0)
printf '<%s>\n' "${arr[@]}"

POSIXLY

find에 NUL 구분 기호가없고 읽을 수있는 -d(또는 -a) 없는 유효한 POSIX 솔루션을 만들려면 완전히 다른 접근 방식이 필요합니다.

-exec쉘을 호출하여 find 에서 컴플렉스를 사용해야합니다 .

find "$dir" -type f -exec sh -c '
    for f do
        echo "<$f>"
    done
    ' sh {} +

또는 필요한 것이 select 인 경우 (select는 sh가 아닌 bash의 일부입니다) :

$ find "$dir" -type f -exec bash -c '
      select f; do echo "<$f>"; break; done ' bash {} +

1) deep/inside/a/dir/file name
2) deep/inside/a/dir/zz last file
3) deep/inside/a/dir/file with a
newline
4) deep/inside/a/dir/directory/file name
5) deep/inside/a/dir/directory/zz last file
6) deep/inside/a/dir/directory/file with a
newline
7) deep/inside/a/dir/directory/file
8) deep/inside/a/dir/file
#? 3
<deep/inside/a/dir/file with a
newline>

[1] 이 트리 (\ 012는 개행) :

$ tree
.
└── deep
    └── inside
        └── a
            └── dir
                ├── directory
                   ├── file
                   ├── file name
                   └── file with a \012newline
                ├── file
                ├── file name
                ├── otherfile
                ├── with a\012newline
                └── zz last file

이 두 가지 명령으로 만들 수 있습니다.

$ mkdir -p deep/inside/a/dir/directory/
$ touch deep/inside/a/dir/{,directory/}{file{,\ {name,with\ a$'\n'newline}},zz\ last\ file}

6

루핑 구문 앞에 변수를 설정할 수 없지만 조건 앞에 변수를 설정할 수 있습니다. 맨 페이지의 세그먼트는 다음과 같습니다.

간단한 명령 또는 기능에 대한 환경 은 PARAMETERS에서 설명한대로 매개 변수 지정 접두어를 사용하여 임시로 보강 할 수 있습니다.

(루프는 간단한 명령 이 아닙니다 .)

다음은 실패 및 성공 시나리오를 보여주는 일반적으로 사용되는 구성입니다.

IFS=$'\n' while read -r x; do ...; done </tmp/file     # Failure
while IFS=$'\n' read -r x; do ...; done </tmp/file     # Success

불행히도 변경 내용 IFSselect관련 처리에 영향 을 주면서 구성 에 포함시키는 방법을 볼 수 없습니다 $(...). 그러나 IFS루프 외부에 설정 되는 것을 막을 수있는 것은 없습니다 .

IFS=$'\n'; while read -r x; do ...; done </tmp/file    # Also success

그리고 내가 볼 수있는 것은이 구조입니다 select.

IFS=$'\n'; select file in $(find -type f -name 'file*'); do echo "$file"; break; done

방어적인 코드를 작성할 때 나는 절 중 하나를 서브 쉘에서 실행하거나 할 것을 권 해드립니다 IFSSHELLOPTS저장 블록 주위에 복원 :

OIFS="$IFS" IFS=$'\n'                     # Split on newline only
OSHELLOPTS="$SHELLOPTS"; set -o noglob    # Wildcards must not expand twice

select file in $(find -type f -name 'file*'); do echo $file; break; done

IFS="$OIFS"
[[ "$OSHELLOPTS" !~ noglob ]] && set +o noglob

5
IFS=$'\n'안전하다고 가정하면 근거가 없습니다. 파일 이름은 개행 문자를 완벽하게 포함 할 수 있습니다.
찰스 더피

4
나는 존재하는 경우에도 액면가에서 가능한 데이터 세트에 대한 그러한 주장을 받아들이는 것을 주저합니다. 내가 가지고있는 최악의 데이터 손실 이벤트는 오래된 백업 정리를 담당하는 유지 관리 스크립트가 임의의 가비지를 덤프 한 잘못된 포인터 역 참조가있는 C 모듈을 사용하여 Python 스크립트로 작성된 파일을 제거하려고 시도한 경우입니다 -공백으로 구분 된 와일드 카드를 포함하여 이름에 포함됩니다.
찰스 더피

2
해당 파일을 정리하는 쉘 스크립트를 작성하는 사람들은 "이름을 일치시킬 수 없습니다"라는 말을 인용하지 않았습니다 [0-9a-f]{24}. 고객 청구를 지원하는 데 사용 된 TB의 데이터 백업이 손실되었습니다.
찰스 더피

4
@CharlesDuffy에 완전히 동의하십시오. 엣지 케이스를 처리하지 않는 것은 대화식으로 작업 할 때만 적합하며 수행중인 작업을 수 있습니다 . select설계 상 스크립트 솔루션을 위한 것이기 때문에 항상 엣지 케이스를 처리하도록 설계되어야합니다.
와일드 카드

2
@ilkkachu 물론 - 혹시 전화를하지 않을 select당신이 명령에있어 입력이 실행하는 쉘에서,하지만 당신이 제공하는 프롬프트에 응답하고 스크립트에서 이 스크립트를 해당 스크립트이고, 및 해당 입력을 기반으로 미리 정의 된 로직 (파일 이름을 모르는 상태로 구축)을 실행합니다.
찰스 더피

4

나는 내 관할권에서 벗어날 수 있지만 아마도 공백과 관련하여 아무런 문제가없는 다음과 같이 시작할 수 있습니다.

find -maxdepth 1 -type f -printf '%f\000' | {
    while read -d $'\000'; do
            echo "$REPLY"
            echo
    done
}

주석에 명시된 바와 같이 잠재적 인 잘못된 가정을 피하려면 위 코드는 다음과 같습니다.

   find -maxdepth 1 -type f -printf '%f\0' | {
        while read -d ''; do
                echo "$REPLY"
                echo
        done
    }

read -d영리한 솔루션입니다. 감사합니다.
DopeGhoti

2
read -d $'\000'정확히 동일 read -d ''하지만 bash의 기능에 대해 오해의 소지가있는 사람들에게 (문자열 내에서 리터럴 NUL을 나타낼 수 있음을 의미합니다.) 를 실행 s1=$'foo\000bar'; s2='foo'한 다음 두 값을 구분하는 방법을 찾으십시오. (향후의 버전은 저장된 값을과 동일하게하여 명령 대체 동작으로 정규화 할 수 foobar있지만 현재는 그렇지 않습니다.)
찰스 더피
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.