셸 : 명령 대체에서 줄 바꿈 ( '\ n')을 유지합니다.


14

후행 줄 바꿈 문자를 포함하여 명령 대체의 정확한 출력을 캡처하고 싶습니다 .

기본적으로 제거되어 있으므로 유지하기 위해 약간의 조작이 필요할 수 있으며 원래 종료 코드를 유지하고 싶습니다 .

예를 들어, 가변 개수의 후행 줄 바꿈 및 종료 코드가있는 명령이 제공된 경우 :

f(){ for i in $(seq "$((RANDOM % 3))"); do echo; done; return $((RANDOM % 256));}
export -f f

나는 다음과 같은 것을 실행하고 싶다 :

exact_output f

출력은 다음과 같습니다.

Output: $'\n\n'
Exit: 5

bashPOSIX와 둘 다에 관심이 있습니다 sh.


1
개행은의 일부 $IFS이므로 인수로 캡처되지 않습니다.
Deathgrip

4
그것은과는 아무 상관이 없습니다 @Deathgrip IFS(시도 ( IFS=:; subst=$(printf 'x\n\n\n'); printf '%s' "$subst" ). 만 줄 바꿈이 제거 얻을. \t그리고``하지 않으며, IFS그 영향을주지 않습니다.
PSkocik



답변:


17

POSIX 쉘

명령의 완전한 표준을 얻는 일반적인 ( 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ) 트릭은

output=$(cmd; ret=$?; echo .; exit "$ret")
ret=$?
output=${output%.}

아이디어는 추가하고 추가하는 것 .\n입니다. 명령 대체는 해당 부분 만 제거 합니다 \n . 그리고 당신은 스트립 .과 함께 ${output%.}.

이외의 셸 zsh에서는 출력에 NUL 바이트가 있으면 여전히 작동하지 않습니다. 을 사용 yash하면 출력이 텍스트가 아닌 경우 작동하지 않습니다.

또한 일부 로케일에서는 끝에 삽입하는 데 사용하는 문자가 중요합니다. .일반적으로 괜찮지 만 일부는 그렇지 않을 수 있습니다. 예를 들어 x(다른 답변에 사용됨) @BIG5, GB18030 또는 BIG5HKSCS 문자 세트를 사용하는 로케일에서는 작동하지 않습니다. 이러한 문자 집합에서 여러 문자 의 인코딩은 또는 (0x78, 0x40) 의 인코딩과 동일한 바이트로 끝납니다 .x@

예를 들어, ūBIG5HKSCS에 0x88의 경우 0x78을 (그리고 xASCII처럼은 0x78이며, 시스템의 모든 캐릭터 세트는 영어 문자를 포함, 휴대용 문자 집합의 모든 문자에 대해 동일한 인코딩해야합니다 @.). 경우에 따라서 cmd이었다 printf '\x88'우리가 삽입 된 x뒤에, ${output%x}것을 제거하기 위해 실패 x로이 $output실제로 포함됩니다 ū.

사용하여 .그와 같은 인코딩의 끝을 인코딩 모든 문자가 있다면 이론적으로 같은 문제가 발생할 수 대신 .하지만, 얼마 전에 확인하는 데, 나는 로케일에서 사용하기 위해 사용할 수있다 캐릭터 세트의 어느 것도 말할 수 없다 데비안, FreeBSD 또는 Solaris 시스템은 나에게 충분한 문자를 가지고 있습니다 (그리고 내가 .영어로 문장의 끝을 나타내는 기호이기도 한 이유는 무엇입니까? ).

@Arrow 에서 논의 된 보다 올바른 접근법 은 마지막 문자 ( ${output%.})를 제거하기 위해 로케일을 C로 변경하는 것입니다.이 문자 는 1 바이트 만 제거되도록하지만 코드를 크게 복잡하게 만들고 잠재적으로 호환성 문제를 일으킬 수 있습니다 자체.

bash / zsh 대안

bash하고 zsh, 출력이 더 NUL을이없는 가정, 당신은 할 수 있습니다 :

IFS= read -rd '' output < <(cmd)

의 종료 상태를 얻으려면에서 cmd할 수 있지만 wait "$!"; ret=$?에서 할 수는 bash없습니다 zsh.

rc / es / akanaga

완벽을 기하기 위해 rc/ es/ akanga연산자가 있습니다. 여기에서 `cmd(또는 `{cmd}더 복잡한 명령으로) 표현되는 명령 대체 는 목록을 리턴합니다 ( $ifs기본적으로 space-tab-newline을 분할하여 ). Bourne과 같은 쉘과 달리 이러한 쉘에서 줄 바꿈 제거는 해당 $ifs분할의 일부로 만 수행됩니다 . 따라서 구분 기호를 지정하는 양식을 비우 $ifs거나 사용할 수 있습니다 ``(seps){cmd}.

ifs = ''; output = `cmd

또는:

output = ``()cmd

어쨌든 명령의 종료 상태가 유실됩니다. 출력에 포함시키고 나중에 추출하여 추악하게 만들어야합니다.

물고기

물고기에서 명령 대체는 (cmd)서브 쉘을 포함하며 서브 쉘을 포함하지 않습니다.

set var (cmd)

작성 $var의 출력의 모든 라인 배열을 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

대안은 다음과 같습니다.

read -z output < (begin; cmd; set ret $status; end | psub)

본 쉘

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 접근 방식의 경우 '다른 문자의 끝에서 인코딩을 찾을 수있는 문자 중 하나 인 경우 문제가 발생하지만 (명령 삽입 취약점이되기 때문에 훨씬 더 나쁩니다) 감사합니다 .. 그중 하나가 아니며 인용 기술은 일반적으로 쉘 코드를 인용하는 모든 것에 의해 사용되는 기술입니다 ( \문제가 있으므로 사용해서는 안됩니다 ( "..."일부 문자에 백 슬래시를 사용해야하는 내부 제외 )) 여기서는 'OK 후에 만 사용합니다 ).

tcsh

명령 치환에서 tcsh가 개행을 유지하는 것을 보자 `...`

(종료 파일을 임시 파일에 저장하여 처리 할 수있는 종료 상태를 처리하지 않음 ( echo $status > $tempfile:q명령 후))


고마워-특히 다른 캐릭터 세트에 대한 단서. 변수에 zsh저장할 수 있다면 NULIFS= read -rd '' output < <(cmd)작동 하지 않습니까? 문자열 길이를 저장할 수 있어야합니다 ... 0 ''바이트 문자열이 \0아닌 1 바이트 문자열로 인코딩 합니까?
Tom Hale

1
예, @TomHale read -d ''read -d $'\0'( 모든 곳 과 동일 bash하지만)로 취급됩니다 . $'\0'''
Stéphane Chazelas

문자와 바이트를 혼동하고 있습니다. 추가 된 내용을 정확하게 제거하면 원래 엔터티가 변경되지 않아야합니다. 그것이 추가 된 경우 호출 된 1 바이트 를 제거하는 것은 어렵지 않습니다 x. 편집 한 답변을 확인하십시오.
Isaac Isaac

@Arrow, 그렇습니다. var=value command eval트릭은 여기 ( 또한 )와 전에 Austin 그룹 메일 링리스트 에서 논의 되었습니다 . 이식성이 없다는 것을 알게 될 것입니다 (그리고 그렇게 사용하려고 a=1 command eval 'unset a; a=2'하지 않았거나 더 나쁜 것을 시도 할 때 분명 합니다). 처음 설정이 해제 savedVAR=$VAR;...;VAR=$savedVAR되었을 때 원하는 것을하지 않는 것과 동일합니다 $VAR. 그것이 이론적 인 문제 (실제로 맞을 수없는 버그) 만 해결해야한다면 IMO는 귀찮은 가치가 없습니다. 그래도 시도해 드리겠습니다.
Stéphane Chazelas

LANG=C문자열에서 바이트를 제거하는 사용을 토론하고 마지막으로 버린 링크가 있습니까? 당신은 실제 요점 주위에 우려를 제기하고 있으며, 모두 쉽게 해결할 수 있습니다. (1) 설정하지 않은 변수가 없습니다. (2) 변수를 변경하기 전에 테스트하십시오. @ StéphaneChazelas
Isaac

3

새로운 질문의 경우이 스크립트는 다음과 같이 작동합니다.

#!/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 사양 에 따라 명령 확장으로 마지막 줄 바꿈 ( S )이 제거 되므로 필요합니다 .

치환이 끝날 때 하나 이상의 문자 시퀀스를 제거합니다.


후행에 대해 x.

이 질문 x에서는 인코딩에서 일부 문자의 후행 바이트와 혼동 될 수 있다고합니다. 그러나 가능한 어떤 인코딩으로 어떤 언어에서 어떤 문자가 더 나은지 또는 어떤 문자가 더 좋을지 추측하는 방법은 무엇입니까?

하나; 그것은 단순히 틀렸다 .

우리가 따라야 할 유일한 규칙은 우리 가 제거하는 것을 정확하게 추가 하는 것입니다.

기존 문자열 (또는 바이트 시퀀스)에 무언가를 추가하고 나중에 정확히 동일한 것을 제거 하면 원래 문자열 (또는 바이트 시퀀스)이 같아야한다는 것을 쉽게 이해할 수 있습니다.

우리는 어디로 잘못 가나 요? 문자바이트를 섞을 때 .

바이트를 추가하면 바이트를 제거해야하며, 문자를 추가 하면 정확히 동일한 문자를 제거해야합니다 .

두 번째 옵션 인 문자 추가 (나중에 정확히 동일한 문자 제거)는 복잡하고 복잡 할 수 있으며, 예를 들어 코드 페이지와 인코딩이 방해를받을 수 있습니다.

그러나 첫 번째 옵션은 가능하며 설명 후에는 단순 해집니다.

ASCII 바이트 (<127) 인 바이트를 추가하고 가능한 적은 복잡성을 유지하기 위해 az 범위의 ASCII 문자를 가정 해 보겠습니다. 또는 우리가 말했듯이 16 진수 범위의 바이트 0x61- 0x7a. x (실제로 byte 값 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

문제없이 작동합니다.

좀 더 어려운 예입니다.

우리가 관심있는 문자열이 바이트로 끝났다고 가정 해 봅시다 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

그냥 간단, 모든 바이트 값을 정확히 한 바이트 인 LC_ALL = C 문자, 제거 쉘에게 0x00로가 0xff.

의견에 대한 해결책 :

주석에서 논의 된 예의 경우, 가능한 한 가지 솔루션 (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

인코딩 문제를 제거합니다.


하나 이상의 후행 줄 바꿈이 제거 될 수 있음을 아는 것이 좋습니다.
Tom Hale


${var%?}이론 상으로는 항상 1 바이트를 제거 하기 위해 로케일을 C로 고정하는 것이 동의 하지만 1- LC_ALLLC_CTYPEoverride $LANG이므로 LC_ALL=C2 를 설정해야 var=${var%?}합니다. 변경 사항과 같이 서브 쉘에서 수행 할 수 없습니다 잃어 버렸으므로 값을 저장하고 복원해야합니다 LC_ALL(또는 비 POSIX local범위 기능에 의존 ). 3 스크립트를 통해 도중에 로케일을 변경하는 것은 yash와 같은 일부 쉘에서 완전히 지원되지 않습니다. 다른 한편으로, 실제로 .는 실제 문자셋에서 문제가되지 않기 때문에 LC_ALL과의 혼합을 피할 수 있습니다.
Stéphane Chazelas

2

일반 출력 후에 문자를 출력 한 다음 제거 할 수 있습니다.

#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 호환 솔루션입니다.


답변을 바탕으로 내 질문이 명확하지 않은 것으로 보입니다. 방금 업데이트했습니다.
Tom Hale
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.