如何检测 shell 在 popen 调用后是否未能执行命令?不要与命令退出状态混淆

How to detect if shell failed to execute a command after popen call? Not to confuse with the command exit status

最近我开始为我的 python 脚本做一些测试。由于一些尴尬的原因,运行s python 脚本和检查其输出的模块是用 C 语言编写的,并添加了一些其他语言。这种方式我暂时用起来比较方便。

单个测试运行s,代码如下:

 FILE *fd = NULL;

 fd = popen("cmd", "r");
 if(NULL == fd){
  fprintf(stderr, "popen: failed\n");
  return 1;
 }
 fprintf(stderr, "res = %d: %s\n", errno, strerror(errno));

 int res = pclose(fd);
 fprintf(stderr, "res = %d: %s\n", res, strerror(errno));

正如您从上面看到的,代码只是在 popen 的帮助下 运行 是一个脚本并检查其退出状态。但是有一天,我 运行 在 popen 给出了错误论据的情况下。发生过这样的事情:

fd = popen("python@$#!", "r");

测试模块返回:

res = 0: Success
sh: 1: python@0!: not found
res = 32512: Success

所以,popen 运行 愉快地犯了上面的错误。并且只有 pclose 返回了一些退出状态。 errno 为 zero。在所有这些之间,shell 也产生了输出。

这是我的问题。如何检测 shell 是否未能执行命令?实际上失败可能是由于任何原因,但主要是脚本没有启动。

关于何时使用的一般评论errno

没有标准 C 或 POSIX 库函数曾经将 errno 设置为零。当 fd 不为 NULL 时根据 errno 打印错误消息是不合适的;错误编号不是来自 popen()(或未设置,因为 popen() 失败)。 pclose()后打印res即可;添加 strerror(errno) 会遇到同样的问题(errno 中的信息可能完全不相关)。您可以在调用函数之前将 errno 设置为零。如果函数 returns 是一个失败指示,那么查看 errno 可能是相关的(查看函数的说明——它是否定义为在失败时设置 errno?)。但是,errno即使成功也可以通过函数设置non-zero。 Solaris 标准 I/O 用于在输出流未连接到终端时设置 errno = ENOTTY,即使操作成功;它可能仍然如此。 Solaris 设置 errno 即使成功也是完全合法的;只有在 (1) 函数报告失败且 (2) 函数被记录为设置 errno(通过 POSIX 或系统手册)时,查看 errno 才是合法的。 =82=]

参见 C11 §7.5 Errors <errno.h> ¶3:

The value of errno in the initial thread is zero at program startup (the initial value of errno in other threads is an indeterminate value), but is never set to zero by any library function.202) The value of errno may be set to nonzero by a library function call whether or not there is an error, provided the use of errno is not documented in the description of the function in this International Standard.

202) Thus, a program that uses errno for error checking should set it to zero before a library function call, then inspect it before a subsequent library function call. Of course, a library function can save the value of errno on entry and then set it to zero, as long as the original value is restored if errno's value is still zero just before the return.

POSIX 类似(errno):

Many functions provide an error number in errno, which has type int and is defined in <errno.h>. The value of errno shall be defined only after a call to a function for which it is explicitly stated to be set and until it is changed by the next function call or if the application assigns it a value. The value of errno should only be examined when it is indicated to be valid by a function's return value. Applications shall obtain the definition of errno by the inclusion of <errno.h>. No function in this volume of POSIX.1-2017 shall set errno to 0. The setting of errno after a successful call to a function is unspecified unless the description of that function specifies that errno shall not be modified.

popen()pclose()

popen() 的 POSIX 规范并没有太大帮助。 popen() 'must fail' 只有一种情况;其他一切都是 'may fail'.

但是,pclose() 的详细信息更有用,包括:

If the command language interpreter cannot be executed, the child termination status returned by pclose() shall be as if the command language interpreter terminated using exit(127) or _exit(127).

Upon successful return, pclose() shall return the termination status of the command language interpreter. Otherwise, pclose() shall return -1 and set errno to indicate the error.

这意味着 pclose() returns 它从 waitpid() 收到的值 — 调用的命令的退出状态。请注意,它必须使用 waitpid()(或等效的选择性函数——在 BSD 系统上寻找 wait3()wait4());除了 popen() 为此文件流创建的进程之外,它无权等待任何其他 child 进程。有规定pclose()必须确保child已经退出,即使在这期间有其他函数等待死child从而导致系统失去状态child 由 popen() 创建。

如果将十进制 32512 解释为十六进制,则得到 0x7F00。如果你使用 <sys/wait.h> 中的 WIFEXITEDWEXITSTATUS 宏,你会发现退出状态是 127 (因为 0x7F127 十进制,退出状态编码在 waitpid().

返回状态的 high-order 位中
int res = pclose(fd);

if (WIFEXITED(res))
    printf("Command exited with status %d (0x%.4X)\n", WEXITSTATUS(res), res);
else if (WIFSIGNALED(res))
    printf("Command exited from signal %d (0x%.4X)\n", WTERMSIG(res), res);
else
    printf("Command exited with unrecognized status 0x%.4X\n", res);

并且记住0是表示成功的退出状态;其他任何情况通常表示某种错误。您可以进一步分析退出状态以查找 127 或中继信号等。您不太可能获得 'signalled' 状态或无法识别的状态。

popen() 告诉你 child 失败了。

当然,有可能执行的命令实际以状态 127 退出;这不可避免地令人困惑,解决它的唯一方法是避免退出状态在 126 到 128 + 'maximum signal number' 范围内(如果有 63 个可识别信号,这可能意味着 126 .. 191)。 POSIX 还使用值 126 来报告在 shebang (#!/usr/bin/interpreter) 中指定的解释器何时丢失(与要执行的程序不可用相反)。 pclose() 是否返回是一个单独的讨论。并且信号报告是由 shell 完成的,因为没有(简单的)方法来报告 child 死于信号。