iBetter Books
수정

ShellCheck로 정적 분석

코드를 실행하기 전에 문제를 찾아내는 것이 정적 분석입니다. ShellCheck는 쉘 스크립트 전용 린터(linter)로, 일반적인 실수, 버그 가능성, 보안 문제를 실행 없이 감지합니다.

ShellCheck가 잡아주는 문제들은 대부분 "작동은 하지만 특정 상황에서 틀리게 작동하는" 코드입니다. 파일명에 공백이 있을 때, 변수가 비어있을 때, 파이프라인이 실패할 때 달라지는 동작들입니다. 이런 문제를 사전에 잡아두면 예측 불가능한 런타임 오류를 크게 줄일 수 있습니다.

설치

# Ubuntu / Debiansudo apt install shellcheck# macOS (Homebrew)brew install shellcheck# Snapsudo snap install shellcheck

설치 후 버전을 확인합니다.

shellcheck --version# ShellCheck - shell script analysis tool# version: 0.10.0

사용법

# 단일 파일 검사shellcheck script.sh# 여러 파일 검사shellcheck *.sh# 특정 쉘 지정 (sh, bash, dash, ksh)shellcheck --shell=bash script.sh# 경고 형식 변경 (gcc, tty, json, checkstyle)shellcheck --format=gcc script.sh

주요 경고 유형과 해결법

ShellCheck 경고에는 SC 접두어와 번호가 붙습니다. 자주 마주치는 경고와 해결법을 표로 정리했습니다.

코드 경고 내용 문제 코드 수정 코드
SC2086 따옴표 없는 변수 echo $var echo "$var"
SC2046 따옴표 없는 명령 치환 ls $(get_dir) ls "$(get_dir)"
SC2034 사용되지 않는 변수 UNUSED="값" 삭제 또는 사용
SC2155 declare와 할당 분리 local var=$(cmd) local var; var=$(cmd)
SC2181 $? 직접 비교 if [ $? -ne 0 ] if ! command
SC2162 read에 -r 누락 read input read -r input

각 경고를 직접 살펴봅니다.

SC2086 — 따옴표 없는 변수

# 잘못된 코드filename="my file.txt"rm $filename          # 'my'와 'file.txt' 두 인수로 분리됨# 올바른 코드rm "$filename"        # 한 인수로 전달됨

변수에 공백이 들어있으면 따옴표 없이 사용하면 단어 분리가 발생합니다. ShellCheck이 가장 많이 잡아내는 경고입니다.

SC2155 — declare와 할당 분리

# 잘못된 코드local result=$(false_command)# local의 종료 코드는 항상 0이므로 false_command의 실패가 무시됨# 올바른 코드local resultresult=$(false_command)# 이제 false_command의 종료 코드가 정확히 전달됨

local이나 declare에서 명령 치환과 할당을 동시에 하면, local의 성공(종료 코드 0)이 명령 치환 실패를 덮어씁니다. set -e 환경에서 에러가 감지되지 않는 원인이 됩니다.

SC2181 — $? 직접 비교

# 잘못된 코드 (의도는 이해되지만 관용적이지 않음)some_commandif [ $? -ne 0 ]; then    echo "실패"fi# 올바른 코드if ! some_command; then    echo "실패"fi# 또는 종료 코드를 저장해야 한다면some_commandresult=$?if [[ $result -ne 0 ]]; then    echo "실패 코드: $result"fi

SC2162 — read에 -r 누락

# 잘못된 코드read line    # 백슬래시가 이스케이프 문자로 처리됨# 올바른 코드read -r line   # 백슬래시를 그대로 읽음

read -r 없이 읽으면 \n, \\ 같은 백슬래시 시퀀스가 해석됩니다. 파일 경로나 정규표현식을 읽을 때 예상치 못한 결과가 나올 수 있습니다.

실제 경고 예시

여러 경고가 섞인 스크립트를 ShellCheck로 검사합니다.

# 파일: shellcheck_test.sh#!/bin/bashprocess_files() {    local dir=$1    local result=$(ls $dir)   # SC2046, SC2086, SC2155    for file in $result; do   # SC2086        if [ $? -eq 0 ]; then  # SC2181 (이전 명령어 없음)            echo $file         # SC2086        fi    done}read input                    # SC2162process_files $input          # SC2086
shellcheck shellcheck_test.sh

실행 결과입니다.

shellcheck_test.sh:4:18: warning: Declare and assign separately to avoid masking return values. [SC2155]
shellcheck_test.sh:4:26: warning: Double quote to prevent globbing and word splitting. [SC2086]
shellcheck_test.sh:5:17: warning: Double quote to prevent globbing and word splitting. [SC2086]
shellcheck_test.sh:6:12: warning: Check exit code directly with if [ .. ], not indirectly with $?. [SC2181]
shellcheck_test.sh:7:18: warning: Double quote to prevent globbing and word splitting. [SC2086]
shellcheck_test.sh:12:5: warning: Use 'read -r' to avoid mangling backslashes. [SC2162]
shellcheck_test.sh:13:17: warning: Double quote to prevent globbing and word splitting. [SC2086]

shellcheck disable — 예외 처리

경고를 알고 있지만 의도적으로 무시하고 싶을 때는 주석으로 비활성화합니다.

# 특정 줄만 비활성화# shellcheck disable=SC2086echo $deliberately_unquoted# 다음 줄에도 적용# shellcheck disable=SC2086,SC2046ls $dir/$(get_subdir)

주석은 경고를 억제하는 줄 바로 위에 붙입니다. 이유를 같이 남기는 것이 좋습니다.

# 이 명령은 의도적으로 word splitting을 활용합니다# shellcheck disable=SC2086eval $command_string

CI에서 ShellCheck 실행

GitHub Actions에서 ShellCheck를 자동으로 실행하는 워크플로우입니다.

# 파일: .github/workflows/shellcheck.ymlname: ShellCheckon:  push:    branches: [ main, develop ]  pull_request:    branches: [ main ]jobs:  shellcheck:    runs-on: ubuntu-latest    steps:      - name: 코드 체크아웃        uses: actions/checkout@v4      - name: ShellCheck 설치        run: sudo apt-get install -y shellcheck      - name: ShellCheck 실행        run: |          find . -name "*.sh" -not -path "./.git/*" | \            xargs shellcheck --severity=warning

--severity=warning을 설정하면 warning 이상 레벨만 검사합니다. --severity=error로 올리면 치명적 오류만 CI를 실패시키고 경고는 허용합니다.

실습 — 경고가 많은 스크립트를 ShellCheck로 개선

아래 스크립트를 ShellCheck로 검사하고 모든 경고를 해결합니다.

# 파일: before_shellcheck.sh#!/bin/bashBACKUP_DIR=/tmp/backupbackup_file() {    local src=$1    local dst=$(mktemp $BACKUP_DIR/backup.XXXXXX)    cp $src $dst    if [ $? -eq 0 ]; then        echo "백업 완료: $dst"    fi}echo "백업할 파일을 입력하세요:"read filebackup_file $file

ShellCheck 실행 후 개선된 스크립트입니다.

#!/usr/bin/env bash# 수정: before_shellcheck.shset -euo pipefailBACKUP_DIR="/tmp/backup"backup_file() {    local src="$1"    local dst    dst=$(mktemp "${BACKUP_DIR}/backup.XXXXXX")    if cp "$src" "$dst"; then        echo "백업 완료: $dst"    else        echo "백업 실패: $src" >&2        return 1    fi}mkdir -p "$BACKUP_DIR"echo "백업할 파일을 입력하세요."read -r filebackup_file "$file"

변경 사항을 정리하면 다음과 같습니다. 모든 변수에 따옴표를 추가했습니다. local과 명령 치환을 분리했습니다. [ $? -eq 0 ] 대신 if cp ...를 직접 사용했습니다. read -r을 사용했습니다. #!/bin/bash 대신 #!/usr/bin/env bash를 사용했습니다. ShellCheck를 통과한 스크립트는 다양한 환경에서 훨씬 안정적으로 작동합니다.