Command Completion

명령에서 사용되는 인수, 옵션이 종류가 많고 이름이 길면 일일이 기억하기도 힘들고 오류 없이 입력하기도 어렵습니다. 이럴 때 tab 키를 이용한 자동완성 기능이 유용하게 사용되는데요. 명령문 자동완성 기능은 각 명령에서 자체적으로 제공하는 것은 아니고 complete, compgen, compopt 명령을 이용해 만들어 사용하는 것입니다. 자주 사용하는 명령에 자동완성 기능이 없다면 만들어서 추가할 수도 있고 이미 설정돼 있는 기능이라도 마음에 들지 않으면 재설정할 수도 있습니다.

자동완성 함수를 만들면 스크립트 연습도 되고 명령의 도움말도 다시 보게 되므로 명령을 이해하는데 도움이 됩니다. 여러분도 마음에 드는 명령이 있으면 자동완성 함수를 만들어서 github 에 올려보세요. github 에 올린것은 bing 에서 검색하면 바로 뜹니다.

# find 명령을 사용하기 위해 'find -' 까지 입력한 뒤에 tab 키를 누르면 
# 이용할 수 있는 옵션 목록을 한눈에 보여줍니다.       
$ find -[tab]
-amin                   -fprint0                -mmin                   -quit
-anewer                 -fprintf                -mount                  -readable
-atime                  -fstype                 -mtime                  -regex
-cmin                   -gid                    -name                   -regextype
-cnewer                 -group                  -newer                  -samefile
-context                -help                   -nogroup                -size
-ctime                  -ignore_readdir_race    -noignore_readdir_race  -true
-daystart               -ilname                 -noleaf                 -type
-delete                 -iname                  -nouser                 -uid
-depth                  -inum                   -nowarn                 -used
-empty                  -ipath                  -ok                     -user
-exec                   -iregex                 -okdir                  -version
-execdir                -iwholename             -path                   -warn
-executable             -links                  -perm                   -wholename
-false                  -lname                  -print                  -writable
-fls                    -ls                     -print0                 -xdev
-follow                 -maxdepth               -printf                 -xtype

자동완성을 위해 현재 등록돼 있는 명령 이름들은 complete builtin 명령을 통해 볼수있습니다.

$ complete -p                 # 전체 목록을 출력
. . .

$ complete -p find
complete -F _find find        # find 명령의 자동완성을 위해 _find 함수(-F) 가 사용됨

$ complete -p help
complete -A helptopic help    # help 명령의 자동완성을 위해 helptopic action(-A) 이 사용됨

현재 설정되어 있는 자동완성을 삭제할 때는 -r 옵션을 사용합니다.

$ complete -p kill
complete -F _kill2 kill

$ complete -r kill

$ complete -p kill
bash: complete: kill: no completion specification

Completion 은 명령문 스트링을 만드는 작업.

사실 자동완성과 명령은 관계가 없습니다. 무슨말이냐 하면 자동완성은 단지 명령문 스트링을 만드는 작업입니다. 다음은 실제 시스템에 'hello' 라는 명령은 없지만 자동완성을 설정하고 있는 예입니다.

# hello 를 위한 자동완성 단어들을 등록
$ complete -W "aaa bbb ccc ddd" hello

# 'hello ' 까지 입력하고 tab 키를 누르면 등록했던 단어들이 표시된다.
$ hello [tab]           
aaa bbb ccc ddd

# 'hello c' 까지 입력하고 tab 키를 누르면 'hello ccc' 명령문이 자동완성 된다.
$ hello c[tab]

# 자동완성 등록확인
$ complete -p hello 
complete -W 'aaa bbb ccc ddd' hello

기본적으로 사용할 수 있는 자동완성 이름들

위의 예에서는 -W wordlist 옵션을 이용해 직접 자동완성 단어를 등록해 사용했지만 completion 에서는 시스템 내에서 기본적으로 사용할 수 있는 여러 이름들을 카테고리 별로 분류하여 제공합니다. 이와 같은 이름들은 -A action 옵션을 이용해 사용할 수 있습니다.

# 자동완성으로 export action 을 사용
$ complete -A export  hello

# 'hello ' 까지 입력하고 tab 키를 누르면 현재 export 된 변수 이름들을 보여준다.
$ hello [tab]
ANT_HOME                  LC_ADDRESS                SCALA_HOME
CDPATH                    LC_IDENTIFICATION         SESSION
CLUTTER_IM_MODULE         LC_MEASUREMENT            SESSION_MANAGER
COLORTERM                 LC_MONETARY               SESSIONTYPE
DART_SDK                  LC_NAME                   SHELL
DBUS_SESSION_BUS_ADDRESS  LC_NUMERIC                SHLVL
...

다음은 -A 옵션으로 사용할 수 있는 action 입니다. 옆에 short 옵션이 함께 있는 경우는 두 가지로 설정이 가능합니다. 예를 들어 alias 를 설정할 경우 complete -A alias 도 되고 complete -a 로도 설정할 수 있습니다. 이것은 compgen 명령에서도 동일하게 사용됩니다.

action 설명
-a | alias 현재 설정되어 있는 alias 이름
arrayvar array 변수 이름
binding Readline 에서 사용하는 key binding 이름.
-b | builtin Shell builtin 명령 이름.
-c | command $PATH 에의해 접근 가능한 시스템 전체 명령 (builtin 명령 포함).
-d | directory 현재 위치에서 디렉토리만 표시
FIGNORE 변수를 이용해 필터링 할 수 있습니다.
disabled Disable 된 shell builtin 명령 이름.
enabled Enable 된 shell builtin 명령 이름.
-e | export Export 된 shell 변수 이름.
-f | file 현재 위치에서 모든 파일 표시 (디렉토리, 심볼릭 링크 모두).
FIGNORE 변수를 이용해 필터링 할 수 있습니다.
compgen 명령의 경우 대상 디렉토리를 지정할 수 있습니다.
compgen -f /some/dir/
function 현재 shell 에 설정되어 있는 함수 이름.
-g | group /etc/group 에 등록된 group 이름
helptopic Help builtin 명령으로 도움말을 볼수있는 이름들.
hostname /etc/hosts 에 등록된 호스트이름. (HOSTFILE 변수 설정도 포함).
-j | job background 로 실행중인 모든 job 이름.
-k | keyword Shell 키워드.
running Running jobs 이름.
-s | service /etc/services 에 등록된 service 이름.
setopt Set 명령에서 사용할 수 있는 옵션 이름.
shopt Shopt 명령에서 사용할 수 있는 옵션 이름.
signal Signal 이름.
stopped Stopped jobs 이름
-u | user /etc/passwd 에 등록된 user 이름.
-v | variable 현재 shell 에 설정되어 있는 모든 변수 이름.
# builtin 빌트인 명령에 -b (builtin) 액션을 설정
$ complete -b builtin
$ builtin [tab]
.          caller     dirs       false    . . .

# set, shopt 빌트인 명령에 각각 setopt, shopt 액션을 설정
$ complete -A setopt set
$ complete -A shopt shopt
$ shopt [tab]
assoc_expand_once        dotglob                  lastpipe   . . .

# command, type 빌트인 명령과 which 외부명령에 -c (command) 액션을 설정
$ complete -c command type which
$ which [tab]
Display all 4220 possibilities? (y or n)

Completion Options

옵션 설정을 통해 completion, compgen 명령의 동작 방식을 변경할 수 있습니다. 이 옵션은 추후에 compopt 명령을 이용해 변경할 수 있습니다. 현재 설정된 옵션 상태는 compopt 명령이름 으로 확인할 수 있으며 - 로 표시된 것은 현재 설정되어있는 상태고 + 로 표시된 것은 설정이 안된 상태입니다.

  • default

    자동완성 이름을 생성하지 못했을때 파일, 디렉토리 이름을 사용하게 합니다.

# 자동완성 이름이 하나도 생성되지 못하게 -W '' 설정
$ complete -W '' hello

$ hello [tab]           # tab 키를 눌러도 아무것도 나타나지 않는다.
$

# 이번에는 '-o default' 옵션 추가
$ complete -o default -W '' hello

# 자동완성 이름을 생성하지 못하자 readline 기본 파일이름 완성을 사용.
$ hello [tab]
2013-03-19 154412.csv  music/                 video/                 
Address.java           ReadObject.class       WriteObject.class      
address.ser            ReadObject.java        WriteObject.java 

# compgen 으로 자동완성 단어 생성
$ compgen -o default -- Read
ReadObject.class
ReadObject.java
  • dirnames

    자동완성 이름을 생성하지 못했을때 디렉토리 이름을 사용하게 합니다.

$ complete -o dirnames -W '' hello

# 자동완성 이름을 생성하지 못하자 디렉토리 이름 완성을 사용.
$ hello [tab]
music/                 video/

# compgen 으로 자동완성 단어 생성
$ compgen -o dirnames -- mu
Music
  • filenames

    이것은 -o dirnames 옵션처럼 파일 이름을 자동완성 단어로 사용하는 옵션이 아니고 자동완성 단어를 파일 이름으로 취급하게 하는 옵션입니다. 이름에 공백이나 특수문자가 있을경우 자동으로 escape 해주고 자동완성 단어가 디렉토리 이름과 매칭이 될경우 뒤에 / 를 붙여줍니다.

$ complete -W 'hoo\ bar foo&bar music'  hello

# 자동완성 이름이 수정없이 그대로 완성된다.
$ hello hoo bar
$ hello foo&bar
$ hello music

# 이번에는 -o filenames 옵션 사용
$ complete -o filenames -W 'hoo\ bar foo&bar music'  hello

# 자동완성 이름을 파일로 취급하여 이름에 공백이나 특수문자가 있을경우 escape 해주고 
# 같은 이름의 디렉토리가 있을경우 '/' 를 붙여준다.
$ hello hoo\ bar
$ hello foo\&bar
$ hello music/

$ compgen -o filenames      # 파일 이름을 자동완성 단어로 사용하는 옵션이 아니다.
$
  • bashdefault

    자동완성 이름을 생성하지 못했을때 default Bash completions 을 사용하게 합니다. 여기에는 variable, function, command, alias, builtin,keyword ... 등이 모두 포함됩니다.

$ complete -o bashdefault -W '' hello

# bash 변수이름 자동완성
$ hello $BASH_[tab]
$BASH_ALIASES                $BASH_COMMAND                $BASH_SOURCE
$BASH_ARGC                   $BASH_COMPLETION_COMPAT_DIR  $BASH_SUBSHELL
$BASH_ARGV                   $BASH_LINENO                 $BASH_VERSINFO
$BASH_CMDS                   $BASH_REMATCH                $BASH_VERSION

# 패턴에 매칭되는 파일을 표시
$ hello Read*[tab]
ReadObject.class  ReadObject.java

$ hello Read*.j*[tab]
$ hello ReadObject.java    # 파일이름 완성     

# compgen 으로 자동완성 단어 생성
$ compgen -o bashdefault -- '$BASH_'
$BASH_ALIASES
$BASH_ARGC
$BASH_ARGV
. . .
  • noquote

    기본적으로 파일 이름을 완성할 때는 공백이나 특수문자를 escape 해주는데 이 옵션을 설정하면 disable 됩니다.

# 현재 디렉토리 목록
$ ls
2013-03-19 154412.csv  address.ser  music/            ReadObject.java  WriteObject.class
Address.java           foo&bar.cvs  ReadObject.class  video/           WriteObject.java

# file action 을 사용하여 현재 디렉토리에 있는 파일이름을 자동완성으로 사용
$ complete -A file hello

# 기본적으로 파일이름은 공백이나 특수문자를 escape 해준다.
$ hello 2013-03-19\ 154412.csv
$ hello foo\&bar.cvs

# 이번에는 -o noquote 옵션 사용
$ complete -o noquote -A file hello

# 파일이름의 자동 escape 기능이 disable 된다. 
$ hello 2013-03-19 154412.csv
$ hello foo&bar.cvs
  • nospace

    단어를 완성하고 나면 다음 단어를 위해 공백을 띄우게 되는데요. 이 옵션은 그걸 방지합니다.

$ complete -W 'aaa= bbb= ccc='  hello

# 단어를 완성하고 나면 자동으로 공백을 띄운다.
$ hello aaa= [stop] 

# 이번에는 -o nospace 옵션 사용
$ complete -o nospace -W 'aaa= bbb= ccc='  hello

# 단어를 완성하고 나서 공백을 띄우지 않는다.
$ hello aaa=[stop]
  • plusdirs

    생성된 자동완성 단어에 디렉토리 이름을 추가 합니다. 또한 자동완성 이름과 매칭되는 파일이 있을경우 공백, 특수문자를 escape 해줍니다.

# 현재 디렉토리 목록
$ ls
2013-03-19 154412.csv  address.ser  music/            ReadObject.java  WriteObject.class
Address.java           foo&bar.cvs  ReadObject.class  video/           WriteObject.java

# -o plusdirs 옵션 설정
$ complete -o plusdirs -W 'aaa bbb ccc' hello

# 자동완성 단어 aaa bbb ccc 외에 디렉토리 이름이 추가 되었다
$ hello [tab]
aaa    bbb    ccc    music/ video/

-W wordlist

직접 생성한 단어 목록을 자동완성 이름으로 사용할때 사용합니다. 이때 단어들은 기본적으로 $IFS 값에 따라 분리됩니다.

words="foo bar zoo"
complete -W "$words" hello     # hello 명령에 자동완성 단어를 설정할때
COMPREPLY=($(compgen -W "$words" -- "$cur"))   # 자동완성 함수에서 COMPREPLY 배열 설정시

다음과 같이 multiple words 로 구성된 문장을 자동완성 이름으로 사용할 수도 있습니다. 이때는 COMPREPLY 배열 설정시 IFS 값을 newline 으로 설정해야 합니다.

_hello() 
{
    local IFS=$' \t\n' cur=$2
    local words="
install
Linked Base for node-1 and node-2
hostname hosts setup
"
    IFS=$'\n'
    COMPREPLY=($(compgen -P \" -S \" -W "$words" -- "$cur"))
    # 또는 quotes 을 붙이고 싶지 않으면
    # COMPREPLY=($(compgen -W "$words" -- "$cur"))
}
complete -F _hello hello

$ hello [tab]
"Linked Base for node-1 and node-2"  "install"
"hostname hosts setup"               

$ hello "Linked Ba[tab]     # 자동완성이 된다.

추출한 단어에 이미 quotes 이 포함되어 있다면 다음과 같이 COMPREPLY 를 설정하면 됩니다.

words='
"IDE Controller"         # 추출한 단어에 이미 quotes 이 포함됨
"SATA Controller"
"SCSI Controller"
'
IFS=$'\n'
COMPREPLY=($(compgen -W '$words' -- \\\"$cur))

-W 옵션 값으로 변수를 사용할때 single quotes 을 사용하면 값에 포함된 quotes 이 유지됩니다 ( 이것은 결과적으로 -W 옵션 값을 eval 한것과 같습니다 ).

$ words='
install
Linked Base for "node-1" and "node-2"    # 값에 " " 이 포함됨
hostname hosts setup
'
$ IFS=$'\n' compgen -W '$words'          # -W '$words' (single quotes 사용)
install
Linked Base for "node-1" and "node-2"    # quotes 이 유지된다.
hostname hosts setup

$ IFS=$'\n' compgen -W "$words"          # -W "$words" (double quotes 사용)
install
Linked Base for node-1 and node-2        # quotes 이 제거된다.
hostname hosts setup
--------------------------------------

$ words='"aa" "bb" "cc"'

$ eval echo '$words'          $ eval echo "$words"
"aa" "bb" "cc"                aa bb cc

-G globpat

globbing 을 자동완성 이름을 생성하는데 사용할 수 있습니다.

$ complete -G "*.java" hello

$ hello [tab]
Address.java      ReadObject.java   WriteObject.java 

$ compgen -G '*.java' 
Address.java
WriteObject.java
ReadObject.java

$ compgen -G '*.java' -- Write      # -- Write 으로 필터링이 되지 않는다.
Address.java                        # -G 옵션은 -X 옵션으로 필터링을 할 수 있습니다.
WriteObject.java
ReadObject.java

-X filterpat

이것은 생성된 자동완성 이름을 패턴을 이용해 필터링 하는 기능입니다.

$ compgen -G '*.java' -X '*Object*'
Address.java

$ complete -W 'aaa.x bbb.y ccc.z' -X "*.z" hello

$ hello         # '*.z' 패턴에 의해 'ccc.z' 가 필터링 되었다.
aaa.x  bbb.y  

$ compgen -W 'aaa.x bbb.y ccc.z' -X "*.z"
aaa.x
bbb.y

-P prefix, -S suffix

생성된 자동완성 이름 앞, 뒤에 prefix, suffix 를 붙일 수 있습니다.

$ complete -W 'aaa bbb ccc' -S "/" hello

$ hello [tab]
aaa/  bbb/  ccc/  

$ compgen -A variable -P "foo/" -S "/"  -- BASH
foo/BASH/
foo/BASHOPTS/
foo/BASHPID/
. . .
# compgen -j 출력값이 vi 일경우 자동완성 이름은 "%vi" 가 된다.
$ complete -j -P '"%' -S '"' disown

-C

tab 키를 누를 때마다 -C 옵션으로 등록한 명령이 실행됩니다. 이때 명령에는 자동완성 함수에서 사용되는 $1( command ) $2( current ) $3( previous ) 세 개의 변수값이 인수로 전달됩니다. 아래는 값이 어떤 식으로 전달되는지 보이기 위해서 test.sh 에서 echo 명령을 사용했지만 -C 옵션으로 등록한 명령에서 출력값이 생기면 자동완성이 제대로 동작하지 않습니다.

$ cat test.sh
#!/bin/sh
echo CMD:$1
echo CUR:$2
echo PRE:$3

$ complete -C ./test.sh -W 'xxx yyy zzz' hello

$ hello [tab]
CMD:hello  CUR:       PRE:hello  xxx        yyy        zzz

$ hello xx[tab]
CMD:hello  CUR:xx     PRE:hello  xxx

$ hello xx yy[tab]
CMD:hello  CUR:yy     PRE:xx     yyy

$ hello xx yy zz[tab]
CMD:hello  CUR:zz     PRE:yy     zzz

-D

자동완성이 설정되지 않은 명령들에 default 로 적용하기 위해 사용

# 자동완성이 설정되지 않은 명령들에 디폴트로 _completion_loader 함수를 적용
# _completion_loader 는 bash-completion 패키지에서 사용하는 lazy loading 위한 함수.
$ complete -D -F _completion_loader

# 터미널을 열고 처음에는 find 명령의 자동완성이 등록되어 있지 않다.
$ complete -p find
bash: complete: find: no completion specification

# tab 을 한번 입력해 주면 _completion_loader 에의해 find 명령의 자동완성이 등록된다.
$ find [tab]

$ complete -p find
complete -F _find find

-E

명령 라인이 empty 상태일 때 적용하기 위해 사용

-I

명령 라인에서 처음단어 ( initial word ) 에 적용하기 위해 사용

$ LD_[tab]      # 현재 시스템에 LD_ 로 시작하는 명령은 없는상태
$
$ complete -I -W "LD_BIND_NOW LD_DEBUG LD_LIBRARY_PATH LD_PRELOAD"

$ LD_[tab]      # initial word 자동완성이 된다.
LD_BIND_NOW          LD_DEBUG         LD_LIBRARY_PATH  LD_PRELOAD 

$ [tab]         # 명령 라인이 empty 상태일때
$
$ complete -E -W "LD_BIND_NOW LD_DEBUG LD_LIBRARY_PATH LD_PRELOAD"

$ [tab]         # empty 상태에서 자동완성이 된다.
LD_BIND_NOW          LD_DEBUG         LD_LIBRARY_PATH  LD_PRELOAD

자동완성 함수

위에서 여러가지 자동완성 이름을 생성하는 방법을 살펴보았지만 사실 주로 사용하는 방법은 -F 옵션을 이용한 자동완성 함수 입니다. 자동완성 함수는 일반 함수와 같고 정의될 때 alias 가 확장됩니다. 기본적으로 export 하지 않으므로 현재 프롬프트 shell 에서만 실행이되고 또한 bash 에서만 사용되는 함수이므로 bash 가 가지는 여러 가지 확장 기능을 사용할 수 있습니다. 자동완성 함수의 이름을 지을때는 보통 다른 명령과 충돌되지 않게 _ 문자로 시작을 합니다.

프롬프트 상에서 _[tab] 을 입력해 보면 자동완성 함수 목록을 볼 수 있습니다.

함수 내에서 자동으로 설정되는 변수들

$1 , $2 , $3 세 개의 변수가 정의되고 의미는 다음과 같습니다. ($# == 3)

mycomm ubuntu fedora arch[tab]
   \            |        \
    \_ $1       |_ $3     \_ $2
  (command)  (previous)  (current)
  • COMP_WORDS

    현재까지 입력한 명령 라인의 인수들을 값으로 가지고 있는 array 변수입니다. 위의 예에서는 COMP_WORDS[0]=mycomm, COMP_WORDS[1]=ubuntu, COMP_WORDS[2]=fedora, COMP_WORDS[3]=arch 가 됩니다.

mycomm ubuntu fedora arch[tab]    # ${#COMP_WORDS[@]} == 4, $COMP_CWORD == 3
mycomm ubuntu fedora [tab]        # ${#COMP_WORDS[@]} == 4, $COMP_CWORD == 3
  • COMP_CWORD

    현재 커서가 위치한 인수의 index 를 나타냅니다. 위의 예에서는 3 이 됩니다. 따라서 $2${COMP_WORDS[COMP_CWORD]} 와 같고 $3${COMP_WORDS[COMP_CWORD-1]} 와 같게 됩니다. 명령 라인에서 커서를 왼쪽으로 이동하면 그에따라 $COMP_CWORD, $2, $3 값도 변경됩니다.

  • COMP_LINE

    현재 프롬프트 상에서 작성 중인 명령 라인 전체를 나타냅니다. 변수, 명령치환, quotes 등이 확장없이 문자 그대로 포함됩니다.

  • COMP_POINT

    명령 라인에서 현재 커서가 위치한 문자수 index 를 나타냅니다.

  • COMPREPLY

    array 변수로, 여러 방법을 통해 만들어진 자동완성 단어를 최종적으로 이 변수에 넣은 후 리턴하게 됩니다. 그럼 tab 키를 이용한 자동완성시에 이 변수의 값들이 사용됩니다. 값이 출력될 때는 자동으로 중복이 제거되고 sort 되어 출력됩니다.

자동완성에서 current, previous .. 값이 설정되는 것은 $IFS 값을 따르는 것이 아니라 $COMP_WORDBREAKS 변수에 설정된 값을 따릅니다. 따라서 스트링이 공백없이 붙어 있어도 구분해서 처리할수가 있습니다. 예를들어 --foo=[커서] 상태에서 tab 키를 누르면 = 문자와 --foo 값을 참조해서 자동완성 단어를 생성할 수 있습니다.

$ echo -n "$COMP_WORDBREAKS" | od -tax1
0000000  sp  ht  nl   "   '  >   <   =   ;   |   &   (   :
         20  09  0a  22  27  3e  3c  3d  3b  7c  26  28  3a

$ command --foo=[tab]                       $ command --foo [tab]
# current: =      previous: --foo           # current: empty     previous: --foo

$ command --foo=bar[tab]
# current: bar    previous: =

변수 사용시 참고할 사항은 만약에 current 에 해당하는 값이 $COMP_WORDBREAKS 변수에 포함된 문자일 경우 ( ex. "=" ) $2 변수값은 empty 가 됩니다. 이때 해당 문자값을 구하고 싶으면 $2 변수 대신에 ${COMP_WORDS[COMP_CWORD]} 를 사용해야 됩니다. 이것은 스크립트 작성 편의를 위해 의도된 기능으로 만약에 current 값에 $COMP_WORDBREAKS 문자가 들어있으면 tab 키를 눌렀을때 자동완성이 시작되지 않겠죠.

$ command --foo=[tab]
# $2: empty     $3: --foo     ${COMP_WORDS[COMP_CWORD]}: =

-----------------------------------------------------------

$ command --foo=abcdefg       # abc[tab]defg 와 같이 단어 중간에서 tab 을 눌렀을 경우
# $2: abc       $3: =         ${COMP_WORDS[COMP_CWORD]}: abcdefg

", ' quotes 문자는 따로 분리되지 않고 값에 포함되는데 current 에 해당하는 $2 에는 포함되지 않고 ${COMP_WORDS[COMP_WORDS]} 에는 포함됩니다. quotes 이 완성되고 난후에는 previous 에 해당하는 $3, ${COMP_CWORD[COMP_CWORD-1]} 값에는 모두 quotes 이 포함됩니다.

$ command --foo="bar[tab]
# $2: bar       $3: =        ${COMP_WORDS[COMP_CWORD]}: "bar

$ command --foo="bar" [tab]
# $2: empty     $3: "bar"    ${COMP_WORDS[COMP_CWORD-1]}: "bar"
bash-completion 패키지

대부분의 리눅스 배포본에 기본으로 설치되는 bash-completion 패키지는 자동완성 함수를 작성할때 사용할수 있게 여러 함수들을 제공하는데 많은 gnu 명령들이 이 함수를 이용해서 작성되어 있습니다. 이 패키지를 설치하면 사용할수 있는 독특한 기능이 명령 라인에서 다른 명령의 자동완성 함수를 호출할수 있습니다.

$ sudo hello --foo [tab]
$ find . -exec hello --foo [tab]     # find 명령 라인에서 hello 명령의 자동완성
$ gdb --args hello --foo [tab]       # 함수를 사용할 수 있다. (find 명령의 자동완성
$ strace hello --foo [tab]           # 함수가 hello 명령의 자동완성 함수를 호출)

문제는 이와같은 기능을 구현하기 위해서 bash 에서 default 로 설정되는 $2, $3 변수값을 변경합니다 ( 변경하지 않게 만들수도 있는데 ). 따라서 bash-completion 패키지를 지원하려면 자동완성 함수를 작성할때 다음과 같은 규칙을 따라야 합니다.

# bash-completion 패키지에서 $2 값은 ${COMP_WORDS[COMP_CWORD]} 로 변경하고 $3 값은 empty
# 가 되는 경우가 있으므로 current 와 previous 값을 다음과 같이 설정해서 사용해야 합니다.

local cmd=$1 cur=${COMP_WORDS[COMP_CWORD]} prev=${COMP_WORDS[COMP_CWORD-1]}
[[ ${COMP_LINE:COMP_POINT-1:1} = " " ]] && cur=""

# 위에서 두번째 라인은 다음과같은 경우 때문에 필요합니다.
$ hello -a foo -b bar      # 이 상태에서 사용자가 -a 옵션 값을 변경하려고 함
$ hello -a [tab]-b bar     # $2: empty    ${COMP_WORDS[COMP_CWORD]}: -b

# 사실 bash-completion 패키지만 아니면 저런것 다 필요없고 아래 한줄이면 됩니다.
# 처음에 잘못만든것을 고치지 않고 계속 써와서 어쩔수 없습니다. 자동완성 함수 만드는게
# 그렇게 어려운게 아닌데 이런것들 때문에 사람들이 자동완성 함수를 잘 안만들려고 합니다.
local cmd=$1 cur=$2 prev=$3

compgen 명령

이 명령은 자동완성 이름을 생성하는데 사용됩니다. 앞서 소개한 complete 명령의 옵션들을 거의 동일하게 사용할 수 있습니다. 한가지 유용한 기능은 마지막에 -- word 를 인수로 주면 word 와 매칭되는 단어들만 선택되게 됩니다. 그러므로 주로 COMPREPLY 배열에 값을 저장하는 용도로 사용됩니다.

# 마지막에 '-- word' 인수를 주면 매칭되는 단어들만 나온다
$ compgen -W 'a111 b222 b333 c444' -- b
b222
b333
-------------------------------------

COMPREPLY=( $(compgen -d -- "$cur") )      # 디렉토리 이름을 자동완성 단어로 사용

compopt 명령

자동완성 함수 실행중에 앞서 소개한 옵션을 compopt 명령을 이용해 변경할 수 있습니다. COMPREPLY 배열 값이 empty 가 되어 자동완성할 단어가 없을 경우 complete 등록시 사용된 옵션 이나 compopt 명령으로 설정한 옵션이 적용됩니다.

# COMPREPLY 값이 empty 여서 완성할 단어가 없을경우
# 디렉토리 목록을 자동완성 단어로 사용합니다.

if [[ $prev == @(-d|--datadir) ]]; then
    compopt -o dirnames
fi    
---------------------------------------

# tab 키를 누르면 기본적으로 자동완성 단어를 입력하고 space 를 띄우는데
# 완성할 단어가 'string=' 형식일경우 space 를 띄우지 않게합니다.

[[ ${COMPREPLY: -1} == "=" ]] && compopt -o nospace

예제 )

다음은 간단하게 2 단계의 옵션을 처리하는 자동완성 함수입니다. 자동완성에 사용되는 단어는 직접 입력하였으나 꼭 그럴 필요는 없고 여러 유틸리티 프로그램을 활용해 생성할 수 있습니다. 최종적으로 COMPREPLY 배열에 단어들을 넣어주고 return 하면 되는 것입니다.

다음 내용을 hello.sh 에 작성하였다면 source hello.sh 한 후 프롬프트 상에서 테스트해봅니다. 파일 마지막에 complete 명령의 -F 옵션으로 hello 의 자동완성 함수를 등록하는 것을 볼 수 있습니다.

_hello() {
    local cmd=$1 cur=$2 prev=$3

    local options="--fruit --planet --animal"
    local fruits="apple orange banana"
    local animals="lion tiger elephant"
    local planets="mars jupiter saturn"

    if (( COMP_CWORD == 1 )); then
        COMPREPLY=( $(compgen -W "$options" -- $cur) )
        return
    fi

    if (( COMP_CWORD == 2 )); then
        case $prev in
            --fruit)
                COMPREPLY=( $(compgen -W "$fruits" -- $cur) )
                return ;;
            --animal)
                COMPREPLY=( $(compgen -W "$animals" -- $cur) )
                return ;;
            --planet)
                COMPREPLY=( $(compgen -W "$planets" -- $cur) )
                return ;;
        esac
    fi
}

complete -F _hello hello

이번에는 자동완성 단어를 uftrace 명령에서 제공하는 help 메시지에서 추출해 사용하는 예입니다.

uftrace 는 Namhyung Kim 님이 만든 명령으로 C/C++ 로 작성된 실행파일을 함수 레벨에서 trace 할 수 있습니다. ( 특히 recursion 을 이용해 작성된 함수의 경우 호출 관계를 한눈에 볼 수 있습니다. )

https://github.com/mug896/uftrace-bash-completion

_uftrace() 
{
    local cmd=$1 cur=${COMP_WORDS[COMP_CWORD]} prev=${COMP_WORDS[COMP_CWORD-1]}
    [[ ${COMP_LINE:COMP_POINT-1:1} = " " ]] && cur=""    # bash-completion 패키지 지원
    local IFS=$' \t\n' words

    if [[ $cur == -* ]]; then     # 현재 입력중인 단어가 "-" 문자로 시작하는 옵션일 경우
        words=$( $cmd --help | 
            sed -En '/^\s+-/{ s/^\s{,10}((-\w),?\s)?(--[[:alnum:]-]+)?.*/\2 \3/p }' )
    elif [[ $prev == --match ]]; then
        words="regex glob"
    elif [[ $prev == @(-!(-*)s|--sort) ]]; then
        words="total self call avg min max"
    elif [[ $prev == --logfile ]]; then
        :     # else 를 skip 하고 -o default 디폴트 설정을 사용하기 위해
    else
        words="record replay live report info dump recv graph script tui"
    fi
    COMPREPLY=( $(compgen -W "$words" -- "$cur") )
}

complete -o default -o bashdefault -F _uftrace uftrace

-s 같은 short 옵션을 매칭할 때 -!(-*)s 와 같이 해야되는 [이유]

2.

이번 예제는 kill 명령을 사용할때 먼저 PID 를 검색할 필요없이 자동완성을 이용해 프로세스 목록을 보여주는 기능입니다. 기존 kill 명령은 신호이름만 자동완성이 되는데 PID 목록도 볼수있으면 편리하겠죠?

_kill2() 
{
    local cur=${COMP_WORDS[COMP_CWORD]} words
    [[ ${COMP_LINE:COMP_POINT-1:1} = " " ]] && cur=""
    # 자동완성 함수는 현재 prompt shell 에서 실행되므로 영향을 미치지 않으려면
    # local 로 설정해 사용해야 합니다.
    local IFS=$' \t\n'

    # '-' 문자로 시작하는 신호값 자동완성 
    if [[ $cur == -* ]]; then
        words=$( kill -l | sed -Ez 's/[0-9]+\) SIG/-/g' )
        COMPREPLY=( $(compgen -W "$words" -- $cur) )

    else
        # PID 목록은 자동완성할 것이 아니므로 앞에 번호를 붙여서 출력합니다.
        # 번호를 붙이지 않으면 자동완성이 적용되어 목록이 제대로 표시되지 않습니다.
        words=$( ps axh -o pid,user,comm | gawk 'BEGIN {IGNORECASE=1} 
        $3~/'"$cur"'/{a[i++]=$0} END{ 
        if (isarray(a)) {
            if (length(a) == 1) print a[0]
            else { 
                len=length(i)
                for (i in a) 
                    printf "%0*d) %s\n", len, i+1, a[i]
            }
        }}')
        # COMPREPLAY array 변수에 자동완성 단어가 설정될때 IFS 값에따라 분리가 됩니다.
        # 여기서는 PID 와 명령을 같이 보여줘야 하므로 IFS 값을 newline 으로 설정합니다.
        IFS=$'\n'
        COMPREPLY=( $words )
    fi
}

complete -F _kill2 kill

3.

이번에는 2번 예제를 가지고 프로세스 목록이 출력됐을때 번호를 입력하면 해당 PID 와 명령이름이 입력되는 것을 해보겠습니다. 이전 검색 결과를 사용하기 위해서는 global 변수에 저장해 놓는것이 필요하기 때문에 여기서는 함수명과 동일한 변수이름을 사용하였습니다.

_kill3() 
{
    local cur=${COMP_WORDS[COMP_CWORD]} words
    [[ ${COMP_LINE:COMP_POINT-1:1} = " " ]] && cur=""
    local IFS=$' \t\n'

    if [[ $cur == -* ]]; then
        words=$( kill -l | sed -Ez 's/[0-9]+\) SIG/-/g' )
        COMPREPLY=( $(compgen -W "$words" -- $cur) )

    # current 그러니까 현재 커서가 위치한 곳의 값이 숫자인 상태에서 tab 키를 눌렀을경우
    # 이전에 $_kill3 변수에 저장해 놓은 목록을 이용해 PID 와 명령이름을 출력합니다.
    elif [[ $cur == +([0-9]) ]]; then
        cur=$(( 10#$cur ))   # 8 진수로 인식되는 것을 방지하기 위해 10 진수로 변경
        COMPREPLY=( "$(<<< $_kill3 gawk $cur' == $1+0 {print $2,$4; exit}')" )

    else
        words=$( ps haxo pid,user,comm | gawk 'BEGIN {IGNORECASE=1} 
        $3~/'"$cur"'/{ a[i++] = $0 } END {
        if (isarray(a)) {
            len = length(i)
            for (i in a)
                printf "%0*d) %s\n", len, i+1, a[i]
            if (length(a) == 1) print " "    # 목록이 1 개일 경우 자동완성 되는것을 방지
        }}')

        # 생성된 프로세스 목록을 global 변수인 $_kill3 에 저장해놓습니다.
        _kill3=$words
        IFS=$'\n' COMPREPLY=( $words )
    fi
}

complete -F _kill3 kill

4.

clang 은 gcc 와 달리 기본적으로 자동완성이 설정되어 있지 않은데 여기서 설정해 보도록 하겠습니다.

clang 은 --autocomplete 옵션을 통해서 자동완성 기능을 지원하고 있습니다.
https://github.com/mug896/clang-bash-completion

# '-std' 스크링과 매칭되는 옵션을 출력
$ clang --autocomplete=-std
-std:   Language standard to compile for
-std=   Language standard to compile for
-stdlib=        C++ standard library to use

$ clang --autocomplete=-stdl
-stdlib=        C++ standard library to use

# '-stdlib=' 옵션에서 사용 가능한 값을 출력
$ clang --autocomplete=-stdlib=
libc++
libstdc++
platform
_clang() 
{
    local cmd=$1 cur=${COMP_WORDS[COMP_CWORD]} 
    [[ ${COMP_LINE:COMP_POINT-1:1} = " " ]] && cur=""
    local prev=${COMP_WORDS[COMP_CWORD-1]} prev2=${COMP_WORDS[COMP_CWORD-2]}
    local words tmp

    if [[ $cur == --* ]]; then      # '--' 로 시작되는 옵션
        words=$( $cmd --autocomplete="$cur" | gawk '{print $1}' )

    elif [[ $cur == -* ]]; then     # '-' 로 시작되는 옵션
        words=$( $cmd --autocomplete="$cur" | gawk '$1 !~ /^--/{print $1}' )

    # $cur 값이 '=' 문자일 경우: -std=[tab] 또는
    # $prev 값이 '=' 문자일 경우: -std=c++[tab]
    # -std 옵션 스트링을 --autocomplete= 값으로 전달해 사용할수 있는 값을 출력합니다.
    elif [[ $cur == "=" || $prev == "=" ]]; then
        [[ $cur == "=" ]] && tmp=$prev || tmp=$prev2
        words=$( $cmd --autocomplete="$tmp=" )

    elif [[ $cur == ":" || $prev == ":" ]]; then
        [[ $cur == ":" ]] && tmp=$prev || tmp=$prev2
        words=$( $cmd --autocomplete="$tmp:" | gawk '{print substr($1, index($1,":")+1)}' )

    else
        # 그외 값들 예를 들어 '-meabi [tab]'
        # exit 부분은 '-c [tab]' 했을때 -c 값이 중복 입력되는 것을 방지
        words=$( $cmd --autocomplete="$prev" | gawk '$1 ~ /^-/{exit}{print $1}' )
    fi
    # $cur 값이 '=' 또는 ':' 와 같이 $COMP_WORDBREAKS 변수에 있는 값일경우 
    # tab 키를 입력했을때 자동완성이 시작되지 못하므로 $cur 값을 empty 로 설정
    # (만약에 $cur 값으로 ${COMP_WORDS[COMP_CWORD]} 대신에 $2 를 사용한다면 필요 없겠죠)
    [[ $COMP_WORDBREAKS == *$cur* ]] && cur=""
    COMPREPLY=( $(compgen -W "$words" -- "$cur") )
    # 완성할 단어가 'string=' 또는 'string:'  형식일경우 space 를 띄우지 않게합니다.
    [[ "${COMPREPLY: -1}" == [:=] ]] && compopt -o nospace
}

# COMPREPLY 값이 empty 가 되어 자동완성할 단어가 없을 경우 디폴트로 default, bashdefault
# 옵션이 적용되게 하고 clang, clang++ 두개의 명령에 _clang 함수를 설정
complete -o default -o bashdefault -F _clang clang clang++

5.

다음은 하나의 코드 베이스로 모바일, 웹, 데스크톱 애플리케이션을 모두 만들 수 있는 Dart 언어용 자동완성입니다. ${COMP_LINE} 변수 활용하는 것을 볼 수 있습니다.

_dart() 
{
    local cmd=$1 cur=${COMP_WORDS[COMP_CWORD]}
    [[ ${COMP_LINE:COMP_POINT-1:1} = " " ]] && cur=""
    local IFS=$' \t\n' words
    local sed_cmd sed_opt
    read -rd "" sed_cmd <<\@
    sed -En '/^Available (sub)?commands:/,${ s/^  ([^[:blank:]]+).*/\1/p }'
@
    read -rd "" sed_opt <<\@
    sed -En -e 's/^ *((-\w), )?(-[^[:blank:]<]+).*/\2\n\3/; tX; b' \
            -e ':X s/\[|\]//g; p; tY; b' -e ':Y s/no-//p'
@
    # $COMP_LINE 변수의 경우 현재 입력중인 미완성 단어가 그대로 값에 포함되므로
    # --help 옵션 적용시 오류가 발생하지 않게 ${COMP_LINE% *} 를 이용해 제거해줍니다.
    if [[ $cur == -* ]]; then
        if words=$( eval "${COMP_LINE% *} --help" 2>&1 ); then
            words=$( <<< $words eval "$sed_opt" )
        else
            echo; <<< $words head -n1 >&2     # 오류 발생시 메시지 출력
            return
        fi
    else
        if (( COMP_CWORD == 1 )); then
            words=$( $cmd --help | eval "$sed_cmd" )
        else
            if words=$( eval "${COMP_LINE% *} --help" 2>&1 ); then
                words=$( <<< $words eval "$sed_cmd" )
            else
                echo; <<< $words head -n1 >&2
                return
            fi
        fi
    fi
    COMPREPLY=( $(compgen -W "$words" -- "$cur") )
    [[ ${COMPREPLY: -1} == "=" ]] && compopt -o nospace
}

complete -o default -o bashdefault -F _dart dart

6.

httpie 명령은 curl 과같은 http client 인데 명령이 python 스크립트로 되어있어서 실행속도가 좀 느립니다. 따라서 tab 키를 누를때마다 매번 명령을 실행하지 않고 처음 실행시에 help 메시지를 변수에 저장해놓고 사용합니다. 또한 --ssl [tab] 형태뿐만 아니라 --ssl=[tab] 에서도 자동완성이 되게 처리합니다.

https://github.com/mug896/httpie-bash-completion

# httpie 설치: pip install --upgrade httpie
_http () 
{
    local cmd=$1 cur=${COMP_WORDS[COMP_CWORD]} prev=${COMP_WORDS[COMP_CWORD-1]}
    [[ ${COMP_LINE:COMP_POINT-1:1} = " " ]] && cur=""
    local IFS=$' \t\n' words _cmd=__$cmd         # $__http 변수에 help 메시지를 저장
    local ver=$(stat -L -c %Y `type -P "$cmd"`)  # 실행파일이 변경되었는지 체크하기 위한 버전값
    local help=${!_cmd#*$'\n'}                   # $__http 변수에서 help 메시지만 추출

    [[ $prev == "=" ]] && prev=${COMP_WORDS[COMP_CWORD-2]}  # --ssl=tls[tab] 처리를 위한것
    if [[ $cur == -* ]]; then
        if [[ -z ${!_cmd} || $ver != ${!_cmd%%$'\n'*} ]]; then
            # $__http 변수에 버전값을 제일 위에 놓고 그다음 help 메시지를 저장.
            eval ${_cmd}='$ver$'"'\\n'"'$( $cmd --help )'
        fi
        words=$(<<< ${!_cmd#*$'\n'} sed -En '/^  -/p' | grep -Eo -- ' -[[:alnum:]-]+\b')
    elif [[ $prev == --ssl ]]; then
        words=$(<<< $help sed -En '/^[ ]{,5}--ssl/{ s/^[^{]*(.*)}.*/\1/; s/,|\{|}/ /g; p }')
    elif [[ $prev == @(-!(-*)A|--auth-type) ]]; then
        words="basic bearer digest"
    elif [[ $prev == @(-!(-*)p|--print) ]]; then
        IFS=$'\n'
        words='
"H" request headers
"B" request body
"h" response headers
"b" response body
"m" response metadata
'
    elif [[ $prev == --pretty ]]; then
        words="all colors format none"
    elif [[ $prev == @(-!(-*)s|--style) ]]; then
        words="abap algol algol_nu arduino auto autumn borland bw
          colorful default dracula emacs friendly
          friendly_grayscale fruity gruvbox-dark gruvbox-light
          igor inkpot lilypond lovelace manni material monokai
          murphy native one-dark paraiso-dark paraiso-light
          pastie perldoc pie pie-dark pie-light rainbow_dash
          rrt sas solarized solarized-dark solarized-light stata
          stata-dark stata-light tango trac vim vs xcode
          zenburn"
    elif [[ $prev == @(-!(-*)o|--output) ]]; then
        :
    else
        words="GET POST PUT HEAD DELETE PATCH OPTIONS CONNECT TRACE"
    fi
    [[ $COMP_WORDBREAKS == *$cur* ]] && cur=""      # --ssl=[tab] 처리를 위한것
    COMPREPLY=( $(compgen -W '$words' -- "$cur") )
}

complete -o default -o bashdefault -F _http http https

7.

Virtualbox 자동완성

virtualbox 에서는 가상머신 이름이나 네트워크 이름 등을 입력할때 multiple words 를 사용할 수 있기 때문에 IFS 값을 IFS=$'\n' 로 사용해서 자동완성 단어가 multiple words 로 구성될 경우 자동으로 'foo bar' quotes 을 추가하는 것을 볼 수 있습니다.

https://github.com/mug896/virtualbox-bash-completion

qemu 명령 자동완성

qemu-system-x86_64 명령에서는 -netdev bridge,id=hostnet0,br=br0,helper=/path/to/helper 와 같이 , 로 분리되는 옵션값 입력이나 -global virtio-blk-device.scsi=off 와 같이 . 으로 이어지는 옵션값 입력이 사용되는데 이와같은 처리를 하기위해 $COMP_WORDBREAKS 변수값을 추가하거나 삭제해서 사용하는 것을 볼 수 있습니다.

https://github.com/mug896/qemu-bash-completion

gcc 명령 자동완성

기존의 gcc 자동완성 함수와 동일하게 자동완성 단어를 생성하기 위해 gcc --completion="str" 명령을 사용하기 때문에 결과에는 차이가 없지만 몇 가지 유용한 기능을 추가하였습니다. 예를 들어 명령문 작성 중에 *, ?, [] glob 문자를 이용해 자동완성 단어를 검색할 수 있습니다. ( gcc -save-temps -*alias*[tab] )

https://github.com/mug896/gcc-bash-completion

ip 명령 자동완성

https://github.com/mug896/ip-bash-completion

ip 로 직접 만들어보는 네트워크 네임스페이스와 브리지 네트워크

# exec 의 명령으로 ip 가 사용될 경우 두 번째와 같이 -n 옵션을 이용해 간단히 할 수 있습니다.
$ ip netns exec foo ip address add 10.200.0.3/24 dev veth1
$ ip -n foo address add 10.200.0.3/24 dev veth1

Go 언어 자동완성

https://github.com/mug896/go-bash-completion

_init_comp_wordbreaks 함수

자동완성 함수를 작성하다 보면 $COMP_WORDBREAKS 값을 변경해 사용하면 좋을때가 있습니다. 하지만 이변수는 global 변수라 변경을 하면 기존값에 따라 작성된 다른 자동완성 함수에 영향을 미치기 때문에 가능하면 변경을하지 않는것이 좋습니다. _init_comp_wordbreaks 함수는 $COMP_WORDBREAKS 변수의 기존값으로의 복구를 자동으로 해줍니다. 따라서 $COMP_WORDBREAKS 변수를 변경해 사용할때 뿐만아니라 모든 자동완성 함수의 시작을 _init_comp_wordbreaks 로 하는것이 좋습니다.

_init_comp_wordbreaks()
{
    if [[ $PROMPT_COMMAND =~ ^:[^\;]+\;COMP_WORDBREAKS ]]; then
        [[ $PROMPT_COMMAND =~ ^:\ ([^;]+)\; ]]
        [[ ${BASH_REMATCH[1]} != "${COMP_WORDS[0]}" ]] && eval "${PROMPT_COMMAND%%$'\n'*}"
    fi
    if ! [[ $PROMPT_COMMAND =~ ^:[^\;]+\;COMP_WORDBREAKS ]]; then
        PROMPT_COMMAND=": ${COMP_WORDS[0]};COMP_WORDBREAKS=${COMP_WORDBREAKS@Q};\
        "$'PROMPT_COMMAND=${PROMPT_COMMAND#*$\'\\n\'}\n'$PROMPT_COMMAND
    fi
}
-------------------------------------------------------------------------

# 만약에 clang 자동완성 함수에서 ":" 문자를 $COMP_WORDBREAKS 에서 제외해서 
# 사용하려면 다음과 같이 추가해주면 됩니다.
_clang() 
{
    _init_comp_wordbreaks
    COMP_WORDBREAKS=${COMP_WORDBREAKS//:/}
    . . .
# _init_comp_wordbreaks 를 이용해 ":" 문자를 $COMP_WORDBREAKS 에서 제외할 경우
$ clang -Zc:t[tab]
-Zc:ternary          -Zc:threadSafeInit-  -Zc:trigraphs-       -Zc:twoPhase-
-Zc:threadSafeInit   -Zc:trigraphs        -Zc:twoPhase

# 기존과 같이 ":" 문자가 $COMP_WORDBREAKS 에 포함될 경우
$ clang -Zc:t[tab]
ternary          threadSafeInit-  trigraphs-       twoPhase-
threadSafeInit   trigraphs        twoPhase

Completion 활용

자동완성 기능을 활용하여 자주 사용하는 명령 옵션이나, 문장을 한번에 입력합니다.

# ~/.bashrc.d 디렉토리에 두어 쉘 실행시 source 명령으로 읽어 들입니다.
$ cat myOptions.sh
complete -W -fsyntax-only clang
complete -W '"-std=c++20 -fsyntax-only"' clang++
complete -W $'"\'BEGIN { }\'"' awk

시스템의 completion 스크립트 파일 위치

bash shell 은 시작시 자동으로 ~/.bash_completion 파일과 /usr/share/bash-completion/ , /etc/bash_completion.d , $HOME/.local/share/bash-completion/completions/(*.bash 확장자) 디렉토리에서 스크립트 파일을 읽어들입니다.

Quiz

위에서 설명을 했지만 bash-completion 패키지를 설치하면 bash 에서 default 로 설정되는 $2, $3 변수를 사용할 수 없습니다. 이것은 자동완성 함수 작성시 [[ $COMP_WORDBREAKS == *$cur* ]] && cur="" 와 같이 불필요한 코드를 추가하게 만드는데요. 이것은 특히 bash-completion 패키지에서 제공하는 sudo 자동완성 함수를 사용하게 될때 격을수 있는데 이 sudo 자동완성 함수를 다시 작성하여 수정해보는 것입니다.

_sudo() 
{
    local IFS=$' \t\n' arr func cmd=${COMP_WORDS[1]} cur=$2 i
    if (( COMP_CWORD == 1 )); then                # sudo comm[tab] 상태일 경우
        COMPREPLY=($( compgen -A command -- "$cur" ))
        return
    fi
    if ! complete -p "$cmd" &> /dev/null; then    # 사용자 자동완성 함수가 등록되지 않았으면
        _completion_loader "$cmd" &> /dev/null    # bash-completion 패키지의 loader 를시도
    fi
    if arr=( $(complete -p "$cmd" 2> /dev/null) ); then
        for (( i = 1; i < ${#arr[@]}; i++ )); do      # 사용자 자동완성 함수 이름을 설정
            [[ ${arr[i]} == -F ]] && { func=${arr[i + 1]}; break ;}
        done
        if [[ -n $func ]]; then 
            COMP_LINE=${COMP_LINE/$1+( )/}        # $COMP_LINE 에서 sudo 제거
            COMP_POINT=${#COMP_LINE}              # COMP_POINT 값 수정
            unset -v 'COMP_WORDS[0]'              # COMP_WORDS 배열에서 sudo 제거
            COMP_WORDS=( "${COMP_WORDS[@]}" )     # COMP_WORDS 배열 재설정
            let COMP_CWORD--                      # COMP_CWORD 를 -1 하고
            "$func" "$cmd" "$2" "$3"              # 사용자 자동완성 함수 호출 
        fi
    fi
}
_hello() 
{
    echo
    echo '$1:'" [$1]"
    echo '$2:'" [$2] / [${COMP_WORDS[COMP_CWORD]}]"
    echo '$3:'" [$3]"
}
complete -o default -o bashdefault -F _sudo sudo command     # command builtin 명령
complete -F _hello hello

---------------------------------------------------------------

# 새로만든 sudo 자동완성 함수                  # 기존 bash-completion sudo 함수
$ sudo hello --foo=[tab]                  $ sudo hello --foo=[tab]
$1: [hello]                               $1: [hello]
$2: [] / [=]                              $2: [=] / [=]
$3: [--foo]                               $3: [--foo]
^C                                        ^C

$ sudo hello --foo="bar[tab]              $ sudo hello --foo="bar[tab]
$1: [hello]                               $1: [hello]
$2: [bar] / ["bar]                        $2: ["bar] / ["bar]
$3: [=]                                   $3: [=]
^C                                        ^C

이번에는 -I ( initial word ) 옵션을 이용해 LD_BIND_NOW LD_DEBUG LD_LIBRARY_PATH LD_PRELOAD LD_TRACE_LOADED_OBJECTS LANG LANGUAGE LC_CTYPE LC_COLLATE LC_ALL 변수들을 추가해 보겠습니다 ( man ld.so ). 명령에 선행하는 변수는 자동으로 COMP_* 변수들 값에서 제외됩니다.

complete -I -o bashdefault -W "LD_BIND_NOW LD_DEBUG LD_LIBRARY_PATH LD_PRELOAD
    LD_TRACE_LOADED_OBJECTS LANG LANGUAGE LC_CTYPE LC_NUMERIC LC_TIME LC_COLLATE LC_ALL"
-------------------------------------------------------------------------

$ LD_[tab]
LD_BIND_NOW              LD_LIBRARY_PATH          LD_TRACE_LOADED_OBJECTS
LD_DEBUG                 LD_PRELOAD

$ LD_DEBUG=libs cla[tab]
clang                     clang-format              clang-refactor
. . .
$ LD_DEBUG=libs clang --std[tab]
--std=     --stdlib=

위에서 작성한 uftrace, kill, httpie ... 자동완성 함수들도 모두 정상적으로 동작하는 것을 볼 수 있습니다. 이와 같이 bash-completion 패키지를 사용하지 않고도 bash 에서 제공하는 기본 기능만으로 충분히 자동완성 함수를 만들수가 있습니다. shell 에서는 자기가 하고싶은 것을 외부명령을 실행해서 다 할수있는데 왜 또 bash-completion 같은 함수가 필요합니까? 그렇죠? bash-completion 패키지도 다 위에서 설명한 bash 에서 제공하는 기능으로 만드는 것입니다.

2 .

현재 디렉토리에서 파일은 제외하고 디렉토리만 출력하려면 어떻게 할까요?

# 현재 디렉토리 구성이 다음과 같을 경우
$ ls -a
./  ../  .aaa/  .bbb/  .ccc/  bar/  foo/  zoo/  .hidden.txt  file1.txt  file2.txt


$ compgen -d                         $ compgen -d $PWD/      # 전체 경로를 출력  
.aaa                                 /home/mug896/tmp/.aaa
.bbb                                 /home/mug896/tmp/.bbb
.ccc                                 /home/mug896/tmp/.ccc
bar                                  /home/mug896/tmp/bar
foo                                  /home/mug896/tmp/foo
zoo                                  /home/mug896/tmp/zoo
-----------------------------------------------------------

# 모든 파일을 출력
$ compgen -f                         $ compgen -f $PWD/      # 전체 경로를 출력
.aaa                                 /home/mug896/tmp/.aaa
.bbb                                 /home/mug896/tmp/.bbb
.ccc                                 /home/mug896/tmp/.ccc
.hidden.txt                          /home/mug896/tmp/.hidden.txt
bar                                  /home/mug896/tmp/bar
file1.txt                            /home/mug896/tmp/file1.txt
file2.txt                            /home/mug896/tmp/file2.txt
foo                                  /home/mug896/tmp/foo
zoo                                  /home/mug896/tmp/zoo
------------------------------------------------------------

# dot 파일은 제외하고 출력               # dot 파일만 출력 
$ compgen -G '*'                     $ compgen -G '.*' -X '+(.)'  # "." ".." 제거
foo                                  .hidden.txt
file2.txt                            .bbb
file1.txt                            .ccc
zoo                                  .aaa
bar

다음은 디렉토리를 제외하고 파일만 출력합니다.

$ res=$( IFS=$'\n'; AA=(`compgen -d`); IFS='|'; eval compgen -f -X '@("${AA[*]}")' )
$ echo "$res"
.hidden.txt
file1.txt
file2.txt

$ find . -maxdepth 1 ! -type d 
./file1.txt
./file2.txt
./.hidden.txt

$ find * .*[^.]* -maxdepth 0 ! -type d          # './' 를 제거하고 출력
file1.txt
file2.txt
.hidden.txt

$ find . -maxdepth 1 ! -type d ! -name '.*'     # hidden 파일은 제외하고 출력
./file1.txt
./file2.txt

$ find . -maxdepth 1 ! -type d ! -name '.*' -printf '%P\n'
file1.txt
file2.txt