如何否定一个进程的返回值?

149

我正在寻找一个简单且跨平台的否定过程,用于否定进程返回的值。它应该将0映射为某个不等于0的值,将任何不等于0的值映射为0,即下面的命令应该返回"yes, nonexistingpath不存在":

ls nonexistingpath | negate && echo "yes, nonexistingpath doesn't exist."

! - 运算符很棒,但不幸的是不与shell独立。


出于好奇,您有没有想到一种特定的系统,默认情况下不包括 bash?我认为您所说的“跨平台”只是指基于nix系统,因为您接受了一个仅在nix系统上工作的答案。 - Parthian Shot
1
ls is not a good tool for testing the existence of a path. If you want to test that a path exists, a perfectly good tool exists for that; it's conveniently called test (often spelled [). if ! test -e nonextantpath; then echo "does not exist" >&2; fi - William Pursell
7个回答

177

以前,答案是将现在的第一部分放在最后一部分呈现出来。

POSIX Shell 包括一个!运算符

在查找其他问题的shell规范时,我最近(2015年9月)注意到POSIX shell支持一个!运算符。例如,它被列为保留字,可以出现在pipeline的开头——其中简单命令是“pipeline”的特殊情况。因此,在符合POSIX标准的shell中,它也可以用于if语句和whileuntil循环中。因此,尽管我有所保留,但它可能比我2008年意识到的更普遍。快速查看POSIX 2004和SUS/POSIX 1997表明,!存在于这两个版本中。

请注意,!运算符必须出现在pipeline的开头,并否定整个pipeline(即最后一个命令)的状态代码。以下是一些示例。

# Simple commands, pipes, and redirects work fine.
$ ! some-command succeed; echo $?
1
$ ! some-command fail | some-other-command fail; echo $?
0
$ ! some-command < succeed.txt; echo $?
1

# Environment variables also work, but must come after the !.
$ ! RESULT=fail some-command; echo $?
0

# A more complex example.
$ if ! some-command < input.txt | grep Success > /dev/null; then echo 'Failure!'; recover-command; mv input.txt input-failed.txt; fi
Failure!
$ ls *.txt
input-failed.txt

可移植的答案——适用于古老的shell

在Bourne(Korn, POSIX, Bash)脚本中,我使用:

if ...command and arguments...
then : it succeeded
else : it failed
fi

这是最便携的形式。'命令和参数'可以是管道或其他复合的命令序列。

not 命令

'!' 操作符,无论是内置于您的 shell 还是由操作系统提供的,都不是普遍可用的。但它并不难写 - 下面的代码至少可以追溯到1991年(尽管我认为我写了一个更早版本)。然而,我不倾向于在我的脚本中使用它,因为它不能可靠地使用。

/*
@(#)File:           $RCSfile: not.c,v $
@(#)Version:        $Revision: 4.2 $
@(#)Last changed:   $Date: 2005/06/22 19:44:07 $
@(#)Purpose:        Invert success/failure status of command
@(#)Author:         J Leffler
@(#)Copyright:      (C) JLSS 1991,1997,2005
*/

#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include "stderr.h"

#ifndef lint
static const char sccs[] = "@(#)$Id: not.c,v 4.2 2005/06/22 19:44:07 jleffler Exp $";
#endif

int main(int argc, char **argv)
{
    int             pid;
    int             corpse;
    int             status;

    err_setarg0(argv[0]);

    if (argc <= 1)
    {
            /* Nothing to execute. Nothing executed successfully. */
            /* Inverted exit condition is non-zero */
            exit(1);
    }

    if ((pid = fork()) < 0)
            err_syserr("failed to fork\n");

    if (pid == 0)
    {
            /* Child: execute command using PATH etc. */
            execvp(argv[1], &argv[1]);
            err_syserr("failed to execute command %s\n", argv[1]);
            /* NOTREACHED */
    }

    /* Parent */
    while ((corpse = wait(&status)) > 0)
    {
            if (corpse == pid)
            {
                    /* Status contains exit status of child. */
                    /* If exit status of child is zero, it succeeded, and we should
                       exit with a non-zero status */
                    /* If exit status of child is non-zero, if failed and we should
                       exit with zero status */
                    exit(status == 0);
                    /* NOTREACHED */
            }
    }

    /* Failed to receive notification of child's death -- assume it failed */
    return (0);
}

当命令执行失败时,该代码返回“success”(成功)的相反结果。我们可以讨论“成功不执行任何操作”选项是否正确;也许当它没有被要求执行任何操作时,它应该报告一个错误。在"stderr.h"中的代码提供了简单的错误报告工具-我在任何地方都使用它。如需源代码,请联系我个人资料页面。


3
Bash环境变量必须遵循!运算符。以下命令对我有效:! MY_ENV=value my_command - Daniel Böhmer
1
否定作用于整个管道,因此您不能执行 ldd foo.exe | ! grep badlib,但是如果您希望在 foo.exe 中未找到 badlib 时退出状态为0,则可以执行 ! ldd foo.exe | grep badlib。从语义上讲,您想要反转 grep 状态,但是反转整个管道会产生相同的结果。 - Mark Lakata
1
@MarkLakata:你是对的:请参见POSIX shell语法和相关章节(转到POSIX获取最佳的外观和感觉)。然而,可以使用ldd foo.exe | { ! grep badlib; }(虽然这很麻烦,特别是那个分号;您可以使用带有()但没有分号的完整子shell)。另一方面,目标是反转管道的退出状态,这是最后一个命令的退出状态(除了在Bash中) 。 - Jonathan Leffler
4
根据这个回答!命令与set -e存在一种不理想的交互方式,即它会导致命令以0或非0的退出码成功执行,而不是仅以非0退出码成功执行。有没有一种简洁的方法可以使其仅在具有非0退出码时成功执行? - Elliott Slaughter
请注意,在“!”后必须有一个空格,否则它似乎会被解释为对先前命令的引用。也就是说,echo "hey" && ! echo "ho" ; echo $? 执行我们想要的操作,但 echo "hey" && !echo "ho" ; echo $? 将展开为依赖于先前命令的某些内容,例如 echo "hey" && echo "hey" && ! echo "ho" ; echo $? "ho" ; echo $? :) - qbolec
显示剩余3条评论

48
在Bash中,使用命令之前加上!操作符。例如:
! ls nonexistingpath && echo "yes, nonexistingpath doesn't exist"

请注意,! 运算符必须出现在管道的开头,并否定整个管道的状态代码(即最后一个命令)。 - Peter Mortensen

21
您可以尝试以下方法:
ls nonexistingpath || echo "yes, nonexistingpath doesn't exist."

或者只需:
! ls nonexistingpath

7
很遗憾,在git bisect run中无法使用!,或者至少我不知道如何使用。 - Attila O.
2
你总是可以将东西放入一个 shell 脚本中;在这种情况下,git bisect run 不会看到 ! - toolforger
@AttilaO — 我采用了shell脚本的方法,这是我最终的做法:echo "! ls nonexistingpath" > /tmp/bisect.sh; chmod u+x /tmp/bisect.sh; git bisect run /tmp/bisect.sh - M. Justin

14

如果出现以下情况,您的shell不是Bash(例如Git脚本或Puppet exec测试),您可以运行:

echo'!ls notexisting' | bash

-> 返回码:0

echo'!ls /' | bash

-> 返回码:1


2
为了在travis-ci的脚本部分中添加另一个面包屑,这是必要的。 - kgraney
1
这对于BitBucket管道也是必要的。 - KumZ
什么是“exec test”? - Peter Mortensen

7

无需使用 !,无需子Shell,无需 if 语句,至少在 Bash 中可行的解决方案:

ls nonexistingpath; test $? -eq 2 && echo "yes, nonexistingpath doesn't exist."

# Alternatively without an error message and handling of any error code > 0
ls nonexistingpath 2>/dev/null; test $? -gt 0 && echo "yes, nonexistingpath doesn't exist."

3
好的,看起来简单而且便携。请注意,在某些系统上,返回代码可能为负数。因此,我建议改用这个命令:test $? -ne 0 - kelvin
什么是这个想法?使用 test 来否定/反转结果代码? - Peter Mortensen

5
! ls nonexistingpath && echo "yes, nonexistingpath doesn't exist."

或者

ls nonexistingpath || echo "yes, nonexistingpath doesn't exist."

我把它从原始问题中复制出来,我以为它是管道的一部分,有时候我有点慢。 ;) - Robert Gamble
1
这两个语句几乎是等价的,但是留在 $? 中的返回值将不同。如果 nonexistingpath 确实存在,则第一个语句可能会留下 $?=1。而第二个语句始终留下 $?=0。如果您在 Makefile 中使用它并需要依赖于返回值,则必须小心。 - Mark Lakata

3
注意:有时您会看到!(命令||其他命令)
这里的!ls nonexistingpath && echo“是的,nonexistingpath不存在。”就足够了。
不需要子shell。
Git 2.22(2019年第二季度)用更好的形式阐述了这一点。

提交 74ec8cf, 提交 3fae7ad, 提交 0e67c32, 提交 07353d9, 提交 3bc2702, 提交 8c3b9f7, 提交 80a539a, 提交 c5c39f4 (2019年3月13日) 作者为SZEDER Gábor (szeder)
请查看提交 99e37c2, 提交 9f82b2a, 提交 900721e (2019年3月13日) 作者为Johannes Schindelin (dscho)
(由Junio C Hamano -- gitster --合并于提交 579b75a,2019年4月25日)

t9811-git-p4-label-import: fix pipeline negation

In 't9811-git-p4-label-import.sh', the test 'tag that cannot be exported' runs:

!(p4 labels | grep GIT_TAG_ON_A_BRANCH)

to check that the given string is not printed by 'p4 labels'.
This is problematic, because according to POSIX:

"If the pipeline begins with the reserved word ! and command1 is a subshell command, the application shall ensure that the ( operator at the beginning of command1 is separated from the ! by one or more <blank> characters.
The behavior of the reserved word ! immediately followed by the ( operator is unspecified."

While most common shells still interpret this '!' as "negate the exit code of the last command in the pipeline", 'mksh/lksh' don't and interpret it as a negative file name pattern instead.
As a result, they attempt to run a command made up of the pathnames in the current directory (it contains a single directory called 'main'), which, of course, fails the test.

We could fix it simply by adding a space between the '!' and '(', but instead let's fix it by removing the unnecessary subshell. In particular, Commit 74ec8cf


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接