5

STDERRコマンドの実行に失敗すると、バックティック呼び出し内のリダイレクトが失われる可能性があることがわかりました。私は私が見ている行動に困惑しています。

$ perl-e'strictを使用します。警告を使用します。私の$out= `DNE`; $out'を印刷  
「DNE」を実行できません:-e行1にそのようなファイルまたはディレクトリはありません。
-e行1の印刷での初期化されていない値の使用。

$ perl-e'strictを使用します。警告を使用します。私の$out= `DNE 2>&1`; $out'を印刷
-e行1の印刷での初期化されていない値の使用。

$ perl-e'strictを使用します。警告を使用します。私の$out= `echo 123; DNE 2>&1`; $out'を印刷
123
sh:DNE:コマンドが見つかりません

私の構文は間違っていますか?

LinuxでPerl5.8.5を使用しています。

4

3 に答える 3

16

構文は正しいですが、ある場合perlにはエラーメッセージをドロップしています。

一般に、初期化中にシステムに必要なコマンドがあることをテストし、コマンドがない場合は早期に失敗することを検討してください。

my $foopath = "/usr/bin/foo";
die "$0: $foopath is not executable" unless -x $foopath;

# later ...

my $output = `$foopath 2>&1`;
die "$0: $foopath exited $?" if $?;

出力の違いを完全に理解するには、Unixシステムプログラミングの詳細を理解する必要があります。読む。

Unixファイル記述子

単純なperl呼び出しについて考えてみます。

perl -e'print "hi \ n"; 警告"さようなら\n" '

その出力は

こんにちは
さよなら

printの出力はSTDOUT標準出力になり、標準エラーにwarn書き込まれることに注意してください。STDERRターミナルから実行すると、両方がターミナルに表示されますが、別の場所に送信できます。例えば

$ perl -e'print "hi \ n"; 警告"さようなら\n">/ dev / null
さよなら

nullデバイスまたは/dev/nullそれに送信された出力を破棄するため、上記のコマンドでは「hi」が消えます。上記のコマンドは略記です

$ perl -e'print "hi \ n"; 警告"さようなら\n"'1> / dev / null
さよなら

つまり、1はのファイル記述子ですSTDOUT。代わりに「さようなら」を捨てるには、

$ perl -e'print "hi \ n"; 警告"さようなら\n"'2> / dev / null
こんにちは

ご覧のとおり、2はのファイル記述子ですSTDERR。(完全を期すために、のファイル記述子STDINは0です。)

Bourneシェルとその派生物では、とをマージすることもSTDOUTできSTDERRます2>&1。「ファイル記述子2の出力をファイル記述子1の出力と同じ場所に移動させる」と読みます。</p>

$ perl -e'print "hi \ n"; 警告"さようなら\n"'2>&1
こんにちは
さよなら

ターミナル出力は区別を強調しませんが、追加のリダイレクトは何が起こっているかを示します。実行することで両方を破棄できます

$ perl -e'print "hi \ n"; 警告"さようなら\n">/ dev / null 2>&1

リダイレクトを左から右の順序で処理するこのシェルファミリでは順序が重要であるため、2つの結果を入れ替えます。

$ perl -e'print "hi \ n"; 警告"さようなら\n"'2>&1> / dev / null
さよなら

これは最初は意外かもしれません。シェルは最初に処理します。これは、ターミナルと同じ宛先に2>&1送信することを意味します。次に、処理してnullデバイスにリダイレクトします。STDERRSTDOUT>/dev/nullSTDOUT

dup2このファイル記述子の重複は、通常はのラッパーであるを呼び出すことによって発生しますfcntl

リダイレクトとパイプライン

ここで、コマンドの出力の各行にプレフィックスを追加するとします。

$ perl -e'print "hi \ n"; 警告"さようなら\n"'| sed -e's / ^ / got:/'
さよなら
取得:こんにちは

STDERR順序は異なりますが、とSTDOUTは異なるストリームであることを忘れないでください。「hi」だけがプレフィックスを持っていることにも注意してください。両方の行を取得するには、両方がに表示される必要がありますSTDOUT

$ perl -e'print "hi \ n"; 警告"さようなら\n"'2>&1 | sed -e's / ^ / got:/'
取得:さようなら
取得:こんにちは

パイプラインを構築するために、シェルはで子プロセスを作成しfork、でリダイレクトを実行しdup2、パイプラインの各ステージをexec適切なプロセスでの呼び出しで開始します。上記のパイプラインの場合、プロセスは次のようになります。

  • シェル:fork実行するプロセスsed
  • sedシェル:からの終了ステータスを待つwaitpid
    • sed:pipe入力をフィードするためのを作成しますperl
    • sed:fork実行するプロセスperl
    • sed:パイプの読み取り端から読み取りdup2を行うSTDIN
    • sed :execコマンドsed
    • sed:入力を待つSTDIN
      • perl:ステップ3からパイプの書き込み端にdup2送信しますSTDOUT
      • perl:の宛先dup2に送信STDERRするSTDOUT
      • perl :execコマンドperl
      • perl:出力を書き込み、最終的にはexit
    • sed:入力ストリームを受信して​​編集します
    • sed:パイプ上のファイルの終わりを検出します
    • sed:reapperlの終了ステータスをwaitpid
    • sed:exit
  • シェル:$?からの戻り値を入力しますwaitpid

子プロセスは右から左の順序で作成されることに注意してください。これは、Bourneファミリのシェルが、パイプラインの終了ステータスを最後のプロセスの終了ステータスとして定義しているためです。

日曜大工のパイプライン

以下のコードを使用して、Perlで上記のパイプラインを構築できます。

#! /usr/bin/env perl

use strict;
use warnings;

my $pid = open my $fh, "-|";
die "$0: fork: $!" unless defined $pid;

if ($pid) {
  while (<$fh>) {
    s/^/got: /;
    print;
  }
}
else {
  open STDERR, ">&=", \*STDOUT or print "$0: dup: $!";
  exec "perl", "-e", q[print "hi\n"; warn "bye\n"]
    or die "$0: exec: $!";
}

のperlfuncドキュメントに記載されているように、最初の呼び出しopenは私たちのために多くの作業を行います。open

MODEがの場合の3つ以上の引数の場合"|-"、ファイル名は出力がパイプされるコマンドとして解釈され、MODEが"-|"の場合、ファイル名は出力をパイプするコマンドとして解釈されます。2つの引数(および1つの引数)の形式では、ダッシュ("-")をコマンドに置き換える必要があります。この例の詳細については、perlipcでのIPCの使用openを参照してください。

その出力は

$ ./simple-pipeline
取得:さようなら
取得:こんにちは

上記のコードは、の複製をハードコードしてSTDOUTいます。これは以下で確認できます。

$ ./simple-pipeline> / dev / null

Perlのバックティック

別のコマンドの出力をキャプチャするにはperl、同じ機械を設定します。これは(in)で確認でき、pp_backtick(in )pp_sys.cを呼び出して子プロセスを作成し、配管(、、)を設定します。子はいくつかの配管を行い、(in )を呼び出して、出力が必要なコマンドを開始します。そこで、関連するコメントに気づきました:Perl_my_popenutil.cforkpipedup2Perl_do_exec3doio.c

/* handle the 2>&1 construct at the end */

実装はシーケンスを認識し2>&1、複製STDOUTし、シェルに渡されるコマンドからリダイレクトを削除します。

if (*s == '>' && s[1] == '&' && s[2] == '1'
    && s > cmd + 1 && s[-1] == '2' && isSPACE(s[-2])
    && (!s[3] || isSPACE(s[3])))
{
    const char *t = s + 3;

    while (*t && isSPACE(*t))
        ++t;
    if (!*t && (PerlLIO_dup2(1,2) != -1)) {
        s[-2] = '\0';
        break;
    }
}

後で見る

PerlProc_execl(PL_sh_path, "sh", "-c", cmd, (char *)NULL);
PERL_FPU_POST_EXEC
S_exec_failed(aTHX_ PL_sh_path, fd, do_report);

S_exec_failedには、

if (ckWARN(WARN_EXEC))
    Perl_warner(aTHX_ packWARN(WARN_EXEC), "Can't exec \"%s\": %s",
                cmd, Strerror(e));

それはあなたがあなたの質問で尋ねた警告の1つです。

タイムライン

perl質問からのコマンドを処理する方法の詳細を見ていきましょう。

予想通り

$ perl-e'strictを使用します。警告を使用します。私の$out= `DNE`; $out'を印刷
「DNE」を実行できません:-e行1にそのようなファイルまたはディレクトリはありません。
-e行1の印刷での初期化されていない値の使用。

ここに驚きはありません。

微妙な詳細を理解することが重要です。社内で処理する上記のコード2>&1は、実行するコマンドの条件が真の場合にのみ実行されます。

if (*s != ' ' && !isALPHA(*s) &&
    strchr("$&*(){}[]'\";\\|?<>~`\n",*s)) {

これは最適化です。backticksのコマンドに上記のシェルメタ文字が含まれている場合は、それperlをシェルに渡す必要があります。ただし、シェルメタ文字が存在しない場合は、コマンドを直接perl実行できるため、シェルの起動コストを節約できます。execfork

存在しないコマンドDNEにはシェルメタ文字が含まれていないため、perlすべての作業が含まれます。コマンドが失敗し、warningsプラグマを有効にしたため、exec-category警告が生成されます。perlopのドキュメントには、コマンドが失敗したときにバッククォートまたはスカラーコンテキストでqx//戻ることが記載undefされているため、未定義の値の出力に関する警告が表示されます$out

警告がありません

$ perl-e'strictを使用します。警告を使用します。私の$out= `DNE 2>&1`; $out'を印刷
-e行1の印刷での初期化されていない値の使用。

失敗したexec警告はどこに行きましたか?

別のコマンドを実行する子プロセスを作成する基本的な手順を覚えておいてください。

  1. 子がその出力を親に送信するためのパイプを作成します。
  2. 呼び出しforkて、ほぼ同一の子プロセスを作成します。
  3. 子では、パイプの書き込み端にdup2接続します。STDOUT
  4. 子でexec、新しく作成された子に代わりに別のプログラムを実行させる。
  5. 親で、パイプの内容を読み取ります。

別のコマンドの出力をキャプチャするにperlは、次の手順を実行します。実行を試みる準備として、子DNE 2>&1perlフォークし、子プロセスSTDERRでの複製が発生しますが、別STDOUTの副作用があります。

if (!*t && (PerlLIO_dup2(1,2) != -1)) {
    s[-2] = '\0';
    break;
}

2>&1がコマンドの最後にあり、dup2成功した場合はperl、リダイレクトの直前にNULバイトを書き込みます。これには、コマンドから削除する効果があります。たとえば、!にDNE 2>&1なります。DNEこれで、コマンドにシェルメタ文字がないためperl 、子プロセスで「自分自身、execこのコマンドを直接実行できます」と考えます。</ p>

が存在しないため、への呼び出しはexec失敗します。DNE子はまだに失敗したexec警告を発しSTDERRます。親に戻るパイプの書き込み終了と同じ場所dup2を指しているため、ターミナルには移動しません。STDERRSTDOUT

親プロセスは、子が異常終了したことを検出し、失敗したコマンド実行の結果がであると文書化されているため、パイプの内容を無視しますundef

別の警告

$ perl-e'strictを使用します。警告を使用します。私の$out= `echo 123; DNE 2>&1`; $out'を印刷
123
sh:DNE:コマンドが見つかりません

ここでは、DNE存在しないという別の診断が表示されます。最初に検出されるシェルメタ文字はです;。したがってperl、コマンドを変更せずにシェルに渡して実行します。はecho正常に完了し、シェルでDNE失敗し、シェルで失敗して親プロセスに戻りますSTDOUTSTDERRの観点perlからは、シェルは正常に実行されたため、警告する必要はありません。

関連事項

warningsプラグマを有効にすると(非常に良い方法です!)、これによりexec警告カテゴリが有効になります。これらの警告の完全なリストを表示するには、perldiagのドキュメントで文字列を検索してくださいW exec

違いを観察してください。

$ perl -Mstrict -Mwarnings -e'my $ out = `DNE`; $out'を印刷
「DNE」を実行できません:-e行1にそのようなファイルまたはディレクトリはありません。
-e行1の印刷での初期化されていない値$outの使用。

$ perl -Mstrict -Mwarnings -M-warnings = exec -e'my $ out = `DNE`; $out'を印刷
-e行1の印刷での初期化されていない値$outの使用。

後者の呼び出しは、

use strict;
use warnings;
no warnings 'exec';

my $out = `DNE`;
print defined($out) ? $out : "command failed\n";

exec、パイプなどで問題が発生したときに、自分のエラーメッセージをフォーマットするのが好きopenです。これは、通常、exec警告を無効にすることを意味しますが、戻り値をテストするために特に注意する必要があることも意味します。

于 2013-02-07T16:33:45.373 に答える
8

ここでの問題は、リダイレクトがシェルによって行われることです。そして、あなたの `...`コマンドはシェルを介して実行されません-Perlは$PATHを使用してDNEプログラムを見つけようとしますが、失敗します。

stdoutとstderrの両方をキャプチャする必要がある場合は、複数の方法を使用できますが、私の意見では、IPC::Open3またはIPC::Runを使用するのが最も安全です。

冒険心があれば、次のようなことを試すことができますが、それは悪い考えだということを忘れないでください。

$ perl -e 'use strict; use warnings; my $o=`sh -c "DNE 2>&1"`; print $o' 
sh: 1: DNE: not found
于 2013-02-07T16:13:01.747 に答える
2
  my $op =`dne 2>&1;`;

これは機能します。;リダイレクトの最後にあるセミコロンに注意してください。

または、以下のコードを使用できます。

#!/usr/bin/perl
use strict;
use warnings;

my $op=`dne 2>&1 1>output.txt`;

print $op;

出力:

sh: dne: command not found

しかし、なぜSTDOUTが正確に印刷されないのかは、私にdne 2>&1はまだわかりません。

ただし、STDOUTのファイルへのリダイレクトを使用すると、出力が出力されます。それは奇妙ですが、動作します。

于 2013-02-08T10:56:29.417 に答える