bash se não várias condições sem subshell?

23

Eu quero combinar várias condições em uma declaração shell if e negar a combinação. Eu tenho o seguinte código de trabalho para uma combinação simples de condições:

if [ -f file1 ] && [ -f file2 ] && [ -f file3 ] ; then
  # do stuff with the files
fi

Isso funciona bem. Se eu quiser negar, posso usar o seguinte código de trabalho:

if ! ( [ -f file1 ] && [ -f file2 ] && [ -f file3 ] ) ; then
  echo "Error: You done goofed."
  exit 1
fi
# do stuff with the files

Isso também funciona conforme o esperado. No entanto, me ocorre que não sei o que os parênteses estão fazendo ali. Eu quero usá-los apenas para agrupar, mas é realmente gerando um subshell? (Como posso saber?) Em caso afirmativo, existe uma maneira de agrupar as condições sem gerar um subshell?

Curinga
fonte
Suponho que também possa usar a lógica booleana para determinar a expressão equivalente: if ! [ -f file1 ] || ! [ -f file 2 ] || ! [ -f file3 ] ; thenmas gostaria de uma resposta mais geral.
Curinga
Você também pode negar dentro do aparelho, por exemploif [[ ! -f file1 ]] && [[ ! -f file2 ]]; then
DopeGhoti 3/15/15
11
Sim, acabei de testar if [ ! 1 -eq 2 ] && [ ! 2 -eq 3 ]; then echo yep; fie funciona. Eu sempre escrevo testes com aparelho duplo por uma questão de hábito. Além disso, para garantir que não seja bash, testei ainda mais if /bin/test ! 1 -eq 2 && /bin/test ! 2 -eq 3 ; then echo yep; fie funciona dessa maneira também.
DopeGhoti
11
@Wildcard - [ ! -e file/. ] && [ -r file ]irá soltar diretórios. negue como quiser. claro, é isso que -dfaz.
mikeserv
11
Pergunta relacionada (discussão semelhante, mas não tanto, e respostas não tão úteis): unix.stackexchange.com/q/156885/135943
Wildcard

Respostas:

32

Você precisa usar em { list;}vez de (list):

if ! { [ -f file1 ] && [ -f file2 ] && [ -f file3 ]; }; then
  : do something
fi

Ambos são comandos de agrupamento , mas { list;}executam comandos no ambiente atual do shell.

Observe que, o ;in { list;}é necessário para delimitar a lista da }palavra reversa, você também pode usar outro delimitador. O espaço (ou outro delimitador) depois {também é necessário.

cuonglm
fonte
Obrigado! Algo que me tropeçou no começo (desde que eu uso chaves nos awkmais frequentemente do que nas bash) é a necessidade de espaço em branco após a chave aberta. Você menciona "você também pode usar outro delimitador"; alguns exemplos?
Curinga
@Wildcard: Sim, o espaço em branco após a chave aberta é necessário. Um exemplo para outro delimitador é uma nova linha, pois geralmente você deseja definir uma função.
precisa saber é
@don_crissti: Em zsh, você pode usar espaços em branco
cuonglm
@don_crissti: &também é um separador, você pode usar { echo 1;:&}, várias linhas novas também podem ser usadas.
precisa saber é
2
Você pode usar qualquer citação metacharacter de manual they must be separated from list by whitespace or another shell metacharacter.No bash são: metacharacter: | & ; ( ) < > space tab . Para esta tarefa específica, acredito que algum & ; ) space tabdeles funcionará. ... .... Do zsh (como um comentário) each sublist is terminated by &', &!', or a newline.
8

Você pode usar inteiramente a testfuncionalidade para alcançar o que deseja. Na página do manual de test:

 ! expression  True if expression is false.
 expression1 -a expression2
               True if both expression1 and expression2 are true.
 expression1 -o expression2
               True if either expression1 or expression2 are true.
 (expression)  True if expression is true.

Portanto, sua condição pode parecer com:

if [ -f file1 -a -f file2 -a -f file3 ] ; then
    # do stuff with the files
fi

Para negar o uso de parênteses escapados:

if [ ! \( -f file1 -a -f file2 -a -f file3 \) ] ; then
    echo "Error: You done goofed."
    exit 1
fi
# do stuff with the files
woodengod
fonte
6

Para portably negar uma condicional complexo no shell, você deve aplicar a lei de De Morgan e empurre a negação todo o caminho dentro das [chamadas ...

if [ ! -f file1 ] || [ ! -f file2 ] || [ ! -f file3 ]
then
    # do stuff
fi

... ou você deve usar then :; else...

if [ -f file1 ] && [ -f file2 ] && [ -f file3 ]
then :
else
  # do stuff
fi

if ! commandnão está disponível de forma portável e nem está [[.

Se você não precisar de portabilidade total, não escreva um script de shell . É mais provável que você encontre /usr/bin/perlum Unix selecionado aleatoriamente do que você bash.

zwol
fonte
11
!é POSIX, que hoje em dia é portátil o suficiente. Mesmo os sistemas que ainda são fornecidos com o shell Bourne também possuem um sh POSIX em outro lugar do sistema de arquivos que você pode usar para interpretar sua sintaxe padrão.
Stéphane Chazelas
@ StéphaneChazelas Isso é tecnicamente verdade, mas, na minha experiência, é mais fácil reescrever um script no Perl do que lidar com o trabalho de localizar e ativar o ambiente de shell compatível com POSIX a partir de /bin/sh. Os scripts do Autoconf fazem o que você sugere, mas acho que todos sabemos quão pouco é esse endosso.
Zwol 04/12/2015
5

outros notaram o agrupamento de {comandos compostos ;}, mas se você estiver executando testes idênticos em um conjunto, poderá usar um tipo diferente:

if  ! for f in file1 file2 file3
      do  [ -f "$f" ] || ! break
      done
then  : do stuff
fi

... como é demonstrado em outro lugar { :;}, não há dificuldade em aninhar comandos compostos ...

Observe que o acima (normalmente) testa arquivos regulares . Se você estiver procurando apenas arquivos legíveis e existentes que não sejam diretórios:

if  ! for f in file1 file2 file3
      do  [ ! -d "$f" ] && 
          [   -r "$f" ] || ! break
      done
then  : do stuff
fi

Se você não se importa se são diretórios ou não:

if  ! command <file1 <file2 <file3
then  : do stuff
fi

... funciona para qualquer arquivo legível e acessível , mas provavelmente ficará travado por quinze sem gravadores.

mikeserv
fonte
2

Esta não é uma resposta completa à sua pergunta principal, mas notei que você mencionou o teste composto (arquivo legível) em um comentário; por exemplo,

if [ -f file1 ] && [ -r file1 ] && [ -f file2 ] && [ -r file2 ] && [ -f file3 ] && [ -r file3 ]

Você pode consolidar isso um pouco, definindo uma função shell; por exemplo,

readable_file()
{
    [ -f "$1" ]  &&  [ -r "$1" ]
}

Adicione o tratamento de erros a (por exemplo, [ $# = 1 ]) a gosto. A primeira ifdeclaração, acima, agora pode ser condensada para

if readable_file file1  &&  readable_file file2  &&  readable_file file3

e você pode encurtar ainda mais, encurtando o nome da função. Da mesma forma, você pode definir not_readable_file()(ou nrfabreviar) e incluir a negação na função.

G-Man Diz 'Reinstate Monica'
fonte
Legal, mas por que não apenas adicionar um loop? readable_files() { [ $# -eq 0 ] && return 1 ; for file in "$@" ; do [ -f "$file" ] && [ -r "$file" ] || return 1 ; done ; } (Disclaimer: Eu testei este código e ele funciona, mas ele não era um one-liner eu adicionei ponto e vírgula para postar aqui, mas não testar sua colocação..)
Wildcard
11
Bom ponto. Eu levantaria a menor preocupação de que oculta mais a lógica de controle (conjunção) na função; alguém que lê o script e vê if readable_files file1 file2 file3não sabe se a função está executando AND ou OR - embora (a) acho que seja bastante intuitivo, (b) se você não estiver distribuindo o script, é bom o suficiente se você o entender, e (c) desde que sugeri abreviar o nome da função para a obscuridade, não estou em posição de falar sobre legibilidade. ... (continua)
G-Man diz 'Restabelecer Monica'
11
(Continua) ... BTW, a função é bem do jeito que você escreveu, mas você pode substituir for file in "$@" ; docom for file do- o padrão é in "$@", e (um pouco contra-intuitiva) o ;não é apenas desnecessário, mas realmente desanimado.
G-Man Diz 'Reinstate Monica'
0

Você também pode negar os testes de chaves, para reutilizar seu código original:

if [[ ! -f file1 ]] || [[ ! -f file2 ]] || [[ ! -f file3 ]] ; then
  # do stuff with the files
fi
DopeGhoti
fonte
2
Você precisa em ||vez de&&
cuonglm
O teste não é " nenhum dos arquivos está lá", o teste é " nenhum dos arquivos está lá". O uso ||executaria o predicado se algum dos arquivos não estiver presente, não se e somente se todos os arquivos não estiverem presentes. NOT (A e B e C) é o mesmo que (NÃO A) E (NÃO B) E (NÃO C); veja a pergunta original.
DopeGhoti
@DopeGhoti, cuonglm está certo. É se não (todos os arquivos lá), então quando você o divide dessa maneira, você precisa em ||vez de &&. Veja meu primeiro comentário abaixo da minha pergunta em si.
Wildcard
2
@DopeGhoti: not (a and b)é o mesmo que (not a) or (not b). Veja en.wikipedia.org/wiki/De_Morgan%27s_laws
cuonglm
11
Deve ser quinta-feira. Eu nunca pude pegar o jeito das quintas-feiras. Corrigido, e deixarei meu pé-na-boca para a posteridade.
DopeGhoti
-1

Eu quero usá-los apenas para agrupar, mas é realmente gerando um subshell? (Como posso eu saber?)

Sim, os comandos dentro do (...)são executados em um subshell.

Para testar se você está em um subshell, você pode comparar o PID do seu shell atual com o PID do shell interativo.

echo $BASHPID; (echo $BASHPID); 

Exemplo de saída, demonstrando que os parênteses geram um subshell e as chaves não:

$ echo $BASHPID; (echo $BASHPID); { echo $BASHPID;}
50827
50843
50827
$ 
David King
fonte
6
()faz subshell..perhaps desova você significou {}....
heemayl
@ heemayl, essa é a outra parte da minha pergunta - há alguma maneira de ver ou demonstrar na minha tela o fato de um subshell ser gerado? (Essa poderia realmente ser uma pergunta separada, mas seria bom saber aqui.) #
Wildcard
11
@heemayl Você está certo! Eu tinha feito echo $$ && ( echo $$ )para comparar os PIDs e eles eram os mesmos, mas, pouco antes de enviar o comentário, dizendo que você estava errado, tentei outra coisa. echo $BASHPID && ( echo $BASHPID )dá PID's diferentes
David King
11
@heemayl echo $BASHPID && { echo $BASHPID; }dê o mesmo PID que você sugeriu que seria o caso. Obrigado pela educação
David King
@ DavidDing, obrigado pelos comandos de teste usando $BASHPID, essa é a outra coisa que estava faltando.
Wildcard