커맨드 파라메터 자동완성

접속해야 하는 서버가 수백대가 넘습니다. 호스트 이름에는 규칙이 있지만, 매번 입력하는게 매우 귀찮군요. 호스트 이름을 자동완성 하는 방법을 찾아봤습니다. 이번 글에서는 배시(bash)쉘의 기능을 이용해서 자동완성(auto completion)하는 방법과 ansible에 정의된 host 파일의 호스트명을 분해한 경험을 공유합니다.

배시 자동완성 Bash auto completion

배시의 자동완성은 크게 두 부분으로 나뉩니다. - 배시 기본 함수인 compgen을 통해 완성할 후보 단어군 수집 - 역시 배시 기본 함수인 completion을 통해 추천 함수 와 원하는 명령어 연결

보다 심도깊은 활용 예는 다음 참고자료에서 확인할 수 있습니다.

위의 예를 참고해서 만든 스크립트의 예입니다.

_prod_hosts()
{
    local cur prev opts

    COMPREPLY=()
    _get_comp_words_by_ref cur

    # cur="${COMP_WORDS[COMP_CWORD]}"
    # prev="${COMP_WORDS[COMP_CWORD-1]}"
    opts=$(<~/hosts)

    if [[ ${COMP_CWORD} == 1 ]] ; then
        COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
        return 0
    fi
}
complete -F _prod_hosts r

저와 같은 목적인 경우 세 군데만 적절하게 바꿔주면 됩니다. 1. _prod_hosts: 임의의 함수 이름입니다. 원하는 어떤 이름도 관계 없습니다. 2. 코드 중간, 10번줄 opts=$(<~/hosts): 홈 디렉터리의 hosts 파일을 읽어서 opts 변수에 저장하는 구문입니다. 3. r: 로그인 스크립트. 실제로 게이트웨이 서버에서 주어진 계정과 적절한 옵션을 담은 스크립트 파일명입니다.

참고로 hosts 파일에는 호스트명이 한 줄에 하나씩 들어있습니다. 예를들어 아래와 같이 작성합니다.

auth-web-001
auth-web-002
store-web-001
store-web-002
store-web-003
store-web-004
cassandra-001
cassandra-002
cassandra-003
redis-001
redis-002

아래에서 hosts 파일을 만드는 방법을 설명합니다.

위의 스크립트를 실제로 사용하는 방법은 다음과 같습니다. 1. 위에서 작성한 스크립트를 ~/.bash_completion.d/ 디렉토리 아래에 저장합니다. 이름은 무엇이든 관계 없습니다. 2. ~/.bash_completion 파일을 생성해서 아래와 같은 내용으로 저장합니다.

for bcfile in ~/.bash_completion.d/* ; do
  . $bcfile
done
  1. 설정을 적용하기 위해 . ~/bash_completion 명령을 수행합니다.

이제 r cass[tab]를 입력하면 r cassandra-00 까지 완성되는 것을 확인할 수 있습니다.

호스트 목록 만들기

대상 호스트가 몇 개 안된다면, 위의 스크립트로 충분합니다. 하지만, 대상이 되는 서버가 수백대가 넘다보니 호스트를 정리하는 것도 작은 일이 아니었습니다. 다행히 ansible을 사용하고 있어서, ansible의 hosts 파일로부터 compgen 에 적합한 형식의 파일을 만드는 스크립트를 작성했습니다.

ansible의 host 파일은 ini 포맷을 따릅니다. 연속되는 호스트명은 hostname-[start:end] 형식으로 표현할 수 있습니다.

[auth]
auth-web-[001:002]

[store-web]
store-web-[001:004]

[store-cache]
redis-[001:002]

[store:children]
store-web
store-cache

[cassandra]
cassandra-[001:003]

hostname-[001:002] 형태의 호스트명을 hostname-001\nhostname-002 형태로 확장하기 위해서 이번에도 배시의 기능을 이용했습니다. brace expansion 이라는 기능은 {} 안의 숫자를 아래처럼 확장해줍니다.

$ echo hostname-{1..3}
hostname-1 hostname-2 hostname-3

기본 아이디어는 []{}:..으로 바꾸는 것입니다. 한 줄로 멋있게 표현하고 싶었지만, 아직 배시 초보라서 여러 줄로 만들었습니다.

HOSTS=( $(cat inventory | grep '^[a-zA-Z]' | sed -e 's/\[/\{/g' | sed -e 's/\]/\}/g' | sed -e 's/\:/../g') )

ARR_N=()
for i in ${HOSTS[@]}; do
  EVALED=`eval echo $i`
  ARR_N+=( $(echo $EVALED | xargs -n1 echo) )
done

echo $ARR_N | xargs -n1 echo | sort -u > hosts

완벽하지는 않지만 실제 사용하는 ansible의 설정파일로부터 각각의 호스트명을 뽑아내는 스크립트가 완성되었습니다.

zsh에 적용하기

zsh도 자동완성을 적용할 수 있습니다. 오히려 탭으로 후보군을 이동하는 기능은 zsh 쪽이 훨씬 뛰어나죠. 다음 두 가지만 주의하면 zsh에서도 위와 같은 과정을 통해 자동완성을 적용할 수 있습니다.

  • . 대신 source
  • zsh 에 없는 함수 추가

. 대신 source

bash 에서 . filename 명령을 통해서 filename에 있는 환경변수 선언을 현재 shell 환경으로 가져올 수 있습니다. 사실 .source의 별칭입니다. bash에서 . filenamesource filename 는 동일합니다. 하지만, zsh에서는 .를 사용하지 않고 source 명령 만을 사용해야 합니다. ~/.bash_completion 에서 .source로 바꿉니다.

for bcfile in ~/.bash_completion.d/* ; do
  source $bcfile
done

zsh 에 없는 함수 _get_comp_words_by_ref 추가

다음 gist의 171:226 줄의 코드를 _prod_hosts() 앞에 복사해 둡니다.

if ! type _get_comp_words_by_ref >/dev/null 2>&1; then
if [[ -z ${ZSH_VERSION:+set} ]]; then
_get_comp_words_by_ref ()
{
	local exclude cur_ words_ cword_
	if [ "$1" = "-n" ]; then
		exclude=$2
		shift 2
	fi
	__git_reassemble_comp_words_by_ref "$exclude"
	cur_=${words_[cword_]}
	while [ $# -gt 0 ]; do
		case "$1" in
		cur)
			cur=$cur_
			;;
		prev)
			prev=${words_[$cword_-1]}
			;;
		words)
			words=("${words_[@]}")
			;;
		cword)
			cword=$cword_
			;;
		esac
		shift
	done
}
else
_get_comp_words_by_ref ()
{
	while [ $# -gt 0 ]; do
		case "$1" in
		cur)
			cur=${COMP_WORDS[COMP_CWORD]}
			;;
		prev)
			prev=${COMP_WORDS[COMP_CWORD-1]}
			;;
		words)
			words=("${COMP_WORDS[@]}")
			;;
		cword)
			cword=$COMP_CWORD
			;;
		-n)
			# assume COMP_WORDBREAKS is already set sanely
			shift
			;;
		esac
		shift
	done
}
fi
fi

_prod_hosts()
{
    local cur prev opts

    COMPREPLY=()
    _get_comp_words_by_ref cur

    # cur="${COMP_WORDS[COMP_CWORD]}"
    # prev="${COMP_WORDS[COMP_CWORD-1]}"
    opts=$(<~/hosts)

    if [[ ${COMP_CWORD} == 1 ]] ; then
        COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
        return 0
    fi
}
complete -F _prod_hosts r

이제 .zshrc 파일에 아래 줄을 추가하고

source .bash_completion

source .zshrc 를 해주면 zsh에서도 자동완성의 완성입니다.

결론

이번 글은 일종의 작업 기록입니다. 조금 더 정제할 필요를 느끼지만 잊기 전에 글을 씀으로 부족한 내용을 확장시키겠습니다.

  • zsh에 적용하기 추가 (2017.4.28)