popenで標準出力・標準エラー出力を両方受け取る

フォーラム(掲示板)ルール
フォーラム(掲示板)ルールはこちら  ※コードを貼り付ける場合は [code][/code] で囲って下さい。詳しくはこちら
suming55

popenで標準出力・標準エラー出力を両方受け取る

#1

投稿記事 by suming55 » 10年前

こんばんは。
コマンドを実行し、標準出力、標準エラー出力の両方をペアにして返す関数を作りたいです。
それぞれ別々に取得するには以下のようにしてできましたが、1度だけのコマンドの実行で実現するにはどうすれば良いでしょうか。
ファイルに書き出すという方法も思い浮かびましたが、できたらパイプやCの関数を上手く使って実現したいです。
よろしくお願いします。

コード:

#include <bits/stdc++.h>
#include <unistd.h>
using namespace std;

string execCommand(string com){
    com += " 2> /dev/null";
    cout << com << endl;
    FILE *p = popen(com.c_str(), "r");
    char c;
    string out;
    while((c = getc(p)) != EOF) out += c;
    pclose(p);
    return out;
}

string execCommand2(string com){
    com += " 2>&1 >/dev/null";
    cout << com << endl;
    FILE *p = popen(com.c_str(), "r");
    char c;
    string out;
    while((c = getc(p)) != EOF) out += c;
    pclose(p);
    return out;
}

int main(){
    {
        cout << "----- [stdout] -----" << endl;
        string out = execCommand("wget \"http://www.google.co.jp\" -O -");
        if(out.size() > 80) out.resize(80);
        cout << out << endl;
    }
    sleep(1);
    {
        cout << "----- [stderr] -----" << endl;
        string out = execCommand2("wget \"http://www.google.co.jp\" -O -");
        if(out.size() > 80) out.resize(80);
        cout << out << endl;
    }
}
やりたいこと

コード:

#include <utility>
pair<string,string> execCommand(string com){
    string out, err;
    // comを一度だけ実行してstdoutとstderrを両方取得
    return make_pair(out, err);
}

かずま

Re: popenで標準出力・標準エラー出力を両方受け取る

#2

投稿記事 by かずま » 10年前

ファイルを使う方法しか思いつきませんでした。

コード:

#include <bits/stdc++.h>
#include <unistd.h>
using namespace std;
 
pair<string,string> execCommand(string com)
{
    com += " 2>.err; printf '\1'; cat .err; rm .err";
    cout << com << endl;
    string out, err;
    FILE *p = popen(com.c_str(), "r");
    int c;
    while (c = getc(p), c != EOF && c != 1) out += c;
    while (c = getc(p), c != EOF) err += c;
    pclose(p);
    return make_pair(out, err);
}
 
int main()
{
    pair<string, string> p = execCommand("wget \"http://www.google.co.jp\" -O -");
    cout << "----- [stdout] -----" << endl;
    if (p.first.size() > 80) p.first.resize(80);
    cout << p.first << endl;
    cout << "----- [stderr] -----" << endl;
    if (p.second.size() > 80) p.second.resize(80);
    cout << p.second << endl;
}

かずま

Re: popenで標準出力・標準エラー出力を両方受け取る

#3

投稿記事 by かずま » 10年前

パイプを 2つ作って、stdout と stderr の両方を FILE * で返す
p2open() と p2close() を作ってみました。ファイルは使いません。

コード:

#include <bits/stdc++.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>
using namespace std;

int p2open(const char *com, FILE **fp)
{
    int fdout[2], fderr[2];

    if (pipe(fdout) < 0) return 1;
    if (pipe(fderr) < 0) {
        close(fdout[0]); close(fdout[1]);
        return 1;
    }   
    int pid = vfork();
    if (pid < 0) {  // error
        close(fdout[0]); close(fdout[1]);
        close(fderr[0]); close(fderr[1]);
        return 1;
    }   
    if (pid == 0) {  // child
        dup2(fdout[1], fileno(stdout));
        close(fdout[1]); close(fdout[0]);
        dup2(fderr[1], fileno(stderr));
        close(fderr[1]); close(fderr[0]);
        execl("/bin/sh", "sh", "-c", com, NULL);
        _exit(127);
    }   
    // parent
    fp[0] = fdopen(fdout[0], "r");
    close(fdout[1]);
    fp[1] = fdopen(fderr[0], "r");
    close(fderr[1]);
    return 0;
}

int p2close(FILE **fp)
{
    int status;
    fclose(fp[0]);
    fclose(fp[1]);
    wait(&status);
    return status;
}

pair<string,string> execCommand(string com)
{
    cout << com << endl;
    string out, err;
    FILE *p[2];
    p2open(com.c_str(), p); 
    int c;
    while (c = getc(p[0]), c != EOF) out += c;
    while (c = getc(p[1]), c != EOF) err += c;
    p2close(p);
    return make_pair(out, err);
}
 
int main()
{
    pair<string, string> p = execCommand("wget \"http://www.google.co.jp\" -O -");
    cout << "----- [stdout] -----" << endl;
    if (p.first.size() > 80) p.first.resize(80);
    cout << p.first << endl;
    cout << "----- [stderr] -----" << endl;
    if (p.second.size() > 80) p.second.resize(80);
    cout << p.second << endl;
}

かずま

Re: popenで標準出力・標準エラー出力を両方受け取る

#4

投稿記事 by かずま » 10年前

パイプを 2つ使うようにしたプログラムムには問題があります。
execCommand() で、子プロセスの標準出力を読み終わってから、
子プロセスの標準エラー出力を読み始めます。

ところが、パイプにはサイズの制限があって、読みだしを行わずに
書き込みだけを続けると、パイプの中にデータがたまって、ついには
書き込みができなくなりプロセスがブロックします。
Ubuntu では、そのサイズが 64KB のようです。したがって、
子プロセスが標準エラー出力に 64KB を越えるデータを書き込むと
そこでブロックし、標準出力をクローズすることもプロセスを
終了することもできなくなってしまいます。

これを回避するためには、パイプの読み出し側である親プロセスは、
select() を使ってデータが来た方から読むとか、スレッドを使って
2つの入力を同時に読むなどの手法を取る必要があります。

スレッドを使ったプログラムです。

コード:

#include <bits/stdc++.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>
#include <pthread.h>
using namespace std;
 
int p2open(const char *com, FILE **fp)
{
    int fdout[2], fderr[2];
 
    if (pipe(fdout) < 0) return 1;
    if (pipe(fderr) < 0) {
        close(fdout[0]); close(fdout[1]);
        return 1;
    }   
    int pid = vfork();
    if (pid < 0) {  // error
        close(fdout[0]); close(fdout[1]);
        close(fderr[0]); close(fderr[1]);
        return 1;
    }   
    if (pid == 0) {  // child
        dup2(fdout[1], fileno(stdout));
        close(fdout[1]); close(fdout[0]);
        dup2(fderr[1], fileno(stderr));
        close(fderr[1]); close(fderr[0]);
        execl("/bin/sh", "sh", "-c", com, NULL);
        _exit(127);
    }   
    // parent
    fp[0] = fdopen(fdout[0], "r");
    close(fdout[1]);
    fp[1] = fdopen(fderr[0], "r");
    close(fderr[1]);
    return 0;
}
 
int p2close(FILE **fp)
{
    int status;
    fclose(fp[0]);
    fclose(fp[1]);
    wait(&status);
    return status;
}
 
struct Param {
    FILE *fp[2];
    string out, err;
};

void *readerr(void *param)
{
    Param *p = (Param *)param;
    int c;
    while (c = getc(p->fp[1]), c != EOF) p->err += c;
    return 0;
}

pair<string, string> execCommand(string com)
{
    cout << com << endl;
    Param p;
    p2open(com.c_str(), p.fp); 
    pthread_t pt;
    pthread_create(&pt, 0, readerr, &p);
    int c;
    while (c = getc(p.fp[0]), c != EOF) p.out += c;
    pthread_join(pt, 0);
    p2close(p.fp);
    return make_pair(p.out, p.err);
}
 
int main()
{
    pair<string, string> p = execCommand("./b5");
    cout << "----- [stdout] -----" << endl;
    if (p.first.size() > 80) p.first.resize(80);
    cout << p.first << endl;
    cout << "----- [stderr] -----" << endl;
    if (p.second.size() > 80) p.second.resize(80);
    cout << p.second << endl;
}

かずま

Re: popenで標準出力・標準エラー出力を両方受け取る

#5

投稿記事 by かずま » 10年前

かずま さんが書きました:

コード:

    pair<string, string> p = execCommand("./b5");
b5 というのは、標準エラー出力に 64KB より大きいデータを書き込むテストプログラムです。
元のプログラムのように wget に置き換えてください。

コード:

    pair<string, string> p = execCommand("wget \"http://www.google.co.jp\" -O -");

閉鎖

“C言語何でも質問掲示板” へ戻る