Kiểm tra trạng thái thoát Bash của một số lệnh một cách hiệu quả


260

Có cái gì đó tương tự như pipefail cho nhiều lệnh, như câu lệnh 'thử' nhưng trong bash. Tôi muốn làm một cái gì đó như thế này:

echo "trying stuff"
try {
    command1
    command2
    command3
}

Và tại bất kỳ thời điểm nào, nếu bất kỳ lệnh nào bị lỗi, hãy bỏ qua và lặp lại lỗi của lệnh đó. Tôi không muốn phải làm một cái gì đó như:

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

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

Và cứ thế ... hoặc bất cứ thứ gì như:

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

Bởi vì các đối số của mỗi lệnh tôi tin (sửa tôi nếu tôi sai) sẽ can thiệp lẫn nhau. Hai phương pháp này có vẻ dài ngoằn ngoèo và khó chịu đối với tôi vì vậy tôi ở đây kêu gọi một phương pháp hiệu quả hơn.



1
@PabloBianchi, set -elà một ý tưởng kinh khủng . Xem các bài tập trong BashFAQ # 105 chỉ thảo luận về một số trường hợp cạnh bất ngờ mà nó đưa ra và / hoặc so sánh cho thấy sự không tương thích giữa các triển khai của các shell khác nhau (và các phiên bản shell ') tại in-ulm.de/~mascheck/various/set -e .
Charles Duffy

Câu trả lời:


274

Bạn có thể viết một hàm khởi chạy và kiểm tra lệnh cho bạn. Giả sử command1command2là các biến môi trường đã được đặt thành một lệnh.

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

mytest "$command1"
mytest "$command2"

32
Đừng sử dụng $*, nó sẽ thất bại nếu bất kỳ đối số nào có khoảng trắng trong chúng; sử dụng "$@"thay thế. Tương tự, đặt $1bên trong dấu ngoặc kép trong echolệnh.
Gordon Davisson

82
Ngoài ra, tôi sẽ tránh tên testđó là một lệnh tích hợp.
John Kugelman

1
Đây là phương pháp tôi đã đi với. Thành thật mà nói, tôi không nghĩ rằng tôi đã đủ rõ ràng trong bài viết gốc của mình nhưng phương pháp này cho phép tôi viết chức năng 'kiểm tra' của riêng mình để sau đó tôi có thể thực hiện một hành động lỗi trong đó Tôi thích điều đó có liên quan đến các hành động được thực hiện trong kịch bản. Cảm ơn :)
jwbensley

7
Không phải mã thoát được trả về bởi test () luôn trả về 0 trong trường hợp có lỗi vì lệnh cuối cùng được thực thi là 'echo'. Bạn có thể cần phải lưu giá trị của $? Đầu tiên.
magiconair

2
Đây không phải là một ý tưởng tốt, và nó khuyến khích thực hành xấu. Hãy xem xét trường hợp đơn giản của ls. Nếu bạn gọi ls foovà nhận được một thông báo lỗi của mẫu ls: foo: No such file or directory\nbạn hiểu vấn đề. Nếu thay vào đó bạn khiến ls: foo: No such file or directory\nerror with ls\nbạn trở nên mất tập trung bởi thông tin thừa. Trong trường hợp này, thật dễ dàng để tranh luận rằng sự siêu phàm là tầm thường, nhưng nó nhanh chóng phát triển. Thông báo lỗi súc tích là quan trọng. Nhưng quan trọng hơn, loại trình bao bọc này khuyến khích các nhà văn quá hoàn toàn bỏ qua các thông báo lỗi tốt.
William Pursell

185

Bạn có ý nghĩa gì khi "thả ra và lặp lại lỗi"? Nếu bạn có nghĩa là bạn muốn tập lệnh chấm dứt ngay khi bất kỳ lệnh nào thất bại, thì hãy làm

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

khi bắt đầu tập lệnh (nhưng lưu ý cảnh báo bên dưới). Đừng bận tâm lặp lại thông báo lỗi: hãy để lệnh không xử lý được. Nói cách khác, nếu bạn làm:

#!/bin/sh

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

và lệnh2 không thành công, trong khi in thông báo lỗi sang stderr, thì có vẻ như bạn đã đạt được những gì bạn muốn. (Trừ khi tôi hiểu sai những gì bạn muốn!)

Như một hệ quả, bất kỳ lệnh nào bạn viết đều phải hoạt động tốt: nó phải báo cáo lỗi cho thiết bị xuất chuẩn thay vì thiết bị xuất chuẩn (mã mẫu trong câu hỏi in lỗi thành thiết bị xuất chuẩn) và nó phải thoát với trạng thái khác không khi nó không thành công.

Tuy nhiên, tôi không còn coi đây là một thực hành tốt. set -eđã thay đổi ngữ nghĩa của nó với các phiên bản bash khác nhau, và mặc dù nó hoạt động tốt đối với một tập lệnh đơn giản, có rất nhiều trường hợp cạnh mà về cơ bản là không thể sử dụng được. (Hãy xem xét những điều như: set -e; foo() { false; echo should not print; } ; foo && echo ok Ngữ nghĩa ở đây có phần hợp lý, nhưng nếu bạn cấu trúc lại mã thành một hàm dựa trên cài đặt tùy chọn để chấm dứt sớm, bạn có thể dễ dàng bị cắn.)

 #!/bin/sh

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

hoặc là

#!/bin/sh

command1 && command2 && command3

1
Hãy lưu ý rằng trong khi giải pháp này là đơn giản nhất, nó không cho phép bạn thực hiện bất kỳ việc dọn dẹp nào khi thất bại.
Josh J

6
Dọn dẹp có thể được thực hiện với bẫy. (ví dụ: trap some_func 0sẽ thực thi some_functại lối ra)
William Pursell

3
Cũng lưu ý rằng ngữ nghĩa của errexit (set -e) đã thay đổi trong các phiên bản bash khác nhau và thường sẽ hoạt động bất ngờ trong khi gọi hàm và các cài đặt khác. Tôi không còn đề nghị sử dụng nó. IMO, tốt hơn là viết || exitrõ ràng sau mỗi lệnh.
William Pursell

87

Tôi có một bộ các chức năng kịch bản mà tôi sử dụng rộng rãi trên hệ thống Red Hat của mình. Họ sử dụng các chức năng hệ thống từ /etc/init.d/functionsđể in màu xanh lá cây [ OK ]và đỏ[FAILED] chỉ báo trạng thái .

Bạn có thể tùy ý đặt $LOG_STEPS biến thành tên tệp nhật ký nếu bạn muốn ghi nhật ký các lệnh thất bại.

Sử dụng

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

Đầu ra

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

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

đây là vàng nguyên chất. Mặc dù tôi hiểu cách sử dụng tập lệnh nhưng tôi không hoàn toàn nắm bắt từng bước, chắc chắn nằm ngoài kiến ​​thức về kịch bản bash của tôi nhưng tôi nghĩ dù sao đó cũng là một tác phẩm nghệ thuật.
kingmilo

2
Công cụ này có một tên chính thức? Tôi rất thích đọc một trang nam về phong cách bước / thử / ghi nhật ký tiếp theo này
ThorSummoner

Các hàm shell này dường như không có trên Ubuntu? Tôi đã hy vọng sử dụng cái này, một cái gì đó di động - mặc dù
ThorSummoner

@ThorSummoner, điều này có thể là do Ubuntu sử dụng Upstart thay vì SysV init và sẽ sớm sử dụng systemd. RedHat có xu hướng duy trì khả năng tương thích ngược trong thời gian dài, đó là lý do tại sao công cụ init.d vẫn còn đó.
dragon788

Tôi đã đăng một bản mở rộng về giải pháp của John và cho phép nó được sử dụng trên các hệ thống không phải RedHat như Ubuntu. Xem stackoverflow.com/a/54190627/308145
Mark Thomson

51

Để biết giá trị của nó, cách viết mã ngắn hơn để kiểm tra từng lệnh thành công là:

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

Nó vẫn tẻ nhạt nhưng ít nhất nó có thể đọc được.


Tôi không nghĩ về điều này, không phải là phương pháp tôi đã thực hiện nhưng nó rất nhanh và dễ đọc, cảm ơn vì thông tin :)
jwbensley

3
Để thực hiện các lệnh một cách im lặng và đạt được điều tương tự:command1 &> /dev/null || echo "command1 borked it"
Matt Byrne

Tôi là một fan hâm mộ của phương pháp này, có cách nào để thực thi nhiều lệnh sau OR không? Một cái gì đó giống nhưcommand1 || (echo command1 borked it ; exit)
AndreasKralj

38

Một cách khác chỉ đơn giản là kết hợp các lệnh cùng với nhau &&để lệnh đầu tiên không thành công ngăn phần còn lại thực thi:

command1 &&
  command2 &&
  command3

Đây không phải là cú pháp bạn yêu cầu trong câu hỏi, nhưng đó là một mẫu chung cho trường hợp sử dụng mà bạn mô tả. Nói chung, các lệnh phải chịu trách nhiệm in các lỗi để bạn không phải thực hiện thủ công (có thể có -qcờ để tắt các lỗi khi bạn không muốn chúng). Nếu bạn có khả năng sửa đổi các lệnh này, tôi sẽ chỉnh sửa chúng để hét lên thất bại, thay vì bọc chúng trong một cái gì đó khác.


Cũng lưu ý rằng bạn không cần phải làm:

command1
if [ $? -ne 0 ]; then

Bạn có thể nói một cách đơn giản:

if ! command1; then

Và khi bạn làm cần phải kiểm tra mã trở lại sử dụng một bối cảnh số học thay vì [ ... -ne:

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

34

Thay vì tạo các hàm chạy hoặc sử dụng set -e, hãy sử dụng 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

Cái bẫy thậm chí có quyền truy cập vào số dòng và dòng lệnh của lệnh đã kích hoạt nó. Các biến là $BASH_LINENO$BASH_COMMAND.


4
Nếu bạn muốn bắt chước một khối thử chặt chẽ hơn nữa, hãy sử dụng trap - ERRđể tắt bẫy ở cuối "khối".
Gordon Davisson

14

Cá nhân tôi rất thích sử dụng một cách tiếp cận nhẹ, như đã thấy ở đây ;

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

Ví dụ sử dụng:

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
Đừng chạy $*, nó sẽ thất bại nếu có bất kỳ đối số nào có khoảng trắng trong chúng; sử dụng "$@"thay thế. (Mặc dù $ * vẫn ổn trong echolệnh.)
Gordon Davisson

6

Tôi đã phát triển một triển khai thử và bắt gần như hoàn hảo trong bash, cho phép bạn viết mã như:

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

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

Bạn thậm chí có thể lồng các khối thử bắt bên trong chính chúng!

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

Mã này là một phần của khung / bash bash của tôi . Nó tiếp tục mở rộng ý tưởng thử và bắt với những thứ như xử lý lỗi với backtrace và ngoại lệ (cộng với một số tính năng hay khác).

Đây là mã chịu trách nhiệm chỉ để thử và bắt:

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
}

Hãy sử dụng, fork và đóng góp - đó là trên GitHub .


1
Tôi đã xem repo và sẽ không sử dụng nó cho bản thân mình, vì nó quá phù hợp với sở thích của tôi (IMO tốt hơn là sử dụng Python nếu một người cần nhiều sức mạnh trừu tượng hơn), nhưng chắc chắn là +1 lớn đối với tôi vì nó trông tuyệt vời.
Alexander Malakhov

Cảm ơn những lời tốt đẹp @AlexanderMalakhov. Tôi đồng ý về số lượng "ma thuật" - đó là một trong những lý do khiến chúng tôi động não phiên bản 3.0 đơn giản hóa của khung, sẽ dễ hiểu hơn nhiều, để gỡ lỗi, v.v. Có vấn đề mở về 3.0 về GH, nếu bạn muốn sứt mẻ trong suy nghĩ của bạn.
niieani

3

Xin lỗi vì tôi không thể đưa ra nhận xét cho câu trả lời đầu tiên Nhưng bạn nên sử dụng trường hợp mới để thực thi lệnh: cmdDefput = $ ($ @)

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

Đối với người dùng vỏ cá , những người vấp ngã trên chủ đề này.

Đặt foolà một hàm không "trả về" (echo) một giá trị, nhưng nó đặt mã thoát như bình thường.
Để tránh kiểm tra$status sau khi gọi hàm, bạn có thể làm:

foo; and echo success; or echo failure

Và nếu quá dài để phù hợp với một dòng:

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

1

Khi tôi sử dụng, sshtôi cần phân biệt giữa các sự cố do sự cố kết nối và mã lỗi của lệnh từ xa trong chế độ errexit( set -e). Tôi sử dụng chức năng sau:

# 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

Bạn có thể sử dụng giải pháp tuyệt vời của @ john-kugelman được tìm thấy ở trên trên các hệ thống không phải RedHat bằng cách nhận xét dòng này trong mã của mình:

. /etc/init.d/functions

Sau đó, dán đoạn mã dưới đây vào cuối. Công bố đầy đủ: Đây chỉ là bản sao và dán trực tiếp các bit có liên quan của tệp được đề cập ở trên được lấy từ Centos 7.

Đã thử nghiệm trên MacOS và 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
} 

0

Kiểm tra trạng thái theo cách chức năng

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)

}

Sử dụng:

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

Đầu ra

Status is 127
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.