popen() 如何工作以及如何在 Linux 上实现它到 C++ 代码中

How does popen() work and how to implement it into c++ code on linux?

本文关键字:实现 代码 C++ 工作 何工作 popen Linux      更新时间:2023-10-16

我在了解如何使用popen()将 stdout 从 Linux 中的子程序抓取到主C++程序时遇到了麻烦。我环顾四周,发现了这段代码,它做了我想要它做的事情。但我不明白这些东西是如何工作的。我知道 c++ 编程的基础知识(我已经做了几个月了),但我很困惑,所以有人可以帮助我解释一下吗?

提前谢谢你。

#include <vector>
#include <string>
#include <stdio.h>
#include <iostream>
using namespace std;
void my_popen( const string& cmd ,vector<string>& out ){
FILE*  fp;
const int sizebuf=1234;
char buff[sizebuf];
out = vector<string> ();
if ((fp = popen (cmd.c_str (), "r"))== NULL){
}
string cur_string = "";
while (fgets(buff, sizeof (buff), fp)) {
cur_string += buff;
}
out.push_back (cur_string.substr (0, cur_string.size() -1));
pclose(fp);
}


int main () {
vector<string> output;
my_popen("which driftnet", output);
for (vector<string>::iterator itr = output.begin();
itr != output.end();
++itr) {
cout << *itr << endl;
}
return 0;
}
如何使用

popen() 从 Linux 中的子程序抓取标准输出到 主要C++程序

您熟悉 UNIX 理念吗? 您可能会阅读维基百科文章"UNIX 哲学"的开头。下面是一个片段:

UNIX哲学由Doug McIlroy[1]在Bell中记录 1978年系统技术期刊:[2]

1)让每个程序做好一件事。要做新的工作,要重新构建 而不是通过添加新的"功能"来使旧程序复杂化。

2)期望每个程序的输出成为另一个程序的输入,如 然而未知,程序。不要用无关紧要的东西弄乱输出 信息。严格避免使用列式或二进制输入格式。不要 坚持交互式输入。

。以及更多...

我更喜欢 popen 访问许多我认为有用的 Linux 工具。 示例包括 sort、unique 和 sha256sum 以及其他几个。但是你可能会注意到许多等价物正在进入C++库(std::sort等),并且,通过努力,我找到了,例如,sha256sum的代码,适合在我的c ++应用程序中编译。


1)启动时,popen被赋予一个字符串,该字符串可以在一个部分中标识要运行的应用程序,也可以在多个部分中,第一个部分是一个应用程序,附加字符串是要传递给该应用程序的参数,就像命令行输入一样。 就像在外壳中一样(提示)

2) 当以"r"模式启动时,sha256sum 的 stdout 管道被定向到您调用 popen 时打开的 FILE* 句柄中。 因此,输出被"反馈"到您的程序。


但我不明白这些东西是如何工作的。

不是一个真正有用的评论,也不是一个问题。你的代码片段对我来说是直截了当的(尽管基本上是c代码)。 也许你应该选择一行代码并询问一些具体的事情?


但是,在你对代码过于努力之前,让我执行一些初步调查,我们将手动运行我们希望与 popen 一起使用的代码,并根据应用的输出捕获结果以供审查。

调查 1 -

在您的代码段中,您正在尝试命令"哪个流网"。 当程序流网在您的 PATH 上不存在时,这是一个不幸的选择。

我的系统上的结果:

$ which driftnet
$

由于该命令不存在,因此命令"which"仅返回空结果。

很难说我们是否做错了什么。 没有流网,我也帮不了你什么忙。


调查 2 - 一个更有趣的试验。

sha256sum 需要一个文件进行输入...在这里,我提供了我的"C++ Hello World"程序,您可以捕获该程序以"HelloWorld.cc"(或您想要的任何位置)或使用您自己的任何文件。

// If your C++ 'Hello World' has no class ... why bother?
#include <iostream>
class Hello_t {
public:
Hello_t()  { std::cout << "n  Hello"  << std::flush; }
~Hello_t() { std::cout <<    "World!" << std::endl; }
void operator() () { std::cout << " C++ "; }
};
int main(int, char**) { Hello_t()(); }

总共 10 行代码,以注释开头,在单个包含之后有 1 个空行。 我已将编辑器设置为自动神奇地删除尾随空格。 如果你留下了一个空间,你的结果可能会有所不同。

那么,当您从命令行计算文件"HelloWorld.cc"的 sha256sum 时会发生什么? 无需推测...试试吧!

在命令行上,调用命令并在下一行查看其响应:

$ sha256sum HelloWorld.cc
f91c6c288325fb4f72546482a9b4f1fa560f05071a12aa2a1268474ea3eeea3e  HelloWorld.cc

也许你可以尝试使用sha256sum和一个已知文件来尝试你的popen代码?

现在你可以看到(你的 popen 代码的)输出可能包含什么。对于此调查,**命令 sha256sum 结果是 1 个长字符串,其中包含一个空格。


仅供参考 - 几个月前,我在对库进行大修时破坏了我的C++ popen 代码。 当我修复它时,我计划重新审视你的努力,看看你的进展如何。

如果您的代码开始工作...我鼓励您提交自己的答案!

如果您可以确定更具体的问题,请优化问题。

我建议您转到C++代码...如果你要学习一些东西,你想学习C++,那么研究你发现的东西是没有用的。


我的代码再次工作。 (更新 2017/8/2)

但是,首先让我分享一下UNIX哲学的几个结果,排名不分先后:


后果 1 - 管道和过滤器

过滤器是做好一件事的程序。 它们接受输入流,并产生输出流,通常是"删除"某些内容。

在代码度量领域,管理层可以查看行数,以了解进度(行数增长减慢)和问题(不减慢)。

每个人都对代码指标有意见...并非文件中的每一行都应计算在内。 例如,我认为空格很重要,但一行空格不应算作代码。 同样,完整的注释行不是代码。 恕我直言。

在以下命令中

f1 < pfn | f2 | f3 | f4

f1..fn 是筛选器,所有筛选器都作为一个命令提交给 shell。 在我的代码度量工具中,我创建了

f1: ebl    erase blank lines 
f2: ecppc  erase cpp comments
f3: ecc    erase c comments
and several more.

因此

ebl < HelloWorld.cc | ecppc | ecc | ebl | wc 

是向外壳提交的单个字符串,最终为 Cout 的 3 个数字。

类似UNIX的命令'wc'可能存在于您的系统上。 在 Ubuntu 15.10 上:

$ wc < HelloWorld.cc
10  52  309

指示 HelloWorld.cc 有 10 行(不过滤)。


后果 2 - 命令列表

命令列表是 shell 命令的列表,每个命令都做好一件事。该列表标识执行的时间顺序。 比管道和过滤器更一般的想法。

例如,我使用分配的键 (F8) 设置了我的编辑器 (emacs) 以提供以下编译命令:

USER_FLAGS='-O0 ' ; export USER_FLAGS ; time make CC='g++-5 -m64 ' dumy514 ; ./dumy514

特定的文件 pfn 可能只输入一次,但保存在 emacs 内部的历史缓冲区中,因此无需重新输入它们。

请注意 3 个分号。 它们在一个提交到 Linux 命令外壳的字符串中分隔多个命令。 一个 3 元素命令列表:设置一个环境变量来覆盖默认选项;导出新标志以使其可供编译器使用;并推出一个品牌。


所以? popen() 接受命令列表。 像shell这样的"命令列表"可能会接受。


我终于足够仔细地阅读了"男人打开"的回应。

从描述:

popen() 函数通过创建管道、分叉和 调用外壳

你给 popen 的命令,在 shell 中运行。

但我不明白这些东西是如何工作的。

现在,这些"东西"应该开始有意义了。我们正在访问 shell 来处理命令/命令列表/管道过滤器。 这将启动"其他"进程,并使用 PIPE 与第一个进程进行交互。

那么,在 shell 中处理的命令的输出是如何传递给我的代码的呢? 以下是一些理解的线索。 包括一些单元测试演示,您可以在其中轻松更改 popen cmd,然后编译并运行新的 cmd。


下面提供了在两种模式之一中使用 popen 的代码。 是的,它们分为 3 类,一个基础和两个派生。希望这允许您一次运行多个这些。 (在我的 2 核机器上不是很有用。随意重构代码,只保留您需要的内容。其余的将在这里为您提供持续指导。

#include <iostream>
#include <sstream>
#include <string>
#include <cstring>   // sterror
#include <cstdio>  // popen, FILE, fgets, fputs
#include <cassert>

class POpen_t    // access to ::popen
{
protected:
FILE*        m_FILE;
std::string  m_cmd;
public:
POpen_t(void) : m_FILE(nullptr)
{ }
virtual ~POpen_t(void) {
if (m_FILE) (void)close();
m_FILE = 0;
}
// on success: 0 == return.size(), else returns error msg
std::string  close()
{
std::stringstream errSS;
do // poor man's try block
{
// pclose() returns the term status of the shell cmd
// otherwise -1 and sets errno.
assert(nullptr != m_FILE);  // tbr - some sort of logic error
// tbr if(0 == m_FILE)  break;  // success?
int32_t pcloseStat = ::pclose(m_FILE);
int myErrno = errno;
if (0 != pcloseStat)
{
errSS << "n  POpen_t::close() errno " << myErrno
<< "  " << std::strerror(myErrno) << std::endl;
break;
}
m_FILE     = 0;
}while(0);
return(errSS.str());
} // std::string  close(void)
}; // class POpen_t

我认为复制/粘贴将它们分成 3 个部分会更容易。 但请记住,我将所有代码都放在一个文件中。 没有一个头文件,但它对我有用。 如果您愿意,可以重构为 .h 和 .cc 文件。

class POpenRead_t : public POpen_t    // access to ::popen read-mode
{
public:
POpenRead_t(void) { }
// ::popen(aCmd): opens a process (fork), invokes shell, 
//                and creates a pipe
// returns NULL if the fork or pipe calls fail,
//              or if it cannot allocate memory.
// on success: 0 == return.size(), else returns error msg
std::string  open (std::string aCmd)
{
std::stringstream errSS; // 0 == errSS.str().size() is success
assert (aCmd.size() > 0);
assert (0 == m_FILE); // can only use serially
m_cmd = aCmd; // capture
do  // poor man's try block
{
if(true) // diagnosis only
std::cout << "n  POpenRead_t::open(cmd): cmd: '"
<< m_cmd << "'n" << std::endl;
// ::popen(aCmd): opens a process by creating a pipe, forking,
//                and invoking the shell.
// returns NULL if the fork or pipe calls fail,
//              or if it cannot allocate memory.
m_FILE = ::popen (m_cmd.c_str(), "r"); // create 'c-stream' (FILE*)
//       ^^ function is not in namespace std::
int myErrno = errno;
if(0 == m_FILE)
{
errSS << "n  POpenRead_t::open(" << m_cmd
<< ") popen() errno "   << myErrno
<< "  " << std::strerror(myErrno) << std::endl;
break;
}
} while(0);
return (errSS.str());
} // std::string  POpenRead_t::open(std::string aCmd)

// success when 0 == errStr.size()
// all outputs (of each command) captured into captureSS
std::string  spinCaptureAll(std::stringstream& captureSS)
{
const int BUFF_SIZE = 2*1024;
std::stringstream errSS; // capture or error
do
{
if(0 == m_FILE)
{
errSS << "n  ERR: POpenRead_t::spinCaptureAll(captureSS) - m_FILE closed";
break;
}
size_t discardedBlankLineCount = 0;
do
{
// allocate working buff in auto var, fill with nulls
char buff[BUFF_SIZE] = { 0 };
if(true) { for (int i=0; i<BUFF_SIZE; ++i) assert(0 == buff[i]); }
// char * fgets ( char * str, int num, FILE * c-stream );
// Reads characters from c-stream and stores them as a C string
// into buff until
//    a) (num-1) characters have been read
//    b) a newline or
//    c) the end-of-file is reached
// whichever happens first.
// A newline character makes fgets stop reading, but it is considered
// a valid character by the function and included in the string copied
// to str
// A terminating null character is automatically appended after the
// characters copied to str.
// Notice that fgets is quite different from gets: not only fgets
// accepts a c-stream argument, but also allows to specify the maximum
// size of str and includes in the string any ending newline character.
// fgets() returns buff or null when feof()
char* stat = std::fgets(buff,      // char*
BUFF_SIZE, // count - 1024
m_FILE);   // c-stream
assert((stat == buff) || (stat == 0));
int myErrno = errno; // capture
if( feof(m_FILE) ) { // c-stream eof detected
break;
}
// when stat is null (and ! feof(m_FILE) ),
//   even a blank line contains "any ending newline char"
// TBD:
// if (0 == stat) {
//   errSS << "0 == fgets(buff, BUFF_SIZE_1024, m_FILE) " << myErrno
//         << "  " << std::strerror(myErrno) << std::endl;
//   break;
// }
if(ferror(m_FILE)) { // file problem
errSS << "Err: fgets() with ferror: " << std::strerror(myErrno);
break;
}
if(strlen(buff))  captureSS << buff; // additional output
else              discardedBlankLineCount += 1;
}while(1);
if(discardedBlankLineCount)
captureSS << "n" << "discarded blank lines: " << discardedBlankLineCount << std::endl;
} while(0);
return (errSS.str());
} // std::string  POpenRead_t::spinCaptureAll(std::stringstream&  ss)
}; // class POpenRead_t

现在POpenWrite_t:

class POpenWrite_t : public POpen_t    // access to ::popen
{
public:
POpenWrite_t(void) { }

// ::popen(aCmd): opens a process (fork), invokes the non-interactive shell,
//                and creates a pipe
// returns NULL if the fork or pipe calls fail,
//              or if it cannot allocate memory.
// on success: 0 == return.size(), else returns error msg
std::string  open (std::string  aCmd)
{
std::stringstream errSS;  // 0 == errSS.str().size() is success
assert (aCmd.size() > 0);
assert (0 == m_FILE);
m_cmd = aCmd;  // capture
do // poor man's try block
{
if(true) // diagnosis only
std::cout << "n  POpenWrite_t::open(cmd): cmd: n  '"
<< "'" << m_cmd << std::endl;
m_FILE = ::popen (m_cmd.c_str(), "w"); // use m_FILE to write to sub-task std::in
int myErrno = errno;
if(0 == m_FILE)
{
errSS << "n  POpenWrite_t::open(" << m_cmd
<< ") popen() errno "        << myErrno
<< "  "  << std::strerror(myErrno)  << std::endl;
break;
}
} while(0);
return (errSS.str());
} // std::string  POpenWrite_t::open(std::string  aCmd)
// TBR - POpenWrite_t::write(const std::string& s) 
//           work in progress - see demo write mode
}; // class POpenWrite_t

接下来,我有我称之为T514_t的类,它定义了我的单元测试或演示工作。 条目位于"exec()"中。

事实证明,popen 中使用的 shell 可能与您在终端中体验的 shell 不同。 在会话启动期间,shell 的一部分决定它是处于交互(终端)还是非交互式(无终端)模式。在我的系统上,终端/交互式外壳是"bash",而 popen/非交互式外壳是"sh"。 在为 popen 开发 shell 命令时请记住这一点。

POpenRead_t的一个有趣用途是方法"std::string shellCheck(std::stringstream&rsltSS)"。

class T514_t
{
const std::string line = "n------------------------------------------------------";
const char escape = 27;
public:
T514_t() = default;
~T514_t() = default;
std::string exec (int argc, char* argv[],
std::stringstream& resultSS )
{
std::string errStr;
errStr = shellCheck(resultSS);  // when not bash, make a choice
if (0 == errStr.size()) // shell is expected
{
// bash reported, continue
switch (argc)
{
case 2 : { errStr = exec (argv, resultSS);       } break;
default: { errStr = "  User error"; usage(argv); } break;
} // switch (argc)
}
return (errStr); // when no err, errStr.size() is 0
} // int exec()
private:
std::string exec(char* argv[], std::stringstream& resultSS)
{
std::string       errStr;
std::cout << clrscr() << std::flush;
switch (std::atoi(argv[1]))
{
case 1:
{
std::cout << clrscr() << boldOff() << line;
errStr = demoReadMode(resultSS);
std::cout << boldOff() << std::endl;
} break;
case 2:
{
std::cout << clrscr() << boldOn() << line;
errStr = demoWriteMode();
std::cout << boldOff() << std::endl;
} break;

default: { usage(argv); } break;
} // switch (std::atoi(argv[1]))
return(errStr);
} // int exec(char* argv[], std::stringstream& resultSS)

// Ubuntu has 5 (or more) shells available
// 1) Bourne shell (' sh'), 2) C shell (' csh'), 3) TC shell (' tcsh'),
// 4) Korn shell (' ksh'), 5) Bourne Again shell (' bash')
//
// bash is  my interactive  shell
// sh is my non-interactive shell
// which (mildly) influences the cmds of the demo's 
//
// when not bash, what do you want to do?
std::string shellCheck(std::stringstream& rsltSS)
{
std::stringstream errValSS;
std::string cmd;
cmd += "ps -p "$$" ";
{
POpenRead_t  popenR;
errno = 0;
std::string rStat = popenR.open(cmd);
int myErrno = errno;
if(0 != rStat.size()) {
errValSS << "n  Err: " << cmd << " failed.  rStat: "
<< std::strerror(myErrno) << std::endl;
return(errValSS.str());
}
do
{
errno = 0;
std::string errSS = popenR.spinCaptureAll (rsltSS);
myErrno = errno;
if (false) { // dianosis only
std::cout << "n  demoReadMode() ss/myErrno/s:n"
<< rsltSS.str() << "  "
<< myErrno << "  '"
<< errSS << "'" << std::endl;
}
break;
if(0 != myErrno)
{
errValSS << "n  Err: popenR.spinCaputureAll():  cmd / strerror(myErrno): "
<< cmd << " / " << std::strerror(myErrno) << std::endl;
return(errValSS.str());
}
if (0 == rsltSS.str().size()) {  // TBR
std::cout << "n  demoReadMode: ss.str().size() is 0, indicating completed" << std::endl;
} break;
}while(1);
// NOTE:  pclose() returns the termination status of the shell command
// otherwise -1 and sets errno.
// errno = 0;
(void)popenR.close(); // return value is term status of the shell command
if(errno != 0)
{
errValSS << "n  Err: POpen_t::close() - cmd: " << cmd
<< "   err:" << std::strerror(errno) << std::endl;
}
}
std::string s = rsltSS.str();
std::string nishell (" sh");     // non-interactive shell expected is: " sh"
size_t indx = s.find(nishell);   // my interactive shell is bash
// clear
rsltSS.str(std::string()); rsltSS.clear(); // too much info
if (std::string::npos != indx)
{
// normally I would not include a 'success' message (not the 'UNIX pholosopy'),
// but here I have added to the (success) results:
if(true)  //<-- feel free to comment out or delete this success action
rsltSS << "n  the reported non-interactive shell is the required '"
<< nishell << "' ... continuing.n" << std::endl;
}
else
{
// TBR - when non-interactive shell is unexpectedly different that nishell,
//    this demo code aborts ... (with no results) and the error msg:
errValSS << "n  the reported non-interactive shell is not " << nishell
<< " n" << s << "n  ... abortingn";
// alternative actions are _always_ possible
// untested examples:
//   the shell can be temporarily changed as part of cmd  (untested)
//   the user could change the non-interactive shell
//   your use of popen (POpen_t classes above)
//        can change 'cmd's based on the discovered shell
}
return(errValSS.str());
} // std::string shellCheck(std::stringstream& rsltSS)

std::string demoReadMode(std::stringstream& rsltSS)
{
std::stringstream errValSS;
std::string cmd;
int i = 1;
cmd += "./HelloWorld ; ";
cmd += "echo  " + std::to_string(i++)  + " ; ";
cmd += "./HelloWorld ; ";
cmd += "sha256sum HelloWorld.cc ; ";
cmd += "echo  " + std::to_string(i++)  + " ; ";
cmd += "./HelloWorld ; ";
cmd += "echo TEST WORKS ; ";
cmd += "./HelloWorld";
{
POpenRead_t  popenR;
errno = 0;
std::string rStat = popenR.open(cmd);
int myErrno = errno;
if(0 != rStat.size()) {
errValSS << "n  Err: " << cmd << " failed.  rStat: "
<< std::strerror(myErrno) << std::endl;
return(errValSS.str());
}
do
{
errno = 0;
std::string errSS = popenR.spinCaptureAll (rsltSS);
myErrno = errno;
if (false) { // dianosis only
std::cout << "n  demoReadMode() ss/myErrno/s:n"
<< rsltSS.str() << "  "
<< myErrno << "  '"
<< errSS << "'" << std::endl;
}
break;
if(0 != myErrno)
{
errValSS << "n  Err: popenR.spinCaputureAll():  cmd / strerror(myErrno): "
<< cmd << " / " << std::strerror(myErrno) << std::endl;
return(errValSS.str());
}
if (0 == rsltSS.str().size()) {  // TBR
std::cout << "n  demoReadMode: ss.str().size() is 0, indicating completed" << std::endl;
} break;
}while(1);
// NOTE:  pclose() returns the termination status of the shell command
// otherwise -1 and sets errno.
// errno = 0;
(void)popenR.close(); // return value is term status of the shell command
if(errno != 0)
{
errValSS << "n  Err: POpen_t::close() - cmd: " << cmd
<< "   err:" << std::strerror(errno) << std::endl;
}
}
return(errValSS.str());
} //    std::string demoReadMode(std::stringstream& rsltSS)

std::string demoWriteMode()
{
std::stringstream errValSS;
std::string cmd;
int i = 1;
cmd += "./HelloWorld ; ";
cmd += "echo  " + std::to_string(i++)  + " ; ";
cmd += "./HelloWorld ; ";
cmd += "sha256sum HelloWorld.cc ; ";
cmd += "echo  " + std::to_string(i++)  + " ; ";
cmd += "./HelloWorld ; ";
{
POpenWrite_t  popenW;  // popen in write mode
// errno = 0;
std::string wStat = popenW.open(cmd);
int myErrno = errno;
if (0 != wStat.size()) {
errValSS << "n  Err: " << cmd << "n failed.  wStat: "
<< std::strerror(myErrno) << std::endl;
return(errValSS.str());
}
// tbd - Work in Progress - what command to receive what data from here
//       login - needs root, tbr - can cmd contain sudo?  probably
//
//
// NOTE:  pclose() returns the termination status of the shell command
// otherwise -1 and sets errno.
// errno = 0;
(void)popenW.close(); // return value is term status of the shell command
if(errno != 0)
{
errValSS << "n  Err: POpen_t::close() - cmd: " << cmd
<< "   err:" << std::strerror(errno) << std::endl;
}
}
return(errValSS.str());
} // std::string demoWriteMode()
void usage(char* argv[])
{
std::cout << "  executable: "<< argv[0]
<< "n  USAGE - user must select test mode"
<< "n  test"
<< "n   1  - demoReadMode"
<< "n   2  - demoWriteMode"
<< std::endl;
}

std::string boldOff(){std::string rV; rV.push_back(escape); rV += "[21m"; return(rV); } // bold off
std::string boldOn() {std::string rV; rV.push_back(escape); rV += "[1m";  return(rV); } // bold on
inline std::string clrscr(void) {
std::stringstream ss;
ss << static_cast<char>(escape) << "[H"   // home
<< static_cast<char>(escape) << "[2J"; // clrbos
return(ss.str());
}
}; // class T514_t

最后,我的主要...我在这里只想做几件事。

int main(int argc, char* argv[])
{
std::string errStr;
std::stringstream resultSS;
{
T514_t   t514;
errStr = t514.exec(argc, argv, resultSS);
}
// display result
if (resultSS.str().size())
std::cout << "nn  "<< resultSS.str() << std::endl;
if(errStr.size())
std::cerr << errStr << std::endl;
return(static_cast<int>(errStr.size()));
}

所以,构建并使用std::string cmd;在你感兴趣的模式下。 祝你好运。

欢迎提问。