Memeriksa status keluar Bash dari beberapa perintah secara efisien

261

Apakah ada sesuatu yang mirip dengan pipefail untuk banyak perintah, seperti pernyataan 'coba' tetapi dalam bash. Saya ingin melakukan sesuatu seperti ini:

echo "trying stuff"
try {
    command1
    command2
    command3
}

Dan pada titik mana pun, jika ada perintah gagal, putus dan gema kesalahan dari perintah itu. Saya tidak mau harus melakukan sesuatu seperti:

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

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

Dan seterusnya ... atau yang seperti:

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

Karena argumen dari setiap perintah yang saya percayai (benar jika saya salah) akan saling mengganggu. Kedua metode ini nampaknya sangat bertele-tele dan tidak menyenangkan bagi saya, jadi saya di sini mencari metode yang lebih efisien.

jwbensley
sumber
2
Lihatlah ke tidak resmi modus ketat pesta : set -euo pipefail.
Pablo A
1
@PabloBianchi, set -eadalah ide yang mengerikan . Lihat latihan di BashFAQ # 105 yang membahas beberapa kasus tepi tak terduga yang diperkenalkannya, dan / atau perbandingan yang menunjukkan ketidaksesuaian antara implementasi shell (dan versi shell) yang berbeda di in-ulm.de/ ~mascheck/various/set -e .
Charles Duffy

Jawaban:

274

Anda dapat menulis fungsi yang meluncurkan dan menguji perintah untuk Anda. Asumsikan command1dan command2merupakan variabel lingkungan yang telah disetel ke perintah.

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

mytest "$command1"
mytest "$command2"
krtek
sumber
32
Jangan gunakan $*, itu akan gagal jika ada argumen di dalamnya; gunakan "$@"saja. Demikian pula, letakkan $1di dalam tanda kutip di echoperintah.
Gordon Davisson
82
Saya juga akan menghindari nama testkarena itu adalah perintah bawaan.
John Kugelman
1
Ini adalah metode yang saya gunakan. Sejujurnya, saya tidak berpikir saya cukup jelas dalam posting asli saya tetapi metode ini memungkinkan saya untuk menulis fungsi 'tes' saya sendiri sehingga saya kemudian dapat melakukan tindakan kesalahan di sana saya suka yang relevan dengan tindakan yang dilakukan di naskah. Terima kasih :)
jwbensley
7
Bukankah kode keluar dikembalikan dengan tes () selalu mengembalikan 0 jika terjadi kesalahan karena perintah terakhir yang dijalankan adalah 'echo'. Anda mungkin perlu menyimpan nilai $? pertama.
magiconair
2
Ini bukan ide yang baik, dan itu mendorong praktik buruk. Pertimbangkan kasus sederhana ls. Jika Anda memohon ls foodan mendapatkan pesan kesalahan dari formulir ls: foo: No such file or directory\nAnda memahami masalahnya. Jika sebaliknya Anda membuat ls: foo: No such file or directory\nerror with ls\nAnda terganggu oleh informasi yang berlebihan. Dalam hal ini, Cukup mudah untuk berpendapat bahwa superfluitas itu sepele, tetapi dengan cepat tumbuh. Pesan kesalahan singkat itu penting. Tetapi yang lebih penting, jenis pembungkus ini mendorong penulis untuk sepenuhnya menghilangkan pesan kesalahan yang baik.
William Pursell
185

Apa yang Anda maksud dengan "drop out and echo the error"? Jika Anda ingin skrip dihentikan segera setelah perintah gagal, lakukan saja

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

di awal skrip (tetapi perhatikan peringatan di bawah). Jangan repot-repot mengulangi pesan kesalahan: biarkan perintah yang gagal mengatasinya. Dengan kata lain, jika Anda melakukannya:

#!/bin/sh

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

dan command2 gagal, saat mencetak pesan kesalahan ke stderr, maka tampaknya Anda telah mencapai apa yang Anda inginkan. (Kecuali saya salah menafsirkan apa yang Anda inginkan!)

Sebagai akibat wajar, perintah apa pun yang Anda tulis harus berperilaku baik: harus melaporkan kesalahan ke stderr alih-alih stdout (kode sampel dalam pertanyaan mencetak kesalahan ke stdout) dan harus keluar dengan status bukan nol ketika gagal.

Namun, saya tidak lagi menganggap ini sebagai praktik yang baik. set -etelah mengubah semantiknya dengan versi bash yang berbeda, dan meskipun itu berfungsi dengan baik untuk skrip sederhana, ada begitu banyak kasus tepi yang pada dasarnya tidak dapat digunakan. (Pertimbangkan hal-hal seperti: set -e; foo() { false; echo should not print; } ; foo && echo ok Semantik di sini agak masuk akal, tetapi jika Anda mengubah kode menjadi fungsi yang bergantung pada pengaturan opsi untuk mengakhiri lebih awal, Anda dapat dengan mudah digigit.) IMO lebih baik untuk menulis:

 #!/bin/sh

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

atau

#!/bin/sh

command1 && command2 && command3
William Pursell
sumber
1
Harap diperhatikan bahwa meskipun solusi ini paling sederhana, ia tidak membiarkan Anda melakukan pembersihan apa pun pada kegagalan.
Josh J
6
Pembersihan dapat dilakukan dengan perangkap. (mis. trap some_func 0akan dieksekusi some_funcsaat keluar)
William Pursell
3
Perhatikan juga bahwa semantik errexit (set -e) telah berubah dalam versi bash yang berbeda, dan sering kali berperilaku tak terduga selama pemanggilan fungsi dan pengaturan lainnya. Saya tidak lagi merekomendasikan penggunaannya. IMO, lebih baik menulis || exitsecara eksplisit setelah setiap perintah.
William Pursell
87

Saya memiliki satu set fungsi scripting yang saya gunakan secara luas di sistem Red Hat saya. Mereka menggunakan fungsi sistem dari /etc/init.d/functionsuntuk mencetak hijau [ OK ]dan merah[FAILED] indikator status .

Anda dapat mengatur $LOG_STEPS variabel ke nama file log jika Anda ingin mencatat perintah yang gagal.

Pemakaian

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

Keluaran

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

Kode

#!/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
}
John Kugelman
sumber
ini adalah emas murni. Meskipun saya mengerti cara menggunakan skrip, saya tidak sepenuhnya memahami setiap langkah, pasti di luar pengetahuan skrip bash saya, tetapi saya pikir itu adalah karya seni.
kingmilo
2
Apakah alat ini memiliki nama resmi? Saya ingin membaca halaman manual tentang gaya langkah / coba / penebangan berikutnya
ThorSummoner
Fungsi shell ini sepertinya tidak tersedia di Ubuntu? Saya berharap untuk menggunakan ini, sesuatu portable-ish
ThorSummoner
@ Tumormoner, ini kemungkinan karena Ubuntu menggunakan Upstart bukan SysV init, dan akan segera menggunakan systemd. RedHat cenderung mempertahankan kompatibilitas ke belakang untuk waktu yang lama, itulah sebabnya hal-hal init.d masih ada.
dragon788
Saya telah memposting ekspansi pada solusi John dan memungkinkannya untuk digunakan pada sistem non-RedHat seperti Ubuntu. Lihat stackoverflow.com/a/54190627/308145
Mark Thomson
51

Untuk apa nilainya, cara yang lebih singkat untuk menulis kode untuk memeriksa setiap perintah untuk sukses adalah:

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

Itu masih membosankan tetapi setidaknya itu bisa dibaca.

John Kugelman
sumber
Tidak memikirkan ini, bukan metode yang saya
gunakan,
3
Untuk menjalankan perintah secara diam-diam dan mencapai hal yang sama:command1 &> /dev/null || echo "command1 borked it"
Matt Byrne
Saya penggemar metode ini, apakah ada cara untuk menjalankan beberapa perintah setelah OR? Sesuatu seperticommand1 || (echo command1 borked it ; exit)
AndreasKralj
38

Alternatifnya adalah dengan menggabungkan perintah bersama-sama &&sehingga yang pertama gagal mencegah sisanya dari mengeksekusi:

command1 &&
  command2 &&
  command3

Ini bukan sintaks yang Anda minta dalam pertanyaan, tetapi ini adalah pola umum untuk use case yang Anda jelaskan. Secara umum perintah harus bertanggung jawab atas kegagalan pencetakan sehingga Anda tidak harus melakukannya secara manual (mungkin dengan -qtanda untuk membungkam kesalahan saat Anda tidak menginginkannya). Jika Anda memiliki kemampuan untuk memodifikasi perintah-perintah ini, saya akan mengeditnya untuk berteriak pada kegagalan, daripada membungkusnya dengan sesuatu yang lain yang melakukannya.


Perhatikan juga bahwa Anda tidak perlu melakukan:

command1
if [ $? -ne 0 ]; then

Anda bisa mengatakan:

if ! command1; then

Dan ketika Anda lakukan perlu untuk memeriksa kembali kode menggunakan konteks aritmatika bukannya [ ... -ne:

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

Alih-alih membuat fungsi runner atau menggunakan set -e, gunakan 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

Perangkap bahkan memiliki akses ke nomor baris dan baris perintah dari perintah yang memicu itu. Variabelnya adalah $BASH_LINENOdan $BASH_COMMAND.

Dijeda sampai pemberitahuan lebih lanjut.
sumber
4
Jika Anda ingin meniru coba blokir lebih dekat, gunakan trap - ERRuntuk mematikan jebakan di akhir "blok".
Gordon Davisson
14

Secara pribadi saya lebih suka menggunakan pendekatan yang ringan, seperti yang terlihat di sini ;

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

Contoh penggunaan:

try apt-fast upgrade -y
try asuser vagrant "echo 'uname -a' >> ~/.profile"
sleepycal
sumber
8
run() {
  $*
  if [ $? -ne 0 ]
  then
    echo "$* failed with exit code $?"
    return 1
  else
    return 0
  fi
}

run command1 && run command2 && run command3
Erik
sumber
6
Jangan dijalankan $*, itu akan gagal jika ada argumen yang memiliki ruang di dalamnya; gunakan "$@"saja. (Meskipun $ * tidak apa-apa dalam echoperintah.)
Gordon Davisson
6

Saya telah mengembangkan implementasi try & catch yang hampir sempurna di bash, yang memungkinkan Anda menulis kode seperti:

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

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

Anda bahkan dapat membuat sarang blok uji coba di dalam diri mereka!

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

Kode ini adalah bagian dari bash boilerplate / framework saya . Lebih lanjut memperluas gagasan mencoba & menangkap dengan hal-hal seperti penanganan kesalahan dengan backtrace dan pengecualian (ditambah beberapa fitur bagus lainnya).

Berikut kode yang bertanggung jawab hanya untuk mencoba & menangkap:

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
}

Jangan ragu untuk menggunakan, garpu, dan berkontribusi - ini ada di GitHub .

niieani
sumber
1
Saya telah melihat repo dan tidak akan menggunakan ini sendiri, karena terlalu banyak sihir untuk seleraku (IMO lebih baik menggunakan Python jika seseorang membutuhkan lebih banyak daya abstraksi), tetapi pasti +1 besar dari saya karena terlihat sangat mengagumkan.
Alexander Malakhov
Terima kasih atas kata-kata baik @AlexanderMalakhov. Saya setuju tentang jumlah "sihir" - itulah salah satu alasan mengapa kami melakukan brainstorming versi 3.0 dari framework, yang akan lebih mudah dipahami, di-debug, dll. Ada masalah terbuka tentang 3.0 pada GH, jika Anda ingin mengingat-ingat.
niieani
3

Maaf saya tidak dapat membuat komentar pada jawaban pertama. Tetapi Anda harus menggunakan contoh baru untuk menjalankan perintah: 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
umount
sumber
2

Untuk cangkang ikan pengguna yang tersandung di utas ini.

Membiarkan foomenjadi fungsi yang tidak "mengembalikan" (gema) nilai, tetapi menetapkan kode keluar seperti biasa.
Untuk menghindari memeriksa $statussetelah memanggil fungsi, Anda dapat melakukan:

foo; and echo success; or echo failure

Dan jika terlalu panjang untuk muat pada satu baris:

foo; and begin
  echo success
end; or begin
  echo failure
end
Dennis
sumber
1

Ketika saya menggunakan sshsaya perlu membedakan antara masalah yang disebabkan oleh masalah koneksi dan kode kesalahan perintah jarak jauh dalam mode errexit( set -e). Saya menggunakan fungsi berikut:

# 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
Tomilov Anatoliy
sumber
1

Anda dapat menggunakan solusi luar biasa @ john-kugelman yang ditemukan di atas pada sistem non-RedHat dengan mengomentari baris ini dalam kodenya:

. /etc/init.d/functions

Kemudian, rekatkan kode di bawah ini di bagian akhir. Pengungkapan penuh: Ini hanya salinan & tempel langsung bit relevan dari file yang disebutkan di atas yang diambil dari Centos 7.

Diuji pada MacOS dan Ubuntu 18.04.


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
} 
Mark Thomson
sumber
0

Memeriksa status secara fungsional

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)

}

Pemakaian:

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

Keluaran

Status is 127
slavik
sumber