여러 명령의 Bash 종료 상태를 효율적으로 확인


260

'try'문과 같지만 bash 내에 여러 명령에 대해 pipefail과 비슷한 것이 있습니까? 나는 이런 식으로하고 싶다 :

echo "trying stuff"
try {
    command1
    command2
    command3
}

어느 시점에서든 명령이 실패하면 해당 명령의 오류를 제거하고 반향하십시오. 나는 다음과 같은 일을하고 싶지 않습니다.

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

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

그리고 등등 ... 또는 같은 것 :

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

내가 믿는 (각각 틀렸다면 교정) 각 명령의 주장이 서로 간섭 할 것이기 때문입니다. 이 두 가지 방법은 엄청나게 길고 불쾌한 것처럼 보이므로 더 효율적인 방법을 원합니다.


2
에 살펴보세요 비공식 bash는 엄격 모드 : set -euo pipefail.
Pablo A

1
@PabloBianchi set -e끔찍한 아이디어입니다. 참조 BashFAQ # (105)의 운동 이 소개 예상치 못한 가장자리의 경우 몇 가지 논의를, 및 / 또는에서 '(쉘 버전')를 구현 다른 쉘 사이의 호환성을 보여주는 비교 in-ulm.de/~mascheck/various/set -e .
Charles Duffy

답변:


274

명령을 시작하고 테스트하는 기능을 작성할 수 있습니다. 명령으로 설정된 환경 변수를 가정 command1하고 가정하십시오 command2.

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

mytest "$command1"
mytest "$command2"

32
사용하지 마십시오. $*인수에 공백이 있으면 실패합니다. 사용하는 "$@"대신. 마찬가지로 명령 $1에서 따옴표 안에 넣습니다 echo.
Gordon Davisson

82
또한 test내장 명령이므로 이름 을 피합니다 .
John Kugelman

1
이것이 내가 함께한 방법입니다. 솔직히 말해서, 나는 원래 게시물에서 충분히 명확하지 않다고 생각하지만이 방법을 사용하면 자체 '테스트'기능을 작성할 수 있으므로 거기에서 수행 된 작업과 관련된 오류 작업을 수행 할 수 있습니다. 스크립트. 감사합니다 :)
jwbensley

7
마지막으로 실행 된 명령이 'echo'이므로 오류가 발생한 경우 test ()에서 반환 한 종료 코드가 항상 0을 반환하지 않습니까? $ 값을 저장해야 할 수도 있습니까? 먼저.
magiconair

2
이것은 좋은 생각이 아니며 나쁜 연습을 장려합니다. 의 간단한 경우를 고려하십시오 ls. 호출 ls foo하여 양식의 오류 메시지가 표시 ls: foo: No such file or directory\n되면 문제점을 이해 한 것입니다. 대신에 당신은 ls: foo: No such file or directory\nerror with ls\n불필요한 정보로 인해 혼란스러워집니다. 이 경우, 불필요한 것이 사소한 것이라고 주장하기는 쉽지만 빨리 자랍니다. 간결한 오류 메시지가 중요합니다. 그러나 더 중요한 것은 이러한 유형의 래퍼는 너무 많은 작성자가 좋은 오류 메시지를 완전히 생략하도록 권장하는 것입니다.
William Pursell

185

"오류를 제거하고 반향"한다는 것은 무엇을 의미합니까? 명령이 실패하자마자 스크립트를 종료하려면 다음을 수행하십시오.

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

스크립트 시작시 (아래 경고에 유의) 오류 메시지를 에코하지 마십시오. 실패한 명령이 처리하도록하십시오. 다시 말해, 다음과 같은 경우 :

#!/bin/sh

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

stderr에 오류 메시지를 인쇄하는 동안 command2가 실패하면 원하는 것을 달성 한 것 같습니다. (내가 원하는 것을 잘못 해석하지 않는 한!)

결론적으로, 작성하는 모든 명령은 제대로 작동해야합니다. stdout 대신 stderr에 오류를보고해야하며 (질문의 샘플 코드는 stdout에 오류를 인쇄 함) 실패하면 0이 아닌 상태로 종료해야합니다.

그러나 나는 더 이상 이것이 좋은 습관이라고 생각하지 않습니다. set -e다른 버전의 bash로 의미를 변경했으며 간단한 스크립트에서는 잘 작동하지만 본질적으로 사용할 수없는 많은 경우가 있습니다. ( set -e; foo() { false; echo should not print; } ; foo && echo ok 여기서 의미를 고려하십시오 : 여기서 의미론은 다소 합리적이지만, 조기 종료 옵션 설정에 의존하는 함수로 코드를 리팩터링하면 쉽게 물릴 수 있습니다.) IMO 작성하는 것이 좋습니다 :

 #!/bin/sh

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

또는

#!/bin/sh

command1 && command2 && command3

1
이 솔루션이 가장 단순하지만 실패시 정리를 수행 할 수는 없습니다.
Josh J

6
트랩으로 정리할 수 있습니다. (예 : trap some_func 0some_func
종료시

3
또한 errexit (set -e)의 의미가 다른 버전의 bash에서 변경되었으며 함수 호출 및 기타 설정 중에 예기치 않게 작동합니다. 더 이상 사용을 권장하지 않습니다. IMO는 || exit각 명령 후에 명시 적으로 작성하는 것이 좋습니다 .
윌리엄 Pursell

87

Red Hat 시스템에서 광범위하게 사용하는 일련의 스크립팅 기능이 있습니다. 그들은 시스템 기능을 사용하여 /etc/init.d/functions녹색 [ OK ]과 빨간색 을 인쇄합니다.[FAILED] 상태 표시기 합니다.

선택적으로 $LOG_STEPS실패한 명령을 기록하려는 경우 변수를 로그 파일 이름 .

용법

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

산출

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
}

이것은 순금입니다. 스크립트를 사용하는 방법을 이해하는 동안 각 단계를 완전히 이해하지는 못하지만 분명히 bash 스크립팅 지식을 벗어 났지만 그럼에도 불구하고 예술 작품이라고 생각합니다.
kingmilo

2
이 도구는 공식적인 이름을 가지고 있습니까? 이 단계 / 시도 / 다음 로깅 스타일에 대한 매뉴얼 페이지를 읽고 싶습니다
ThorSummoner

이 쉘 기능은 우분투에서 사용할 수없는 것 같습니다. 나는 이것을 휴대용처럼 사용하기를 바랐다
ThorSummoner

@ThorSummoner, Ubuntu는 SysV init 대신 Upstart를 사용하고 곧 systemd를 사용할 것이기 때문입니다. RedHat은 이전 버전과의 호환성을 오랫동안 유지하는 경향이 있으므로 init.d 항목이 여전히 존재합니다.
dragon788

John의 솔루션에 대한 확장을 게시했으며 우분투와 같은 비 RedHat 시스템에서 사용할 수 있습니다. 참조 stackoverflow.com/a/54190627/308145
마크 톰슨

51

가치있는 것을 위해, 각 명령의 성공 여부를 확인하는 코드를 작성하는 짧은 방법은 다음과 같습니다.

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

여전히 지루하지만 적어도 읽을 수 있습니다.


이것을 생각하지 않았다, 내가 갔던 방법이 아니라 정보 덕분에 빠르고 읽기 쉽다. :)
jwbensley

3
명령을 자동으로 실행하고 동일한 작업을 수행하려면 :command1 &> /dev/null || echo "command1 borked it"
Matt Byrne

나는이 방법의 팬입니다 .OR 다음에 여러 명령을 실행하는 방법이 있습니까? command1 || (echo command1 borked it ; exit)
AndreasKralj

38

대안은 단순히 명령을 함께 결합하여 &&실패한 첫 번째 명령 이 나머지를 실행하지 못하게하는 것입니다.

command1 &&
  command2 &&
  command3

이것은 질문에서 요구 한 구문이 아니지만 설명하는 사용 사례의 일반적인 패턴입니다. 일반적으로 명령은 인쇄 실패에 대한 책임이 있으므로 수동으로 -q할 필요가 없습니다 (원하는 경우 오류를 끄는 플래그가있을 수 있음). 이러한 명령을 수정하는 기능이 있다면 다른 명령으로 감싸기보다는 실패시 소리를 내도록 편집합니다.


또한 다음을 수행 할 필요가 없습니다.

command1
if [ $? -ne 0 ]; then

간단히 말할 수 있습니다.

if ! command1; then

당신이 때 어떻게 검사 할 필요가 리턴 코드는 산술 컨텍스트 대신를 사용합니다 [ ... -ne:

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

34

러너 함수를 작성하거나을 set -e사용하는 대신 다음을 사용하십시오 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

트랩은 심지어 행 번호와이를 트리거 한 명령의 명령 행에 액세스 할 수 있습니다. 변수는 $BASH_LINENO$BASH_COMMAND입니다.


4
try 블록을 더 자세히 모방 trap - ERR하려면 "block"끝에서 트랩을 끄십시오.
Gordon Davisson

14

개인적으로 나는 여기 에서 볼 수 있듯이 가벼운 접근법을 선호합니다 .

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

사용법 예 :

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
실행하지 마십시오. $*인수에 공백이 있으면 실패합니다. 사용하는 "$@"대신. ( echo명령 에 $ *는 괜찮지 만 )
Gordon Davisson

6

bash에서 거의 완벽한 try & catch 구현을 개발하여 다음과 같은 코드를 작성할 수 있습니다.

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

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

try-catch 블록을 자체 안에 중첩시킬 수도 있습니다!

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

코드는 내 배쉬 보일러 플레이트 / 프레임 워크 의 일부입니다 . 또한 역 추적 및 예외를 포함한 오류 처리 (및 기타 유용한 기능)를 사용하여 try & catch 아이디어를 확장합니다.

try & catch를 담당하는 코드는 다음과 같습니다.

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
}

자유롭게 사용하고 포크하고 기여하십시오 -GitHub있습니다 .


1
내 취향에 그것의 너무 많은 마법 때문에 (IMO는 파이썬을 사용하는 것이 좋습니다 경우 하나 개의 요구에 더 추상화 전원), REPO을 검토 한 결과이 나 자신을 사용하는 거 아니지만, 확실히 큰 +1 그냥 멋진 외모 때문에 나를에서.
Alexander Malakhov

@AlexanderMalakhov라는 친절한 단어에 감사드립니다. 나는 "매직"의 양에 동의합니다. 이것이 우리가 이해하기 쉽고 디버깅하기가 훨씬 쉬운 프레임 워크의 단순화 된 3.0 버전을 브레인 스토밍하는 이유 중 하나입니다. 당신은 당신의 생각에 칩을 원할 것입니다.
niieani

3

첫 번째 답변에 대한 의견을 제시 할 수 없어서 죄송합니다. 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

들어 물고기 쉘 이 스레드에서 우연히 발견 사용자.

하자 foo없다 "반환"(에코) 값을 수행하는 함수가 될 수 있지만, 평소와 같이 종료 코드를 설정합니다. 함수를 호출 한 후
확인하지 않으려면 $status다음을 수행하십시오.

foo; and echo success; or echo failure

한 줄에 맞추기가 너무 길면 :

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

1

사용할 때 ssh연결 문제로 인한 문제와 errexit( set -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 이외의 시스템에서 위 의 @ john-kugelman의 멋진 솔루션 을 다음 코드에서 주석 처리하여 사용할 수 있습니다 .

. /etc/init.d/functions

그런 다음 아래 코드를 끝에 붙여 넣으십시오. 전체 공개 : Centos 7에서 가져온 위에서 언급 한 파일의 관련 비트를 직접 복사하여 붙여 넣습니다.

MacOS 및 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

기능적인 방식으로 상태 확인

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)

}

용법:

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

산출

Status is 127
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.