How can I negate the return-value of a process?

梦想的初衷 提交于 2019-11-27 12:58:02
Jonathan Leffler

Previously, the answer was presented with what's now the first section as the last section.

POSIX Shell includes a ! operator

Poking around the shell specification for other issues, I recently (September 2015) noticed that the POSIX shell supports a ! operator. For example, it is listed as a reserved word and can appear at the start of a pipeline — where a simple command is a special case of 'pipeline'. It can, therefore, be used in if statements and while or until loops too — in POSIX-compliant shells. Consequently, despite my reservations, it is probably more widely available than I realized back in 2008. A quick check of POSIX 2004 and SUS/POSIX 1997 shows that ! was present in both those versions.

Note that the ! operator must appear at the beginning of the pipeline and negates the status code of the entire pipeline (i.e. the last command). Here are some examples.

# 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

Portable answer — works with antique shells

In a Bourne (Korn, POSIX, Bash) script, I use:

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

This is as portable as it gets. The 'command and arguments' can be a pipeline or other compound sequence of commands.

A not command

The '!' operator, whether built-in to your shell or provided by the o/s, is not universally available. It isn't dreadfully hard to write, though - the code below dates back to at least 1991 (though I think I wrote a previous version even longer ago). I don't tend to use this in my scripts, though, because it is not reliably available.

/*
@(#)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);
}

This returns 'success', the opposite of failure, when it fails to execute the command. We can debate whether the 'do nothing successfully' option was correct; maybe it should report an error when it isn't asked to do anything. The code in '"stderr.h"' provides simple error reporting facilities - I use it everywhere. Source code on request - see my profile page to contact me.

In Bash, use the ! operator before the command. For instance:

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

You could try:

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

or just:

! ls nonexistingpath

If somehow happens that you don't have Bash as your shell (for ex.: git scripts, or puppet exec tests) you can run:

echo '! ls notexisting' | bash

-> retcode: 0

echo '! ls /' | bash

-> retcode: 1

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

or

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

Note: sometimes you will see !(command || other command).
Here ! ls nonexistingpath && echo "yes, nonexistingpath doesn't exist." is enough.
No need for a sub-shell.

Git 2.22 (Q2 2019) illustrates that better form with:

Commit 74ec8cf, commit 3fae7ad, commit 0e67c32, commit 07353d9, commit 3bc2702, commit 8c3b9f7, commit 80a539a, commit c5c39f4 (13 Mar 2019) by SZEDER Gábor (szeder).
See commit 99e37c2, commit 9f82b2a, commit 900721e (13 Mar 2019) by Johannes Schindelin (dscho).
(Merged by Junio C Hamano -- gitster -- in commit 579b75a, 25 Apr 2019)

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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!