追いかけることについてx。

追いかけることについてx。

コマンド置換の正確な出力をキャプチャできるようにしたいです。末尾の改行を含む

デフォルトでは削除されているので、これを保存するには少しの操作が必要になることがあります。元の終了コードを維持したいと思います。

たとえば、さまざまな数の末尾の改行と終了コードを含むコマンドがあります。

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です(xASCIIの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)。また、元の値を復元する必要があります(たとえば、localePOSIXと互換性のない値が関数に使用されます)。ただし、一部のシェルはPOSIX要件にもかかわらず、実行時のロケール変更をサポートしていません。

.またはを使用すると、/これらすべてを回避できます。

bash/zshの代替

出力に NUL がないとし、bashand を使用すると、次のこともできます。zsh

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

終了ステータスを取得するには、cmd一部のバージョンでは実行できますが、実行できませんが、で作成して終了ステータスを取得できます。wait "$!"; ret=$?bashzshzshcmd | 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)

$varif 出力のすべての行を含む配列を作成するか、最も多くの行を削除します。cmd$IFScmd一つ(大みんな他のほとんどのシェルから)$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シェルのように動作します。出力は1行に分割されませんが、すべての末尾の改行は削除されます。

ボンシェル

Bourneシェルはフォーム$(...)${var%pattern}演算子をサポートしていないため、実装するのは難しいです。 1つの方法は、評価と参照を使用することです。

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

evalPOSIXアプローチの場合、他の文字の終わりでエンコーディングを見つけることができる文字の1つは'問題が発生します(コマンド注入の脆弱性になるため、より悪い問題).使用される技術(これには問題があるため使用しないでください(特定の文字にバックスラッシュが必要な場合も含まれませ\ん)。"..."ここでは')の後にのみ使用してください。

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のとおりです。

1つ追加x

s=$(printf "%s" "${1}x"); s=${s%?}

これは最後の新しい行です(S) コマンド拡張で削除POSIX仕様:

交換が完了したら、1つ以上の文字シーケンスを削除します。


追いかけることについてx

誰かがこの質問で、xいくつかのエンコーディングの特定の文字の末尾のバイトと混同される可能性があると言いました。しかし、特定の言語の可能な特定のエンコーディングでどの文字がより良いかを推測するのは難しい提案です。

しかし、それは単に間違った

私たちが従うべき唯一のルールは、以下を追加することです。正確に私たちが削除するコンテンツ。

既存の文字列(またはバイトシーケンス)に何かを追加してから削除すると、理解しやすくなります。正確に生の文字列(またはバイトシーケンス)と同じです。〜しなければならない同じです。

私たちはどこで間違っていますか?いつ私たちがミックス 数値そしてバイト

バイトを追加するとバイトを削除し、文字を追加すると削除する必要があります。全く同じキャラクター

2番目のオプションである文字を追加してから、まったく同じ文字を削除するのは複雑で複雑な場合があります。コードページとエンコードが邪魔になることがあります。

しかし、最初のオプションは非常に可能性が高く、説明すると非常に簡単になります。

バイト、つまりASCIIバイト(<127)を追加し、複雑さを最小限に抑えるために、azの範囲にASCII文字があるとします。または私たちが言う必要があるように、16進範囲のバイト0x61- 0x7a。そのうちのx(実際にはバイト値)の1つを選択してみましょう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の他のキャラクターには適用されません。

コメントソリューション:

コメントで説明されている例では、可能な解決策の1つ(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は予想通りです。

関連情報