シェル:コマンド置換で末尾の改行( '\ 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あるため、引数としてキャプチャされません。
デスグリップ

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実際に含まれていますū

.代わりにを使用すると.、エンコードがのエンコードと同じエンコードで終わる文字がある場合、理論的には同じ問題が発生する可能性がありますが、しばらく前に確認したため、ロケールで使用できる文字セットはどれもありませんDebian、FreeBSD、またはSolarisシステムには、私にとって十分な文字があります(そして、.これが文の終わりを英語でマークする記号でもあるので適切だと思います)。

@Arrowで説明されているより適切なアプローチは、最後の文字(${output%.})を削除するためだけにロケールをCに変更することです。これにより、1バイトのみが削除されますが、コードが大幅に複雑になり、それ自身。

bash / zshの代替

bashzsh、出力は何NULsを持っていないと仮定すると、あなたも行うことができます。

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までストリッピング1(とは対照的に、すべての場合に、他のほとんどのシェルで)改行文字$IFS空です。

そのため、そこにはまだ問題が(printf 'a\nb')あり(printf 'a\nb\n')、emptyであっても同じものに拡張されます$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}演算子もサポートしていなかったので、そこで達成するのは非常に困難です。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

に渡されるeval。POSIXアプローチについては'、エンコードが他の文字の末尾にある文字の1つである場合、問題が発生します(コマンドインジェクションの脆弱性になるため、さらに悪い問題)。しかし、ありがたいことに.、それはそれらの1つではなく、その引用技術は一般的にシェルコードを引用するもので使用されるものです(\問題があることに注意してください、したがって使用しないでください("..."一部の文字にバックスラッシュを使用する必要がある内部も除外します))ここ'では、OKの後にのみ使用しています)。

tcsh

コマンド置換 `...`でtcshが改行を保持するを参照してください

(終了ステータスは考慮せず、一時ファイルに保存することで対処できます(echo $status > $tempfile:qコマンドの後))


おかげで-特に異なる文字セットの手がかりを。変数にzsh保存できる場合NUL、なぜ機能しないのIFS= read -rd '' output < <(cmd)ですか?文字列の長さを保存できる必要があります... 0 ''バイトの文字列で\0はなく、1バイトの文字列としてエンコードしますか?
トム・ヘイル

1
@TomHale、はい、read -d ''として扱われますread -d $'\0'(どこにでも同じbashこと$'\0'がありますが'')。
ステファンシャゼラス

文字とバイトを統合しています。追加されたものを正確に削除する場合、元のエンティティは変更しないでください。それが追加されたものである場合に呼び出される1バイトを削除するのはそれほど難しくありませんx。編集した回答をご覧ください。
矢印

@Arrow、はいvar=value command evalトリックが議論された、ここで)と前オースティン・グループのメーリングリストに。あなたはそれがポータブルではないことに気付くでしょう(そして、あなたがa=1 command eval 'unset a; a=2'そのようなことをしようとしているとき、それがそのように使用されることを意図していなかったことは非常に明白です)。最初に設定が解除されたsavedVAR=$VAR;...;VAR=$savedVARとき$VARに必要なことを実行しない場合も同じです。それが理論上の問題(実際にはヒットしないバグ)のみを回避する場合、IMO、それは面倒の価値はありません。それでも、私は試してみてあなたをサポートします。
ステファンシャゼラス

LANG=C文字列からバイトを削除するための使用を破棄し、最終的に破棄した場所へのリンクがありますか?あなたは本当のポイントを中心に懸念を提起している、すべてが簡単に解決できます。(1)未設定は使用されていません(2)変数を変更する前にテストします。@StéphaneChazelas–
アロー

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)が削除されるため、これが必要です。

置換の最後で1つ以上の文字のシーケンスを削除します。


末尾についてx

この質問ではx、あるエンコーディングのある文字の後続バイトと混同される可能性があると言われています。しかし、いくつかの可能なエンコーディングでは、どの言語でどの文字が優れているのか、どの文字を推測するのか、それは控えめに言っても難しい命題です。

しかしながら; それは単に間違っています。

従う必要がある唯一のルールは、削除するものを正確に追加することです。

既存の文字列(またはバイトシーケンス)に何かを追加し、後でまったく同じものを削除する場合、元の文字列(またはバイトシーケンス)は同じでなければならないことを理解するのは簡単です。

どこがおかしいのでしょうか?文字バイト混ぜる とき。

バイトを追加する場合、バイトを削除する必要があります。文字を追加する場合、まったく同じ文字を削除する必要があります。

2番目のオプションである文字の追加(および後でまったく同じ文字の削除)は複雑で複雑になる可能性があり、はい、コードページとエンコードが邪魔になる場合があります。

ただし、最初のオプションは非常に可能性が高いため、説明した後は単純になります。

バイト、ASCIIバイト(<127)を追加し、物事を可能な限り複雑にしないように、a〜zの範囲のASCII文字を使用します。または、言っておくべきように、16進数の範囲のバイト0x61- 0x7aです。それらのいずれか、多分x(実際にはvalueのバイト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 é

まさに私が望んだもので、最後の2バイトはutf8の1文字です(だから誰でも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文字を削除するようシェルに指示します。これは、0x00to からのすべてのバイト値に対して正確に1バイトです0xff

コメントの解決策:

コメントで説明した例の場合、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

これにより、エンコードの問題が解消されます。


複数の末尾の改行が削除される可能性があることを知っておくと便利です。
トム・ヘイル


${var%?}常に1バイトを削除するようにロケールをCに修正することは、理論上はより正確ですが、1- LC_ALLおよびLC_CTYPEoverride $LANGなので、LC_ALL=C2 を設定する必要があります。var=${var%?}変更のようにサブシェルで実行することはできません。失われるため、値と状態を保存および復元する必要がありますLC_ALL(または非POSIX localスコープ機能に頼る)3-スクリプトの途中でロケールを変更することは、yashなどの一部のシェルでは完全にサポートされていません。一方、実際に.は、実際の文字セットでは問題になることはないため、これを使用するとLC_ALLとの混合が回避されます。
ステファンシャゼル

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準拠のソリューションです。


回答に基づいて、私の質問は不明瞭でした。更新しました。
トム・ヘイル
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.