스물네번째 날: use Complete::Bash;

저자

John_Kang - 시스템 엔지니어

시작하며

펄을 접할 때 단연코 가장 흥미로운 것은 "게으름은 미학이다!"라는 철학입니다. 게으름(laziness) 그 자체를 어떻게 바라보는지는 크게 관심 없습니다만, 게을러지기 위한 노력들은 삶을 윤택(?)하게 만들어 주기 때문이 아닐까합니다. 이번에 다룰 주제를 통해서 다시 한번 게을러 보고자 합니다 :)

인기있는 유용한 유틸리티들은 항상 다양한 기능을 제공하기 위해 명령줄 옵션을 제공할 뿐만 아니라 때때로 부명령(subcommand)까지도 지원합니다. 하지만 이런 다양한 명령과 옵션을 오랫동안 기억하는 것은 쉬운 일이 아닙니다. 심지어 입력하다보면 오타가 발생하기도 하죠. --help 옵션을 이용해 도움말을 확인하면 될 일이지만, 번거롭긴 합니다.

자! 명령줄 자동 완성 기능을 이용해 좀 더 게을러져 보죠!!! :-)

준비물

필요한 모듈은 다음과 같습니다.

직접 CPAN을 이용해서 설치한다면 다음 명령을 이용해서 모듈을 설치합니다.

$ sudo cpan Complete::Bash Complete::Util

사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나 perlbrew를 이용해서 자신만의 Perl을 사용하고 있다면 다음 명령을 이용해서 모듈을 설치합니다.

$ cpan Complete::Bash Complete::Util

더불어 perlbrew가 필요하므로 다음 명령을 이용해 perlbrew를 설치합니다.

$ wget -O - http://install.perlbrew.pl | bash

쉘 자동 완성

우선 쉘이 명령줄을 어떻게 파싱하는지 알 필요가 있습니다.

두 번째 규칙은 너무나도 친숙하기 때문에 규칙이라는 점을 인지하지 못하는 경우가 대부분입니다. 명령어의 자동 완성 기능은 항상 첫 번째 인자에서만 활성화됩니다. 두 번째 인자부터는 명령어를 자동 완성 시키지 못합니다. 물론 time과 같이 명령어를 인자로 취하는 경우는 쉘의 명령줄 파싱 규칙에 의해서 파싱되는것이 아니라 complete에 미리 정의된 함수를 이용해 실행 가능한 명령어를 찾는 것입니다.

Bash 자동 완성

Bash는 다양한 방법으로 자동 완성을 도와주는 complete 내장 함수를 지원합니다. somecmd 명령어에 대해서 Wordlist를 작성해 두고 사용하려면 방법은 다음과 같습니다.

$ complete -W "one two three four" somecmd
$ somecmd t<TAB><TAB>
two  three

함수(-F)를 사용할 수도 있는데, 이 때 해당 함수는 두 개의 입력 인자를 전달받습니다.

그리고 반드시 COMPREPLY에 사용 가능한 자동 완성 요소 목록을 담아야 합니다.

$ _foo()
{
    local cur
    COMPREPLY=()
    cur=${COMP_WORDS[COMP_CWORD]}
    COMPREPLY=($( compgen -W '--help --verbose --version' -- $cur ) )
}
$ complete -F _foo foo
$ foo <TAB><TAB>
--help  --verbose  --version

Complete::Bash

다른 방법으로 펄 등의 외부 명령을 실행해서 자동 완성 목록을 만들 수도 있습니다.

지금부터 perlbrew를 자동 완성을 지원할 대상으로 정하고 하나씩 살펴보겠습니다.

쉘로 전달된 인자 파싱

가장 먼저 쉘에서 <TAB><TAB>키가 눌렸을 때 전달된 인자가 무엇인지를 확인해야 합니다. Complete::Bash 모듈이 제공하는 parse_cmdline() 함수를 통해 쉘의 인자들을 파싱할 수 있습니다. 실제로는 인자를 그대로 파싱하는 것이 아니라 Bash 쉘로부터 전달된 COMP_LINE, COMP_POINT 환경 변수를 파싱하는 것입니다. parse_cmdline() 함수는 다음과 같은 구조로 파싱한 결과를 반환합니다.

$cur_idx<TAB><TAB>을 사용한 시점의 커서 인덱스 값입니다.

#!/usr/bin/env perl

use strict;
use warning;

use Complete::Bash qw( parse_cmdline format_completion );
use Complete::Util qw( complete_array_elem );

my ( $words, $cur_idx ) = @{ parse_cmdline() };
my $given = $words->[$cur_idx];

## from bash : perlbrew <TAB><TAB>
## $words   = [ 'perlbrew', '' ];
## $cur_idx = 1;
## <TAB><TAB>을 타이핑한 시점에 명령줄에서 정의된 인자 리스트는 총 2개 입니다.
## perlbrew 명령어와 작성중인 2번째 인자 입니다.
## 현재 커서는 2번째 배열인덱스에 있으므로 커서의 값은 배열 인덱스인 1입니다.

자동 완성 리스트 작성 및 출력

perlbrew 명령어는 주동작을 결정짓는 부명령(subcommand)을 필수 인자로 취하고 부가적인 옵션들을 취합니다.

$ perlbrew
Usage:
    perlbrew command syntax:

        perlbrew <command> [options] [arguments]

    Commands:

        init           Initialize perlbrew environment.
        info           Show useful information about the perlbrew installation

        install        Install perl
        uninstall      Uninstall the given installation
        available      List perls available to install
        lib            Manage local::lib directories.
        alias          Give perl installations a new name
        upgrade-perl   Upgrade the current perl

        list           List perl installations
        use            Use the specified perl in current shell
        off            Turn off perlbrew in current shell
        switch         Permanently use the specified perl as default
        switch-off     Permanently turn off perlbrew (revert to system perl)
        exec           exec programs with specified perl enviroments.

        self-install       Install perlbrew itself under PERLBREW_ROOT/bin
        self-upgrade       Upgrade perlbrew itself.

        install-patchperl  Install patchperl
        install-cpanm      Install cpanm, a friendly companion.
        install-multiple   Install multiple versions and flavors of perl

        download       Download the specified perl distribution tarball.
        mirror         Pick a preferred mirror site
        clean          Purge tarballs and build directories
        version        Display version
        help           Read more detailed instructions

    Generic command options:

        -q --quiet     Be quiet on informative output message.
        -v --verbose   Tell me more about it.

    See `perlbrew help` for the full documentation of perlbrew, or

    See `perlbrew help <command>` for detail description of the command.

먼저 command에 해당하는 부명령에 대해 자동 완성을 작성합니다. Complete::Util 모듈이 제공하는 complete_array_elem() 함수는 미리 작성된 자동 완성 목록과 검색에 사용할 문자열을 취해 사용가능한 자동 완성 목록을 반환합니다. 그리고 우리는 Complete::Bash 모듈이 제공하는 format_completion() 함수를 이용해 출력합니다.

if ( $cur_idx == 1 ) {
    my $complete = complete_array_elem(
        #
        # 자동 완성 목록
        # 
        array => [qw(
            init
            info
            install
            uninstall
            available
            lib
            alias
            upgrade-perl
            list
            use
            off
            switch
            switch-off
            exec
            self-install
            self-upgrade
            install-patchperl
            install-cpanm
            install-multiple
            download
            mirror
            clean
            version
            help
        )],
        #
        # 검색할 문자열
        # 
        word  => $given,
    );

    print format_completion $complete;
}

perlbrew 명령의 필수 인자인 부명령의 자동 완성 목록을 완성했습니다. 이제 해당 부명령에 필요한 옵션 및 인자를 처리해야겠죠.

옵션이 필요 없는 부명령

옵션이 필요없는 명령어의 경우 더 이상의 자동 완성 기능이 필요없습니다. 다음은 옵션이 필요없는 부명령의 목록입니다.

추가로 모든 명령에 대응하는 일반 옵션 항목은 다음과 같습니다.

명령줄에서 전달한 목록중 마지막 두 번째 요소($words[-2])를 이용하면 perlbrew를 실행할 때 사용한 부명령을 확인할 수 있습니다.

if (
       $words->[-2] eq 'init'
    || $words->[-2] eq 'list'
    || $words->[-2] eq 'off '
    || $words->[-2] eq 'switch-off'
    || $words->[-2] eq 'install-cpanm'
    || $words->[-2] eq 'install-patchperl'
    || $words->[-2] eq 'self-upgrade'
    || $words->[-2] eq 'version'
    || $words->[-2] eq 'upgrade-perl'
    || $words->[-2] eq 'list-modules'
)
{
    generic_options($given);
}

sub generic_options {
    my $given = shift;

    completion( [qw( --quite --verbose )], $given );
}

sub completion {
    my ( $available_opts, $given ) = @_;

    my $res = complete_array_elem(
        array => $available_opts,
        word  => $given,
    );

    print format_completion $res;
    exit 0;
}

info

perlbrewinfo 부명령은 모듈을 인자로 받은 후 해당 모듈의 정보를 화면에 출력합니다.

COMMAND: INFO
    info [module]
        Usage: perlbrew info [ <module> ]

        Display useful information about the perlbrew installation.

        If a module is given the version and location of the module is
        displayed.

설치한 모듈의 목록을 후보로 보여줄 수 있으면 편리하겠죠? perlbrew list-modules 명령으로 획득한 목록을 사용해 자동 완성 시켜보죠.

if ( $words->[-2] eq 'info' ) {
    completion( list_modules(), $given );
}

sub list_modules {
    my @modules = qx( perlbrew list-modules );

    return [ map { chomp; $_ } @modules ];
}

switch / uninstall / use

switchuninstall, use 부명령은 현재 perlbrew로 설치한 펄의 버전 정보를 인자로 취합니다. 이번에는 perlbrew list 명령으로 획득한 펄 버전 정보 목록을 사용해 자동 완성 시켜보죠.

if (
       $words->[-2] eq 'uninstall'
    || $words->[-2] eq 'use'
    || $words->[-2] eq 'switch'
)
{
    completion( installed_perl(), $given );
}

sub installed_perl {
    my @installed_perl = qx( perlbrew list );
    return [ map { /(perl-\d\.\d\d\.\d)/ } @installed_perl ];
}

전체 코드

코드 구문을 하나 하나 설명하다보니 전체 코드가 눈에 들어오지 않겠군요. 작성한 전체 코드는 다음과 같습니다.

#!/usr/bin/env perl

use strict;
use warnings;

use Complete::Bash qw( parse_cmdline format_completion );
use Complete::Util qw( complete_array_elem );

my ( $words, $cur_idx ) = @{ parse_cmdline() };
my $given = $words->[$cur_idx];

if ( $cur_idx == 1 ) {
    my $complete = complete_array_elem(
        #
        # 자동 완성 목록
        # 
        array => [qw(
            init
            info
            install
            uninstall
            available
            lib
            alias
            upgrade-perl
            list
            use
            off
            switch
            switch-off
            exec
            self-install
            self-upgrade
            install-patchperl
            install-cpanm
            install-multiple
            download
            mirror
            clean
            version
            help
        )],
        #
        # 검색할 문자열
        # 
        word  => $given,
    );

    print format_completion $complete;
}

if (
       $words->[-2] eq 'init'
    || $words->[-2] eq 'list'
    || $words->[-2] eq 'off '
    || $words->[-2] eq 'switch-off'
    || $words->[-2] eq 'install-cpanm'
    || $words->[-2] eq 'install-patchperl'
    || $words->[-2] eq 'self-upgrade'
    || $words->[-2] eq 'version'
    || $words->[-2] eq 'upgrade-perl'
    || $words->[-2] eq 'list-modules'
)
{
    generic_options($given);
}

if ( $words->[-2] eq 'info' ) {
    completion( list_modules(), $given );
}

if ( $words->[-2] eq 'install' ) {
    my $install_opt = [qw/
        -f --force -j -n --notest --switch --as --noman --thread
        --multi --64int --64all --ld --debug --clang -D -U -A
        --sitecustomize
    /];
    completion( $install_opt, $given );
}

if ( $words->[-2] eq 'available' ) {
    completion( [qw / --all /], $given );
}

if (
       $words->[-2] eq 'uninstall'
    || $words->[-2] eq 'use'
    || $words->[-2] eq 'switch'
)
{
    completion( installed_perl(), $given );
}

sub completion {
    my ( $available_opts, $given ) = @_;

    my $res = complete_array_elem(
        array => $available_opts,
        word  => $given,
    );

    print format_completion $res;
    exit 0;
}

sub generic_options {
    my $given = shift;

    completion( [qw( --quite --verbose )], $given );
}

sub list_modules {
    my @modules = qx( perlbrew list-modules );

    return [ map { chomp; $_ } @modules ];
}

sub installed_perl {
    my @installed_perl = qx(perlbrew list);

    return [ map { /(perl-\d\.\d\d\.\d)/ } @installed_perl ];
}

적용하기

작성된 Bash 자동 완성 스크립트를 사용려면 몇 가지 작업을 더 필요합니다. 우선 실행 권한을 추가하고 complete Bash 명령을 이용해 스크립트를 등록합니다.

$ chmod +x completion-perlbrew.pl
$ complete -C completion-perlbrew.pl perlbrew

더불어 마지막으로 자동 완성 스크립트가 실행 가능해야 하기 때문에 이를 위해 PATH 변수를 이용해 위치를 지정해야 합니다. 또는 처음부터 스크립트를 등록할 때 절대 경로를 이용해도 됩니다.

#
# PATH 변수 수정
# 
$ export PATH=$PATH:/home/user/bin

# 또는

#
# 절대 경로 등록
#
$ complete -C /home/user/bin/completion-perlbrew.pl perlbrew

자동 완성을 적용하는 일련의 과정은 .bashrc 등의 파일을 이용하면 편리하겠죠. :)

정리하며

지금까지 Bash 내장 명령어인 complete와 펄이 어떻게 조화를 이룰 수 있는지 살펴보았습니다. 기사에서는 이해를 돕기 위해 간단한 부분을 소개했지만, 더 자세한 내용이 궁금하다면 CPAN의 Complete::Bash 모듈CPAN의 Complete::Util 모듈 문서를 참고하세요. perlbrew는 많은 부명령과 옵션을 지원하는데, 소개하지 않은 나머지 부분에 대한 자동 완성은 여러분의 몫으로 남깁니다. :)

여러분이 만들 사용자 프로그램을 배포할 때도 자동 완성 스크립트를 같이 배포한다면 프로그램의 편의성을 한껏 높일 수 있을 것입니다. 래리가 말한 게으름이란 참 반어적 표현입니다. 게으름은 필연적으로 부지런함을 수반하죠. 명령어를 입력할 때마다 일일이 키보드를 치는 부지런함을 부릴지, 아니면 탭키를 누를 때마다 후보가 뜨게 하기 위해 키보드를 치는 부지런함을 부릴지는 순전히 여러분의 선택 사항입니다. :-D

Enjoy Your Perl & Merry Christmas! ;-)

EOT

blog comments powered by Disqus