Como posso negar o valor de retorno de um processo?

103

Estou procurando um processo de negação simples, mas de plataforma cruzada, que nega o valor que um processo retorna. Ele deve mapear 0 para algum valor! = 0 e qualquer valor! = 0 para 0, ou seja, o seguinte comando deve retornar "sim, caminho inexistente não existe":

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

O ! - operator é ótimo, mas infelizmente não é independente de shell.

segredo
fonte
Por curiosidade, você tinha um sistema específico em mente que não inclui o bash por padrão? Suponho que por "plataforma cruzada" você quis dizer apenas baseado em * nix, já que aceitou uma resposta que só funcionaria em um sistema * nix.
Parthian Shot

Respostas:

115

Anteriormente, a resposta era apresentada com o que agora é a primeira seção como a última seção.

POSIX Shell inclui um !operador

Pesquisando na especificação do shell para outros problemas, recentemente (setembro de 2015) notei que o shell POSIX oferece suporte a um !operador. Por exemplo, ele é listado como uma palavra reservada e pode aparecer no início de um pipeline - onde um comando simples é um caso especial de 'pipeline'. Ele pode, portanto, ser usado em ifinstruções e / whileou untilloops também - em shells compatíveis com POSIX. Consequentemente, apesar de minhas reservas, provavelmente está mais disponível do que eu percebi em 2008. Uma verificação rápida do POSIX 2004 e do SUS / POSIX 1997 mostra que !estava presente em ambas as versões.

Observe que o !operador deve aparecer no início do pipeline e nega o código de status de todo o pipeline (ou seja, o último comando). Aqui estão alguns exemplos.

# 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

Resposta portátil - funciona com conchas antigas

Em um script Bourne (Korn, POSIX, Bash), eu uso:

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

Isso é tão portátil quanto possível. O 'comando e argumentos' pode ser um pipeline ou outra sequência composta de comandos.

Um notcomando

O '!' operador, seja embutido em seu shell ou fornecido pelo o / s, não está universalmente disponível. No entanto, não é terrivelmente difícil de escrever - o código a seguir data de pelo menos 1991 (embora eu ache que escrevi uma versão anterior ainda mais atrás). Eu não costumo usar isso em meus scripts, porque não está disponível de forma confiável.

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

Isso retorna 'sucesso', o oposto de falha, quando falha ao executar o comando. Podemos debater se a opção 'não fazer nada com sucesso' estava correta; talvez ele deva relatar um erro quando não for solicitado a fazer nada. O código em ' "stderr.h"' fornece recursos de relatório de erros simples - eu o uso em qualquer lugar. Código-fonte a pedido - consulte minha página de perfil para entrar em contato comigo.

Jonathan Leffler
fonte
+1 para o 'comando e argumentos' pode ser um pipeline ou outra sequência composta de comandos. Outras soluções não funcionam com pipeline
HVNSweeting
3
As variáveis ​​de ambiente Bash devem seguir o !operador. Isso funcionou para mim:! MY_ENV=value my_command
Daniel Böhmer
1
A negação funciona em todo o canal, então você não pode fazer, ldd foo.exe | ! grep badlibmas pode fazer ! ldd foo.exe | grep badlibse quiser que o status de saída seja 0 se badlib não for encontrado em foo.exe. Semanticamente, você deseja inverter o grepstatus, mas inverter o tubo inteiro dá o mesmo resultado.
Mark Lakata
@MarkLakata: Você está certo: veja a sintaxe do shell POSIX e seções relacionadas (vá para POSIX para a melhor aparência). No entanto, é viável a utilização ldd foo.exe | { ! grep badlib; }(embora seja um incômodo, especialmente o ponto e vírgula; você poderia usar um sub-shell completa com (e )e sem o ponto e vírgula). OTOH, o objetivo é inverter o status de saída do pipeline, e esse é o status de saída do último comando (exceto no Bash, às vezes).
Jonathan Leffler
3
De acordo com esta resposta, o !comando tem uma interação indesejável com set -e, ou seja, faz com que o comando seja bem-sucedido com um código de saída 0 ou diferente de zero, em vez de ter sucesso apenas com um código de saída diferente de zero. Existe uma maneira concisa de torná-lo bem-sucedido apenas com um código de saída diferente de zero?
Elliott Slaughter
40

No Bash, use o! operador antes do comando. Por exemplo:

! ls nonexistingpath && echo "yes, nonexistingpath doesn't exist"
Jay Conrod
fonte
17

Você poderia tentar:

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

ou apenas:

! ls nonexistingpath
Eduard Wirch
fonte
5
Infelizmente !não pode ser usado com git bisect run, ou pelo menos não sei como.
Attila O.
1
Você sempre pode colocar coisas em um script de shell; git bisect run não vê o !nesse caso.
toolforger
10

Se, de alguma forma, você não tiver o Bash como shell (por exemplo: scripts git ou testes exec fantoches), você pode executar:

echo '! ls notexisting' | bash

-> retcode: 0

echo '! ls /' | bash

-> retcode: 1

Chris Suszyński
fonte
1
Para colocar outra localização atual aqui, isso é necessário para as linhas na seção de script do travis-ci.
kgraney
Isso também era necessário para pipelines BitBucket.
KumZ
3
! ls nonexistingpath && echo "yes, nonexistingpath doesn't exist."

ou

ls nonexistingpath || echo "yes, nonexistingpath doesn't exist."
Robert Gamble
fonte
Copiei se da pergunta original, pensei que fosse parte de um pipeline, às vezes sou um pouco lento;)
Robert Gamble
Essas duas instruções são quase equivalentes, MAS o valor de retorno restante $?será diferente. O primeiro pode sair $?=1se o nonexistingpathexistir de fato. O segundo sempre sai $?=0. Se você estiver usando isso em um Makefile e precisar contar com o valor de retorno, deve ter cuidado.
Mark Lakata
1

Nota: às vezes você verá !(command || other command).
Aqui ! ls nonexistingpath && echo "yes, nonexistingpath doesn't exist."é o suficiente.
Não há necessidade de um sub-shell.

Git 2.22 (Q2 2019) ilustra essa forma melhor com:

Commit 74ec8cf , cometer 3fae7ad , cometer 0e67c32 , cometer 07353d9 , cometer 3bc2702 , cometer 8c3b9f7 , cometer 80a539a , cometer c5c39f4 (13 de março de 2019) por SZEDER Gábor ( szeder) .
Consulte commit 99e37c2 , commit 9f82b2a , commit 900721e (13 de março de 2019) de Johannes Schindelin ( dscho) .
(Incorporado por Junio ​​C Hamano - gitster- no commit 579b75a , 25 de abril de 2019)

t9811-git-p4-label-import: corrigir negação de pipeline

Em ' t9811-git-p4-label-import.sh', o teste ' tag that cannot be exported' executa:

!(p4 labels | grep GIT_TAG_ON_A_BRANCH)

para verificar se a string fornecida não foi impressa por ' p4 labels'.
Isso é problemático, porque de acordo com POSIX :

“Se o pipeline começa com a palavra reservada !e command1é um comando subshell, o aplicativo deve garantir que o (operador no início de command1seja separado do !por um ou mais <blank>caracteres.
O comportamento da palavra reservada !imediatamente seguida pelo (operador não é especificado. "

Embora os shells mais comuns ainda interpretem isso ' !' como "negar o código de saída do último comando no pipeline", ' mksh/lksh' não o fazem e o interpretam como um padrão de nome de arquivo negativo.
Como resultado, eles tentam executar um comando composto de nomes de caminhos no diretório atual (ele contém um único diretório chamado ' main'), o que, obviamente, falha no teste.

Poderíamos corrigi-lo simplesmente adicionando um espaço entre ' !' e ' (', mas, em vez disso, vamos corrigi-lo removendo o subshell desnecessário. Em particular, Commit 74ec8cf

VonC
fonte
1

Solução sem!, Sem subshell, sem if, e deve funcionar pelo menos no bash:

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

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