자바스크립트를 활성화 해주세요

zsh 명령어 자동완성 기능 작성하기 with chatGPT

 ·  ☕ 7 min read

나는 쉘 스크립트를 직접 작성하는 것이 너무 싫다. 굳이 쉘 스크립트를 싫어하는 이유를 따져보자면 이런 것들이 있다.

  1. 쉘 스크립팅에 자주 사용되는 명령어에 익숙하지 않다. (ex. awk, grep, sort)
  2. 스크립팅 언어라서 문법 검사 등이 약하다. (오타에 민감하지만 확인이 힘들다.)
  3. 시스템에 사이드 이펙트가 생기는 명령어는 매번 원상복귀 명령어를 입력해야 한다.
  4. 문법이 마음에 안든다. (변수 선언 및 사용, 조건 검사 방식 등)

4번은 태생적으로 언어 자체가 그 모양이라서 특별한 해결법이 없지만, 나머지는 어느정도 해결방법이 있다.

  1. 이건 어찌 보면 악순환이다. 자주 개발하거나 파이프를 많이 활용하면 익숙해진다.
  2. 문법 검사는 shellcheck를 활용하면 검사가 가능하다.
  3. 모든 명령어가 지원하는 것은 아니지만, dry run을 하면 실제 동작 및 반영은 생략된다.

뭐 어쨋든 작성하기 싫은 것은 사실이다. 여기에서 chatGPT를 사용하면 어느정도 귀찮음을 해결할 수 있을까 해서 적용해봤고, 생각보다 괜찮은 결과를 얻게 되어 이를 공유하고자 한다.

문제정의

회사에서 많이 사용하는 CLI 도구가 있는데, 보통 명령어에 옵션을 사용하는 방식보단 환경변수로 미리 설정해서 명령어 옵션을 단순하게 하는 방식을 선호하는 편이다. 그리고 회사에서는 이 환경변수를 설정하는 쉘 스크립트 파일들을 서로 공유한다.

$ source env1.rc

문제는 이 환경변수 설정을 상황마다 자주 바꿔야한다. 아예 특정 디렉토리 안에서만 작업할 것이라면 direnv를 활용할 수 있겠지만, 나의 상황에선 적합하진 않다.

요구사항을 정리하자면 다음과 같다.

  1. zsh에서 호환되는 명령어/도구여야 한다.
  2. 여러 개의 환경변수 설정 파일은 한 디렉토리에 모아놓고 관리할 것이다.
  3. 현재 쉘 경로와 관계 없이, 모아놓은 환경변수 설정 파일 중 하나를 반영하고 싶다.
  4. 환경변수 파일을 가리킬 때는 절대 경로가 아니라 적절한 상대경로였으면 좋겠다.
  5. 명령어를 실행하기 전에 환경변수 설정 파일 후보들을 자동완성하고 싶다.

환경변수 파일을 모아놓자

$ ls ~/Workspace/.envscripts/
a-project/user.rc       a-project/admin.rc      a-project/service.rc
b-project/user.rc       b-project/service.rc    

임의의 경로에서 반영하고 싶다

$ pwd
~/Documents/testdata

# 아래 명령어로 source ~/Workspace/.envscripts/a-project/user.rc 와 동일한 효과가 발생한다.
$ loadenv a-project/user.rc

자동완성으로 파일 이름을 반영했으면 좋겠다

# 명령어 다음 인자로 적절히 입력한 상태에서 TAB을 누르면 그 이후 일치하는 후보를 보여준다.
$ loadenv a-project/<TAB>
a-project/user.rc       a-project/admin.rc      a-project/service.rc

# 자동완성 후보 파일이 하나뿐이라면 해당 파일 이름으로 완성되어야 한다.
$ loadenv a-project/u<TAB>
$ loadenv a-project/user.rc # TAB 뒤의 "ser.rc"가 자동으로 입력되어야 한다.

chatGPT에게 물어보다

chatGPT와 대화한 내역은 자료를 정리하면서 대화 기록을 날려버려서 전체적인 정황만 적도록 하겠다. (추후 해당 대화랑 비슷한 상황을 재현할 수 있다면 수정해서 올려보도록 하겠다.)

문제 상황을 이해시키기

앞에 나온 요구사항은 한번에 정리된게 아니라 작성하면서 추가된 요구사항도 있다보니 주먹구구식으로 작성되었다.

처음에는 문제 상황을 정확하게 이해 못하고 위처럼 direnv를 추천하기도 했고, 해당 도구가 부적합하다고 하니 다른 도구를 추천했다. 문제라면 아무리 검색해봐도 현실에 존재하지 않는 도구였다. 마치 세종대왕 맥북 던짐사건을 말하는 것 처럼 할루시네이션의 일종으로 보였다.

결국 이 문제를 해결해 줄 수 있는 도구가 없다는 것을 이해시켰고, 직접 명령어를 작성해야하는 상황임을 납득시켰다.

간단하게 아래와 같이 관리할 디렉토리를 정하고, 해당 경로 아래에 있는 파일을 불러올 수 있게 하는 쉘 스크립트를 작성해줬다.

export RC_DIR="$HOME/Workspace/.envscripts"

funciton loadenv() {
	source "$RC_DIR/$1"
}

매우 간단한 해결법이긴 한데, 일단 함수만 정의되고 $RC_DIR이 설정되지 않으면 문제가 생길 것 같아, 기본 디렉토리를 추가할 수 있게 수정해달라고 해서 아래와 같이 처리되었다.

export RC_DIR="$HOME/Workspace/.envscripts"

funciton loadenv() {
    local rc_dir=${RC_DIR:-$HOME/.env}
	source "$rc_dir/$1"
}

자동완성 기능 요청하기

위의 예시에서는 고작 5개의 환경변수 파일밖에 없었지만, 실제로는 대략 10~20개는 된다. 물론 내가 파일 이름을 직관적으로 변경해놓고 디렉토리별로 분류해놓긴 했지만, 모두 외우기는 힘들다. 안정적인 명령어 실행 및 타자 입력을 줄이기 위해서라도 해당 명령어에 대한 자동완성 기능을 요청했다.

function _loadenv_complete() {
    local rc_dir=${RC_DIR:-$HOME/.rc}
    local search_dir=${1:-$rc_dir}
    local rc_files=(${(f)"$(find $search_dir -name '*.rc')"})

    compadd -U -W "${rc_files[@]}"
}
compdef _loadenv_complete loadenv

현재 예시 내용은 주석을 제외했지만, 각 변수나 명령어의 용도도 간략히 설명하는 주석도 포함되어있었다.

문제는 이렇게 설정하면 자동완성은 해주지만 절대경로로 자동완성하고, 비슷한 파일 이름 후보도 잘 찾이 못하는 문제가 있었다.

제일 처음 문제 제기한대로 예시를 들어가면서 설명을 하니 문제가 있었는데, 예시는 일부에 불과할 뿐 일반화된 코드를 원하는 것이었는데, 그 예시를 하나의 요구사항처럼 이해하고 강제로 반영하기 시작한다는 것이었다.

자동완성 함수 안에서 대놓고 $project_dir같은 변수명이 등장하기도 했고, 예시로 들은 디렉토리들이 하드코딩되어있기도 했다. 회사의 대외비 정보 등을 숨기기 위해 간접적으로 말했을 뿐, 실제 project라고 부르지도 않는다.

아무리 상황이 잘못되어있다고 설명을 해도 계속 이놈의 $project_dir이 사라질 생각을 하지 않아서, 결국 대화 내역을 다 지워서 리셋하고 다시 자동완성에 필요한 정보만 정확히 잡아 요청했고, 나름 개선된 결과를 받을 수 있었다.

function _loadenv_complete() {
    local rc_dir=${RC_DIR:-$HOME/.rc}
    local search_dir=${1:-$rc_dir}
    local rc_files=(${(f)"$(find $search_dir -name '*.rc')"})
    local completions=()

    for file in ${rc_files[@]}; do
        completions+=("${file#$rc_dir/}")
    done

    compadd -U -W "${completions[@]}"
}
compdef _loadenv_complete loadenv

중간에 for문이 들어감으로서, 검색된 파일 이름에서 절대경로 부분을 설정 경로까지 제거해서 환경변수로 나오게 하는 것까지 성공했다. 하지만 아직 완벽하진 않았다.

$ ls ~/Workspace/.envscripts/
a-project/user.rc       a-project/admin.rc      a-project/service.rc
b-project/user.rc       b-project/service.rc 

# 명령어 이후 TAB을 누르면 모든 파일 목록을 보여준다.
$ loadenv <TAB>
a-project/user.rc       a-project/admin.rc      a-project/service.rc
b-project/user.rc       b-project/service.rc 

# loadenv 이후 TAB을 누르면 이전 입력을 고려해서 남은 후보를 찾아줘야 한다.
$ loadenv a-project/u<TAB>
$ loadenv # 이전에 입력하던 내용이 모두 날아가고 모든 검색된 파일들이 나온다.
a-project/user.rc       a-project/admin.rc      a-project/service.rc
b-project/user.rc       b-project/service.rc 

지금 입력된 내용을 제발 반영해줘

이 부분을 수정하는 것이 제일 어려웠다. 여러 번 말을 해도 알아먹지를 못하고 이전 코드와 비슷한 수준의 자동완성 결과만 나오고 있었다. 몇 번을 리셋하고 다시 처음부터 요청해도 크게 개선되진 않았다.

처음 말했던 것 처럼 정말 쉘 스크립트를 이해하기 싫었지만, 도무지 답이 나오지 않는 것 같아서 각 라인별로 무슨 의미인지 하나씩 문의해봤다. 결국 자동완성에 제일 중요한 부분은 compadd라는 것을 알게되었고, 몇가지 옵션을 설정할 수 있다는 것을 알게 되었다.

또한 지금까지 입력된 인자를 얻어오는 방법으로 $words[CURRENT]라는 변수가 있다는 것도 알게 되었다.

function _loadenv_complete() {
	local rc_dir=${RC_DIR:-$HOME/.rc}
	local search_dir=${1:-$rc_dir}
	local rc_files=(${(f)"$(find $search_dir -name '*.rc')"})
	local completions=()

	for file in ${rc_files[@]}; do
		if [[ ${file#$rc_dir/} == $words[CURRENT]* ]]; then
			completions+=("${file#$rc_dir/}")
		fi
	done

	if [[ ${#completions[@]} -eq 1 ]]; then
		compadd "${completions[1]}"
	else
		compadd -U -M 'm:{a-zA-Z}={A-Za-z}' -W "${completions[@]}"
	fi
}
compdef _loadenv_complete loadenv

이후 대부분의 상황에서 자동완성이 잘 되었지만, 가끔 이름이 비슷한 파일이 몇개 있는 경우에 자동완성을 이상하게 하고, 모든 후보를 보여주지 않는 문제가 있어 추가 수정을 했다.

여기에 추가로 source env.sh arg1 arg2와 같이 인자 추가가 필요한 환경변수 파일을 반영하게, 다양한 확장자 추가, 변수명 리팩토링 등을 통해 아래와 같은 코드를 얻었고, 아직 특별한 오류는 없어서 계속 사용하고 있다.

export ENV_DIR="$HOME/Workspace/.envscripts"

function loadenv() {
	local env_dir=${ENV_DIR:-$HOME/.env}
	local file=$1
	local args=("${@:2}")
	source "$env_dir/$file" "${args[@]}"
}
function _loadenv_complete() {
	local env_dir=${ENV_DIR:-$HOME/.env}
	local search_dir=${1:-$env_dir}
	local env_files=(${(f)"$(find $search_dir -name '*.env' -o -name '*.rc' -o -name '*.sh')"})
	local completions=()

	for file in ${env_files[@]}; do
		if [[ ${file#$env_dir/} == $words[CURRENT]* ]]; then
			completions+=("${file#$env_dir/}")
		fi
	done

	compadd -a completions
}
compdef _loadenv_complete loadenv

꼭 chatGPT가 필요했을까?

chatGPT를 사용하면서 열받는 부분이 꽤 있었다. 코드를 잘 알려주는 듯 하다가도 가끔 이상하게 이해하고 애먼 답변을 준다거나, 수렁에 빠진 것 처럼 이상한 개념에 꽂혀서 계속 잘못된 방향의 코드만 제공하는 부분이 열받았다.
아예 말을 못 알아듣는 도구였다면 화가 나지도 않았겠지만, 마치 좀 알아듣는듯 싶다가 이상한 결과만 내놓으니 애매해서 더 화가 나는 부분이 컸다. 일부 질문 내역은 내가 질문에 누락된 내용이 많기도 했지만, 내가 진짜 정확하게 질문했어도 못 알아듣거나, 요구사항을 명확히 전달하기 위해 들었던 예시에 깊이 빠지는 부분은 많이 답답했다.

오히려 없는 도구를 추천하는 식의 할루시네이션 문제는 이전에 이야기를 많이 들어서 그런지 크게 당황스럽지 않았다. 그냥 chatGPT에게 잘못되었다고 명확하게 이야기하면 그 뒤로는 거의 없어지기 때문에 chatGPT가 말하는 내용을 너무 맹신하지만 않으면 될 것 같았다.

뭔가 불만이 많아보이는데 이번 개발을 하는 데 있어서 chatGPT의 역할은 결정적이었다. 내가 쉘 스크립트를 짜기 싫어서보다 더 중요한건 검색 키워드 필터링에 큰 노력을 들이지 않아도 된다는 점이었다.
평소 내 구글 검색 능력이라면 zsh 자동 완성 기능 작성 같은 식의 키워드로 검색을 할텐데, 이 경우 대부분 zsh에서 제공하는 자동완성 기능을 활성화 시키는 방법에 관한 사이트만 찾아줄 뿐, 내가 직접 자동완성 함수를 작성하는 방법에 대한 글은 찾기 힘들다. 이렇게 검색하려는 키워드의 의미가 쉽게 오염되는 상황에서는 원하는 자료를 찾기 힘든데, 이 부분에서 chatGPT가 정확하게 이해를 해 줬기 때문에 쉽게 진행할 수 있었다


JaeSang Yoo
글쓴이
JaeSang Yoo
The Programmer

목차