92

一時ファイルを使用せずに、stdoutとstderrを異なる変数に保存またはキャプチャすることは可能ですか?現在、実行時にstdoutoutとstderrを取得するためにこれを実行していますが、一時ファイルは避けたいと思います。errsome_command

error_file=$(mktemp)
out=$(some_command 2>$error_file)
err=$(< $error_file)
rm $error_file
4

18 に答える 18

28

これは、異なる変数で stdout と stderr をキャッチするためのものです。 そのままにしておくだけでキャッチしたい場合は、より良い、より短い解決策がありstderrstdoutます。

読者の利益のためにすべてを要約すると、ここに

簡単に再利用できるbashソリューション

このバージョンはサブシェルを使用し、tempfiles なしで実行されます。(tempfileサブシェルなしで実行されるバージョンについては、他の回答を参照してください。)

: catch STDOUT STDERR cmd args..
catch()
{
eval "$({
__2="$(
  { __1="$("${@:3}")"; } 2>&1;
  ret=$?;
  printf '%q=%q\n' "$1" "$__1" >&2;
  exit $ret
  )";
ret="$?";
printf '%s=%q\n' "$2" "$__2" >&2;
printf '( exit %q )' "$ret" >&2;
} 2>&1 )";
}

使用例:

dummy()
{
echo "$3" >&2
echo "$2" >&1
return "$1"
}

catch stdout stderr dummy 3 $'\ndiffcult\n data \n\n\n' $'\nother\n difficult \n  data  \n\n'

printf 'ret=%q\n' "$?"
printf 'stdout=%q\n' "$stdout"
printf 'stderr=%q\n' "$stderr"

これは印刷します

ret=3
stdout=$'\ndiffcult\n data '
stderr=$'\nother\n difficult \n  data  '

そのため、深く考えずに使用できます。catch VAR1 VAR2any の前に置くだけcommand args..で完了です。

if cmd args..; thenになるものもありますif catch VAR1 VAR2 cmd args..; then。本当に複雑なことは何もありません。

補遺:「厳格モード」で使用

catch厳密モードでも同じように機能します。唯一の注意点は、上記の例ではエラー コード 3 が返されることです。厳密モードでは、ERR トラップが呼び出されます。したがって、(0 だけでなく) 任意のエラー コードを返すことが予想されるコマンドを実行する場合は、以下に示すset -eように、リターン コードを変数にキャッチする必要があります。 && ret=$? || ret=$?

dummy()
{
echo "$3" >&2
echo "$2" >&1
return "$1"
}

catch stdout stderr dummy 3 $'\ndifficult\n data \n\n\n' $'\nother\n difficult \n  data  \n\n' && ret=$? || ret=$?

printf 'ret=%q\n' "$ret"
printf 'stdout=%q\n' "$stdout"
printf 'stderr=%q\n' "$stderr"

討論

Q: どのように機能しますか?

ここでの他の回答からのアイデアを関数にラップするだけで、簡単に再利用できます。

catch()基本的evalに、2 つの変数を設定するために使用します。これはhttps://stackoverflow.com/a/18086548に似ています

の呼び出しを考えてみましょうcatch out err dummy 1 2a 3b:

  • eval "$({と はスキップしましょう__2="$(。これについては後で説明します。

  • __1="$("$("${@:3}")"; } 2>&1;実行し、後で使用するためにdummy 1 2a 3b保存します。となります。また、外部キャッチが収集できるように、をにリダイレクトします。stdout__1__12astderrdummystdoutstdout

  • ret=$?;終了コードをキャッチします。1

  • printf '%q=%q\n' "$1" "$__1" >&2;に出力out=2astderrます。 stderrがここで使用されているのは、現在のコマンドstdoutが既にコマンドの役割を引き継いでいるためです。stderrdummy

  • exit $ret次に、終了コード ( 1) を次のステージに転送します。

外側に__2="$( ... )"

  • これは、呼び出しstdoutの である上記を変数にキャッチします。(ここで再利用することもできますが、以前は混乱を少なくするために使用していました。) そうなるstderrdummy__2__1__2__23b

  • ret="$?";(返された)リターンコード1(からdummy)を再びキャッチします

  • printf '%s=%q\n' "$2" "$__2" >&2;に出力err=3astderrます。 stderrもう一方の variable を出力するために既に使用されているため、再度使用されますout=2a

  • printf '( exit %q )' "$ret" >&2;次に、適切な戻り値を設定するコードを出力します。変数に割り当てるには変数名が必要であり、変数名は の最初または2番目の引数として使用できないため、より良い方法は見つかりませんでしたcatch

最適化として、これら 2つを"$__2" "$ret"`printfのように 1 つのものとして記述することもできます。printf '%s=%q\n( exit %q )

では、これまでのところ何がありますか?

stderr には次のように書かれています。

out=2a
err=3b
( exit 1 )

ここで、outは から$12aは から、は から、はから、は からの戻りコードからです。stdoutdummyerr$23bstderrdummy1dummy

%qの形式でprintfは、シェルが の場合に適切な (単一の) 引数を認識するように、引用に注意してくださいeval2a非常に3b単純なので、文字通りコピーされます。

外側にeval "$({ ... } 2>&1 )";

これは上記のすべてを実行し、2 つの変数と を出力し、exitそれをキャッチし (したがって2>&1)、 を使用して現在のシェルに解析しevalます。

このようにして、2 つの変数が設定され、戻りコードも同様に設定されます。

Q:evalどちらが悪いのですか?それで安全ですか?

  • バグがない限り、printf %q安全なはずです。ただし、常に非常に注意する必要があります。ShellShock について考えてみてください。

Q: バグ?

  • 以下を除いて、明らかなバグは知られていません。

    • 大きな出力をキャッチするには、すべてが変数になり、シェルによって逆解析される必要があるため、大きなメモリと CPU が必要です。だから賢く使ってください。

    • いつものように、最後のものだけでなく、$(echo $'\n\n\n\n') すべての linefeed を飲み込みます。これは POSIX 要件です。LF を無傷にする必要がある場合は、次のレシピのように、出力に末尾の文字を追加し、後で削除するだけです (拡張子を見て、末尾xが a のファイルを指すソフトリンクを読み取ることができます$'\n')。

          target="$(readlink -e "$file")x"
          target="${target%x}"
      
    • シェル変数はバイト NUL ( $'\0') を運ぶことはできません。stdoutまたはで発生した場合、単に無視されますstderr

  • 指定されたコマンドは、サブサブシェルで実行されます。したがって、にアクセスすること$PPIDも、シェル変数を変更することもできません。catchビルトインでさえ、シェル関数を使用できますが、シェル変数を変更することはできません (内部で実行されているすべてのもの$( .. )はこれを行うことができないため)。したがって、現在のシェルで関数を実行して stderr/stdout をキャッチする必要がある場合は、tempfiles を使用して通常の方法でこれを行う必要があります。(通常、シェルを中断しても残骸が残らないようにする方法はいくつかありますが、これは複雑であり、独自の回答に値します。)

Q: バッシュのバージョンは?

  • Bash 4以降が必要だと思います(理由によりprintf %q

Q: これでもまだぎこちなく見えます。

  • 右。 ここでの別の答えkshは、それをよりきれいに行う方法を示しています。しかし、私は に慣れていないkshので、同様の簡単に再利用できるksh.

Q: なぜ使わないのkshですか?

  • これがbash解決策だから

Q: スクリプトを改善することができます

  • もちろん、いくつかのバイトを絞り出し、より小さく、またはより理解できないソリューションを作成できます。ただそれのために行く;)

Q:タイプミスがあります。 : catch STDOUT STDERR cmd args..読む# catch STDOUT STDERR cmd args..

  • 実はこれは意図したものです。 コメントが静かに飲み込ま:れている間に現れます。bash -xしたがって、関数定義にタイプミスがあった場合に、パーサーがどこにあるかを確認できます。これは古いデバッグ トリックです。しかし、少し注意してください:.

;編集:からシングルライナーを作成しやすくするために、さらにいくつか追加しましたcatch()。そして、それがどのように機能するかのセクションを追加しました。

于 2016-12-09T22:08:40.443 に答える
17

技術的には、名前付きパイプは一時ファイルではなく、ここでは誰も言及していません。それらはファイルシステムに何も保存せず、それらを接続するとすぐに削除できます(したがって、それらは表示されません):

#!/bin/bash -e

foo () {
    echo stdout1
    echo stderr1 >&2
    sleep 1
    echo stdout2
    echo stderr2 >&2
}

rm -f stdout stderr
mkfifo stdout stderr
foo >stdout 2>stderr &             # blocks until reader is connected
exec {fdout}<stdout {fderr}<stderr # unblocks `foo &`
rm stdout stderr                   # filesystem objects are no longer needed

stdout=$(cat <&$fdout)
stderr=$(cat <&$fderr)

echo $stdout
echo $stderr

exec {fdout}<&- {fderr}<&- # free file descriptors, optional

この方法で複数のバックグラウンド プロセスを実行し、都合のよいときに標準出力と標準エラー出力を非同期に収集することができます。

これが 1 つのプロセスだけに必要な場合は、{fdout}/{fderr}構文 (フリー fd を見つける) の代わりに、3 や 4 などのハードコードされた fd 番号を使用することもできます。

于 2015-10-16T09:06:33.760 に答える
15

このコマンドは、現在実行中のシェルに stdout (stdval) と stderr (errval) の両方の値を設定します。

eval "$( execcommand 2> >(setval errval) > >(setval stdval); )"

この関数が定義されている場合:

function setval { printf -v "$1" "%s" "$(cat)"; declare -p "$1"; }

execcommand を「ls」、「cp」、「df」などのキャプチャされたコマンドに変更します。


これはすべて、関数 setval を使用してすべてのキャプチャ値をテキスト行に変換できるという考えに基づいており、setval を使用してこの構造体の各値をキャプチャします。

execcommand 2> CaptureErr > CaptureOut

各キャプチャ値を setval 呼び出しに変換します。

execcommand 2> >(setval errval) > >(setval stdval)

実行呼び出し内にすべてをラップし、それをエコーし​​ます。

echo "$( execcommand 2> >(setval errval) > >(setval stdval) )"

各 setval が作成する宣言呼び出しを取得します。

declare -- stdval="I'm std"
declare -- errval="I'm err"

そのコードを実行する (そして vars セットを取得する) には、eval を使用します。

eval "$( execcommand 2> >(setval errval) > >(setval stdval) )"

最後に、設定された変数をエコーし​​ます。

echo "std out is : |$stdval| std err is : |$errval|

戻り (終了) 値を含めることもできます。
完全な bash スクリプトの例は次のようになります。

#!/bin/bash --

# The only function to declare:
function setval { printf -v "$1" "%s" "$(cat)"; declare -p "$1"; }

# a dummy function with some example values:
function dummy { echo "I'm std"; echo "I'm err" >&2; return 34; }

# Running a command to capture all values
#      change execcommand to dummy or any other command to test.
eval "$( dummy 2> >(setval errval) > >(setval stdval); <<<"$?" setval retval; )"

echo "std out is : |$stdval| std err is : |$errval| return val is : |$retval|"
于 2015-03-01T16:47:40.537 に答える
14

ジョナサンは答えを持っています。参考までに、これは ksh93 のトリックです。(非古代バージョンが必要です)。

function out {
    echo stdout
    echo stderr >&2
}

x=${ { y=$(out); } 2>&1; }
typeset -p x y # Show the values

生産する

x=stderr
y=stdout

構文は、${ cmds;}サブシェルを作成しない単なるコマンド置換です。コマンドは現在のシェル環境で実行されます。先頭のスペースが重要です ({は予約語です)。

内部コマンド グループの stderr は stdout にリダイレクトされます (内部置換に適用されるように)。次に、 の stdoutoutが に割り当てられy、リダイレクトされた stderr が によって取得されます。コマンド置換のサブシェルへxの通常の損失はありません。y

出力をキャプチャするすべての構成では、プロデューサーをサブシェルに配置する必要があるため、他のシェルでは不可能です。この場合、サブシェルには割り当てが含まれます。

更新: mksh でもサポートされるようになりました。

于 2012-06-23T20:09:38.933 に答える
4

evalが気に入らなかったので、リダイレクトのトリックを使用してプログラム出力を変数にキャプチャし、その変数を解析してさまざまなコンポーネントを抽出するソリューションを次に示します。-w フラグはチャンク サイズを設定し、中間形式の std-out/err メッセージの順序に影響を与えます。1 は、オーバーヘッドを犠牲にして潜在的に高い解像度を提供します。

#######                                                                                                                                                                                                                          
# runs "$@" and outputs both stdout and stderr on stdin, both in a prefixed format allowing both std in and out to be separately stored in variables later.                                                                  
# limitations: Bash does not allow null to be returned from subshells, limiting the usefullness of applying this function to commands with null in the output.                                                                   
# example:                                                                                                                                                                                                                       
#  var=$(keepBoth ls . notHere)                                                                                                                                                                                                  
#  echo ls had the exit code "$(extractOne r "$var")"                                                                                                                                                                            
#  echo ls had the stdErr of "$(extractOne e "$var")"                                                                                                                                                                            
#  echo ls had the stdOut of "$(extractOne o "$var")"                                                                                                                                                                            
keepBoth() {                                                                                                                                                                                                                     
  (                                                                                                                                                                                                                              
    prefix(){                                                                                                                                                                                                                    
      ( set -o pipefail                                                                                                                                                                                                          
        base64 -w 1 - | (                                                                                                                                                                                                                                                                                                                                                                                                                                                                              
          while read c                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  
          do echo -E "$1" "$c"                                                                                                                                                                                                                                                                                                                                                                                                                                                                          
          done                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          
        )                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               
      )                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 
    }                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   
    ( (                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 
        "$@" | prefix o >&3                                                                                                                                                                                                                                                                                                                                                                                                                                                                             
        echo  ${PIPESTATUS[0]} | prefix r >&3                                                                                                                                                                                                                                                                                                                                                                                                                                                           
      ) 2>&1 | prefix e >&1                                                                                                                                                                                                                                                                                                                                                                                                                                                                             
    ) 3>&1                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              
  )                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     
}                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       

extractOne() { # extract                                                                                                                                                                                                                                                                                                                                                                                                                                                                                
  echo "$2" | grep "^$1" | cut --delimiter=' ' --fields=2 | base64 --decode -                                                                                                                                                                                                                                                                                                                                                                                                                           
}                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       
于 2018-01-08T09:23:25.413 に答える
3

これは、@madmurphy の非常に優れたソリューションがどのように機能するかを示す図です。

@madmurhy のソリューションの図

そして、ワンライナーのインデントされたバージョン:

catch() {
  {
      IFS=$'\n' read -r -d '' "$out_var";
      IFS=$'\n' read -r -d '' "$err_var";
      (IFS=$'\n' read -r -d '' _ERRNO_; return ${_ERRNO_});
  }\
  < <(
    (printf '\0%s\0%d\0' \
      "$(
        (
          (
            (
              { ${3}; echo "${?}" 1>&3-; } | tr -d '\0' 1>&4-
            ) 4>&2- 2>&1- | tr -d '\0' 1>&4-
          ) 3>&1- | exit "$(cat)"
        ) 4>&1-
      )" "${?}" 1>&2
    ) 2>&1
  )
}
于 2022-01-17T02:36:10.130 に答える
2

どうでしょう... =D

GET_STDERR=""
GET_STDOUT=""
get_stderr_stdout() {
    GET_STDERR=""
    GET_STDOUT=""
    unset t_std t_err
    eval "$( (eval $1) 2> >(t_err=$(cat); typeset -p t_err) > >(t_std=$(cat); typeset -p t_std) )"
    GET_STDERR=$t_err
    GET_STDOUT=$t_std
}

get_stderr_stdout "command"
echo "$GET_STDERR"
echo "$GET_STDOUT"
于 2015-12-10T17:04:52.260 に答える
2

ハックですが、このページのいくつかの提案よりもおそらく直感的な回避策の 1 つは、出力ストリームにタグを付け、それらをマージし、後でタグに基づいて分割することです。たとえば、stdout に「STDOUT」プレフィックスを付けてタグ付けする場合があります。

function someCmd {
    echo "I am stdout"
    echo "I am stderr" 1>&2
}

ALL=$({ someCmd | sed -e 's/^/STDOUT/g'; } 2>&1)
OUT=$(echo "$ALL" | grep    "^STDOUT" | sed -e 's/^STDOUT//g')
ERR=$(echo "$ALL" | grep -v "^STDOUT")

```

stdout や stderr が制限された形式であることがわかっている場合は、許可されたコンテンツと競合しないタグを考え出すことができます。

于 2016-06-02T21:19:54.853 に答える
2

端的に言えば、答えは「いいえ」だと思います。キャプチャ$( ... )は、変数への標準出力のみをキャプチャします。標準エラーを別の変数に取り込む方法はありません。だから、あなたが持っているものは、それが得られるのと同じくらいきれいです。

于 2012-06-14T06:33:42.503 に答える
1

警告: (まだ?) 動作していません!

以下は、一時ファイルを作成せずに、POSIX sh のみで動作させる可能性のあるリードのようです。ただし、base64が必要であり、エンコード/デコードが効率的でなく、「より大きな」メモリも使用する可能性があるためです。

  • 単純なケースでも、最後の stderr 行に改行がない場合は、すでに失敗しています。これは、exe を "{ exe ; echo >&2 ; }" に置き換える、つまり改行を追加することで、少なくとも場合によっては修正できます。
  • ただし、主な問題は、すべてが際どいように見えることです。次のようなexeを使用してみてください:

    exe() { cat /usr/share/hunspell/de_DE.dic cat /usr/share/hunspell/en_GB.dic >&2 }

たとえば、base64 でエンコードされた行の一部がファイルの先頭にあり、一部が末尾にあり、デコードされていない stderr が中央にあることがわかります。

たとえ以下のアイデアが機能しないとしても (私はそう思います)、このように機能させることができると誤って信じている人々の反例として役立つかもしれません.

アイデア(または反例):

#!/bin/sh

exe()
{
        echo out1
        echo err1 >&2
        echo out2
        echo out3
        echo err2 >&2
        echo out4
        echo err3 >&2
        echo -n err4 >&2
}


r="$(  { exe  |  base64 -w 0 ; }  2>&1 )"

echo RAW
printf '%s' "$r"
echo RAW

o="$( printf '%s' "$r" | tail -n 1 | base64 -d )"
e="$( printf '%s' "$r" | head -n -1  )"
unset r    

echo
echo OUT
printf '%s' "$o"
echo OUT
echo
echo ERR
printf '%s' "$e"
echo ERR

与えます(stderr-newline修正あり):

$ ./ggg 
RAW
err1
err2
err3
err4

b3V0MQpvdXQyCm91dDMKb3V0NAo=RAW

OUT
out1
out2
out3
out4OUT

ERR
err1
err2
err3
err4ERR

(少なくとも Debian のダッシュとバッシュでは)

于 2016-07-27T18:13:42.713 に答える
0

コマンドが 1) ステートフルな副作用がなく、2) 計算コストが低い場合、最も簡単な解決策はコマンドを 2 回実行することです。私はこれを主に、ディスクが動作するかどうかまだわからないブート シーケンス中に実行されるコードに使用しました。私の場合は小さかっsome_commandたので、2 回実行してもパフォーマンスに影響はなく、コマンドに副作用はありませんでした。

主な利点は、これがクリーンで読みやすいことです。ここでの解決策は非常に巧妙ですが、より複雑な解決策を含むスクリプトを維持しなければならないのは私ではありません。シナリオがそれで機能する場合は、シンプルで 2 回実行するアプローチをお勧めします。これは、はるかにクリーンで維持しやすいためです。

例:

output=$(getopt -o '' -l test: -- "$@")
errout=$(getopt -o '' -l test: -- "$@" 2>&1 >/dev/null)
if [[ -n "$errout" ]]; then
        echo "Option Error: $errout"
fi

繰り返しますが、getopt には副作用がないため、これは問題ありません。親コードがプログラム全体でこれを呼び出す回数は 100 回未満であり、ユーザーは 100 回の getopt 呼び出しと 200 回の getopt 呼び出しに気付かないため、パフォーマンスが安全であることはわかっています。

于 2014-06-18T21:53:16.613 に答える