[번역] Vim의 가장 생산적인 단축키는 무엇인가요?

Jim Dennis - 2009년 8월 3일



Vim에 대한 당신의 문제는 당신이 vi를 제대로 이해(grok)하지 못하고 있다는 점입니다.

당신은 yy로 잘라내기를 언급하며 줄 전체를 잘라내고 싶을 때가 거의 없다고 불평했습니다. 사실 소스 코드를 편집하는 프로그래머들은 줄 전체, 줄의 범위, 그리고 코드 블록 단위로 작업하고 싶어 하는 경우가 매우 많습니다. 하지만 yy는 텍스트를 익명 복사 버퍼(또는 vi에서 부르는 "레지스터(register)")로 복사(yank)하는 수많은 방법 중 하나일 뿐입니다.
vi의 "선(Zen)"은 당신이 하나의 언어를 말하고 있다는 것입니다. 첫 번째 y는 동사입니다. yy라는 문장은 y_의 동의어입니다. y를 두 번 쓰는 것은 매우 흔한 작업이기 때문에 타이핑하기 편하도록 중복시킨 것입니다.
이것은 dd P로도 표현될 수 있습니다(현재 줄을 삭제하고 그 자리에 다시 붙여넣기; 부수 효과로 익명 레지스터에 복사본을 남김). yd라는 "동사"는 모든 이동(movement) 명령을 "목적어"로 취합니다. 따라서 yW는 "여기(커서)부터 현재/다음 (큰) 단어의 끝까지 복사"이고, y'a는 "여기부터 'a'라는 이름의 마크(mark)가 있는 줄까지 복사"입니다.
만약 당신이 위, 아래, 왼쪽, 오른쪽과 같은 기본적인 커서 이동만 이해하고 있다면, vi는 당신에게 "메모장(notepad)"보다 나을 게 없는 도구일 것입니다. (물론 구문 강조(syntax highlighting) 기능이 있고 45KB 정도의 푼돈 같은 크기보다 큰 파일을 다룰 수 있긴 하겠지만, 제 말을 끝까지 들어보세요.)
vi에는 26개의 "마크"와 26개의 "레지스터"가 있습니다. 마크는 m 명령어를 사용하여 어떤 커서 위치에든 설정할 수 있습니다. 각 마크는 하나의 소문자로 지정됩니다. 따라서 ma는 현재 위치에 'a' 마크를 설정하고, mz는 'z' 마크를 설정합니다. '(작은따옴표) 명령어를 사용하여 마크가 포함된 줄로 이동할 수 있습니다. 따라서 'a는 'a' 마크가 포함된 줄의 시작 부분으로 이동합니다. `(백틱) 명령어를 사용하면 마크의 정확한 위치로 이동할 수 있습니다. 따라서 `z는 'z' 마크의 정확한 위치로 직접 이동합니다.
이것들은 "이동" 명령이기 때문에 다른 "문장"의 목적어로도 사용될 수 있습니다.
따라서 임의의 텍스트 선택 영역을 잘라내는 한 가지 방법은 마크를 찍는 것입니다. (저는 보통 첫 번째 마크로 'a', 다음 마크로 'z', 또 다른 마크로 'b', 그리고 'e'를 사용합니다. 15년 동안 vi를 사용하면서 대화형으로 마크를 4개 넘게 사용해 본 기억이 없습니다. 매크로가 사용자의 대화형 컨텍스트를 방해하지 않도록 마크와 레지스터를 어떻게 사용할지에 대해서는 자신만의 관례를 만들게 됩니다.) 그런 다음 원하는 텍스트의 반대쪽 끝으로 이동합니다. 어느 쪽 끝에서 시작하든 상관없습니다. 그다음 단순히 da를 사용하여 잘라내거나 ya를 사용하여 복사할 수 있습니다. 즉, 전체 과정에서 5번의 키 입력 오버헤드가 발생합니다(만약 "입력(insert)" 모드에서 시작해서 명령 모드로 나오기 위해 Esc를 눌러야 했다면 6번입니다). 일단 잘라내거나 복사했다면, 복사본을 붙여넣는 것은 단 한 번의 키 입력 p로 가능합니다.
이것이 텍스트를 잘라내거나 복사하는 한 가지 방법이라고 말씀드렸습니다. 하지만 이것은 수많은 방법 중 하나일 뿐입니다. 종종 우리는 커서를 움직여 마크를 찍지 않고도 텍스트 범위를 더 간결하게 설명할 수 있습니다. 예를 들어 텍스트 단락 안에 있다면, 단락의 시작이나 끝으로 이동하는 {} 이동 명령을 사용할 수 있습니다. 그래서 텍스트 단락을 이동시키려면 { d} (3번의 키 입력)를 사용하여 잘라냅니다. (만약 이미 단락의 첫 번째 줄이나 마지막 줄에 있다면 단순히 d} 또는 d{를 사용하면 됩니다.)
"단락(paragraph)"의 개념은 기본적으로 직관적으로 타당한 설정으로 되어 있습니다. 따라서 산문뿐만 아니라 코드에서도 잘 작동하는 경우가 많습니다.
종종 우리는 관심 있는 텍스트의 한쪽 끝이나 다른 쪽 끝을 표시하는 어떤 패턴(정규 표현식)을 알고 있습니다. 앞으로 찾기나 뒤로 찾기는 vi에서 이동 명령입니다. 따라서 이것들 역시 우리 "문장"의 "목적어"로 사용될 수 있습니다. 그래서 d/foo를 사용하여 현재 줄부터 "foo"라는 문자열을 포함하는 다음 줄까지 잘라낼 수 있고, y?bar를 사용하여 현재 줄부터 "bar"를 포함하는 가장 최근(이전) 줄까지 복사할 수 있습니다. 줄 전체를 원하지 않는다면 검색 이동 명령을 (그 자체의 문장으로) 사용하고, 마크를 찍은 뒤 앞서 설명한 `x 명령을 사용할 수 있습니다.
"동사"와 "주어(이동 명령)" 외에도 vi에는 (문법적 의미에서의) "목적어"가 있습니다. 지금까지는 익명 레지스터의 사용법만 설명했습니다. 하지만 "목적어" 참조 앞에 "(큰따옴표 수식어)를 접두사로 붙여 26개의 "이름이 지정된" 레지스터 중 하나를 사용할 수 있습니다. 따라서 "add를 사용하면 현재 줄을 잘라내어 'a' 레지스터에 넣는 것이고, "by/foo를 사용하면 여기부터 "foo"를 포함하는 다음 줄까지의 텍스트 복사본을 'b' 레지스터로 복사하는 것입니다. 레지스터에서 붙여넣으려면 붙여넣기 명령 앞에 동일한 수식어 시퀀스를 붙이면 됩니다. "ap는 'a' 레지스터의 내용 복사본을 커서 뒤에 붙여넣고, "bP는 'b' 레지스터의 복사본을 현재 줄 앞에 붙여넣습니다.
이러한 "접두사" 개념은 텍스트 조작 "언어"에 문법적인 "형용사"와 "부사"에 해당하는 요소도 추가합니다. 대부분의 명령어(동사)와 이동 명령(문맥에 따라 동사 또는 목적어)은 숫자 접두사를 취할 수 있습니다. 따라서 3J는 "다음 세 줄을 합치기"를 의미하고, d5}는 "현재 줄부터 아래로 다섯 번째 단락의 끝까지 삭제"를 의미합니다.
이 모든 것은 vi의 중급 수준입니다. 이 중 어떤 것도 Vim 전용 기능이 아니며, 배울 준비가 되었다면 vi에는 훨씬 더 고급 기술들이 있습니다. 만약 당신이 이러한 중급 개념들만 마스터한다면, 텍스트 조작 언어가 에디터의 "네이티브" 언어를 사용하여 대부분의 작업을 충분히 쉽게 수행할 수 있을 만큼 간결하고 표현력이 풍부하기 때문에 매크로를 작성할 필요가 거의 없다는 것을 알게 될 것입니다.



더 고급 기술들의 샘플:

수많은 : 명령어들이 있으며, 가장 유명한 것은 :% s/foo/bar/g 전역 치환 기술입니다. (이것은 고급 기술은 아니지만, 다른 : 명령어들은 그럴 수 있습니다.) : 명령어 세트 전체는 역사적으로 vi의 이전 화신인 ed(라인 에디터)와 이후의 ex(확장 라인 에디터) 유틸리티에서 상속되었습니다. 사실 vi라는 이름은 ex의 시각적 인터페이스(visual interface)이기 때문에 붙여진 이름입니다.
: 명령어는 보통 텍스트의 줄 단위로 작동합니다. edex는 터미널 화면이 흔치 않았고 많은 터미널이 "텔레타이프(TTY)" 장치였던 시대에 작성되었습니다. 그래서 텍스트의 인쇄된 사본을 보며 매우 간결한 인터페이스를 통해 명령어를 사용하는 것이 일반적이었습니다. (일반적인 연결 속도는 110 보(baud), 즉 초당 약 11자였는데, 이는 빠른 타자수보다 느린 속도였습니다. 다중 사용자 대화형 세션에서는 지연이 흔했으며, 종이를 절약해야 할 동기도 있었습니다.)
따라서 대부분의 : 명령어 구문은 주소 또는 주소 범위(줄 번호) 뒤에 명령어가 오는 형식을 가집니다. 당연히 리터럴 줄 번호를 사용할 수 있습니다. :127,215 s/foo/bar는 127행과 215행 사이의 각 줄에서 "foo"가 처음 나타나는 것을 "bar"로 바꿉니다. 또한 현재 줄과 마지막 줄을 각각 의미하는 .이나 $와 같은 약어를 사용할 수도 있습니다. 현재 줄 앞뒤의 오프셋을 참조하기 위해 상대 접두사 +-를 사용할 수도 있습니다. 따라서 :.,$j는 "현재 줄부터 마지막 줄까지 모두 하나의 줄로 합치기"를 의미합니다. :%:1,$(모든 줄)와 동의어입니다.
:... g:... v 명령어는 믿을 수 없을 정도로 강력하므로 설명이 좀 필요합니다. :... g는 패턴(정규 표현식)과 일치하는 모든 줄에 후속 명령어를 "전역적으로(globally)" 적용하기 위한 접두사이며, :... v는 주어진 패턴과 일치하지 않는 모든 줄에 해당 명령어를 적용합니다 ("v"는 "반대(conVerse)"에서 따옴). 다른 ex 명령어와 마찬가지로 이것들도 주소/범위 참조를 접두사로 가질 수 있습니다. 따라서 :.,+21g/foo/d는 "현재 줄부터 다음 21줄까지 "foo" 문자열을 포함하는 모든 줄을 삭제"하는 것을 의미하며, :.,$v/bar/d는 "여기부터 파일 끝까지 "bar" 문자열을 포함하지 않는 모든 줄을 삭제"하는 것을 의미합니다.
일반적인 Unix 명령어인 grep이 실제로는 이 ex 명령어에서 영감을 받았다는 점(그리고 문서화된 방식에서 이름이 유래했다는 점)은 흥미롭습니다. ex 명령어 :g/re/p (grep)는 "정규 표현식(re)"을 포함하는 줄을 "전역적으로(globally)" "인쇄(print)"하는 방법을 문서화한 방식이었습니다. edex를 사용할 때 :p 명령어는 누구나 가장 먼저 배우는 명령어 중 하나였으며, 파일을 편집할 때 종종 가장 먼저 사용되는 명령어였습니다. 그것이 현재 내용(보통 :.,+25p 등을 사용하여 한 번에 한 페이지 분량)을 인쇄하는 방법이었기 때문입니다.
:% g/.../d 또는 (그 반대인) :% v/.../d가 가장 흔한 사용 패턴이라는 점에 유의하세요. 하지만 기억할 가치가 있는 몇 가지 다른 ex 명령어들이 있습니다.
m을 사용하여 줄을 이동시키고, j를 사용하여 줄을 합칠 수 있습니다. 예를 들어 목록이 있고 특정 패턴과 일치하는(또는 반대로 일치하지 않는) 모든 항목을 삭제하지 않고 분리하고 싶다면, :% g/foo/m$와 같은 명령을 사용할 수 있습니다. 그러면 모든 "foo" 줄이 파일의 끝으로 이동하게 됩니다. (파일의 끝을 스크래치 공간으로 사용하는 것에 대한 다른 팁을 참고하세요.) 이렇게 하면 나머지 목록에서 "foo" 줄들을 추출하면서도 그 줄들 사이의 상대적인 순서는 유지됩니다. (이것은 1G!GGmap!Ggrep foo<ENTER>1G:1,'a g/foo'/d와 같은 작업을 하는 것과 비슷합니다. 즉, 파일을 자신의 꼬리 부분에 복사하고, 꼬리 부분을 grep으로 필터링한 뒤, 머리 부분에서 해당 내용을 삭제하는 것입니다.)
줄을 합칠 때, 저는 보통 이전 줄과 합쳐져야 하는 모든 줄에 대한 패턴을 찾을 수 있습니다 (예를 들어 어떤 글머리 기호 목록에서 "^ * " 대신 "^ "로 시작하는 모든 줄). 이 경우 :% g/^ /-1j를 사용합니다 (일치하는 모든 줄에 대해, 한 줄 위로 올라가서 합치기). (참고로, 글머리 기호 목록에서 글머리 기호 줄을 찾아 다음 줄과 합치려고 시도하는 것은 몇 가지 이유로 잘 작동하지 않습니다. 글머리 기호 줄끼리 합쳐질 수도 있고, 글머리 기호 줄을 그에 딸린 모든 연속된 줄과 합쳐주지 않기 때문입니다. 일치하는 항목들에 대해 쌍으로만 작동할 것입니다.)
말할 필요도 없이, 우리의 오랜 친구 s(치환)를 gv(전역/반대 전역) 명령어와 함께 사용할 수 있습니다. 보통은 그렇게 할 필요가 없습니다. 하지만 다른 어떤 패턴과 일치하는 줄에서만 치환을 수행하고 싶은 경우를 생각해 보세요. 종종 캡처를 포함한 복잡한 패턴을 사용하고 역참조(back reference)를 사용하여 변경하고 싶지 않은 줄의 부분을 보존할 수 있습니다. 하지만 일치 조건과 치환을 분리하는 것이 더 쉬울 때가 많습니다. :% g/foo/s/bar/zzz/g -- "foo"를 포함하는 모든 줄에 대해, 모든 "bar"를 "zzz"로 치환합니다. (:% s/\(.*foo.*\)bar\(.*\)/\1zzz\2/g와 같은 식은 동일한 줄에서 "foo"가 "bar"보다 먼저 나오는 경우에만 작동할 것입니다. 이미 충분히 보기 흉하며, "bar"가 "foo"보다 먼저 나오는 모든 경우를 잡으려면 더 엉망으로 만들어야 할 것입니다.)
핵심은 ex 명령어 세트에는 p, s, d 줄 명령어 이상의 것들이 있다는 점입니다.
: 주소는 마크를 참조할 수도 있습니다. 따라서 :'a,'bg/foo/j를 사용하여 'a'와 'b' 마크 사이의 줄들 중에서 "foo" 문자열을 포함하는 모든 줄을 다음 줄과 합칠 수 있습니다. (네, 앞서 언급한 모든 ex 명령어 예제들은 이러한 종류의 주소 지정 표현식을 접두사로 붙여 파일 줄의 하위 집합으로 제한할 수 있습니다.)
이것은 꽤나 모호한 기능입니다 (저도 지난 15년 동안 이런 식의 명령을 몇 번밖에 사용하지 않았습니다). 하지만 제대로 된 주문을 생각할 시간을 가졌더라면 더 효율적으로 할 수 있었을 일들을, 저는 종종 반복적이고 대화형으로 처리해 왔음을 솔직히 인정합니다.
또 다른 매우 유용한 vi 또는 ex 명령어는 다른 파일의 내용을 읽어오는 :r입니다. 따라서 :r foo는 "foo"라는 이름의 파일 내용을 현재 줄에 삽입합니다.
더 강력한 것은 :r! 명령어입니다. 이것은 명령어의 실행 결과를 읽어옵니다. 이것은 vi 세션을 일시 중단하고, 명령어를 실행하고, 출력을 임시 파일로 리다이렉션하고, vi 세션을 재개한 뒤 임시 파일에서 내용을 읽어오는 것과 같습니다.
심지어 더 강력한 것은 ! (뱅)과 :... ! (ex 뱅) 명령어입니다. 이것들 역시 외부 명령어를 실행하고 결과를 현재 텍스트로 읽어옵니다. 하지만, 이것들은 우리가 선택한 텍스트를 명령어를 통해 필터링합니다! 따라서 1G!Gsort를 사용하여 파일의 모든 줄을 정렬할 수 있습니다 (Gvi의 "이동" 명령어입니다. 기본적으로 파일의 마지막 줄로 이동하지만, 첫 번째 줄인 1과 같은 줄 번호를 접두사로 붙일 수 있습니다). 이것은 ex 변형인 :1,$!sort와 동일합니다. 작가들은 종종 텍스트 선택 영역의 형식을 다시 맞추거나 "줄 바꿈"을 하기 위해 Unix의 fmtfold 유틸리티와 함께 !를 사용합니다. 매우 흔한 매크로는 {!}fmt (현재 단락의 형식 다시 맞추기)입니다. 프로그래머들은 때때로 자신의 코드나 코드의 일부를 indent나 다른 코드 형식 재지정 도구에 통과시키기 위해 이것을 사용합니다.
:r!! 명령어를 사용한다는 것은 모든 외부 유틸리티나 필터를 우리 에디터의 확장 기능으로 취급할 수 있다는 의미입니다. 저는 가끔 데이터베이스에서 데이터를 가져오는 스크립트나, 웹사이트에서 데이터를 긁어오는 wget 또는 lynx 명령어, 혹은 원격 시스템에서 데이터를 가져오는 ssh 명령어와 함께 이것들을 사용해 왔습니다.
또 다른 유용한 ex 명령어는 :so (:source의 약자)입니다. 이것은 파일의 내용을 일련의 명령어로 읽어들입니다. vi를 시작할 때 보통 암시적으로 ~/.exinitrc 파일에 대해 :source를 수행합니다 (그리고 Vim은 당연히 ~/.vimrc에 대해 이를 수행합니다). 이것의 용도는 새로운 매크로, 약어, 에디터 설정 세트를 단순히 소싱(sourcing)함으로써 에디터 프로필을 즉석에서 변경할 수 있다는 점입니다. 영리하게 이용한다면, 필요할 때 파일에 적용할 ex 편집 명령어 시퀀스를 저장하는 트릭으로 사용할 수도 있습니다.
예를 들어, 저는 파일을 wc에 통과시키고 해당 단어 수 데이터를 포함하는 C 스타일 주석을 파일 상단에 삽입하는 7줄짜리 파일(36자)을 가지고 있습니다. 저는 다음과 같은 명령어를 사용하여 해당 "매크로"를 파일에 적용할 수 있습니다: vim +'so mymacro.ex' ./mytarget
(viVim+ 명령행 옵션은 보통 특정 줄 번호에서 편집 세션을 시작하는 데 사용됩니다. 하지만 + 뒤에 제가 여기서 한 것처럼 "source" 명령어와 같은 유효한 ex 명령어/표현식을 붙일 수 있다는 사실은 잘 알려져 있지 않습니다. 간단한 예로, 저는 서버 세트를 재이미징하는 동안 대화형 방식이 아니게 SSH known hosts 파일에서 항목을 제거하기 위해 vi +'/foo/d|wq!' ~/.ssh/known_hosts를 호출하는 스크립트를 가지고 있습니다.)
보통 이러한 "매크로"는 Perl, AWK, sed (사실 grep처럼 ed 명령어에서 영감을 받은 유틸리티입니다)를 사용하여 작성하는 것이 훨씬 쉽습니다.
@ 명령어는 아마도 가장 모호한 vi 명령어일 것입니다. 거의 10년 동안 고급 시스템 관리 과정을 가끔 가르치면서 이 명령어를 사용해 본 사람을 거의 만나지 못했습니다. @는 레지스터의 내용을 마치 vi 또는 ex 명령어인 것처럼 실행합니다. 예: 저는 종종 :r!locate ...를 사용하여 시스템에서 어떤 파일을 찾고 그 이름을 제 문서로 읽어옵니다. 거기서 불필요한 검색 결과는 삭제하고 제가 관심 있는 파일의 전체 경로만 남깁니다. 경로의 각 구성 요소를 힘들게 Tab으로 완성해가는 대신 (또는 더 나쁘게도, vi에 탭 완성 지원이 없는 머신에 갇힌 경우), 저는 그냥 다음을 사용합니다:
  1. 0i:r (현재 줄을 유효한 :r 명령어로 변환),
  1. "cdd (해당 줄을 삭제하여 "c" 레지스터에 넣기), 그리고
  1. @c 해당 명령어를 실행.
이것은 단 10번의 키 입력뿐입니다 (그리고 "cdd @c라는 표현은 저에게 사실상 손가락 매크로와 같아서, 흔한 6글자 단어만큼이나 빠르게 타이핑할 수 있습니다).



진지하게 생각해 볼 점

저는 vi 파워의 겉핥기만 했을 뿐이며, 제가 여기서 설명한 것 중 어느 것도 vim이라는 이름의 유래가 된 "개선 사항(improvements)"의 일부조차 아닙니다! 제가 여기서 설명한 모든 것은 20~30년 전의 어떤 오래된 vi에서도 작동할 것입니다.
세상에는 제가 앞으로 사용할 것보다 훨씬 더 많은 vi의 기능을 사용하는 사람들이 있습니다.
0
7

댓글

?

아직 댓글이 없습니다.

첫 번째 댓글을 작성해보세요!

Inkyu Oh님의 다른 글

더보기

유사한 내용의 글