script - Comprobando el estado de salida Bash de varios comandos de manera eficiente
shell script linux español (13)
¿Qué quiere decir con "abandonar y repetir el error"? Si quiere decir que desea que la secuencia de comandos termine tan pronto como falla cualquier comando, simplemente haga
set -e
al inicio de la secuencia de comandos (pero tenga en cuenta la advertencia a continuación). No se moleste en repetir el mensaje de error: deje que el comando que falla lo maneje. En otras palabras, si lo haces:
#!/bin/sh
set -e # Use caution. eg, don''t do this
command1
command2
command3
y command2 falla, mientras se imprime un mensaje de error a stderr, entonces parece que ha logrado lo que desea. (A menos que malinterprete lo que quieres!)
Como corolario, cualquier comando que escriba debe comportarse bien: debe informar los errores a stderr en lugar de stdout (el código de muestra en la pregunta imprime los errores a stdout) y debe salir con un estado distinto de cero cuando falla.
Sin embargo, ya no considero que esto sea una buena práctica. set -e
ha cambiado su semántica con diferentes versiones de bash, y aunque funciona bien para un script simple, hay tantos casos de borde que es esencialmente inutilizable. (Considere cosas como: set -e; foo() { false; echo should not print; } ; foo && echo ok
La semántica aquí es algo razonable, pero si refactoriza el código en una función que dependía de la configuración de la opción para terminar antes , puede ser mordido fácilmente.) OMI, es mejor escribir:
#!/bin/sh
command1 || exit
command2 || exit
command3 || exit
o
#!/bin/sh
command1 && command2 && command3
¿Hay algo similar a pipefail para múltiples comandos, como una declaración ''try'' pero dentro de bash. Me gustaría hacer algo como esto:
echo "trying stuff"
try {
command1
command2
command3
}
Y en cualquier momento, si algún comando falla, abandone y haga eco del error de ese comando. No quiero tener que hacer algo como:
command1
if [ $? -ne 0 ]; then
echo "command1 borked it"
fi
command2
if [ $? -ne 0 ]; then
echo "command2 borked it"
fi
Y así sucesivamente ... o algo así:
pipefail -o
command1 "arg1" "arg2" | command2 "arg1" "arg2" | command3
Porque los argumentos de cada comando que creo (me corrigen si me equivoco) interferirán entre sí. Estos dos métodos me parecen horriblemente largos y desagradables, por lo que estoy aquí buscando un método más eficiente.
Cuando uso ssh
necesito distinguir entre los problemas causados por problemas de conexión y los códigos de error del comando remoto en el modo errexit
( set -e
). Yo uso la siguiente función:
# prepare environment on calling site:
rssh="ssh -o ConnectionTimeout=5 -l root $remote_ip"
function exit255 {
local flags=$-
set +e
"$@"
local status=$?
set -$flags
if [[ $status == 255 ]]
then
exit 255
else
return $status
fi
}
export -f exit255
# callee:
set -e
set -o pipefail
[[ $rssh ]]
[[ $remote_ip ]]
[[ $( type -t exit255 ) == "function" ]]
rjournaldir="/var/log/journal"
if exit255 $rssh "[[ ! -d ''$rjournaldir/'' ]]"
then
$rssh "mkdir ''$rjournaldir/''"
fi
rconf="/etc/systemd/journald.conf"
if [[ $( $rssh "grep ''#Storage=auto'' ''$rconf''" ) ]]
then
$rssh "sed -i ''s/#Storage=auto/Storage=persistent/'' ''$rconf''"
fi
$rssh systemctl reenable systemd-journald.service
$rssh systemctl is-enabled systemd-journald.service
$rssh systemctl restart systemd-journald.service
sleep 1
$rssh systemctl status systemd-journald.service
$rssh systemctl is-active systemd-journald.service
En lugar de crear funciones de corredor o usar set -e
, usa una trap
:
trap ''echo "error"; do_cleanup failed; exit'' ERR
trap ''echo "received signal to stop"; do_cleanup interrupted; exit'' SIGQUIT SIGTERM SIGINT
do_cleanup () { rm tempfile; echo "$1 $(date)" >> script_log; }
command1
command2
command3
La trampa incluso tiene acceso al número de línea y la línea de comando del comando que lo activó. Las variables son $BASH_LINENO
y $BASH_COMMAND
.
He desarrollado una implementación casi perfecta de prueba y captura en bash, que te permite escribir código como:
try
echo ''Hello''
false
echo ''This will not be displayed''
catch
echo "Error in $__EXCEPTION_SOURCE__ at line: $__EXCEPTION_LINE__!"
¡Incluso puedes anidar los bloques try-catch dentro de ellos mismos!
try {
echo ''Hello''
try {
echo ''Nested Hello''
false
echo ''This will not execute''
} catch {
echo "Nested Caught (@ $__EXCEPTION_LINE__)"
}
false
echo ''This will not execute too''
} catch {
echo "Error in $__EXCEPTION_SOURCE__ at line: $__EXCEPTION_LINE__!"
}
El código es parte de mi plantilla / marco de bash . Además, amplía la idea de probar y capturar cosas como el manejo de errores con retroceso y excepciones (más algunas otras características interesantes).
Aquí está el código que es solo responsable de probar y atrapar:
set -o pipefail
shopt -s expand_aliases
declare -ig __oo__insideTryCatch=0
# if try-catch is nested, then set +e before so the parent handler doesn''t catch us
alias try="[[ /$__oo__insideTryCatch -gt 0 ]] && set +e;
__oo__insideTryCatch+=1; ( set -e;
trap /"Exception.Capture /${LINENO}; /" ERR;"
alias catch=" ); Exception.Extract /$? || "
Exception.Capture() {
local script="${BASH_SOURCE[1]#./}"
if [[ ! -f /tmp/stored_exception_source ]]; then
echo "$script" > /tmp/stored_exception_source
fi
if [[ ! -f /tmp/stored_exception_line ]]; then
echo "$1" > /tmp/stored_exception_line
fi
return 0
}
Exception.Extract() {
if [[ $__oo__insideTryCatch -gt 1 ]]
then
set -e
fi
__oo__insideTryCatch+=-1
__EXCEPTION_CATCH__=( $(Exception.GetLastException) )
local retVal=$1
if [[ $retVal -gt 0 ]]
then
# BACKWARDS COMPATIBILE WAY:
# export __EXCEPTION_SOURCE__="${__EXCEPTION_CATCH__[(${#__EXCEPTION_CATCH__[@]}-1)]}"
# export __EXCEPTION_LINE__="${__EXCEPTION_CATCH__[(${#__EXCEPTION_CATCH__[@]}-2)]}"
export __EXCEPTION_SOURCE__="${__EXCEPTION_CATCH__[-1]}"
export __EXCEPTION_LINE__="${__EXCEPTION_CATCH__[-2]}"
export __EXCEPTION__="${__EXCEPTION_CATCH__[@]:0:(${#__EXCEPTION_CATCH__[@]} - 2)}"
return 1 # so that we may continue with a "catch"
fi
}
Exception.GetLastException() {
if [[ -f /tmp/stored_exception ]] && [[ -f /tmp/stored_exception_line ]] && [[ -f /tmp/stored_exception_source ]]
then
cat /tmp/stored_exception
cat /tmp/stored_exception_line
cat /tmp/stored_exception_source
else
echo -e " /n${BASH_LINENO[1]}/n${BASH_SOURCE[2]#./}"
fi
rm -f /tmp/stored_exception /tmp/stored_exception_line /tmp/stored_exception_source
return 0
}
Siéntase libre de usar, bifurcar y contribuir, está en GitHub .
Lo siento, no puedo hacer un comentario a la primera respuesta. Pero debe usar una nueva instancia para ejecutar el comando: cmd_output = $ ($ @)
#!/bin/bash
function check_exit {
cmd_output=$($@)
local status=$?
echo $status
if [ $status -ne 0 ]; then
echo "error with $1" >&2
fi
return $status
}
function run_command() {
exit 1
}
check_exit run_command
Para lo que vale, una forma más corta de escribir código para verificar que cada comando tenga éxito es:
command1 || echo "command1 borked it"
command2 || echo "command2 borked it"
Todavía es tedioso, pero al menos es legible.
Personalmente prefiero usar un enfoque ligero, como se ve here ;
yell() { echo "$0: $*" >&2; }
die() { yell "$*"; exit 111; }
try() { "$@" || die "cannot $*"; }
asuser() { sudo su - "$1" -c "${*:2}"; }
Ejemplo de uso:
try apt-fast upgrade -y
try asuser vagrant "echo ''uname -a'' >> ~/.profile"
Puede escribir una función que inicie y pruebe el comando por usted. Supongamos que command1
y command2
son variables de entorno que se han establecido en un comando.
function mytest {
"$@"
local status=$?
if [ $status -ne 0 ]; then
echo "error with $1" >&2
fi
return $status
}
mytest $command1
mytest $command2
Tengo un conjunto de funciones de scripting que uso ampliamente en mi sistema Red Hat. Utilizan las funciones del sistema de /etc/init.d/functions
para imprimir los indicadores de estado verde [ OK ]
y [FAILED]
rojos.
Opcionalmente, puede establecer la variable $LOG_STEPS
en un nombre de archivo de registro si desea registrar qué comandos fallan.
Uso
step "Installing XFS filesystem tools:"
try rpm -i xfsprogs-*.rpm
next
step "Configuring udev:"
try cp *.rules /etc/udev/rules.d
try udevtrigger
next
step "Adding rc.postsysinit hook:"
try cp rc.postsysinit /etc/rc.d/
try ln -s rc.d/rc.postsysinit /etc/rc.postsysinit
try echo $''/nexec /etc/rc.postsysinit'' >> /etc/rc.sysinit
next
Salida
Installing XFS filesystem tools: [ OK ]
Configuring udev: [FAILED]
Adding rc.postsysinit hook: [ OK ]
Código
#!/bin/bash
. /etc/init.d/functions
# Use step(), try(), and next() to perform a series of commands and print
# [ OK ] or [FAILED] at the end. The step as a whole fails if any individual
# command fails.
#
# Example:
# step "Remounting / and /boot as read-write:"
# try mount -o remount,rw /
# try mount -o remount,rw /boot
# next
step() {
echo -n "$@"
STEP_OK=0
[[ -w /tmp ]] && echo $STEP_OK > /tmp/step.$$
}
try() {
# Check for `-b'' argument to run command in the background.
local BG=
[[ $1 == -b ]] && { BG=1; shift; }
[[ $1 == -- ]] && { shift; }
# Run the command.
if [[ -z $BG ]]; then
"$@"
else
"$@" &
fi
# Check if command failed and update $STEP_OK if so.
local EXIT_CODE=$?
if [[ $EXIT_CODE -ne 0 ]]; then
STEP_OK=$EXIT_CODE
[[ -w /tmp ]] && echo $STEP_OK > /tmp/step.$$
if [[ -n $LOG_STEPS ]]; then
local FILE=$(readlink -m "${BASH_SOURCE[1]}")
local LINE=${BASH_LINENO[0]}
echo "$FILE: line $LINE: Command /`$*'' failed with exit code $EXIT_CODE." >> "$LOG_STEPS"
fi
fi
return $EXIT_CODE
}
next() {
[[ -f /tmp/step.$$ ]] && { STEP_OK=$(< /tmp/step.$$); rm -f /tmp/step.$$; }
[[ $STEP_OK -eq 0 ]] && echo_success || echo_failure
echo
return $STEP_OK
}
Una alternativa es simplemente unir los comandos junto con &&
para que el primero en fallar impida que el resto se ejecute:
command1 &&
command2 &&
command3
Esta no es la sintaxis que solicitó en la pregunta, pero es un patrón común para el caso de uso que describe. En general, los comandos deben ser responsables de los errores de impresión para que no tenga que hacerlo manualmente (tal vez con una -q
para silenciar los errores cuando no los desee). Si tiene la capacidad de modificar estos comandos, los editaría para gritar si falla, en lugar de envolverlos en otra cosa que lo haga.
Note también que no necesita hacer:
command1
if [ $? -ne 0 ]; then
Simplemente puedes decir:
if ! command1; then
Verificando el estado de manera funcional.
assert_exit_status() {
lambda() {
local val_fd=$(echo $@ | tr -d '' '' | cut -d'':'' -f2)
local arg=$1
shift
shift
local cmd=$(echo $@ | xargs -E '':'')
local val=$(cat $val_fd)
eval $arg=$val
eval $cmd
}
local lambda=$1
shift
eval $@
local ret=$?
$lambda : <(echo $ret)
}
Uso:
assert_exit_status ''lambda status -> [[ $status -ne 0 ]] && echo Status is $status.'' lls
Salida
Status is 127
Para usuarios de cáscara de pescado que tropiezan con este hilo
Sea foo
una función que no "devuelve" (eco) un valor, pero establece el código de salida como de costumbre.
Para evitar verificar el $status
después de llamar a la función, puede hacer:
foo; and echo success; or echo failure
Y si es demasiado largo para caber en una línea:
foo; and begin
echo success
end; or begin
echo failure
end
run() {
$*
if [ $? -ne 0 ]
then
echo "$* failed with exit code $?"
return 1
else
return 0
fi
}
run command1 && run command2 && run command3