Bash 린팅은(는) 무엇을 하나요?
ShellCheck와 shfmt를 통합한 Bash 스크립트 품질 보증
ShellCheck
설치
# Debian/Ubuntu
apt install shellcheck
# macOS
brew install shellcheck
# 소스에서 빌드
cabal update && cabal install ShellCheck
기본 사용법
# 단일 파일 검사
shellcheck script.sh
# 여러 파일 검사
shellcheck *.sh
# 특정 셸 방언 지정
shellcheck --shell=bash script.sh
shellcheck --shell=sh script.sh
# 특정 규칙 제외
shellcheck --exclude=SC2086 script.sh
shellcheck --exclude=SC2086,SC2046 script.sh
# 출력 형식
shellcheck --format=gcc script.sh # GCC 스타일
shellcheck --format=json script.sh # 도구 연동용 JSON
shellcheck --format=diff script.sh # 통합 diff
일반적인 ShellCheck 코드
| 코드 | 문제 | 해결 방법 |
|---|---|---|
| SC2086 | 글로빙/분할 방지를 위해 따옴표 사용 | "$var" |
| SC2046 | 명령어 치환을 따옴표로 감싸기 | "$(cmd)" |
| SC2006 | 백틱 대신 $() 사용 | $(cmd) |
| SC2034 | 변수가 사용되지 않음 | 제거하거나 export |
| SC2155 | 선언과 할당을 분리 | local var; var=$(...) 형식으로 |
| SC2164 | cd ... || exit 사용 | cd 실패 처리 |
| SC2181 | 종료 상태를 직접 확인 | if cmd; then |
| SC2129 | 쓰기 작업 그룹화 고려 | { } > file 사용 |
| SC1090 | 소싱된 파일을 추적할 수 없음 | # shellcheck source=경로 사용 |
| SC2154 | 변수가 선언되지 않음 | 초기화 또는 선언 |
ShellCheck 지시문
# 다음 줄에서만 비활성화
# shellcheck disable=SC2086
echo $unquoted_var
# 전체 파일에서 비활성화 (파일 상단)
# shellcheck disable=SC2086,SC2046
# 소싱할 파일의 경로 지정
# shellcheck source=./lib/functions.sh
source "$SCRIPT_DIR/lib/functions.sh"
# 셸 방언 지정
# shellcheck shell=bash
# 블록 비활성화 (지원 안 됨 - 줄 단위로 사용)
인라인 지시문 패턴
# 설명과 함께 특정 경고 비활성화
# shellcheck disable=SC2034 # 스크립트에서 소싱하여 사용하는 변수
readonly CONFIG_VERSION="1.0"
# 여러 코드 비활성화
# shellcheck disable=SC2086,SC2046
result=$(echo $var)
# 동적 경로에 대한 소스 지시문
# shellcheck source=/dev/null
source "${DYNAMIC_PATH}/config.sh"
shfmt
설치
# macOS
brew install shfmt
# Go 설치
go install mvdan.cc/sh/v3/cmd/shfmt@latest
# Snap
snap install shfmt
# 바이너리 다운로드
# https://github.com/mvdan/sh/releases 참조
기본 사용법
# 포맷팅하여 stdout으로 출력
shfmt script.sh
# 파일 자체를 포맷팅
shfmt -w script.sh
# 포맷팅 검사 (포맷팅 안 되었으면 exit 1)
shfmt -d script.sh
# 디렉토리 재귀적 포맷팅
shfmt -w .
shfmt -w scripts/
포맷팅 옵션
# 들여쓰기
shfmt -i 2 script.sh # 2칸 공백 들여쓰기
shfmt -i 4 script.sh # 4칸 공백 들여쓰기
shfmt -i 0 script.sh # 탭 (기본값)
# 이진 연산자를 줄 시작에 배치
shfmt -bn script.sh
# switch/case 들여쓰기
shfmt -ci script.sh
# 리다이렉션 연산자 뒤에 공백 추가
shfmt -sr script.sh
# 열 정렬 유지
shfmt -kp script.sh
# 함수 여는 중괄호를 별도 줄에 배치
shfmt -fn script.sh
# 조합
shfmt -i 4 -ci -bn script.sh
설정 (.editorconfig)
# .editorconfig
[*.sh]
indent_style = space
indent_size = 4
shell_variant = bash
binary_next_line = true
switch_case_indent = true
space_redirects = true
변환 예시
shfmt 적용 전:
if [ -f "$file" ];then
echo "exists"
fi
for i in 1 2 3;do
process $i
done
shfmt -i 4 -ci 적용 후:
if [ -f "$file" ]; then
echo "exists"
fi
for i in 1 2 3; do
process $i
done
Pre-commit 통합
.pre-commit-config.yaml
repos:
- repo: https://github.com/koalaman/shellcheck-precommit
rev: v0.9.0
hooks:
- id: shellcheck
args: ["--severity=warning"]
- repo: https://github.com/scop/pre-commit-shfmt
rev: v3.7.0-1
hooks:
- id: shfmt
args: ["-i", "4", "-ci", "-w"]
# 대안: 로컬 훅
- repo: local
hooks:
- id: shellcheck
name: shellcheck
entry: shellcheck
language: system
types: [shell]
args: ["--severity=warning", "-x"]
- id: shfmt
name: shfmt
entry: shfmt
language: system
types: [shell]
args: ["-i", "4", "-ci", "-w"]
Pre-commit 실행
# 훅 설치
pre-commit install # 또는: prek install
# 모든 파일에 대해 실행
pre-commit run --all-files # 또는: prek run --all-files
# 특정 훅 실행
pre-commit run shellcheck --all-files
pre-commit run shfmt --all-files
# 특정 파일에 대해 실행
pre-commit run --files script.sh
CI/CD 통합
GitHub Actions
name: Shell 린트
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: ShellCheck 실행
uses: ludeeus/action-shellcheck@master
with:
severity: warning
- name: shfmt로 포맷팅 검사
uses: mvdan/github-action-shfmt@master
with:
flags: -d -i 4 -ci
GitLab CI
shellcheck:
image: koalaman/shellcheck-alpine:stable
script:
- find . -name "*.sh" -exec shellcheck {} +
shfmt:
image: mvdan/shfmt:latest
script:
- shfmt -d -i 4 -ci .
일반적인 문제 수정
SC2086: 분할 방지를 위해 따옴표 사용
# 나쁜 예
echo $var
# 좋은 예
echo "$var"
printf '%s\n' "$var"
SC2155: 선언과 할당 분리
# 나쁜 예 - 종료 상태를 가림
local var=$(some_command)
# 좋은 예
local var
var=$(some_command)
SC2164: cd || exit 사용
# 나쁜 예
cd "$dir"
rm -rf *
# 좋은 예
cd "$dir" || exit 1
rm -rf *
# 또는 서브쉘 사용
(cd "$dir" && rm -rf *)
SC2181: 종료 상태 직접 확인
# 나쁜 예
command
if [ $? -eq 0 ]; then
# 좋은 예
if command; then
SC1090/SC1091: 소스 관련 문제
# 동적 소스에 지시문 추가
# shellcheck source=/dev/null
source "$DYNAMIC_PATH/lib.sh"
# 또는 실제 경로 지정
# shellcheck source=./lib/functions.sh
source "$SCRIPT_DIR/lib/functions.sh"
편집기 통합
VS Code
Timon Wong의 "ShellCheck" 확장 프로그램을 설치하세요.
// settings.json
{
"shellcheck.enable": true,
"shellcheck.run": "onSave",
"shellcheck.executablePath": "shellcheck",
"editor.formatOnSave": true,
"[shellscript]": {
"editor.defaultFormatter": "foxundermoon.shell-format"
}
}
Vim/Neovim
" ALE 사용
let g:ale_linters = {'sh': ['shellcheck']}
let g:ale_fixers = {'sh': ['shfmt']}
let g:ale_sh_shfmt_options = '-i 4 -ci'
" coc.nvim 사용
" coc-sh 확장 설치
모범 사례
- ShellCheck를 조기에 실행 - 편집기와 CI에 통합
- 문제를 수정하고, 억제하지 마세요 - 타당한 이유 없이 비활성화 금지
- 비활성화한 경우 문서화 - 규칙을 비활성화한 이유를 설명
- 심각도 수준 사용 - CI에서는
--severity=warning사용 - 일관된 포맷팅 - pre-commit에서 shfmt 사용
- 도구 버전 고정 - CI/pre-commit에서 버전을 고정