개요
탭(Tab) 자동완성은 명령어 입력 시 반복 입력을 줄여주고 사용성을 크게 향상시킵니다. 이 문서에서는 간단한 예제로 zsh와 Bash용 자동완성 핸들러를 만들고, 이를 포터블하게 배포하는 방법과 실제 운영 환경에서 고려해야 할 모범 사례, 테스트 및 트러블슈팅 체크리스트까지 설명합니다.
정의 한 줄: 자동완성은 사용자가 탭을 눌렀을 때 가능한 입력 옵션을 제시하는 기능입니다.
중요: 이 가이드의 모든 코드 블록은 실제로 동작하는 예제입니다. 파일에 저장해 실행하거나 셸 설정에서 source하면 테스트할 수 있습니다.
탭 자동완성의 동작 원리
자동완성은 크게 세 가지 유형으로 나눌 수 있습니다.
- 명령어 자동완성: PATH 환경변수를 검사해 입력한 접두사와 일치하는 실행 파일을 제안합니다.
- 파일/경로 자동완성: 현재 디렉터리 또는 절대경로에 있는 파일과 디렉터리를 제안합니다.
- 인수(Argument) 자동완성: 커맨드 이름 이후에 나오는 옵션, 서브커맨드, 또는 동적 제안을 처리합니다. 이 글은 주로 세 번째 유형을 다룹니다.
자동완성 시스템은 셸마다 내부 작동 방식이 다릅니다. zsh는 완성 시스템(compinit/compadd)이 잘 발달되어 있고, Bash는 개발자에게 더 많은 제어와 책임을 요구합니다.
예제 커맨드: todos
예제로 사용할 간단한 스크립트는 개인 TODO 파일을 관리하는 명령어입니다. 실제 기능은 단순하며, 아래 발췌는 관련 부분만 보여줍니다.
#!/usr/bin/env zsh
FILE="$HOME/.local/state/todos/data"
if [ "$#" -eq "1" ] && [ "$1" = "edit" ] ; then
"${EDITOR:-vi}" "$FILE"
elif [ "$#" -eq "1" ] && [ "$1" = "help" ] ; then
echo "Usage: $(basename $0) [ edit | help ]"
else
<"$FILE" grep -v ^~
fi
스크립트를 실행 가능한 상태로 PATH에 넣으면 다음처럼 동작합니다.
- todos: 데이터 파일의 내용을 출력(앞에 ~로 시작하는 줄은 무시)
- todos help: 사용법 출력
- todos edit: 편집기에서 데이터 파일 열기
이 예제에서는 서브커맨드를 자동완성으로 제안하도록 만들겠습니다. 스크립트의 실제 동작은 예제 목적상 중요하지 않습니다 — 이름만 같으면 자동완성은 동작합니다.
zsh용 커스텀 자동완성 작성
가장 기본적인 zsh 자동완성 핸들러는 매우 간단합니다. 아래 코드는 compadd를 직접 호출해 고정된 제안 목록을 추가합니다.
_complete_todos() {
compadd help edit
}
autoload -Uz compinit
compinit
compdef _complete_todos todos
설명:
- _complete_todos 함수는 compadd를 호출해 제안 단어를 등록합니다.
- compadd는 zsh의 compinit 모듈에 포함된 함수입니다.
- autoload -Uz compinit; compinit은 완성 시스템을 초기화합니다.
- compdef로 명령어 todos와 핸들러 _complete_todos를 연결합니다.
사용방법:
- .zshrc에 위 코드를 추가하거나, _todos라는 파일로 fpath에 넣을 수 있습니다.
- 개발 중에는 터미널에서 직접 위 코드를 실행해 테스트할 수 있습니다.
장점: 구현이 간단하고, 고정된 서브커맨드에는 즉시 적용됩니다.
제한사항: 동적 제안(파일 내용 기반, 원격 API 호출 등)을 하려면 함수 내부에서 조건문 및 외부 호출을 추가해야 합니다.
Bash의 차이점과 처리 방식
Bash는 자동완성 처리에 더 많은 책임을 부여합니다. 기본적인 Bash 핸들러 예시는 다음과 같습니다.
_complete_todos_bash() {
COMPREPLY=(help edit)
}
complete -F _complete_todos_bash todos
설명:
- complete -F로 todos 명령에 대한 핸들러 함수를 등록합니다.
- 핸들러는 COMPREPLY 배열을 설정해 Bash에게 제안 목록을 전달합니다.
문제: 위 구현은 입력된 접두사를 고려하지 않기 때문에 “todos he
유용한 Bash 변수:
- COMP_WORDS: 현재 명령줄을 단어별로 나눈 배열입니다.
- COMP_CWORD: COMP_WORDS에서 커서가 위치한 단어의 인덱스입니다.
compgen 명령어:
- compgen은 다양한 소스(명령어, 파일, 함수, 정적 단어 목록 등)로부터 제안 목록을 생성합니다.
- -W 옵션은 고정 단어 목록을 검색합니다.
예시:
$ compgen -W "one two three" o
one
$ compgen -W "one two three" t
two
three
따라서 실제로는 아래처럼 compgen을 사용해 접두사를 고려한 COMPREPLY를 만들면 됩니다.
_complete_todos_bash() {
COMPREPLY=( $( compgen -W "edit help" -- "${COMP_WORDS[$COMP_CWORD]}" ) )
}
complete -F _complete_todos_bash todos
이렇게 하면 “todos he
zsh와 Bash를 모두 지원하는 포터블 스크립트
실제 배포용 스크립트는 실행 중인 셸을 감지해 각각의 초기화 절차를 실행해야 합니다. 아래 예시는 zsh와 Bash 둘 다 지원합니다.
SUBCOMMANDS=(help edit halt)
_complete_todos_zsh() {
compadd $SUBCOMMANDS
}
_complete_todos_bash() {
COMPREPLY=( $( compgen -W "${SUBCOMMANDS[*]}" -- "${COMP_WORDS[$COMP_CWORD]}" ) )
}
if [ -n "${ZSH_VERSION:-}" ]; then
autoload -Uz compinit
compinit
compdef _complete_todos_zsh todos
elif [ -n "${BASH_VERSION:-}" ]; then
complete -F _complete_todos_bash todos
fi
설명 및 팁:
- ZSH_VERSION과 BASH_VERSION 환경변수를 통해 현재 셸을 감지합니다.
- SUBCOMMANDS 배열을 사용해 제안 목록을 한곳에서 관리합니다.
- Bash는 배열과 문자열 사이 변환이 필요하므로 “${SUBCOMMANDS[*]}” 같은 문법을 사용합니다.
- 이 파일을 completion.sh로 저장한 뒤 .bashrc나 .zshrc에서 source하면 됩니다:
. /path/to/completion.sh
동적 제안: 고정 목록을 넘어서
실무에서는 고정 목록만으로 충분하지 않을 때가 많습니다. 예를 들어 현재 디렉터리의 파일 목록, 원격 API에서 조회한 리소스, 또는 로컬 설정 파일의 섹션명을 제안해야 할 수 있습니다.
- zsh: compadd 함수는 동적 제안 생성에 최적화되어 있습니다. 함수 내부에서 명령 실행 결과를 compadd에 파이프로 넘기면 됩니다.
- Bash: compgen으로 파일명, 사용자명 등을 생성할 수 있고, 직접 명령을 실행해 결과를 COMPREPLY로 변환하면 됩니다.
예시(파일 기반 동적 제안):
zsh:
_complete_files_zsh() {
compadd -- $(ls *.md 2>/dev/null)
}
bash:
_complete_files_bash() {
local cur
cur="${COMP_WORDS[$COMP_CWORD]}"
COMPREPLY=( $(compgen -W "$(ls *.md 2>/dev/null)" -- "$cur") )
}
주의: 외부 명령을 호출할 때는 성능과 보안(특히 사용자 입력을 명령으로 전달할 때의 쉘 인젝션)을 고려해야 합니다. 가능한 경우 고정 목록이나 안전한 파싱을 우선하세요.
설치 및 배포 권장 방식
- 개인 사용: .zshrc 또는 .bashrc에 직접 소스 또는 함수 정의.
- 여러 사용자/시스템 공유: /etc/bash_completion.d 또는 시스템의 zsh fpath에 파일을 배포.
- 패키지화: 스크립트를 패키지(배포 파일)로 만들고 설치 스크립트에 자동으로 source되도록 구성.
파일 권한: 스크립트는 읽을 수 있어야 하고, 실행을 요구하지 않는 경우가 많으므로 보통 “644” 권한이면 충분합니다.
테스트 케이스와 수용 기준
간단한 테스트 케이스 목록(수동 또는 자동화 가능):
- 기본 제안: 아무 입력 없이 todos
를 눌러 모든 서브커맨드가 제시되는가. - 접두사 필터링: todos he
가 “help”만 제안하는가. - 경계조건: 공백이 여러 개일 때 올바른 단어를 완성하는가.
- 동적 제안: 파일 기반 제안이 최신 파일 목록을 반영하는가.
- 멀티플랫폼: zsh와 bash에서 각각 동일한 동작을 보이는가.
수용 기준: 위 테스트가 모두 통과하고, 성능 저하(탭 누름 시 200ms 이상 지연)나 보안 취약점이 없을 것.
배포 전 체크리스트(역할별)
개발자:
- 스크립트에 하드코딩된 절대 경로가 없는지 확인.
- 외부 명령에 사용자 입력을 직접 전달하지 않는지 확인.
운영자:
- 시스템 전체 배포 시 /etc 또는 /usr/local 경로 권한 검토.
- 충돌하는 complete/compdef 엔트리가 없는지 확인.
테스터:
- 모든 주요 셸(zsh, bash)에서 수동 테스트 수행.
- 여러 환경(POSIX 쉘과의 호환성 등)에서 자동화 테스트 실행.
트러블슈팅 및 롤백(runbook)
- 현상: 자동완성 동작 안 함.
- 확인: 해당 셸에서 complist/complete 등록 여부 확인.
- zsh: type compdef로 등록 확인, compinit이 호출되었는지 점검.
- bash: complete -p todos로 핸들러 등록 확인.
- 현상: 제안 목록이 오래된 값만 보여줌.
- 원인: 캐싱 또는 동적 제안 생성 시 외부 데이터가 갱신 안 되는 경우.
- 조치: 제안 생성 부분에서 캐시를 사용하고 있다면 무효화 로직 추가.
- 롤백: 문제 발생 시 .bashrc/.zshrc에서 source 라인을 주석 처리하고 셸 재시작.
보안 및 개인정보 주의사항
- 자동완성 핸들러에서 민감한 데이터를 노출하지 않도록 주의하세요(예: 홈 디렉터리 구조, 인증 토큰 등).
- 동적 제안에서 외부 API 호출을 할 경우 인증 정보를 안전하게 관리하고, 로깅 시 민감한 필드를 마스킹하세요.
중요: 자동완성 로직 자체는 사용자의 로컬 데이터에 접근할 수 있으므로, 공개 저장소에 올리기 전에 민감한 경로가 포함되어 있지 않은지 확인해야 합니다.
대안 및 확장 접근법
- 외부 라이브러리 사용: 복잡한 자동완성 로직은 쉘 전용 라이브러리나 프레임워크(예: bash-completion 프로젝트)를 활용해 구현할 수 있습니다.
- 도구 통합: CLI 프레임워크(예: Python의 Click, Go의 Cobra 등)는 자동완성 스크립트를 자동 생성해주는 기능을 제공합니다. 이미 프레임워크를 사용 중이라면 해당 기능을 검토하세요.
- GUI 보조: 대규모 사용자 대상이라면 CLI 대신 간단한 GUI 또는 TUI도 고려할 수 있습니다.
정신 모델(heuristics)
- 단순성 우선: 가능한 경우 정적 목록을 유지하고 동적 제안은 필요 시 추가.
- 안전성 우선: 사용자 입력을 외부 명령에 바로 전달하지 않음.
- 일관성 유지: zsh와 bash에서 동작이 최대한 유사하도록 설계.
호환성 및 마이그레이션 팁
- 오래된 Bash(예: 3.x)와 최신 Bash(4.x 이상) 사이에 배열 처리 방식 차이가 있을 수 있으므로, 배열을 문자열로 변환할 때 표준 POSIX 호환 문법을 사용하세요.
- zsh의 compadd는 고급 기능을 제공하므로, zsh 전용 로직은 파일명을 언더스코어로 시작하는 파일로 분리해 fpath에 두는 것이 깔끔합니다.
예제 체크리스트 템플릿
- SUBCOMMANDS 배열에 모든 서브커맨드가 포함되어 있는가?
- zsh에서 compinit 및 compdef가 호출되는가?
- bash에서 complete -F로 핸들러가 등록되는가?
- 접두사 기반 필터링이 작동하는가?
- 동적 제안의 성능이 허용 범위인가?
간단한 사고 흐름(결정 트리)
flowchart TD
A[탭 자동완성 적용 대상 확인] --> B{서브커맨드 고정 목록인가?}
B -- Yes --> C[정적 목록으로 compadd/COMPREPLY 등록]
B -- No --> D{동적 데이터가 로컬인가?}
D -- Yes --> E[동적 생성'파일/디렉터리 기반']
D -- No --> F[외부 API 사용: 캐시/비동기/오류 처리 설계]
E --> G[성능 및 보안 검토]
F --> G
C --> G
G --> H[테스트 및 배포]
요약
- zsh: compadd + compinit + compdef를 사용해 자동완성을 쉽게 추가할 수 있습니다.
- bash: COMPREPLY와 compgen을 사용하되, COMP_WORDS 및 COMP_CWORD로 컨텍스트를 파악해야 합니다.
- 포터블 스크립트: ZSH_VERSION과 BASH_VERSION을 검사해 각각의 초기화 루틴을 호출하세요.
- 배포 전 테스트, 보안 검토, 성능 검증을 반드시 수행하세요.
핵심 포인트:
- 고정 목록이면 구현이 쉽고 안전하며 빠릅니다.
- 동적 제안은 편의성이 크지만 성능과 보안 주의가 필요합니다.
- 셸마다 초기화/등록 방식이 다르므로 포터블 스크립트는 셸 감지를 포함해야 합니다.
추가 자료 및 다음 단계 제안:
- 자신의 CLI에 대해 더 정교한 제안을 만들고 싶다면, 서브커맨드별로 옵션/파일/리소스 제안을 추가해 보세요.
- CLI 프레임워크를 사용 중이라면 해당 프레임워크의 자동완성 생성 기능을 확인하세요.