 |
|
난이도 : 초급 Damian Conway, Dr., CEO and Chief Trainer, Thoughtstream
원문 게재일 : 2009 년 7 월 07 일 번역 게재일 : 2009 년 9 월 01 일 사용자 정의 함수는 실제 환경에서 사용하고 있는 복잡한 프로그래밍 작업을
관리하기 위해 애플리케이션을 쉽게 관리할 수 있는 올바른 구성 요소로 분해하는 데 사용되는
필수 도구입니다. 시리즈의
두 번째 기사에 해당하는 이 기사에서는 몇 가지 실용적인 예제를 통해 Vimscript 언어에서 새 함수를 작성하고
전개하는 방법에 대해 설명합니다.
사용자 정의 함수
Haskell 또는 Scheme 프로그래머에게 물어보면 프로그래밍 언어의 가장 중요한 기능은
함수라고 답할 것이다. C 또는 Perl 프로그래머에게 물어봐도 똑같은 대답을 듣게 될 것이다.
함수의 기본적인 두 가지 장점은 다음과 같다.
- 복잡한 계산 작업을 사람이 쉽게 파악할 수 있을 정도로 작은 여러 단위로 나눌 수 있다.
- 분할된 단위에 논리적이고 알기 쉬운 이름을 지정할 수 있기 때문에 효율적인 관리가 가능하다.
Vimscript도 프로그래밍 언어이기 때문에 기본적으로 사용자 정의 함수의 작성이
지원된다. 실제로 Vimscript의 사용자 정의 함수 지원이 Scheme, C 또는 Perl보다 낫다는
의견도 있다. 이 기사에서는 Vimscript 함수의 다양한 기능을 살펴본 후 이러한 기능을 사용하여
Vim의 내장 기능을 관리하기 편한 방식으로 개선 및 확장하는 방법에 대해 설명한다.
함수 선언하기
Vimscript에서 함수를 정의할 때는 먼저 function 키워드를
입력한 뒤에 함수의 이름과 매개변수 목록(인수가 없는 경우에도 필수)을 차례로 지정한다. 그 다음 행부터
함수의 본문이 시작되며 일치하는 endfunction 키워드가 나오게 되면 함수의
본문이 끝난다. 예를 들면, 다음과 같다.
Listing 1. 올바르게 구조화된 함수
function ExpurgateText (text)
let expurgated_text = a:text
for expletive in [ 'cagal', 'frak', 'gorram', 'mebs', 'zarking']
let expurgated_text
\ = substitute(expurgated_text, expletive, '[DELETED]', 'g')
endfor
return expurgated_text
endfunction
|
함수의 리턴 값은 return 명령문을 사용하여 지정한다. return
명령문은 필요에 따라 원하는 수만큼 지정할 수 있다. 함수를 프로시저로서만 사용하고 리턴 값이
필요 없는 경우에는 이 명령문을 사용하지 않을 수도 있다. 하지만 Vimscript 함수는 값을 항상
리턴하므로 return을 지정하지 않으면 0 값이 자동으로 리턴된다.
Vimscript의 함수 이름은 대문자로 시작해야 한다.
Listing 2. 대문자로 시작하는 함수 이름
function SaveBackup ()
let b:backup_count = exists('b:backup_count') ? b:backup_count+1 : 1
return writefile(getline(1,'$'), bufname('%') . '_' . b:backup_count)
endfunction
nmap <silent> <C-B> :call SaveBackup()<CR>
|
이 예제에서는 현재 버퍼의 b:backup_count 변수의
값을 증가시키거나 이 값이 없는 경우 1로 초기화하는 함수를 정의한다. 그런 다음 현재 파일에 있는
모든 행을 한 줄씩 가져와서(getline(1,'$')) 내장 writefile()
함수를 호출하여 디스크에 기록한다. writefile()의 두 번째 인수는 기록할
새 파일의 이름이다. 이 예제의 경우에는 카운터의 새 값이 추가되는 현재 파일의 이름이다(bufname('%')). 그리고
writefile() 호출의 성공/실패 값이 리턴된다. 마지막으로 nmap은
현재 파일에 대한 번호가 매겨진 백업을 작성하는 함수를 호출하도록 CTRL-B를 설정한다.
선행 대문자를 사용하는 대신 Part 1에서
설명한 변수처럼 명시적인 범위 접두어를 사용하여 Vimscript 함수를 선언할 수도 있다. 가장 많이
사용되는 접두어는 s:로 해당 함수를 현재 스크립트 파일의 로컬 함수로
지정한다. 이 방식으로 범위가 지정된 함수는 소문자도 유효한 식별자로 인식되기 때문에 대문자로
시작하는 이름을 사용하지 않아도 된다. 하지만 범위가 명시적으로 지정된 함수는 항상 해당 범위 접두어를
사용해서 호출해야 한다. 예를 들면, 다음과 같다.
Listing 3. 범위 접두어를 사용하여 함수 호출하기
" Function scoped to current script file...
function s:save_backup ()
let b:backup_count = exists('b:backup_count') ? b:backup_count+1 : 1
return writefile(getline(1,'$'), bufname('%') . '_' . b:backup_count)
endfunction
nmap <silent> <C-B> :call s:save_backup()<CR>
|
재선언 가능한 함수
Vimscript의 함수 선언은 런타임 명령문이므로 스크립트를 두 번 로드하게 되면 스크립트
내의 모든 함수 선언이 두 번 실행되면서 해당 함수가 다시 작성된다.
함수 재선언은 치명적인 오류로 간주된다. (이는 서로 다른 두 스크립트에서 우연히
같은 이름의 함수를 선언했을 때 발생할 수 있는 충돌을 막기 위한 것이다.) 따라서 사용자
정의 구문 강조 스크립트와 같이 반복적으로 로드되어야 하는 스크립트의 경우 함수를 작성하기가
어렵다.
따라서 Vimscript에서는 키워드 한정자(function!)를
사용하여 필요에 따라 안전하게 다시 로드될 수 있는 함수 선언임을 지정할 수 있다.
Listing 4. 안전하게 다시 로드될 수 있는 함수 선언임을 지정하기
function! s:save_backup ()
let b:backup_count = exists('b:backup_count') ? b:backup_count+1 : 1
return writefile(getline(1,'$'), bufname('%') . '_' . b:backup_count)
endfunction
|
이 키워드 한정자를 사용하여 정의된 함수에 대해서는 재선언 검사가 수행되지
않으므로 명시적으로 범위가 지정된 함수와 사용하기에 적합하다. (범위가 지정되어 있기 때문에
스크립트 간 함수 충돌이 발생하지 않는다.)
함수 호출하기
함수를 호출한 다음 리턴 값을 더 큰 표현식의 일부로 사용하려면 리턴 값에 이름을
지정한 후 괄호로 묶은 인수 목록을 추가하면 된다.
Listing 5. 함수의 리턴 값 사용하기
"Clean up the current line...
let success = setline('.', ExpurgateText(getline('.')) )
|
Vimscript에서는 C나 Perl과는 달리 함수의 리턴 값을 사용하지 않으면 버릴 수 없다는
데 주의해야 한다. 따라서 함수를 프로시저나 서브루틴으로 사용하고 함수의 리턴 값을 무시하려면
호출 앞에 call 명령을 사용해야 한다.
Listing 6. 리턴 값을 사용하지 않고 함수 사용하기
"Checkpoint the text...
call SaveBackup()
|
이 방법을 사용하지 않을 경우 Vimscript는 해당 함수 호출을 실제
내장 Vim 명령으로 간주하고서 그러한 명령이 없다는 메시지를 표시한다. 이 시리즈의 후속 기사에서
함수와 명령의 차이에 대해 살펴볼 것이다.
매개변수 목록
Vimscript에서는 명시적 매개변수와 가변 매개변수 목록을 정의할
수 있으며 이 둘의 조합까지도 정의할 수 있다.
서브루틴의 이름을 선언한 직후 명시적으로 이름이 지정된 매개변수를 20개까지
지정할 수 있다. 지정한 후에는 매개변수 이름에 a: 접두어를 지정하여
함수 내에서 현재 호출에 해당하는 인수 값에 액세스할 수 있다.
Listing 7. 함수 내에서 인수 값에 액세스하기
function PrintDetails(name, title, email)
echo 'Name: ' a:title a:name
echo 'Contact:' a:email
endfunction
|
함수에 지정된 인수의 수를 모르는 경우에는 명명된 매개변수 대신 줄임표(...)를
사용하여 가변 매개변수 목록을 지정할 수 있다. 이렇게 하면 인수의 수에 제한 없이 함수를
호출할 수 있으며 해당 값은 단일 변수 즉, a:000이라는
이름의 배열에 수집된다. 또한 개별 인수에는 a:1,
a:2,
a:3 등과 같은 위치 매개변수 이름이
지정되며 a:0에는 인수의 수가 들어 있다. 예를 들며,
다음과 같다.
Listing 8. 가변 매개변수 목록 지정 및 사용하기
function Average(...)
let sum = 0.0
for nextval in a:000 "a:000 is the list of arguments
let sum += nextval
endfor
return sum / a:0 "a:0 is the number of arguments
endfunction
|
이 예제에서 sum은 명시적 부동소수점 값으로
초기화되어야 한다. 그렇지 않으면 이후 모든 계산이 정수 연산으로 수행된다.
명명된 변수와 가변 변수 결합하기
명명된 매개변수 목록 뒤에 가변 줄임표를 추가하여 명명된 변수와 가변
변수를 같은 함수에서 사용할 수 있다.
예를 들어, 문자열을 받아서 다양한 프로그래밍 언어에 적합한 주석 블록으로
변환하는 CommentBlock() 함수를 작성하려고 한다. 이러한 함수를
사용할 때는 호출자가 항상 형식을 변환할 문자열을 제공해야 하므로 해당 매개변수에 명시적으로
이름이 지정되어야 한다. 하지만 주석 시작 기호, "포장" 문자 및 주석의 너비는 모두 생략할
경우 기본값이 적용되는 선택 사항으로 지정하려고 한다. 이럴 때 다음과 같이 호출할 수 있다.
Listing 9. 간단한 CommentBlock 함수 호출
call CommentBlock("This is a comment")
|
이 호출은 다음과 같이 여러 행으로 구성된 문자열을 리턴한다.
Listing 10. CommentBlock 리턴
//*******************
// This is a comment
//*******************
|
반면 추가 인수를 제공하면 주석 시작 기호, "포장" 문자 및 주석의 너비에 기본값이 아닌 값이 지정된다. 이 경우에는 다음과 같이 호출할 수 있다.
Listing 11. 좀 더 복잡한 CommentBlock 함수 호출
call CommentBlock("This is a comment", '#', '=', 40)
|
다음 문자열이 리턴된다.
Listing 12. CommentBlock 리턴
#========================================
# This is a comment
#========================================
|
이러한 함수는 다음과 같이 구현될 수 있다.
Listing 13. CommentBlock 구현
function CommentBlock(comment, ...)
"If 1 or more optional args, first optional arg is introducer...
let introducer = a:0 >= 1 ? a:1 : "//"
"If 2 or more optional args, second optional arg is boxing character...
let box_char = a:0 >= 2 ? a:2 : "*"
"If 3 or more optional args, third optional arg is comment width...
let width = a:0 >= 3 ? a:3 : strlen(a:comment) + 2
" Build the comment box and put the comment inside it...
return introducer . repeat(box_char,width) . "\<CR>"
\ . introducer . " " . a:comment . "\<CR>"
\ . introducer . repeat(box_char,width) . "\<CR>"
endfunction
|
적어도 한 개의 선택적 인수가 있다면(a:0 >= 1)
시작 기호 매개변수에 첫 번째 옵션(즉, a:1)이 할당되며 그렇지
않은 경우에는 기본값 "//"이 할당된다. 이와 마찬가지로 두 개 이상의
선택적 인수가 있는 경우에는(a:0 >= 2) box_char
변수에 두 번째 옵션(a:2)이 할당되며 그렇지 않은 경우에는
기본값 "*"이 할당된다. 세 개 이상의 선택적 인수가 있는 경우에는
세 번째 옵션이 width 변수에 할당된다. width 인수가 제공되지
않은 경우에는 주석 인수를 기반으로 적합한 너비가 자동 계산된다(strlen(a:comment)+2).
마지막으로 모든 매개변수 값이 해석된 후 주석 상자의 맨 위 행과 맨 아래 행이
선행 주석 시작 기호, 주석 텍스트 및 포장 문자의 반복 수(repeat(box_char,width))를
차례로 사용하여 생성된다.
물론 이 함수를 사용하려면 호출해야 한다. 그리고 이 작업에는 삽입 맵을 사용하는 것이 좋다.
Listing 14. 삽입 맵을 사용하여 함수 호출하기
"C++/Java/PHP comment...
imap <silent> /// <C-R>=CommentBlock(input("Enter comment: "))<CR>
"Ada/Applescript/Eiffel comment...
imap <silent> --- <C-R>=CommentBlock(input("Enter comment: "),'--')<CR>
"Perl/Python/Shell comment...
imap <silent> ### <C-R>=CommentBlock(input("Enter comment: "),'#','#')<CR>
|
이러한 각 맵에서는 먼저 내장 input() 함수가
호출되면서 사용자에게 주석 텍스트를 입력하도록 요청한다. 그런 다음 CommentBlock()
함수가 호출되면서 해당 텍스트가 주석 블록으로 변환된다. 마지막으로 선행 <C-R>=가
결과 문자열을 삽입한다.
첫 번째 맵핑에서는 단일 인수만을 전달하므로 기본적으로 //가
주석 기호로 사용된다. 두 번째 및 세 번째 맵핑에서는 두 번째 인수를 전달하여 각각 #
또는 --를 해당 주석 시작 기호로 지정한다. 또한 마지막 맵핑에서는 세 번째
인수를 전달하여 "포장" 문자를 해당 주석 시작 기호에 일치시킨다.
함수 및 행 범위
기본 행 범위를 사용하여 call을 포함한 표준 Vim 명령을
호출할 수 있으며 이 경우 범위 내의 모든 행에 대해 명령이 한 번씩 반복된다.
"Delete every line from the current line (.) to the end-of-file ($)...
:.,$delete
"Replace "foo" with "bar" everywhere in lines 1 to 10
:1,10s/foo/bar/
"Center every line from five above the current line to five below it...
:-5,+5center
Vim 세션에서 :help cmdline-ranges를 입력하여 이
기능에 대한 자세한 정보를 확인할 수 있다.
call 명령의 경우 범위를 지정하면 범위 내의 각 행에
대해 요청된 함수가 한 번씩 반복적으로 호출된다. 이 방법의 유용성을 확인하기 위해 현재 행에 있는
모든 "원시" 앰퍼샌드를 적절한 XML & 엔터티로 변환하면서도
다른 엔터티에 속해 있는 앰퍼샌드는 무시할 수 있는 함수를 작성하는 방법을 생각해 보자. 이러한 함수는
다음과 같이 구현될 수 있다.
Listing 15. 앰퍼샌드를 변환하는 함수
function DeAmperfy()
"Get current line...
let curr_line = getline('.')
"Replace raw ampersands...
let replacement = substitute(curr_line,'&\(\w\+;\)\@!','&','g')
"Update current line...
call setline('.', replacement)
endfunction
|
DeAmperfy()의 첫 번째 행에서는 편집기 버퍼에서 현재
행을 가져온다(getline('.')). 두 번째 행에서는 부정 검색 패턴
'&\(\w\+;\)\@!'을 사용하여 해당 행에서 뒤에 식별자와 콜론이
없는 모든 &를 찾는다(자세한 정보는 :help \@!
참조). 그런 다음 substitute() 호출이 모든 "원시" 앰퍼샌드를 XML
엔터티 &로 바꾼다. 마지막으로 DeAmperfy()의
세 번째 행에서는 현재 행을 수정된 텍스트로 갱신한다.
다음과 같이 명령행에서 이 함수를 호출하면
:call DeAmperfy()
현재 행에서만 바꾸기 작업이 수행된다. 하지만 다음과 같이 call 앞에 범위를 지정하면
:1,$call DeAmperfy()
범위의 각 행에 대해(이 경우에는 파일의 모든 행에 대해) 한 번씩 함수가 호출된다.
함수 행 범위 내부화하기
이처럼 각 행에 대해 함수를 반복적으로 호출하는(call-the-function-repeatedly-for-each-line)
동작은 편리한 기본 동작이다. 하지만 범위를 지정한 후 함수를 한 번만 호출한 다음 함수 자체
내에서 범위 시맨틱을 처리하려는 경우도 있다. 이 또한 Vimscript에서 쉽게 수행할 수 있다. 다음과
같이 특수 한정자(range)를 함수 선언에 추가하면 된다.
Listing 16. 함수 내 범위 시맨틱
function DeAmperfyAll() range
"Step through each line in the range...
for linenum in range(a:firstline, a:lastline)
"Replace loose ampersands (as in DeAmperfy())...
let curr_line = getline(linenum)
let replacement = substitute(curr_line,'&\(\w\+;\)\@!','&','g')
call setline(linenum, replacement)
endfor
"Report what was done...
if a:lastline > a:firstline
echo "DeAmperfied" (a:lastline - a:firstline + 1) "lines"
endif
endfunction
|
매개변수 목록 뒤에 range 한정자가 지정되어
있는 경우 다음과 같이 범위를 사용하여 DeAmperfyAll()을 호출하면
:1,$call DeAmperfyAll()
함수가 한 번만 호출되면서 특수 인수인 a:firstline과
a:lastline이 범위 내의 첫 번째 행 번호와 마지막 행 번호로 설정된다. 범위를
지정하지 않은 경우 경우에는 a:firstline과 a:lastline이
모두 현재 행 번호로 설정된다.
이 함수는 먼저 관련 행 번호가 모두 포함된 목록을 작성한다(range(a:firstline,
a:lastline)). 여기서 내장 range() 함수에 대한 호출은 range
한정자를 함수 선언의 일부로 사용하는 것과 아무런 관련이 없다. range()
함수는 단순히 목록 생성자이며 Python의 range() 함수나 Haskell 또는 Perl의
.. 연산자와 매우 유사하다.
처리할 행 번호 목록이 결정된 후 이 함수는 for 루프를 사용하여 각 행을 반복한다.
for linenum in range(a:firstline, a:lastline)
그런 다음 원래 DeAmperfy()에서와 같이 각 행을 적절하게 갱신한다.
마지막으로 범위에 포함된 행이 여러 개인 경우(다시 말해서 a:lastline >
a:firstline인 경우) 이 함수는 갱신 행 수를 보고한다.
시각적 범위
여러 행으로 구성된 범위에서 작동할 수 있는 함수를 호출할 경우에는 Visual
모드를 통해 해당 함수를 호출하는 것이 좋다(자세한 정보는 :help Visual-mode
참조).
예를 들어, 커서가 텍스트 블록 내에 있을 경우 다음과 같은 명령을 통해 괄호
안에 있는 모든 앰퍼샌드를 인코딩할 수 있다.
Vip:call DeAmperfyAll()
Normal 모드에서 V를 입력하면 Visual 모드로 전환된다. 그런
다음 ip를 지정했으므로 Visual 모드에서 현재 커서가 있는 전체 단락이 강조
표시된다. 그런 다음 :이 있으므로 Command 모드로 전환되면서 명령의 범위가
조금 전 Visual 모드에서 선택한 행 범위로 자동으로 설정된다. 이제 DeAmperfyAll()을
호출하여 범위 내의 모든 앰퍼샌드를 변환한다.
이 경우에는 다음과 같은 명령을 통해서도 같은 결과를 얻을 수 있다.
Vip:call DeAmperfy()
유일한 차이점은 DeAmperfy() 함수가 반복적으로
호출된다는 것이다. 즉, 이 함수는 Vip로 인해 Visual 모드에서
강조 표시된 각 행에 대해 한 번씩 호출된다.
코딩에 도움이 되는 함수
Vimscript에서 사용되는 대부분의 사용자 정의 함수에서는 매개변수를 많이 사용하지
않을 뿐만 아니라 전혀 사용하지 않는 경우도 있다. 이는 이들 함수가 데이터를 가져올 때 일반적으로
현재 편집기 버퍼와 컨텍스트 정보(현재 커서 위치, 현재 단락 크기, 현재 창 크기 또는 현재 행의 내용
등)에서 직접 데이터를 가져오기 때문이다.
게다가 함수에서는 인수 목록을 사용하는 것보다 컨텍스트를 통해 데이터를
가져오는 방법이 훨씬 효율적이고 편리하다. 예를 들어, 소스 코드를 관리하다 보면 할당
연산자가 여러 행에서 연달아 사용될 때 정렬이 되지 않아서 코드의 가독성이 떨어지는 문제를
자주 볼 수 있다.
Listing 16. 정렬되지 않은 할당 연산자
let applicants_name = 'Luke'
let mothers_maiden_name = 'Amidala'
let closest_relative = 'sister'
let fathers_occupation = 'Sith'
|
새 명령문을 추가할 때 마다 수동으로 다음과 같이 다시 정렬하려면 매우 번거로울 것이다.
Listing 17. 수동으로 다시 정렬한 할당 연산자
let applicants_name = 'Luke'
let mothers_maiden_name = 'Amidala'
let closest_relative = 'sister'
let fathers_occupation = 'Sith'
|
일상적인 코딩 작업에서 이러한 번거로움을 줄이기 위해 키 맵핑(예: ;=)을
작성할 수 있다. 이러한 키 맵핑은 현재 코드 블록을 선택한 후 할당 연산자가 있는 모든 행을
찾아서 해당 연산자를 자동으로 정렬한다. 다음과 같다.
Listing 18. 할당 연산자를 정렬하는 함수
function AlignAssignments ()
"Patterns needed to locate assignment operators...
let ASSIGN_OP = '[-+*/%|&]\?=\@<!=[=~]\@!'
let ASSIGN_LINE = '^\(.\{-}\)\s*\(' . ASSIGN_OP . '\)'
"Locate block of code to be considered (same indentation, no blanks)
let indent_pat = '^' . matchstr(getline('.'), '^\s*') . '\S'
let firstline = search('^\%('. indent_pat . '\)\@!','bnW') + 1
let lastline = search('^\%('. indent_pat . '\)\@!', 'nW') - 1
if lastline < 0
let lastline = line('$')
endif
"Find the column at which the operators should be aligned...
let max_align_col = 0
let max_op_width = 0
for linetext in getline(firstline, lastline)
"Does this line have an assignment in it?
let left_width = match(linetext, '\s*' . ASSIGN_OP)
"If so, track the maximal assignment column and operator width...
if left_width >= 0
let max_align_col = max([max_align_col, left_width])
let op_width = strlen(matchstr(linetext, ASSIGN_OP))
let max_op_width = max([max_op_width, op_width+1])
endif
endfor
"Code needed to reformat lines so as to align operators...
let FORMATTER = '\=printf("%-*s%*s", max_align_col, submatch(1),
\ max_op_width, submatch(2))'
" Reformat lines with operators aligned in the appropriate column...
for linenum in range(firstline, lastline)
let oldline = getline(linenum)
let newline = substitute(oldline, ASSIGN_LINE, FORMATTER, "")
call setline(linenum, newline)
endfor
endfunction
nmap <silent> ;= :call AlignAssignments()<CR>
|
AlignAssignments() 함수는 먼저 다음과 같이 정규 표현식을
설정한다(Vim의 정규 표현식 구문에 대한 필수 정보는 :help pattern 참조).
let ASSIGN_OP = '[-+*/%|&]\?=\@<!=[=~]\@!'
let ASSIGN_LINE = '^\(.\{-}\)\s*\(' . ASSIGN_OP . '\)'
|
ASSIGN_OP의 패턴은 =,
+=, -=, *= 등의
표준 할당 연산자와 일치하며 =가 포함된 다른 연산자(예: ==,
=~ 등)와는 일치하지 않는다. 자주 사용하는 언어에 다른 할당 연산자(예:
.=, ||=, ^= 등)가
있는 경우에는 해당 연산자를 인식하도록 ASSIGN_OP 정규 표현식을
확장할 수 있다. 또는 다른 유형의 "정렬 가능한 항목"(예: 주석 시작 기호 또는 열 표시 기호)을 인식해서
정렬하도록 ASSIGN_OP를 다시 정의할 수도 있다.
ASSIGN_LINE의 패턴은 행의 시작 부분에서만 일치하며(^)
최소 문자 수(.\{-}), 공백(\s*) 및 할당 연산자와
차례로 일치한다.
초기 "최소 문자 수" 서브패턴과 연산자 서브패턴은 모두 \(...\)와
같이 괄호 내에 지정된다. 정규 표현식의 이들 두 구성 요소에 의해 캡처된 하위 문자열은 나중에
내장 submatch() 함수 호출을 사용하여 추출된다. 구체적으로 말해서,
submatch(1)를 호출하면 연산자 앞에 있는 모든 내용이 추출되고
submatch(2)를 호출하면 연산자가 추출된다.
그런 다음 AlignAssignments()는 작업을 수행할 행 범위를 찾는다.
let indent_pat = '^' . matchstr(getline('.'), '^\s*') . '\S'
let firstline = search('^\%('. indent_pat . '\)\@!','bnW') + 1
let lastline = search('^\%('. indent_pat . '\)\@!', 'nW') - 1
if lastline < 0
let lastline = line('$')
endif
|
이전 예제들에서 살펴본 함수는 명시적 명령 범위나 Visual 모드 선택을 사용하여
작업할 행을 결정했지만 이 함수는 범위를 직접 계산한다. 구체적으로 살펴보면, 이 함수는 먼저
내장 matchstr() 함수를 호출하여 현재 행의 시작 부분에(getline('.'))
나타나는 선행 공백('^\s*')을 확인한다. 그런 다음 비어 있지 않은
행의 시작 부분에 있는 공백 시퀀스와 정확히 일치하는 새 정규 표현식(이 경우에는 후행 '\S')을
indent_pat에 작성한다.
그런 다음 AlignAssignments()는 내장 search()
함수를 호출하여 위쪽 방향으로 검색을 시작하면서('bnW' 플래그 사용)
커서 위쪽에 있는 행 중에서 들여쓰기가 일치하지 않는 첫 번째 행을 찾는다. 이 행 번호에
1을 추가하면 찾으려는 범위의 시작 행 번호 즉, 현재 행과 동일한 들여쓰기가 적용된 첫 번째 인접
행의 번호가 된다.
두 번째 search() 함수는 아래쪽으로('nW')
검색하여 동일한 들여쓰기가 적용된 마지막 인접 행의 번호인 lastline을
결정한다. 이 두 번째 검색 작업은 다른 형태의 들여쓰기가 적용된 행을 찾지 못한 채로 파일의 끝에
도달할 수 있으며, 이렇게 되면 search() 함수가 -1을
리턴한다. 이 경우를 올바르게 처리하기 위해 뒤에 있는 if 명령문에서
명시적으로 lastline을 파일 끝의 행 번호(즉, line('$')에서
리턴된 행 번호)로 설정한다.
이 두 번의 검색 작업을 통해 AlignAssignments()는
현재 행과 동일한 형태의 들여쓰기가 적용되어 있으면서 현재 행의 바로 위나 아래에 있는 행 범위를
알게 된다. 이 함수는 이 정보를 사용하여 동일한 코드 블록 내에서 동일한 범위 수준에 있는 할당
명령문만을 정렬하게 된다. 물론 코드의 들여쓰기가 해당 범위를 올바르게 반영하고 있지 않은 경우에는
만족스럽지 못한 정렬 결과가 발생한다.
AlignAssignments()의 첫 번째 for
루프에서는 할당 연산자를 정렬할 열이 결정된다. 즉, 다음과 같이 선택된 범위에 있는 행 목록(getline(firstline,
lastline)에서 검색된 행)을 반복하면서 각 행에 할당 연산자(앞에 공백이 있을 수 있음)가 있는지
여부를 검사한다.
let left_width = match(linetext, '\s*' . ASSIGN_OP)
|
행에 연산자가 없으면 내장 match() 함수가 일치
항목을 찾지 못한 채로 -1을 리턴한다. 이 경우에는 루프가 다음
행으로 이동한다. 연산자가 있을 경우 match() 함수는 해당
연산자의 위치에 해당하는 인덱스(양수)를 리턴한다. 그런 다음 if
명령문에서 내장 max() 함수를 사용하여 이 최신 열 위치가 이전에
검색된 연산자보다 오른쪽에 있는지 여부를 확인한다. 이렇게 하면 범위 내의 모든 할당 기호를
정렬하는 데 필요한 최대 열 위치를 추적할 수 있다.
let max_align_col = max([max_align_col, left_width])
|
if의 나머지 두 행에서는 내장 matchstr()
함수를 사용하여 실제 연산자를 검색한 다음 내장 strlen()을 사용하여 연산자의
길이를 결정한다. 예를 들어, 연산자가 "="인 경우에는 1이고, '+='
또는 '-='인 경우에는 2이다. 그런 다음 max_op_width
변수를 사용하여 범위 내의 다양한 연산자를 정렬하는 데 필요한 최대 너비를 추적한다.
let op_width = strlen(matchstr(linetext, ASSIGN_OP))
let max_op_width = max([max_op_width, op_width+1])
|
정렬 영역의 위치와 너비가 결정된 후에는 범위 내의 행을 반복하면서 결정된
위치와 너비에 맞게 행의 형식을 다시 지정하는 작업만 수행하면 된다. 행의 형식을 다시 지정하기
위해 이 함수에서는 내장 printf() 함수를 사용한다. 이 함수는 매우
유용하기는 하지만 이름이 좋지 못하다. 이 함수는 C, Perl 또는 PHP의 printf
함수와는 다른 함수이다. 실제로 이 함수는 이들 언어의 sprintf
함수와 같다. 즉, Vimscript에서 printf는 형식이 지정된 버전의 데이터
인수 목록을 인쇄하지 않는다. 대신 형식이 지정된 버전의 데이터 인수 목록이 포함된 문자열을
리턴한다.
각 행의 형식을 다시 지정하기 위해 AlignAssignments()에서
내장 substitute() 함수를 사용하여 해당 연산자까지의 모든 내용을 해당
텍스트의 printf 재정렬로 대체할 수 있다면 이상적이겠지만 아쉽게도 substitute()는
함수 호출이 아닌 고정 문자열을 대체 값으로 기대한다.
따라서 printf()를 사용하여 각 대체 텍스트의 형식을
다시 지정하려면 특수 포함 대체 양식인 "\=expr"를 사용해야
한다. 대체 문자열의 앞쪽에 있는 \=는 substitute()에게
뒤에 오는 표현식을 평가하여 결과를 대체 텍스트로 사용하도록 지시한다. 이 동작은 내장 substitute()
함수의 대체 문자열(또는 표준 :s/.../.../ Vim
명령)에 대해서만 작동한다는 점을 제외하면 Insert 모드에서 사용되는 <C-R>= 메커니즘과 비슷하다.
이 예제에서는 모든 행에 대해 동일한 printf가 특수
대체 양식으로 사용되므로 두 번째 for 루프가 시작되기 전에 다음과
같이 특수 대체 양식이 FORMATTER 변수에 미리 저장된다.
let FORMATTER = '\=printf("%-*s%*s", max_align_col, submatch(1),
\ max_op_width, submatch(2))'
|
substitute()에서 호출되면 이 포함된 printf()
함수는 %-*s 자리 표시자를 사용하여 연산자의 왼쪽에 있는 모든 내용을
왼쪽 맞춤한(submatch(1)) 후 결과를 max_align_col
문자 너비의 필드에 저장한다. 그런 다음 %*s를 사용하여 연산자 자체를 max_op_width
문자 너비의 두 번째 필드로 오른쪽 맞춤한다(submatch(2)). :help printf()를
실행하면 - 및 * 옵션이 이 예제에서 사용된 두 %s
형식 지정자를 수정하는 방법에 대한 자세한 정보를 볼 수 있다.
이제 형식 지정자를 사용할 수 있게 되었으므로 두 번째 for
루프에서 범위 내의 모든 행 번호를 반복하여 한 번에 하나씩 해당 텍스트 버퍼 내용을 검색할 수 있다.
for linenum in range(firstline, lastline)
let oldline = getline(linenum)
|
그런 다음 이 루프에서는 substitute()를 사용하여
할당 연산자까지의 모든 내용(할당 연산자 포함)을 일치시킨 후(ASSIGN_LINE의
패턴 사용) 해당 텍스트를 printf() 호출의 결과로 대체하는(FORMATTER에
지정된 대로) 방식으로 해당 내용을 변환한다.
let newline = substitute(oldline, ASSIGN_LINE, FORMATTER, "")
call setline(linenum, newline)
endfor
|
for 루프에서 모든 행을 반복하고 나면 범위 내의
모든 할당 연산자가 올바르게 정렬된다. 이제 마지막으로 다음과 같이 키 맵핑을 작성하여
AlignAssignments()를 호출하면 된다.
nmap <silent> ;= :call AlignAssignments()<CR>
|
후속 기사 소개
함수는 실제 환경에서 사용하고 있는 복잡한 Vim 프로그래밍 작업을
관리하기 위해 애플리케이션을 쉽게 관리할 수 있는 올바른 구성 요소로 분해하는 데 사용되는
필수 도구이다.
Vimscript에서는 고정 또는 가변 매개변수 목록을 사용하여 함수를 정의한 후
함수끼리 자동으로 또는 편집기의 텍스트 버퍼에 있는 행 범위를 이용하여 사용자가 제어할 수
있는 방식으로 상호 작용하도록 제어할 수 있다. 함수는 Vim의 내장 기능(예: search()
또는 substitute())을 호출할 수 있으며 편집기 상태 정보에 직접
액세스하거나(line('.')을 통해 커서가 있는 현재 행 결정) 현재
편집 중인 텍스트 버퍼와 상호 작용할 수 있다(getline() 및 setline() 사용).
이 기능이 강력한 기능이라는 점은 분명하지만 상태와 내용을 프로그래밍 방식으로
조작하는 기능은 코드가 작동되는 데이터를 깔끔하고 정확하게 표현할 수 있는 능력에 따라 제한되기
마련이다. 이 시리즈의
기사에서는 지금까지 단일 스칼라 변수(숫자, 문자열 및 부울)만을 사용했었지만 이후 두 기사에서는
훨씬 더 강력하고 편리한 데이터 구조인 순서가 지정된 목록과 임의 액세스 딕셔너리를 사용하는 방법에
대해 설명한다.
참고자료 교육
제품 및 기술 얻기
- Vim
배포판 다운로드 페이지에서 사용자의 플랫폼에 해당하는 최신 버전의 Vim으로 업그레이드할 수 있다.
- developerWorks에서 직접 다운로드할 수 있는 IBM
시험판 소프트웨어를 사용하여 Linux와 관련된 후속 개발 프로젝트를 구현해 볼 수 있다.
토론
- 사용자의 개인 프로파일과 사용자 정의 홈 페이지가 제공되는 My
developerWorks community에서는 관심을 가지고 있는 developerWorks의 여러 주제를 추적할 수 있으며 다른 developerWorks 사용자들과 의견을 나눌 수도 있다.
필자소개  | 
|  | Damian Conway는 오스트레일리아의 Monash University에서 교류 교수로서 컴퓨터 과학을
강의하고 있으며 국제 IT 교육 회사인 Thoughtstream의
CEO이다. 25년 이상 동안 날마다 vi를 사용하고 있으며 아마도 그는 평생 동안 이 중독에서 벗어나지
못할 것이다. |
기사에 대한 평가
 |
| 이 문서 북마킹 하기
|
|