쉘 스크립트를 사용하여 문자열의 2D 구조를 입력하고 출력하는 개념을 탐구 중이지만 문자열에 개행 문자가 포함되어 있지 않다는 제한이 있습니다. 공백을 포함할 수 있습니다.
내가 시도하고 싶은 방법은 이스케이프되지 않은 공백을 가로 축의 구분 기호로 사용하고 새 줄을 세로 축의 구분 기호로 사용하는 것입니다.
실험을 위해 다음 가젯을 사용하여 매개변수를 확인합니다.
❯ cat argshow
#!/bin/bash
ITERATION=1
while (( "$#" )); do
echo '$'"${ITERATION}: ${1}"
shift
(( ITERATION ++ ))
done
예:
❯ argshow abc\ def zzz
$1: abc def
$2: zzz
제가 설정한 규칙을 기반으로 한 첫 번째 테스트 사례는 다음과 같습니다.
echo 'abc\ def ghi\njkl'
값을 나타냅니다(JSON 형식).
[["abc def", "ghi"], ["jkl"]]
좋아요 계속 가세요.
나는 이것을 먼저 시도했다. 나는 이것을 zsh 쉘에서 실행했습니다.
❯ echo 'abc\ def ghi\njkl' | while read -r line; do argshow $line; done
$1: abc\ def ghi
$1: jkl
argshow
첫 번째 줄에서 인수가 두 개여야 하는데 인수 하나를 받았기 때문에 이것이 마음에 들지 않습니다 . -r
for를 사용하여 백슬래시를 복구할 수 있다는 것은 좋은 일입니다 read
(탭에 대해서는 나중에 걱정하겠습니다...).
Bash 쉘에서 동일한 명령을 실행했습니다.
$ echo 'abc\ def ghi\njkl' | while read -r line; do argshow $line; done
$1: abc\
$2: def
$3: ghi\njkl
$ echo $'abc\ def ghi\njkl' | while read -r line; do argshow $line; done
$1: abc\
$2: def
$3: ghi
$1: jkl
이것이 내 bash 지식이 한계에 도달하는 곳입니다. 이 중 어느 것도 내가 예상한 것이 아니었습니다. eval
내가 원하는 방식으로 작동하려면 왜 하나를 연결해야 합니까 ? :
$ echo $'abc\ def ghi\njkl' | while read -r line; do eval argshow $line; done
$1: abc def
$2: ghi
$1: jkl
bash 변수를 배치할 때 따옴표를 사용하지 않으면 변수가 확장되어 필요한 모든 것으로 분할될 것이라는 인상을 받았습니다. 결국, shellcheck
쉘 스크립트에서 쉘 변수를 큰따옴표로 묶지 않고 사용할 때 그것이 나에게 소리치는 것입니다. 항상 확장될 것이라고 생각했기 때문에 변수에 공백 문자가 2개 있으면 예상되는 인수 1개가 아닌 인수 3개가 됩니다.
정기적으로 사용하는 스크립트를 만드는 데 이 방법을 사용할 수 없습니다. eval
스크립팅을 시작하는 데에는 많은 사악한 구멍이 있습니다. 이는 인수를 따옴표로 묶는 것과 같은 무료 기능을 허용하고 개행 문자를 이스케이프할 수 있기 때문에 유혹적일 수 있으며 구문은 익숙하기 때문에 기억하기 쉽지만 스크립트에서 사용하기에는 너무 위험할 수 있습니다. 코드 실행을 유발하기 위해 이스케이프되지 않은 세미콜론을 삽입합니다.
아마도 나는 쉘 스크립팅의 한계를 실제로 발견했고, 이를 실제 프로그래밍 언어로 구현해야 할 것 같습니다. 공평하지만 이 문제를 해결하고 해킹을 진행하는 데 사용할 수 있는 몇 가지 간단한 옵션이 있었으면 좋겠습니다.
예, 또한 bash 배열 전달과 같은 작업을 수행할 수 있다는 것도 알고 있지만 이러한 데이터 스트림을 간단한 파일로 덤프할 수 있기를 원합니다. 그게 가장 큰 매력입니다. 암시적으로 매우 기본적인 Function human- 읽을 수 있는 형식.
당면한 작업에 대한 배경 지식을 좀 더 알아보겠습니다. 저는 컴퓨터를 활용하는 데이터 관리 방식을 좀 더 활용하는 방식으로 전환하고 있습니다. 내 생각에는 카메라/드론 등에 512GB SD 카드와 같은 것을 사용하는 경우가 있습니다. 편의와 작업 흐름을 위해 일부 오래된 데이터를 메모리 카드에 보관하고 싶습니다. 예를 들어 때로는 더 쉽게 볼 수 있도록 Macbook에 무언가를 덤프하고 싶습니다. 이것을 내 워크스테이션의 ZFS 풀로 마이그레이션할 때 어떤 파일이 저장되어 있는지에 대한 의문이 제기되므로 해당 파일을 다시 복사하여 공간을 낭비할 필요가 없습니다. 때로는 이 측면에 너무 집중하다 보면 백업해야 할 새로운 콘텐츠를 받아들일 수 없는 경우가 있습니다. 다시 사용할 수 있도록 디스크를 지우기 전에 디스크의 남은 내용(거의 가득 찼을 수 있음)을 확인하는 것은 항상 시간이 많이 걸리는 프로세스입니다.
그래서 제가 지금 사용하고 있는 접근 방식은 주기적으로 스토리지 풀 데이터베이스를 구축하여 find /pool /pool2 -type f -printf "%M %s %t %p\\n" > ~/Dropbox/find_zfs
다양한 용도로 쉽게 사용할 수 있도록 하는 것입니다. 이 문제가 발생한 이유는 다음 스크립트(이름을 trawl
)를 작성하는 동안 발생했습니다.
#!/bin/bash
# provide a list of dirs and for each of their immediate child file and folder names, trawl through
# a standard file listing record (this is the FIRST arg) (the kind I ususally use with fzf) to
# look for any matches. emit human readable output on stderr, and stdout will be a list paths.
# That output will be organized for now as a flat list. The idea here is that the list-of-lists
# structure of it can trivially be recovered by a wrapping script that knows the input args and can
# correlate them back out. Inefficient, but simple.
# TODO TODO TODO implement a sanitizer on the input and do a fuzzy check for that to further
# eliminate manual checking via fzf and suggest matches when they are found
FIRSTARG="$1"
shift
for DIR in "$@"; do
>&2 echo "CHECKING DIR: $DIR"
ls -1 "$DIR" | while read -r CHILD; do
RG_OUT=$(rg -F "$CHILD" "$FIRSTARG")
if [ $? -eq 0 ]; then
>&2 echo "$DIR/$CHILD is found with $(echo "$RG_OUT" | wc -l) hits; skipping!"
else
>&2 printf "NOT FOUND; adding: "
echo "$DIR/$CHILD"
fi
done
done
또한 주어진 디렉터리의 첫 번째 수준 하위 파일이 아닌 각 파일을 더 철저하게 검사하기 위해 재귀 버전을 만들었습니다.
보시다시피, 이것의 아이디어는 기본적으로 반자동 파일 백업 중복 제거를 달성할 수 있다는 것입니다.
물론 누가 언급하기 전에 아마도 ZFS 자체에 대한 중복 제거를 활성화할 것입니다. 그러나 일반적인 합의는 (주로) 대량 미디어 백업과 같은 매우 좁은 사용 사례에서 부과되는 다양한 성능 비용만큼 가치가 있다는 것입니다. 그들 중 하나.
이것이 어떻게 들어 맞느냐고 물으실 수도 있습니다. trawl
여기서는 "동기화 지점"을 원했습니다. 2D 구조의 두 축은 (X) 입력 디렉터리의 모든 개별 하위 파일과 (Y)입니다. ) 개별적으로 디렉토리를 입력합니다.
내 스크립트 주석에서 볼 수 있듯이 결국 2D 요구 사항을 회피하게 되었습니다. 왜냐하면 그것이 그다지 중요하지 않은 것으로 판명되었고 나중에 이를 되돌릴 수 있었기 때문입니다. 실제로 저장하고 싶다면 JSON을 내보내는 것이 가장 합리적입니다.
이를 통해 워크플로우는 이제
- 디스크 삽입(해당하는 경우)
trawl ~/Dropbox/find_zfs srcdir/ srcdir2/
- 출력을 시각적으로 검사하여 의미가 있는지 확인합니다.
- 다시 실행하되 입력을 파이프하십시오.
mkdir -p /pool/backup-$(date +%F); while read -r LINE; do mv "$LINE" /pool/backup-$(date +%F); done
같은 일을 하기 위해 작성한 실제 TypeScript 프로그램이 있고 기능도 동일하지만 대화형의 유연한 작업 흐름을 구현하는 것이 자연스럽지 않기 때문에 좀 웃깁니다.
답변1
하나의 변수에 여러 단어를 포함하려면 스칼라 변수가 아닌 배열을 사용하고, 이를 이스케이프 연산자(필드 구분 기호 및 줄 연속의 경우)로 read
처리 하려면 생략하세요. 이것이 기본값입니다. 다음 작업은 다음 작업이 아닙니다. 수행.\
-r
read
따라서 ksh/zsh/yash에서:
read -A fields; showargs "${fields[@]}"
또는 bash에서:
read -a fields; showargs "${fields[@]}"
( 기본적으로 공백, 탭 및 줄 바꿈(zsh에서는 nul)을 포함하는 문자 read
분할에 유의하세요.)$IFS
$IFS
zsh에서는 다른 Bourne 유사 셸에서와 마찬가지로 (니모닉: 가위처럼 보임)을 사용하여 따옴표 $=line
가 없는 $line
스칼라 변수 확장 문자 분할을 수행할 수 있습니다 . $=
또는 $=~line
다른 쉘에 의해 실행되는 전체 분할+glob의 경우 shellcheck가 경고합니다. 또는 현재 포함된 것 ${(s[ ])line}
이상으로 공간을 명시적으로 분할합니다 . $IFS
하지만 여기서는 백슬래시에는 도움이 되지 않습니다.
백슬래시(셸 구문 토큰화 및 따옴표 제거용)뿐만 아니라 임의의 셸 구문 따옴표를 처리 eval
하려면@thrig의 훌륭한 답변zsh z
및 Q
매개변수 확장 플래그 정보. 하지만 빈 요소를 유지하려면 다음을 수행해야 합니다.
$ line='foo\ bar "" blah'
$ fields=( "${(Q@)${(z)line}}" )
$ typeset fields
fields=( 'foo bar' '' blah )
또는 Z[n]
대체 z
개행 문자도 명령 구분 기호 토큰 대신 토큰 구분 기호로 처리됩니다.
그럼에도 불구하고 이것은 단지 인용 처리가 아닌 전체 쉘 구문(이 경우 zsh 구문) 토큰화를 수행하는 것입니다. 예를 들어, 행은 예상한 것과는 다르게 a$(b c)<y>y<<<"a b"
으로 표시됩니다 .fields=( 'a$(b c)' '<' y '>' y '<<<' 'a b' )
fields=( 'a$(b' 'c)<y>y<<<a b' )
다차원 배열 및 데이터 (역)직렬화1를 지원하는 셸의 경우 ksh93을 참조하세요.
$ printf '%s\n' $'( (\'a b\' a) (\'b\\c\n\' d) )' |
> ksh -c 'read -C a; typeset -p a; printf "<%s>\n" "${a[1][0]}"'
typeset -a a=(('a b' a) ($'b\\c\n' d) )
<b\c
>
내가 마지막으로 테스트했을 때(수년 전) 버그가 많았고 아마도 여전히 그럴 가능성이 높습니다. eval
입력이 유효한 직렬화된 스트림이라는 보장이 없다면 .
오! 사실 아니다:
$ printf '%s\n' '( [`uname>&2`]=1 )' | ksh -c 'read -C a'
Linux
또한 JSON 문자열에는 NUL이 포함될 수 있지만 zsh 변수에만 NUL이 포함될 수 있습니다. 복잡한 데이터 구조를 처리하려면 쉘 대신 적절한 프로그래밍 언어를 사용하는 것이 더 합리적입니다.
1 기록에 따르면, AT&T는 개발 팀이 해체되기 전에 ksh93의 마지막 베타 버전을 출시했습니다. ksh2020(현재는 사용되지 않음)의 기반이 되는 ksh93도 JSON (역)직렬화에 대한 일부 실험적 지원을 제공했습니다. ksh2020에서 제거됨). 또한 CSV 입력/출력을 지원합니다.
답변2
read
모든 추가 열을 지정된 성에 할당하면 모든 열이 line
변수에 포함됩니다. ZSH에서:
% echo 'abc\ def ghi' | while read -r line; do print -Rl $line; done
abc\ def ghi
% echo 'abc\ def ghi' | while read -r line and; do print -Rl $line $and; done
abc\
def ghi
% echo 'abc\ def ghi' | while read -r line and another; do print -Rl $line $and $another; done
abc\
def
ghi
따라서 백슬래시와 리터럴 문자열을 처리하려면 실제로 보간( eval
최악의 경우) 이 필요합니다 . ZSH에서는 "zsh 명령줄과 같은 단어 분할"이 매우 유사합니다.line
\n
(z)
% echo 'abc\ def ghi' | while read -r line; do cols=(${(z)line}); printf ">%s<\n" $cols[@]; done
>abc\ def<
>ghi<
echo
이식성이 좋지도 않으며 가능한 경우 ZSH printf
와 같은 쉘 관련 내장 기능으로 대체해야 합니다.print
% printf "abc\ def ghi\njkl" | while read -r line; do cols=(${(z)line}); printf ">%s<\n" $cols[@]; done
>abc\ def<
>ghi<
이런, jkl
우리 어디 갔었어? 자동 데이터 손실은 재미가 없습니다.
% printf "abc\ def ghi\njkl\n" | while read -r line; do cols=(${(z)line}); printf ">%s<\n" $cols[@]; done
>abc\ def<
>ghi<
>jkl<
그게 다야... 하지만 여전히 \
경우가 있는데, 역참조할 수 있을까요? 아직 ZSH에 있음:
% printf "abc\ def ghi\njkl\n" | while read -r line; do cols=(${(z)line}); print -l ${(Q)cols}; done
abc def
ghi
jkl
텍스트가 나타나면 printf
줄 바꿈이 제공됩니다 .\n
이러한 옵션을 삽입할 수도 있습니다., 더 많은 작업입니다. 대부분의 다른 셸에서는 eval
ZSH 작업을 수행하는 데 적합한 인수 플래그가 없을 수 있으므로 작업을 수행합니다 . 따라서 eval
이 프로젝트에서 셸을 계속 사용하려면 아무것도 실행하지 않기를 바라면서 여기 저기 붙어 있거나 두 가지 다른 버전의 스크립트가 있는 것입니다. 어쩌면 (z)
일부 테스트에서는 ZSH가 항상 입력 형식으로 원하는 작업을 수행할 것이라고 100% 확신하지 못할 수도 있습니다.
while
(특히 그런 까다로운 것을 사용 하거나 스크립트가 약 20줄보다 긴 경우에는 쉘에서 전환하기 위한 임계값을 매우 낮게 설정합니다 .)
무작위 벤치마크
.에서 시작되는 265625줄 입력 파일의 경우 Perl은 ZSH보다 1282% 빠릅니다 /etc/passwd
.
#!/usr/bin/env perl
#our @array; # I'll let the shell folks puzzle out multi-dimensional arrays
while (readline) {
chomp;
#push @array, [ map { s/\\ / /gr } split /(?<!\\) / ];
print join(' ', map { s/\\ / /gr } split /(?<!\\) /), "\n";
}
#!/usr/bin/env zsh
# do you like silent data loss? if so, remove the || check
while IFS= read line || [ -n "$line" ]; do
fields=( "${(Q@)${(z)line}}" )
typeset -p fields
done
답변3
공교롭게도 분할(따옴표 없는 변수 확장)이나 읽기를 처리할 때 쉘은 매우 얕을 수 있습니다.
코드가 다음과 같을 때 이 문제가 발생합니다.argshow $line 실행
배시 사용:
$ set -- 1 2 3 4
$ echo ${@@A}
set -- '1' '2' '3' '4'
$ var='1 2 3 4'
$ set -- $var
$ echo ${@@A}
set -- '1' '2' '3' '4' # that seems to work.
$ var='1 2\ 3 4'; set -- $var
$ echo ${@@A}
set -- '1' '2\' '3' '4' # oops, why is that `\` ignored ?
변수에서 확장할 때 특별한 구문을 \
제공하는 규칙이 없기 때문에 무시됩니다 .\
\
예, 명령줄을 구문 분석할 때 (여전히) 특별한 규칙이 있습니다.
$ set -- 1 2\ 3 4
$ echo ${@@A}
set -- '1' '2 3' '4'
그러나 이것은 변수 확장에서는 작동하지 않습니다.
$'...'
읽으려면 별도의 줄이 필요하지만 printf 또는 ANSI-C 이스케이프 형식을 사용하여 수행할 수 있습니다.
$ printf '%s]n' a b c d
a
b
c
d
$ i=1; printf '%s\n' a b c d | while read -r line; do printf '%s %s \n' $((i++)) $line; done
1 a
2 b
3 c
4 d
$ var='a b c d'
$ i=1; printf '%s\n' $var | while read -r line; do printf '%s %s \n' $((i++)) $line; done
1 a
2 b
3 c
4 d # it seems to work.
$ var='a b\ c d'
$ i=1; printf '%s\n' $var | while read -r line; do printf '%s %s \n' $((i++)) $line; done
1 a
2 b\
3 c
4 d
그래서 "참조변수 확장"에 대한 글이 많이 올라오고 있습니다. 배열 작업은 문자열 작업과 다릅니다.
$ var=( a b\ c d)
$ printf '%s\n' "${var[@]}"
a
b c
d
# Or even
$ set -- "${var[@]}"
$ printf '%s\n' "$@"
a
b c
d
즉, 변수 확장은 다음과 같이 분할됩니다.오직IFS 구분 기호. 일반적으로 한 가지 유형의 구분 기호(공백)로 축소됩니다.
$ ( IFS=''; var='a b\ c d'; printf '<%s> ' $var; echo )
<a b\ c d>
$ ( IFS=' '; var='a b\ c d'; printf '<%s> ' $var; echo )
<a> <b\> <c> <d>
\
여기에는 명령줄에 사용된 인용 공백( )의 영향이 포함되지 않습니다 .
쉘은 zsh
훨씬 더 얕아서 (필요하지 않은 경우) 변수 확장을 전혀 분할하지 않습니다.
% var='a b\ c d'
% printf '<%s>\n' $var
<a b\ c d>
% printf '<%s>\n' $=var
a
b\
c
d
그러나 \
문제는 여전히 동일합니다.