web-dev-qa-db-ja.com

stdoutとstderrを異なる変数にキャプチャします

一時ファイルを使用せずに、stdoutとstderrを異なる変数に保存またはキャプチャできますか?現時点では、some_commandの実行時にoutでstdoutを、errでstderrを取得するためにこれを行いますが、一時ファイルは避けたいと思います。

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

OK、少し、いですが、ここに解決策があります:

unset t_std t_err
eval "$( (echo std; echo err >&2) \
        2> >(readarray -t t_err; typeset -p t_err) \
         > >(readarray -t t_std; typeset -p t_std) )"

(echo std; echo err >&2)は実際のコマンドに置き換える必要があります。 stdoutの出力は、改行($t_std)とstderr-tに省略する行ごとに、配列$t_errに保存されます。

配列が気に入らない場合はできます

unset t_std t_err
eval "$( (echo std; echo err >&2 ) \
        2> >(t_err=$(cat); typeset -p t_err) \
         > >(t_std=$(cat); typeset -p t_std) )"

これは、var=$(cmd)の動作をほとんど模倣しますが、$?の値は例外で、最後の変更が行われます。

unset t_std t_err t_ret
eval "$( (echo std; echo err >&2; exit 2 ) \
        2> >(t_err=$(cat); typeset -p t_err) \
         > >(t_std=$(cat); typeset -p t_std); t_ret=$?; typeset -p t_ret )"

ここで$?$t_retに保存されます

[〜#〜] gnu [〜#〜]bash、バージョン4.2.37(1)-を使用してDebian wheezyでテスト済みリリース(i486-pc-linux-gnu)

36
TheConstructor

ジョナサンには 答え があります。参考までに、これは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にリダイレクトされます(内部置換に適用されるように)。次に、outのstdoutがyに割り当てられ、リダイレクトされたstderrがxによってキャプチャされます。コマンド置換のyは通常失われません。サブシェル。

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

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

15
ormaaj

このコマンドは、現在実行中のシェルに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"

そのコードを実行(および変数セットを取得)するには、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|"
14
user2350426

これは、異なる変数でstdoutとstderrをキャッチするためのものです。 stderrをキャッチするだけで、stdoutをそのままにしたい場合は、より適切で短いソリューションがあります

sum すべて p 読者のために、ここに

簡単に再利用可能bashソリューション

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

: 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 VAR2command args..の前に置くだけで完了です。

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

討論

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

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

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

catch out err dummy 1 2a 3bの呼び出しを検討してください。

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

  • __1="$("$("${@:3}")"; } 2>&1;dummy 1 2 3を実行し、後で使用するためにstdout__1に保存します。したがって、__12aになります。また、stderr of dummystdoutにリダイレクトし、外側のキャッチがstdoutを収集できるようにします。

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

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

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

次に、外側の__2="$( ... )"

  • これは、上記のstdoutをキャッチして、stderr呼び出しのdummyを変数__2に取り込みます。 (ここで__1を再利用できますが、混乱を少なくするために__2を使用しました。) __23bになります

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

  • printf '%s=%q\n' "$2" "$__2" >&2;は、err=3astderrに出力します。 stderrは、他の変数out=2aの出力にすでに使用されているため、再び使用されます。

  • printf '( exit %q )' "$ret" >&2; then outputs the code to set the proper return value. I did not find a better way, as assignig it to a variable needs a variable name, which then cannot be used as first oder second argument to catch`。

最適化として、これら2つのprintfprintf '%s=%q\n( exit %q ) "$ __ 2" "$ ret" `のような単一のものとして書くこともできます。

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

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

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

ここで、out$1から、2astdout of dummyから、err$2から、3bstderr of dummyから、そして1は、dummyからの戻りコードからのものです。

printfの形式の%qは、evalになるとシェルが適切な(単一の)引数を認識するように、引用を処理します。 2a3bは非常に単純なので、文字通りコピーされます。

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

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

このようにして、2つの変数が設定され、リターンコードも取得されます。

Q:evalを使用していますが、これは悪です。それで安全ですか?

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

Q:バグ?

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

    • すべてが変数になり、シェルによって逆解析される必要があるため、大きな出力をキャッチするには大きなメモリとCPUが必要です。賢明に使用してください。
    • 通常どおり、$(echo $'\n\n\n\n')は、最後の改行だけでなく、すべての改行を飲み込みます。これはPOSIXの要件です。 LFを無害にする必要がある場合は、次のレシピのように、出力に末尾の文字を追加して削除します(末尾のxを見ると、$'\n'で終わるファイルを指すソフトリンクを読み取ることができます):

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

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

Q:Bashバージョン?

  • 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に表示され、コメントは黙って飲み込まれます。そのため、関数定義にタイプミスがある場合、パーサーの場所を確認できます。これは古いデバッグのトリックです。ただし、少し注意してください。:の引数内に、きちんとしたsideffectsを簡単に作成できます。

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

12
Tino

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

#!/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番号を使用することもできます。

9
Irfy

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 -                                                                                                                                                                                                                                                                                                                                                                                                                           
}                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       
3
mncl

簡潔に言うと、答えは「いいえ」だと思います。キャプチャ$( ... )は、変数への標準出力のみをキャプチャします。標準エラーを別の変数にキャプチャする方法はありません。だから、あなたが持っているのはそれが得るほどきれいです。

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"
2
Eduardo Lucio

読者の利益のために、tempfilesを使用したソリューションがあります。

問題はtempfilesを使用しないことでした。ただし、これは、シェルが停止した場合のtempfileによる_/tmp/_の不要な汚染による可能性があります。 _kill -9_の場合、いくつかの_trap 'rm "$tmpfile1" "$tmpfile2"' 0_は起動しません。

tempfileを使用できるが、残骸を残さないできる状況にある場合behind、これがレシピです。

再び、それはcatch()と呼ばれ(私の その他の答え として)、同じ呼び出し構文を持ちます:

_catch stdout stderr command args.._

_# Wrappers to avoid polluting the current Shell's environment with variables

: catch_read returncode FD variable
catch_read()
{
eval "$3=\"\`cat <&$2\`\"";
# You can use read instead to skip some fork()s.
# However read stops at the first NUL byte,
# also does no \n removal and needs bash 3 or above:
#IFS='' read -ru$2 -d '' "$3";
return $1;
}
: catch_1 tempfile variable comand args..
catch_1()
{
{
rm -f "$1";
"${@:3}" 66<&-;
catch_read $? 66 "$2";
} 2>&1 >"$1" 66<"$1";
}

: catch stdout stderr command args..
catch()
{
catch_1 "`tempfile`" "${2:-stderr}" catch_1 "`tempfile`" "${1:-stdout}" "${@:3}";
}
_

それが何をする:

  • tempfilestdoutに対して2つのstderrを作成します。ただし、これらはすぐに削除されるため、非常に短時間しか存在しません。

  • catch_1()は、stdout(FD 1)を変数にキャッチし、stderrstdoutに移動して、次(「左」)_catch_1_はそれをキャッチできます。

  • catchの処理は右から左に行われるため、左の_catch_1_が最後に実行され、stderrをキャッチします。

起こり得る最悪の事態は、いくつかの一時ファイルが_/tmp/_に表示されることですが、その場合は常に空です。 (それらは満たされる前に削除されます。)。通常、LinuxではtmpfsがメインメモリのGBあたり約128Kのファイルをサポートするため、これは問題になりません。

  • 指定されたコマンドは、すべてのローカルシェル変数にアクセスして変更することもできます。したがって、sideffectsがあるShell関数を呼び出すことができます!

  • これは、tempfile呼び出しに対して2回だけ分岐します。

バグ:

  • tempfileが失敗した場合の適切なエラー処理がありません。

  • これは、通常の_\n_シェルの削除を行います。 catch_read()のコメントを参照してください。

  • ファイル記述子_66_を使用してデータをコマンドにパイプすることはできません。必要な場合は、_42_などの別の記述子をリダイレクトに使用します(非常に古いシェルは最大9個のFDしか提供しないことに注意してください)。

  • これは、stdoutおよびstderrのNULバイト(_$'\0'_)を処理できません。 (NULは無視されます。readバリアントの場合、NULの背後にあるものはすべて無視されます。)

ご参考までに:

  • Unixでは、削除されたファイルへの参照(開いているファイルハンドルなど)を保持している限り、削除されたファイルにアクセスできます。このようにして、開いてから削除できます。
2
Tino

回避策の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が制限された形式であることがわかっている場合、許可されたコンテンツと競合しないタグを作成できます。

0
Warbo

以下は、OPが望んでいたものではないが、他のオプションとは異なる、より単純なバリエーションです。ファイル記述子を再配置することで、必要なものを取得できます。

テストコマンド:

%> cat xx.sh  
#!/bin/bash
echo stdout
>&2 echo stderr

それ自体は:

%> ./xx.sh
stdout
stderr

次に、stdoutを出力し、stderrを変数にキャプチャし、stdoutをファイルに記録します

%> export err=$(./xx.sh 3>&1 1>&2 2>&3 >"out")
stdout
%> cat out    
stdout
%> echo
$err 
stderr

または、stdoutをログに記録し、stderrを変数にキャプチャします。

export err=$(./xx.sh 3>&1 1>out 2>&3 )
%> cat out
stdout
%> echo $err
stderr

あなたはアイデアを得る。

0
Bruce Edge

警告:NOT(まだ?)WORKING!

以下は、一時ファイルを作成せずに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のダッシュとバッシュでは)

0
calestyo

コマンド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呼び出しに決して気付かないため、パフォーマンスセーフであることを知っています。

0
Hamy