여러 명령의 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.