2P by GN⁺ | ★ favorite | 댓글 1개
  • Bash 스크립트가 예상과 다르게 동작할 때는 실행되는 명령을 그대로 보는 방식만으로도 원인 추적이 쉬워짐
  • set -x 는 각 줄을 변수 확장 후 출력해, 스크립트가 실제로 어떤 명령을 실행하는지 확인하게 해줌
  • 명령줄에서 bash -x script.sh 로 실행하면 script.sh 상단에 set -x를 넣은 것과 같은 효과를 냄
  • DEBUG 트랩read를 함께 쓰면 각 줄 실행 전에 멈춰 파일명, 줄 번호, 다음 명령을 확인할 수 있음
  • die() { echo $1 >&2; exit 1; } 함수는 실패한 명령 뒤에 붙여 표준 에러 메시지 출력 후 종료하는 흐름을 단순하게 만듦

실행 흐름을 눈으로 확인하기

  • set -x 는 스크립트가 실행하는 줄을 출력하고, 변수는 확장된 값으로 보여줌
  • 스크립트 상단에 set -x를 넣어 사용할 수 있음
  • 같은 동작은 명령줄에서도 실행 가능함
    • $ bash -x script.sh
    • 이는 script.sh 맨 위에 set -x를 넣는 것과 같음

줄마다 멈춰 확인하기

  • DEBUG 트랩은 각 코드 줄이 실행되기 전에 동작함
  • 스크립트 시작 부분에 아래 코드를 넣으면, 다음 명령을 실행하기 전에 Enter 입력을 기다림
    • trap '(read -p "\[$BASH_SOURCE: $LINENO] $BASH_COMMAND")' DEBUG
    • read -p는 메시지를 출력하고 Enter 입력을 기다림
    • $BASH_SOURCE는 스크립트 파일 이름임
    • $LINENO는 줄 번호임
    • $BASH_COMMAND는 다음에 실행될 명령임

실패 시 메시지를 남기고 종료하기

  • die 함수는 명령 실패 시 메시지를 출력하고 프로그램을 종료하는 데 사용할 수 있음
    • die() { echo $1 >&2; exit 1; }
    • 실패할 수 있는 명령 뒤에 some_command || die "oh no!"처럼 붙이면 됨
  • 이 함수는 메시지를 표준 에러로 보내고 exit 1로 종료함

댓글과 토론

Hacker News 의견들
  • ZFSBootMenu에서 디버깅을 돕기 위해 몇 가지 괜찮은 자체 함수를 씀
    코드 곳곳에 zdebug 로깅 함수가 들어가 있음: https://github.com/zbm-dev/zfsbootmenu/blob/master/zfsbootme...
    디버그 로깅을 켠 상태에서 메인 메뉴에서 Ctrl-T를 누르면 이런 화면이 나옴: https://i.imgur.com/Ge75zkP.png
    https://github.com/zbm-dev/zfsbootmenu/blob/master/zfsbootme...로 켤 수 있는 플레임그래프 프로파일링도 있고, 직렬 포트로 덤프한 데이터를 다시 조립하면 https://raw.githubusercontent.com/zbm-dev/zfsbootmenu/master... 같은 그래프를 만들 수 있음
    Bash는 의외로 유연함
  • set -x를 쓸 때 PS4='+ ${BASH_SOURCE:-}:${FUNCNAME[0]:-}:L${LINENO:-}: '가 아주 유용함
    이렇게 하면 파일명, 함수명, 줄 번호가 표시돼서 큰 Bash 스크립트를 디버깅할 때 꽤 도움이 됨
  • shellcheck도 추천함. 문제를 직접 잡아내지 못하더라도 잠재적인 문제를 알려줌
    또 스크립트를 다른 언어로 다시 쓰는 것도 추천함. 회사에서는 Bash 스크립트를 Rust로 바꾸고 있는데, 진입 비용은 높지만 결과 코드는 훨씬 유지보수하기 쉽고 신뢰도도 높음
    Bash는 빠른 스크립트에는 여전히 좋지만, 100줄쯤 넘어가면 더 강한 보장을 주는 언어를 쓸 만함
    • 동의하지만, 그 얘기를 CI/CD 엔지니어링과 YAML 파이프라인 쪽에도 해야 함
  • 종료 코드를 이런 식으로 쓰면 디버깅을 더 개선할 수 있음
    die()는 오류 메시지를 표준 오류로 출력한 뒤 지정한 오류 코드로 종료하는 도우미 함수임
    더 많은 셸 스크립트 종료 코드와 도우미 함수는 여기 있음: https://github.com/SixArm/unix-shell-script-kit/blob/main/un...
    • 좋은 목록이고, 숙련된 사용자라면 다들 자기만의 도우미 함수가 있을 것 같음
      다만 그 die의 철학에는 약간 아쉬움이 있음. die 함수는 기본적으로 실패한 명령의 종료 코드를 전달하고, 그 명령의 오류 출력도 숨기지 않아야 함
      큰 스크립트에서 명령 실패에 내가 직접 의미를 부여하고 싶다면 더 특화된 die를 따로 쓸 것임. 내 die는 대략 __errex "$?" "${LINENO}" "$0" 형태로 치명적 오류, 줄 번호, 스크립트명, 메시지를 출력하고 해당 종료 코드로 끝냄
  • Bash 함수를 많이 쓴다면 일종의 스택 추적을 만드는 것도 가능함
    한 구현 예시는 여기 있음: https://github.com/TritonDataCenter/sdc-headnode/blob/master...
    • 또 다른 스택 추적 구현이 있음: https://github.com/runag/runag/blob/main/lib/fail.sh
      some-command || fail "message"처럼 쓰면 some-command가 0이 아닌 종료 상태를 반환할 때 스택 추적을 만들고 셸을 종료함
      함수에서 스택 추적을 만들고 반환하고 싶다면 some-command || softfail "message" || return $?처럼 쓸 수 있음
  • Bash가 여전히 사실상의 셸 스크립팅 언어인 이유가 레거시 관성 말고도 있는지 궁금함
    필요한 일은 할 수 있지만 투박하고 문법도 끔찍함. 스크립트가 어느 정도 크기나 복잡도에 도달하면 제대로 된 언어로 옮기게 강제하니, 어쩌면 의도된 설계일 수도 있겠다는 생각이 듦
    • 레거시 사용량이 인기에 큰 부분을 차지하는 건 맞음
      현대 배포판이면 대체로 꽤 최신 버전의 Bash가 있고, 배열 같은 걸 쓰려는 게 아니라면 버전도 크게 신경 쓰지 않아도 됨
      Bash의 매력은 다른 언어와 도구 사이에서 차지하는 위치에 있음. 다른 도구들을 엮기에 이상적이고 운영체제와 충분히 가까워서 편하며, Python처럼 라이브러리 설치를 요구하지도 않음
      더 복잡한 스크립팅은 Python 같은 언어로 옮기라는 말을 자주 듣지만, 장기적으로는 도움이 안 될 수 있는 복잡성 층이 추가됨. 20년 전에 쓴 Bash 스크립트는 여전히 잘 돌아가지만, 20년 전 Python 프로그램은 버전 문제를 겪을 가능성이 큼
    • Bourne 셸 스크립팅은 충분히 괜찮아서 대체가 거의 불가능함
      Plan 9의 rc는 좀 더 깔끔하지만, “비슷한데 더 깔끔한 것”만으로는 아무도 갈아타지 않음. 지금 당장 https://pkgsrc.se/shells에서 비슷하지만 더 나은 걸 설치할 수 있는데도 안 쓰고 있고, 다른 사람에게도 실행 방식이 달라지지 않음
      자리 잡은 기술을 대체하려면 핵심 측면에서 몇 배는 더 좋아야 함. Plan 9도 UNIX 계열보다 낫지만 대체할 만큼 충분히 좋지는 않았음
      Bourne 셸 스크립팅의 틈새를 대체할 만큼 충분히 좋은 무언가를 만들기는 어려움. 그 정도로 좋아지기 전에 이미 Perl, Python, Ruby 같은 진짜 스크립트 언어의 생태적 지위나 문제 영역으로 넘어가 버림
      좁은 문제 영역에서는 지역 최적해가 공기를 다 빨아들여, 이론적 전역 최적해에 가까운 경쟁자가 나오기 어렵게 만듦
    • 정말로 레거시와 관성 때문이라고 봄
      최근 추가 기능들이 sh/Bash 위에 잘 얹히긴 했지만, 결국 셸 스크립팅은 목적을 위한 수단이고 일반 프로그래밍 언어보다 훨씬 느리게 진화해야 함
      Bash/sh의 핵심 특징은 반엔트로피적이라는 데 있음. 개발이나 진화가 거의 없어서 의존성이나 새 기능 때문에 골치 아플 가능성이 낮고, 20년 전에 되던 것들이 계속 기본 도구로 남음
      그 설계상 변화에 거부적인 시스템이 되고, 한계에 부딪히면 사람들이 그 바깥으로 나가게 만드는 유인이 생김
    • 그게 정말 Bash인지 확실하지 않음
      FreeBSD의 스크립트 대부분은 sh용으로 작성되어 있고, sh는 POSIX 표준의 일부라 훨씬 널리 지원된다고 느낌. Bash는 그냥 인기 있는 편이라고 봄
    • 어디에나 있다는 점이 큼
      하지만 Bash가 너무 별로라서 Groovy 스크립트를 쓰려고 이름공간을 줄인 유틸리티를 잔뜩 만들었음. IDE로 개발할 수 있고, 라이브러리 시스템도 안전하며, Groovy가 Java의 불편함을 거의 다 다듬어줘서 훨씬 나았음
  • 꽤 강력한 gdb 스타일 실제 디버거도 있음: https://bashdb.sourceforge.net/
  • 약간 관련된 홍보를 하자면, 예전에 중간 출력을 보존하는 Bash 파이프라인 디버거를 만들었음
    몇 가지 제한은 있지만 일반적으로 유용할 수 있음: https://github.com/ketancmaheshwari/pd
  • die() 기법은 좋지만 Bash에는 짜증나는 특성이 있음. 서브셸 안에서 exit 하려고 하면 서브셸만 종료되고 나머지 스크립트는 계속 실행됨
    예를 들어 cat myfile | while read line; do ... die "Found match" ... done 같은 파이프라인 안에서 die를 호출하면 뒤의 echo "I don't want this line"이 여전히 출력됨
    서브셸을 피할 수 있는 경우가 많고, 이 예시는 shellcheck가 UUOC를 지적하는 게 맞으며 그걸 고치면 서브셸 안 die 문제도 해결됨
    하지만 가끔은 서브셸을 피할 수 없거나 피하려면 스크립트가 너무 복잡해짐. 그럴 때는 스크립트 시작 부분에서 MYPID=$$로 PID를 잡아두고 die() { echo "$1" >&2; kill -9 $MYPID; exit 1; }처럼 죽일 수 있음
    물론 이것도 절충이 있음. 이렇게 죽이는 방식은 꽤 거칠고, 이유는 모르겠지만 완전히 신뢰할 만하지도 않았음
    • set -e만 추가해도 서브셸이 0이 아닌 오류 코드로 종료될 때 스크립트가 같이 종료됨
      어떤 셸 스크립트에서든 set -e를 빼야 할 이유가 잘 떠오르지 않음
    • 그렇게 PID를 죽이면 좀비 프로세스가 생길 수 있지 않을까?
  • Bash 스크립트 맨 위에는 항상 set -euxo pipefail을 넣음
    조건 테스트가 좀 더 어려워지긴 하지만, 특히 pipefail 하나만으로도 여러 번 제값을 했음