명령 대체의 정확한 출력을 캡처할 수 있기를 원합니다.후행 줄바꿈 포함.
기본적으로 제거되어 있으므로 이를 보존하려면 약간의 조작이 필요할 수 있다는 것을 알고 있습니다.원래 종료 코드를 유지하고 싶습니다..
예를 들어, 다양한 수의 후행 줄 바꿈과 종료 코드가 포함된 명령이 제공됩니다.
f(){ for i in $(seq "$((RANDOM % 3))"); do echo; done; return $((RANDOM % 256));}
export -f f
나는 다음과 같은 것을 실행하고 싶습니다 :
exact_output f
출력은 다음과 같습니다.
Output: $'\n\n'
Exit: 5
bash
나는 POSIX와 POSIX 모두에 관심이 있습니다 sh
.
답변1
POSIX 쉘
대개(1 2 삼 4 5 6 7 8 9 10 11 12 13 14 15 ) 명령의 완전한 표준 출력을 얻는 비결은 다음과 같습니다.
output=$(cmd; ret=$?; echo .; exit "$ret")
ret=$?
output=${output%.}
아이디어는 .\n
제거할 추가 .command 대체를 추가하는 것입니다.저것 \n
. 그런 다음 .
껍질을 사용합니다 ${output%.}
.
Python 이외의 쉘에서는 zsh
출력에 NUL 바이트가 있는 경우 이 방법이 여전히 작동하지 않습니다. 의 경우 yash
출력이 텍스트가 아니면 이 방법이 작동하지 않습니다.
또한 일부 로케일에서는 끝에 어떤 문자가 삽입되는지가 중요합니다. .
일반적으로 괜찮지만(아래 참조) 일부는 그렇지 않을 수도 있습니다. 예를 들어 x
(다른 답변에 사용됨) 또는 @
BIG5, GB18030 또는 BIG5HKSCS 문자 세트를 사용하는 로케일에서는 작동하지 않습니다. 이러한 문자 집합에는 많은 문자 인코딩이 사용됩니다.마치다x
또는 (0x78, 0x40)의 인코딩과 @
동일한 바이트
예를 들어 ū
BIG5HKSCS에서는 0x88 0x78입니다( x
ASCII의 0x78과 유사하며 시스템의 모든 문자 세트는 영어 알파벳 @
및 를 포함하여 이식 가능한 문자 세트의 모든 문자에 대해 동일한 인코딩을 가져야 합니다 .
). 따라서 cmd
그 printf '\x88'
자체가 해당 인코딩에서 유효한 문자가 아니라 단지 바이트 시퀀스 인 경우 x
그 뒤에 삽입하면 실제로 포함된 것 (바이트를 구성하는 2바이트) 으로 ${output%x}
이를 제거할 수 있는 방법이 없습니다. 시퀀스는 이 인코딩에서 유효한 문자입니다.)x
$output
ū
사용하거나 .
그래야 /
한다전반적으로 좋음, POSIX 요구 사항에 따라:
<period>
" ,<slash>
및 와<newline>
연관된 인코딩 값은<carriage-return>
구현에서 지원되는 모든 로케일에서 변경되지 않고 유지됩니다." 이는 이러한 값이 모든 로케일/인코딩에서 동일한 이진 표현을 갖음을 의미합니다.- "마찬가지로
<period>
,<slash>
, 및 를 인코딩하는 데 사용되는<newline>
바이트 값은<carriage-return>
모든 로케일에서 다른 문자의 일부로 발생해서는 안 됩니다." 이는 이러한 바이트/문자가 유효한 문자의 부분 바이트 시퀀스를 완료할 수 없기 때문에 위의 상황이 발생하지 않음을 의미합니다. 로케일/인코딩에서. (바라보다6.1 이식 가능한 문자 세트)
위 내용은 Portable Character Set의 다른 캐릭터에는 적용되지 않습니다.
다음과 같은 또 다른 접근 방식@Isaac이 토론함, 로케일을 다음으로 변경합니다 C
(이는 또한임의의 단일 바이트올바르게 제거), 마지막 문자( ${output%.}
)만 제거합니다. 일반적으로 이것을 사용해야 합니다 LC_ALL
(원칙적으로는 LC_CTYPE
충분하지만 이미 설정된 항목에 의해 실수로 무시될 수 있습니다 LC_ALL
). 또한 원래 값을 복원해야 합니다(또는 예를 들어 locale
POSIX와 호환되지 않는 값이 함수에 사용됨). 그러나 일부 셸은 POSIX 요구 사항에도 불구하고 런타임 시 로케일 변경을 지원하지 않습니다.
.
또는 를 사용하면 /
이 모든 것을 피할 수 있습니다.
bash/zsh 대안
출력에 NUL이 없다고 가정하고 bash
and 를 사용하면 다음을 수행할 수도 있습니다.zsh
IFS= read -rd '' output < <(cmd)
종료 상태를 얻으려면 cmd
의 일부 버전에서는 수행할 수 있지만 에서는 수행할 수 없지만 에서 작성하고 종료 상태를 얻을 수 있습니다.wait "$!"; ret=$?
bash
zsh
zsh
cmd | IFS= read -rd '' output
$pipestatus[1]
rc/es/아카나가
rc
완전성을 위해 // 연산자 es
가 있다는 점에 유의하세요 . akanga
여기에서 `cmd
(또는 `{cmd}
더 복잡한 명령의 경우)로 표현되는 명령 대체는 목록(기본적으로 분할 $ifs
, 공백 탭 개행)을 반환합니다. Bourne과 같은 쉘과 달리 이러한 쉘에서는 개행 제거가 $ifs
분할의 일부로만 발생합니다. 따라서 지정된 구분 기호가 있는 양식을 비우 $ifs
거나 사용할 수 있습니다.``(seps){cmd}
ifs = ''; output = `cmd
또는:
output = ``()cmd
그럼에도 불구하고 명령의 종료 상태는 손실됩니다. 이를 출력에 포함시킨 다음 추출해야 하는데 이는 보기 흉합니다.
물고기
물고기에서 명령 대체는 서브쉘을 사용하며 (cmd)
서브쉘을 포함하지 않습니다.
set var (cmd)
$var
if 출력의 모든 행을 포함하는 배열을 생성 하거나 가장 많은 행을 제거합니다.cmd
$IFS
cmd
하나(대모두대부분의 다른 쉘에서) $IFS
비어 있으면 개행입니다.
따라서 이는 비어 있는 경우에도 여전히 문제가 됩니다 (printf 'a\nb')
.(printf 'a\nb\n')
$IFS
이 문제를 해결하기 위해 제가 생각할 수 있는 가장 좋은 방법은 다음과 같습니다.
function exact_output
set -l IFS . # non-empty IFS
set -l ret
set -l lines (
cmd
set ret $status
echo
)
set -g output ''
set -l line
test (count $lines) -le 1; or for line in $lines[1..-2]
set output $output$line\n
end
set output $output$lines[-1]
return $ret
end
버전 3.4.0(2022년 3월 출시)부터 대신 다음을 수행할 수 있습니다.
set output (cmd | string collect --allow-empty --no-trim-newlines)
이전 버전의 경우 다음을 수행할 수 있습니다.
read -z output < (begin; cmd; set ret $status; end | psub)
출력이 없으면 $output
이는 빈 요소가 있는 목록이 아니라 빈 목록입니다.
버전 3.4.0은 $(...)
또한 (...)
큰따옴표 안에 사용할 수 있다는 점을 제외하고 해당 동작에 대한 지원을 추가했습니다. 이 경우 POSIX 셸처럼 동작합니다. 출력은 한 줄로 분할되지 않지만 모든 후행 줄 바꿈은 삭제됩니다.
본 쉘
Bourne 쉘은 양식 $(...)
이나 ${var%pattern}
연산자를 지원하지 않으므로 구현하기가 어렵습니다. 한 가지 방법은 평가 및 참조를 사용하는 것입니다.
eval "
output='`
exec 4>&1
ret=\`
exec 3>&1 >&4 4>&-
(cmd 3>&-; echo \"\$?\" >&3; printf \"'\") |
awk 3>&- -v RS=\\\\' -v ORS= -v b='\\\\\\\\' '
NR > 1 {print RS b RS RS}; {print}; END {print RS}'
\`
echo \";ret=\$ret\"
`"
여기서 우리는
output='output of cmd
with the single quotes escaped as '\''
';ret=X
eval
POSIX 접근 방식의 경우 '
다른 문자의 끝에서 인코딩을 찾을 수 있는 문자 중 하나라면 문제가 발생하지만(명령 주입 취약점이 되기 때문에 더 나쁜 문제 ) 고맙게도 .
그것은 그중 하나가 아니며 인용 기술은 일반적으로 쉘 코드를 인용하는 데 사용되는 기술입니다(이에는 문제가 있으므로 사용해서는 안 됩니다( 특정 문자에 백슬래시가 필요한 경우 \
도 포함되지 않음 ). "..."
여기서는 '
) 뒤에만 사용합니다.
tcsh
바라보다tcsh는 명령 대체 `...`에서 줄바꿈을 유지합니다.
(종료 상태는 신경 쓰지 마세요. 임시 파일에 저장하면 문제를 해결할 수 있습니다( echo $status > $tempfile:q
명령 뒤))
답변2
새로운 질문의 경우 이 스크립트가 작동합니다.
#!/bin/bash
f() { for i in $(seq "$((RANDOM % 3 ))"); do
echo;
done; return $((RANDOM % 256));
}
exact_output(){ out=$( $1; ret=$?; echo x; exit "$ret" );
unset OldLC_ALL ; [ "${LC_ALL+set}" ] && OldLC_ALL=$LC_ALL
LC_ALL=C ; out=${out%x};
unset LC_ALL ; [ "${OldLC_ALL+set}" ] && LC_ALL=$OldLC_ALL
printf 'Output:%10q\nExit :%2s\n' "${out}" "$?"
}
exact_output f
echo Done
실행 시:
Output:$'\n\n\n'
Exit :25
Done
더 긴 설명
삭제를 처리하는 POSIX 쉘에 대한 일반적인 지혜는 다음 \n
과 같습니다.
하나 추가
x
s=$(printf "%s" "${1}x"); s=${s%?}
이는 마지막 새 줄(에스) 명령 확장을 통해 삭제POSIX 사양:
교체가 끝나면 하나 이상의 문자 시퀀스를 제거합니다.
뒤쫓는 것에 대해 x
.
누군가 이 질문에서 x
일부 인코딩의 특정 문자에 대한 후행 바이트와 혼동될 수 있다고 말했습니다. 그러나 특정 언어의 가능한 특정 인코딩에서 어떤 문자가 더 나은지 어떻게 추측하는가는 어려운 제안입니다.
그러나 그것은 단순히잘못된.
우리가 따라야 할 유일한 규칙은 다음을 추가하는 것입니다.정확히우리가 제거하는 콘텐츠.
기존 문자열(또는 바이트 시퀀스)에 무언가를 추가한 다음 제거하면 이해하기 쉬워야 합니다.정확히원시 문자열(또는 바이트 시퀀스)과 동일합니다.~ 해야 하다동일합니다.
우리가 어디에서 잘못됐나요? 언제 우리가혼합 수치그리고바이트.
바이트를 추가하면 바이트를 삭제해야 하고, 문자를 추가하면 삭제해야 합니다.완전 똑같은 캐릭터네.
두 번째 옵션인 문자를 추가한 다음 정확히 동일한 문자를 제거하는 것은 복잡하고 복잡할 수 있으며, 그렇습니다. 코드 페이지와 인코딩이 방해가 될 수 있습니다.
그러나 첫 번째 옵션은 매우 가능성이 높으며 설명하고 나면 매우 간단해집니다.
바이트, 즉 ASCII 바이트(<127)를 추가하고 복잡성을 최소화하기 위해 az 범위에 ASCII 문자가 있다고 가정해 보겠습니다. 또는 우리가 말해야 하는 것처럼 16진수 범위의 바이트 0x61
- 0x7a
. 그 중 x(실제로는 바이트 값) 중 하나를 선택해 보겠습니다 0x78
. x를 문자열에 연결하여 다음과 같은 바이트를 추가할 수 있습니다 é
.
$ a=é
$ b=${a}x
문자열을 바이트 시퀀스로 처리하면 다음과 같습니다.
$ printf '%s' "$b" | od -vAn -tx1c
c3 a9 78
303 251 x
x로 끝나는 문자열 시퀀스입니다.
x(바이트 값 0x78
)를 제거하면 다음과 같은 결과를 얻습니다.
$ printf '%s' "${b%x}" | od -vAn -tx1c
c3 a9
303 251
문제없이 작동합니다.
조금 더 어려운 예입니다.
관심 있는 문자열이 byte로 끝난다고 가정합니다 0xc3
.
$ a=$'\x61\x20\x74\x65\x73\x74\x20\x73\x74\x72\x69\x6e\x67\x20\xc3'
바이트 값을 추가해 봅시다0xa9
$ b=$a$'\xa9'
이제 문자열은 다음과 같습니다.
$ echo "$b"
a test string é
마지막은 내가 딱 원하는거다둘바이트는하나utf8의 문자(누구나 utf8 콘솔에서 이 결과를 재현할 수 있음)
문자를 삭제하면 원래 문자열이 변경됩니다. 그러나 그것은 우리가 추가한 것이 아니라 x로 쓰여지는 바이트 값을 추가했지만 어쨌든 바이트입니다.
바이트를 문자로 잘못 해석하는 것을 피해야 합니다. 우리에게 필요한 것은 우리가 사용하는 바이트를 삭제하는 작업입니다 0xa9
. 실제로 ash, bash, lksh 및 mksh는 모두 다음과 같은 작업을 수행하는 것 같습니다.
$ c=$'\xa9'
$ echo ${b%$c} | od -vAn -tx1c
61 20 74 65 73 74 20 73 74 72 69 6e 67 20 c3 0a
a t e s t s t r i n g 303 \n
하지만 ksh나 zsh는 아닙니다.
고치기는 쉽지만 알려드리겠습니다모두바이트 제거를 수행하는 쉘:
$ LC_ALL=C; echo ${b%$c} | od -vAn -tx1c
그게 전부입니다. 테스트된 모든 쉘은 yash(문자열의 마지막 부분)를 제외하고 작동합니다.
ash : s t r i n g 303 \n
dash : s t r i n g 303 \n
zsh/sh : s t r i n g 303 \n
b203sh : s t r i n g 303 \n
b204sh : s t r i n g 303 \n
b205sh : s t r i n g 303 \n
b30sh : s t r i n g 303 \n
b32sh : s t r i n g 303 \n
b41sh : s t r i n g 303 \n
b42sh : s t r i n g 303 \n
b43sh : s t r i n g 303 \n
b44sh : s t r i n g 303 \n
lksh : s t r i n g 303 \n
mksh : s t r i n g 303 \n
ksh93 : s t r i n g 303 \n
attsh : s t r i n g 303 \n
zsh/ksh : s t r i n g 303 \n
zsh : s t r i n g 303 \n
그것은 간단합니다 . 까지의 0x00
모든 바이트 값에서 정확히 1바이트 인 LC_ALL=C 문자를 제거하도록 쉘에 지시합니다.0xff
일부 셸은 POSIX 요구 사항에도 불구하고 런타임 시 로케일 변경을 지원하지 않습니다.
일반적으로 로케일을 변경하지 않고도 작동하는 솔루션
위 코드는 모든(개행 또는 null 제외) 바이트에 대해 센티넬 값으로 작동하지만 로케일을 변경하지 않고도 더 쉽게 만들 수 있습니다.
사용하거나 .
그래야 /
한다전반적으로 좋음, POSIX 요구 사항에 따라:
<period>
" ,<slash>
및 와<newline>
연관된 인코딩 값은<carriage-return>
구현에서 지원되는 모든 로케일에서 변경되지 않고 유지됩니다." 이는 이러한 값이 모든 로케일/인코딩에서 동일한 이진 표현을 갖음을 의미합니다.- "마찬가지로
<period>
,<slash>
, 및 를 인코딩하는 데 사용되는<newline>
바이트 값은<carriage-return>
모든 로케일에서 다른 문자의 일부로 발생해서는 안 됩니다." 이는 이러한 바이트/문자가 유효한 문자의 부분 바이트 시퀀스를 완료할 수 없기 때문에 위의 상황이 발생하지 않음을 의미합니다. 로케일/인코딩에서. (바라보다6.1 이식 가능한 문자 세트)
위 내용은 Portable Character Set의 다른 캐릭터에는 적용되지 않습니다.
댓글 솔루션:
주석에서 논의된 예의 경우 가능한 솔루션 중 하나(zsh에서는 실패함)는 다음과 같습니다.
#!/bin/bash
LC_ALL=zh_HK.big5hkscs
a=$(printf '\210\170');
b=$(printf '\170');
unset OldLC_ALL ; [ "${LC_ALL+set}" ] && OldLC_ALL=$LC_ALL
LC_ALL=C ; a=${a%"$b"};
unset LC_ALL ; [ "${OldLC_ALL+set}" ] && LC_ALL=$OldLC_ALL
printf '%s' "$a" | od -vAn -c
이렇게 하면 인코딩 문제가 제거됩니다.
답변3
일반 출력 후에 문자를 출력한 다음 제거할 수 있습니다.
#capture the output of "$@" (arguments run as a command)
#into the exact_output` variable
exact_output()
{
exact_output=$( "$@" && printf X ) &&
exact_output=${exact_output%X}
}
이는 POSIX 호환 솔루션입니다.
답변4
이것은 @Isaac이 설명한 LC_ALL=C 기술을 캡슐화하는 bash 함수입니다.
# This function provides a general solution to the problem of preserving
# trailing newlines in a command substitution.
#
# cmdsub <command goes here>
#
# If the command succeeded, the result will be found in variable CMDSUB_RESULT.
cmdsub() {
local -r BYTE=$'\x78'
local result
if result=$("$@"; ret=$?; echo "$BYTE"; exit "$ret"); then
local LC_ALL=C
CMDSUB_RESULT=${result%"$BYTE"}
else
return "$?"
fi
}
노트:
$'\x78'
더미 바이트는 이 Q&A 토론에서 논의된 특수 사례를 테스트하기 위해 선택되었지만 줄 바꿈(0x0A
) 및 NUL(0x00
)을 제외한 모든 바이트를 사용할 수 있습니다.- 이를 함수로 래핑하면 LC_ALL을 로컬 변수로 만들 수 있으므로 해당 값을 저장하고 복원할 필요가 없다는 추가 이점이 있습니다.
- 호출자가 결과를 저장해야 하는 변수의 이름을 제공할 수 있도록 bash 4.3의 nameref 기능을 사용하는 것을 고려했지만 이전 버전의 bash를 지원하는 것이 더 나을 것이라고 결정했습니다.
- 원칙적으로는 이면
LC_CTYPE
충분하지만,LC_ALL
"외부"가 이미 설정되어 있으면 전자를 덮어쓰게 됩니다.
BIG5HKSCS 엣지 케이스는 bash 4.1을 사용하여 성공적으로 테스트되었습니다.
#!/bin/bash
LC_ALL=zh_HK.big5hkscs
cmdsub() {
local -r BYTE=$'\x78'
local result
if result=$("$@"; ret=$?; echo "$BYTE"; exit "$ret"); then
local LC_ALL=C
CMDSUB_RESULT=${result%"$BYTE"}
else
return "$?"
fi
}
cmd() { echo -n $'\x88'; }
if cmdsub cmd; then
v=$CMDSUB_RESULT
printf '%s' "$v" | od -An -tx1
else
printf "The command substitution had a non-zero status code of %s\n" "$?"
fi
결과 88
는 예상대로입니다.