Eu gostaria de ter um script bash que me permita chamar uma função para ver se alguns dados estão contidos em um arquivo e, se não estiver, falha no script principal, como o seguinte, que é simplificado para manter isso no ponto.
Isso não funciona (não sai do script principal quando o subshell falha).
Como posso escrever a require_line
função para que eu pudesse dizer mais de 20 delas em um arquivo como este
VALUE1=$(require_line "myKey1")
VALUE2=$(require_line "myKey2")
...
e não exigir um if em torno de cada um?
#!/bin/bash
set -eo pipefail
VALUE=$(require_line "myKey")
require_line(){
local KEY=$1
local DATA=$(cat /tmp/myfile)
local INFO=$(echo "$DATA" | grep "$KEY")
if [ ! -z "$INFO" ]
then
echo "Key not found in $DATA, key: $KEY"
exit 1;
fi
echo "$INFO"
}
Responder1
Sair do subshell não sairá do script principal como você o experimentou.
Penso em 3 (e meia) soluções:
- use
set -e
para que qualquer comando (ou subshell) com falha ("não testado") saia imediatamente do script principal (isso pode ser um exagero ou causar outros problemas), - enviar um sinal da função e capturá-lo com um
trap
- use
|| exit $?
depois de cada umVALUEn=$(...)
assimVALUE=$(require_line "myKey") || exit $?
, - combine (3.) com um loop (não tão elegante) usando
eval
.
Esse terceiro não "requer exatamente um if em torno de cada um" e ainda seria uma sintaxe IMHO bastante compacta.
Por falar nisso, está linha
echo "Key not found in $DATA, key: $KEY"
...é realmente inútil se você sair de todo o script logo depois disso, porque a frase será armazenada na $VALUEn
variável que não será exibida.
Sugiro imprimir stderr
assim:
echo "my error" 1>&2
Exemplos
Exemplo para solução 1
#!/bin/sh
set -e
myfunc(){
echo $1
if [ "$1" != "OK" ] ; then exit 1 ; fi
}
VALUE1=$(myfunc "OK")
echo $VALUE1
VALUE2=$(myfunc "NO WAY")
echo $VALUE2
echo "main script did not exit"
$ ./test.sh
OK
zsh: exit 1 ./test.sh
Mas se eu remover set -e
desde o início, recebo:
$ ./test.sh
OK
NO WAY
main script did not exit
Exemplo para solução 2
#!/bin/sh
trap "exit $?" USR1
myfunc(){
echo $1
if [ "$1" != "OK" ] ; then kill -USR1 $$ ; fi
}
VALUE1=$(myfunc "OK")
echo $VALUE1
VALUE2=$(myfunc "NO WAY")
echo $VALUE2
echo "main script did not exit"
$ ./test.sh
OK
Exemplo para solução 3
#!/bin/sh
myfunc(){
echo $1
if [ "$1" != "OK" ] ; then exit 1 ; fi
}
VALUE1=$(myfunc "OK") || exit $?
echo $VALUE1
VALUE2=$(myfunc "NO WAY") || exit $?
echo $VALUE2
echo "main script did not exit"
$ ./test.sh
OK
zsh: exit 1 ./test.sh
Exemplo para solução 4
#!/bin/sh
myfunc(){
echo $1
if [ "$1" != "OK" ] ; then exit 1 ; fi
}
I=1
for key in "OK" "OK" "NO WAY": ; do
eval "VALUE$I=\$(myfunc \"$key\")" || exit $?
eval "echo \$VALUE$I"
I=$(($I+1))
done
echo "main script did not exit"
$ ./test.sh
OK
OK
zsh: exit 1 ./test.sh
Responder2
Vamos reestruturar require_line
um pouco o seu roteiro para te ajudar.
Primeiro, podemos nos livrar do inútil cat | grep
. Segundo, podemos usar
grep
o comportamento intrínseco de para indicar sucesso ou fracasso na busca por
KEY
, bem como imprimir a chave, se encontrada, para stdout
.
require_line(){
local KEY="$1"
local FILE="/tmp/myfile"
if grep "$KEY" "$FILE"
then
return 0
else
printf 'Key not found in:\n\n"%s"\n\nKey: "%s"\n' "$(cat "$FILE")" "$KEY" >&2
return 1
fi
}
Isso aproveita o comportamento integrado do grep
. Se a chave for encontrada, grep
imprime a linha correspondente e retorna sucesso. Caso contrário, a else
ramificação será tomada e uma mensagem será impressa indicando que a chave não foi encontrada. Além disso, no caso de grep
falha, a mensagem de erro é impressa para stderr
que a mensagem de erro não seja confundida com uma correspondência válida encontrada em $FILE
.
Você pode modificar ainda mais require_line
para aceitar um nome de arquivo como $2
parâmetro, alterando a linha para read local FILE="$2"
e passando o nome de arquivo desejado sempre que invocar require_line
.
Agora, com isso no lugar....
Você realmente precisa armazenar cada VALUEn para KEYn ou apenas precisa garantir que todos estejam presentes?
Agora que você tem uma require_line
função que retorna de forma limpa um valor de sucesso ou falha, você pode simplesmente AND
reunir todos os seus testes. Assim que qualquer um deles falhar, o teste geral falhará.
Supondo que você precise dos valores reais de correspondência, a maneira demorada de fazer isso seria:
if value1=$(require_line "key1") &&
value2=$(require_line "key2") &&
value3=$(require_line "key3")
then
printf "%s\n" "$value1" "$value2" "$value3"
else
printf "One or more keys failed.\n" >&2
fi
Isso será entediante se você tiver várias chaves para verificar. Usar uma matriz pode ser melhor:
#!/usr/bin/env bash
require_line(){
local KEY="$1"
local FILE="/tmp/myfile" # or perhaps "$2"
if grep "$KEY" "$FILE"
then
return 0
else
printf 'Key not found in:\n\n"%s"\n\nKey: "%s"\n' "$(cat "$FILE")" "$KEY" >&2
return 1
fi
}
declare keys=("this" "is" "a" "test" "\." "keyN")
N=${#keys[@]}
declare values=()
j=0
while [ $j -lt $N ] && values[$j]="$(require_line "${keys[j]}")"
do
j=$(($j+1))
done
if [ $j -lt $N ]
then
printf 'error: found only %d keys out of %d:\n' $j $N
printf ' "%s"\n' "${values[@]}"
fi
Executando esse código com alguns dados de exemplo:
$ cat /tmp/myfile
this is a test.
$ ./test.sh
Key not found in:
"this is a test."
Key: "keyN"
error: found only 5 keys out of 6:
"this is a test."
"this is a test."
"this is a test."
"this is a test."
"this is a test."
""
Por último, se você realmente precisa apenas verificar se todas as chaves estão presentes sem precisar saber quais são os valores de correspondência, o código orientado a array acima pode ser simplificado para simplesmente fazer um loop até que todas as chaves sejam encontradas ou para abortar na primeira chave que está faltando.