Birkaç komutun Bash çıkış durumunun etkin bir şekilde kontrol edilmesi


260

Birden fazla komut için pipefail'e benzer bir şey var mı, 'try' ifadesi gibi ama bash içinde. Böyle bir şey yapmak istiyorum:

echo "trying stuff"
try {
    command1
    command2
    command3
}

Ve herhangi bir noktada, herhangi bir komut başarısız olursa, o komutun hatasını bırakın ve yankılayın. Gibi bir şey yapmak istemiyorum:

command1
if [ $? -ne 0 ]; then
    echo "command1 borked it"
fi

command2
if [ $? -ne 0 ]; then
    echo "command2 borked it"
fi

Ve benzeri ... ya da benzeri bir şey:

pipefail -o
command1 "arg1" "arg2" | command2 "arg1" "arg2" | command3

Çünkü inandığım her komutun argümanları (yanılıyorsam beni düzeltin) birbirini etkileyecektir. Bu iki yöntem bana çok uzun soluklu ve kötü görünüyor, bu yüzden burada daha verimli bir yöntem için başvuruyorum.


2
Bir göz atın gayri resmi bash Katı modda : set -euo pipefail.
Pablo A

1
@PabloBianchi, set -ebir olan korkunç bir fikir. Bkz BashFAQ # 105 yılında egzersizleri onu tanıtır beklenmedik uç örnekleri sadece birkaç tartışırken, ve / veya en (ve kabuk versiyonlarını) uygulamaları farklı kabuklar arasındaki uyumsuzlukları gösteren karşılaştırma in-ulm.de/~mascheck/various/set -e .
Charles Duffy

Yanıtlar:


274

Komutu sizin için başlatan ve test eden bir işlev yazabilirsiniz. Komuta ayarlanmış ortam değişkenlerini varsayalım command1ve command2bunlar.

function mytest {
    "$@"
    local status=$?
    if (( status != 0 )); then
        echo "error with $1" >&2
    fi
    return $status
}

mytest "$command1"
mytest "$command2"

32
Kullanmayın $*, argümanlarda boşluk varsa başarısız olur; kullanın "$@". Benzer şekilde, komuttaki $1tırnak içine koyun echo.
Gordon Davisson

82
Ayrıca testbu yerleşik bir komut olduğu için adı önlemek istiyorum .
John Kugelman

1
Bu benim gittiğim yöntem. Dürüst olmak gerekirse, orijinal yazımda yeterince açık olduğumu sanmıyorum, ancak bu yöntem kendi 'test' işlevimi yazmama izin veriyor, böylece orada yaptığım eylemlerle ilgili bir hata eylemi gerçekleştirebiliyorum senaryo. Thanks :)
jwbensley

7
Test () tarafından döndürülen çıkış kodu, en son yürütülen komut 'echo' olduğundan hata olması durumunda her zaman 0 döndürmez. $ Değerini kaydetmeniz gerekebilir? ilk.
magiconair

2
Bu iyi bir fikir değil ve kötü uygulamayı teşvik ediyor. Basit durumunu düşünün ls. ls fooFormu çağırır ve bir hata mesajı ls: foo: No such file or directory\nalırsanız, sorunu anlarsınız. Bunun yerine ls: foo: No such file or directory\nerror with ls\ngereksiz bilgiler sizi rahatsız eder. Bu durumda, gereksizliğin önemsiz olduğunu iddia etmek yeterince kolaydır, ancak hızla büyür. Kısa hata mesajları önemlidir. Ancak daha da önemlisi, bu tür bir sarıcı, çok yazarları iyi hata mesajlarını tamamen atlamaya teşvik eder.
William Pursell

185

"Hatayı bırakmak ve yankılamak" ile ne demek istiyorsun? Komutun başarısız olması durumunda komut dosyasının sonlanmasını istediğinizi düşünüyorsanız,

set -e    # DON'T do this.  See commentary below.

komut dosyasının başlangıcında (ancak aşağıdaki uyarıyı not edin). Hata mesajının yankılanmasını zahmet etmeyin: başarısız komutun bunu halletmesine izin verin. Başka bir deyişle, şunları yaparsanız:

#!/bin/sh

set -e    # Use caution.  eg, don't do this
command1
command2
command3

ve command2 başarısız olur, stderr'a bir hata mesajı yazdırılırken, istediğinizi elde etmişsinizdir. (Ne istediğini yanlış anlamadım!)

Sonuç olarak, yazdığınız herhangi bir komut iyi davranmalıdır: hataları stdout yerine stderr'a bildirmelidir (sorudaki örnek kod hataları stdout'a yazdırır) ve başarısız olduğunda sıfırdan farklı bir durumla çıkmalıdır.

Ancak, artık bunun iyi bir uygulama olduğunu düşünmüyorum. set -eanlambilimini bash'ın farklı sürümleriyle değiştirdi ve basit bir komut dosyası için iyi çalışmasına rağmen, temelde kullanılamaz olduğu birçok kenar durumu var. (Şöyle bir şey düşünün: set -e; foo() { false; echo should not print; } ; foo && echo ok Buradaki anlambilim biraz makul, ancak kodu erken sonlandırma seçeneğine dayanan bir işleve yeniden yönlendirirseniz, kolayca ısırılabilir.) IMO yazmak daha iyidir:

 #!/bin/sh

 command1 || exit
 command2 || exit
 command3 || exit

veya

#!/bin/sh

command1 && command2 && command3

1
Bu çözüm en basit olsa da, arıza durumunda herhangi bir temizlik yapmanıza izin vermediğini unutmayın.
Josh J

6
Temizleme tuzaklarla yapılabilir. (örneğin çıkışta trap some_func 0yürütülecek some_func)
William Pursell

3
Ayrıca errexit (set -e) semantiğinin bash'ın farklı sürümlerinde değiştiğini ve işlev çağırma ve diğer ayarlar sırasında genellikle beklenmedik bir şekilde davranacağını unutmayın. Artık kullanımını tavsiye etmiyorum. IMO, || exither komuttan sonra açıkça yazmak daha iyidir .
William Pursell

87

Red Hat sistemimde yaygın olarak kullandığım bir dizi komut dosyası işlevim var. /etc/init.d/functionsYeşil [ OK ]ve kırmızı yazdırmak için sistem işlevlerini kullanırlar.[FAILED] durum göstergelerini .

İsteğe bağlı olarak $LOG_STEPSHangi komutların başarısız olduğunu günlüğe kaydetmek istiyorsanız değişkeni olarak bir günlük dosyası adına .

kullanım

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

Çıktı

Installing XFS filesystem tools:        [  OK  ]
Configuring udev:                       [FAILED]
Adding rc.postsysinit hook:             [  OK  ]

kod

#!/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
}

bu saf altın. Senaryoyu nasıl kullanacağımı anlasam da her basamağı tam olarak kavrayamıyorum, kesinlikle bash senaryo bilgim dışında ama yine de bu bir sanat eseri.
kingmilo

2
Bu aracın resmi bir adı var mı? Bu tarz adım / deneme / sonraki günlük kaydı hakkında bir adam sayfası okumak isterim
ThorSummoner

Bu kabuk işlevleri Ubuntu'da kullanılamıyor gibi görünüyor mu? Bunu, taşınabilir bir şey olsa kullanmayı umuyordum
ThorSummoner

@ThorSummoner, bunun nedeni büyük olasılıkla Ubuntu'nun SysV init yerine Upstart kullanması ve yakında systemd kullanmasıdır. RedHat, uzun süre geriye dönük uyumluluk sağlama eğilimindedir, bu nedenle init.d şeyler hala oradadır.
dragon788

John'un çözümüne bir genişleme gönderdim ve bunun Ubuntu gibi RedHat olmayan sistemlerde kullanılmasına izin verdim. Bkz. Stackoverflow.com/a/54190627/308145
Mark Thomson

51

Değeri ne olursa olsun, her komutu başarı için kontrol etmek için kod yazmanın daha kısa bir yolu:

command1 || echo "command1 borked it"
command2 || echo "command2 borked it"

Hala sıkıcı ama en azından okunabilir.


Bunu düşünmedim, ben gittim yöntemi değil ama hızlı ve kolay, bilgi için teşekkürler :)
jwbensley

3
Komutları sessizce yürütmek ve aynı şeyi başarmak için:command1 &> /dev/null || echo "command1 borked it"
Matt Byrne

Ben bu yöntemin hayranıyım, OR'den sonra birden fazla komut yürütmenin bir yolu var mı? Something likecommand1 || (echo command1 borked it ; exit)
AndreasKralj

38

Bir alternatif, sadece komutların birleştirilmesidir, &&böylece başarısız olan ilk komut , geri kalanın yürütülmesini engeller:

command1 &&
  command2 &&
  command3

Bu, soruda istediğiniz sözdizimi değil, ancak açıkladığınız kullanım durumu için ortak bir modeldir. Genel olarak komutlar, el ile yapmak zorunda kalmamanız için yazdırma hatalarından sorumlu olmalıdır (belki -qistemediğinizde hataları susturmak için bir bayrakla). Bu komutları değiştirme yeteneğiniz varsa, bunu yapan başka bir şeye sarmak yerine onları hataya bağırmak için düzenlerim.


Ayrıca yapmanız gerekmediğine dikkat edin:

command1
if [ $? -ne 0 ]; then

Basitçe şunu söyleyebilirsiniz:

if ! command1; then

Eğer zaman yapmak kontrol etme ihtiyacı dönüş kodları bir aritmetik bağlamı yerine kullanmak [ ... -ne:

ret=$?
# do something
if (( ret != 0 )); then

34

Koşucu işlevleri oluşturmak veya kullanmak yerine set -e, aşağıdakileri kullanın 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

Tuzak, onu tetikleyen komutun satır numarasına ve komut satırına bile erişebilir. Değişkenler $BASH_LINENOve $BASH_COMMAND.


4
Bir deneme bloğunu daha da yakından taklit etmek istiyorsanız trap - ERR, "bloğun" sonunda kapanı kapatmak için kullanın .
Gordon Davisson

14

Şahsen ben burada görüldüğü gibi hafif bir yaklaşım kullanmayı tercih ediyorum ;

yell() { echo "$0: $*" >&2; }
die() { yell "$*"; exit 111; }
try() { "$@" || die "cannot $*"; }
asuser() { sudo su - "$1" -c "${*:2}"; }

Örnek kullanım:

try apt-fast upgrade -y
try asuser vagrant "echo 'uname -a' >> ~/.profile"

8
run() {
  $*
  if [ $? -ne 0 ]
  then
    echo "$* failed with exit code $?"
    return 1
  else
    return 0
  fi
}

run command1 && run command2 && run command3

6
Kaçma $*herhangi argümanlar bunları boşluklar varsa başarısız olacak; kullanın "$@". (Komutta $ * iyi olmasına rağmen echo.)
Gordon Davisson

6

Bash'da neredeyse kusursuz bir deneme ve yakalama uygulaması geliştirdim, bu da aşağıdaki gibi kod yazmanıza izin verir:

try 
    echo 'Hello'
    false
    echo 'This will not be displayed'

catch 
    echo "Error in $__EXCEPTION_SOURCE__ at line: $__EXCEPTION_LINE__!"

Try-catch bloklarını kendi içlerine bile yerleştirebilirsiniz!

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__!"
}

Kod benim bash boilerplate / framework'ün bir parçası . Ayrıca, backtrace ve istisnalar (artı diğer bazı güzel özellikler) ile hata işleme gibi şeylerle deneme ve yakalama fikrini genişletir.

İşte sadece try & catch'den sorumlu olan kod:

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
}

Kullanmaktan, çatallanmaktan ve katkıda bulunmaktan çekinmeyin - GitHub'da .


1
Repoya baktım ve bunu kendim kullanmayacağım, çünkü benim zevkime göre çok fazla sihir var (IMO, daha fazla soyutlama gücüne ihtiyaç duyuyorsa Python'u kullanmak daha iyidir), ama benden kesinlikle büyük +1 çünkü harika görünüyor.
Alexander Malakhov

@AlexanderMalakhov nazik sözleriniz için teşekkürler. "Büyü" miktarı konusunda hemfikirim - çerçevenin basitleştirilmiş 3.0 sürümünü beyin fırtınası yapmamızın nedenlerinden biri, anlaşılması, hata ayıklaması vb. İçin çok daha kolay olacak. düşüncelerinizi çözmek istersiniz.
niieani

3

Maalesef ilk cevaba yorum yapamıyorum Ancak komutu yürütmek için yeni bir örnek kullanmalısınız: 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

2

İçin balık kabuk bu parçacığı üzerinde yanılmak kullanıcılar.

Izin vermek foobir değer "döndürmez (echo) bir işlev olmak, ama her zamanki gibi çıkış kodunu ayarlar.
İşlevi $statusçağırdıktan sonra kontrol etmekten kaçınmak için şunları yapabilirsiniz:

foo; and echo success; or echo failure

Ve bir hatta sığacak kadar uzunsa:

foo; and begin
  echo success
end; or begin
  echo failure
end

1

Kullandığımda ( ) modunda sshbağlantı sorunlarının neden olduğu sorunları ve uzaktan komutun hata kodlarını ayırt etmem gerekiyor . Aşağıdaki işlevi kullanıyorum:errexitset -e

# 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

1

RedHat olmayan sistemlerde yukarıda bulunan @ john-kugelman'ın müthiş çözümünü , kodunda bu satırı yorumlayarak kullanabilirsiniz:

. /etc/init.d/functions

Ardından, aşağıdaki kodu sonuna yapıştırın. Tam açıklama: Bu sadece yukarıda belirtilen dosyanın Centos 7'den alınan ilgili bitlerinin doğrudan bir kopyasıdır.

MacOS ve Ubuntu 18.04'te test edildi.


BOOTUP=color
RES_COL=60
MOVE_TO_COL="echo -en \\033[${RES_COL}G"
SETCOLOR_SUCCESS="echo -en \\033[1;32m"
SETCOLOR_FAILURE="echo -en \\033[1;31m"
SETCOLOR_WARNING="echo -en \\033[1;33m"
SETCOLOR_NORMAL="echo -en \\033[0;39m"

echo_success() {
    [ "$BOOTUP" = "color" ] && $MOVE_TO_COL
    echo -n "["
    [ "$BOOTUP" = "color" ] && $SETCOLOR_SUCCESS
    echo -n $"  OK  "
    [ "$BOOTUP" = "color" ] && $SETCOLOR_NORMAL
    echo -n "]"
    echo -ne "\r"
    return 0
}

echo_failure() {
    [ "$BOOTUP" = "color" ] && $MOVE_TO_COL
    echo -n "["
    [ "$BOOTUP" = "color" ] && $SETCOLOR_FAILURE
    echo -n $"FAILED"
    [ "$BOOTUP" = "color" ] && $SETCOLOR_NORMAL
    echo -n "]"
    echo -ne "\r"
    return 1
}

echo_passed() {
    [ "$BOOTUP" = "color" ] && $MOVE_TO_COL
    echo -n "["
    [ "$BOOTUP" = "color" ] && $SETCOLOR_WARNING
    echo -n $"PASSED"
    [ "$BOOTUP" = "color" ] && $SETCOLOR_NORMAL
    echo -n "]"
    echo -ne "\r"
    return 1
}

echo_warning() {
    [ "$BOOTUP" = "color" ] && $MOVE_TO_COL
    echo -n "["
    [ "$BOOTUP" = "color" ] && $SETCOLOR_WARNING
    echo -n $"WARNING"
    [ "$BOOTUP" = "color" ] && $SETCOLOR_NORMAL
    echo -n "]"
    echo -ne "\r"
    return 1
} 

0

İşlevsel olarak durum kontrolü

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)

}

Kullanımı:

assert_exit_status 'lambda status -> [[ $status -ne 0 ]] && echo Status is $status.' lls

Çıktı

Status is 127
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.