vi를 진정으로 이해해라

05 Mar 2017

Vim 3D

  1. A sampling of more advanced tricks:
  2. A sobering thought
    1. 추가링크

원문 : Your problem with Vim is that you don’t grok vi

소스코드를 편집하는 프로그래머는 전체 라인, 여러 라인 및 코드 블록에서 작업하기를 원한다. 그러나 yy는 텍스트를 익명 복사 버퍼(vi에서는 “레지스터”라고 한다)로 복사하는 많은 방법 중 하나다.

vi의 “Zen”은 일종의 언어를 말하는 것이다. 초기 y는 동사(verb)다. yy 명령문(statement)은 y_와 동의어입니다. y는 일반적인 조작이므로 입력하기가 쉽도록 반복한다.

Vim의 선은 언어를 말하는 것이다. 첫 번째 y는 동사다. yy 명령문은 y_의 동의어다. 널리 쓰이기 때문에 타이핑하기 쉽게 y를 두 번 반복한다.

이것은 dd P (현재 행을 삭제하고 사본을 도로 붙여넣기; 추가 효과로 사본을 익명 레지스터에 남겨두기)로 표현할 수도 있다. yd “동사(verbs)”는 움직임을 “대상(subject)”으로 사용한다. 그러므로 yW는 “여기서(커서 위치)부터 현재/다음 큰 단어(WORD)의 끝까지 복사하고, y'a는 “여기에서 ‘a’ 마크가 포함된 줄까지 복사한다.”

vi에는 26 “마크”와 26 “레지스터”가 있다. 마크는 m 명령으로 커서의 위치를 설정한다. 각 마크는 소문자 하나로 지정된다. 따라서 ma는 ‘a’ 마크를 현재 위치로 설정하고 mz는 ‘z’ 마크를 설정한다. '(작은따옴표) 명령을 사용하여 마크가 있는 라인으로 이동할 수 있다. 'a는 ‘a’ 마크가 있는 라인의 처음으로 이동한다. ` (backquote) 명령으로 마크의 정확한 위치로 이동할 수 있다. 따라서 `z는 ‘z’ 마크의 정확한 위치로 바로 이동한다.

이들은 “이동”이기 때문에 다른 “명령문”의 대상으로 사용할 수도 있다.

텍스트를 원하는 대로 선택해서 잘라내는 방법의 하나는 마크하는 것이다 (보통 ‘a’를 첫 번째 마크, ‘z’를 다음 마크, ‘b’는 다른 마크로, ‘e’를 또 다른 마크로 사용한다). 또 다른 (vi를 사용하는 15년 동안 4개 이상의 마크를 사용한 것은 기억나지 않는다; 사용자들은 문맥을 방해하지 않도록 마크와 레지스터를 매크로에서 사용하는 자신만의 규칙을 만든다) 그런 다음 원하는 텍스트의 다른 쪽 끝으로 이동한다: 다른 끝에서 시작할 수 있다, 중요하지 않다. d`a로 잘라내거나 y`a로 복사할 수 있다. 전체 프로세스가 5개의 키 입력이다. (“입력” 모드에서 시작하면 <Esc>로 명령 모드를 나가야 하므로 6이다) 잘라내거나 복사한 다음 복사본을 붙여넣는 것은 단일 키 입력이다: p.

이것이 텍스트를 잘라내거나 복사하는 하나의 방법이다. 그러나 많은 방법의 하나이다. 커서를 움직이거나 마크하지 않고도 텍스트의 범위를 더 간결하게 설명할 수 있다. 예를 들어 텍스트의 단락 안에서 단락의 처음이나 끝까지 각각 {}로 이동할 수 있다. 텍스트의 단락을 옮기려면 { d} (3 키 입력)으로 잘라낸다. 이미 단락의 첫 줄 또는 마지막 줄에 있을 때는 간단히 d} 또는 d{를 사용할 수 있다.

“단락” 개념은 보통 직관적으로 그럴듯한 것을 기본으로 한다. 평문뿐만 아니라 코드에서도 종종 동작한다.

원하는 텍스트의 한쪽 끝 또는 다른 끝을 마크하는 패턴(정규식)을 안다. 앞으로 또는 뒤로 검색하는 것은 vi에서 이동이다. 따라서 “명령문”에서 “대상”으로 사용할 수 있다. d/foo로 현재 줄에서 “foo” 문자열을 포함하는 다음 줄까지 잘라낼 수 있다. y?bar로 현재 줄에서 “bar” 문자열을 포함하는 가장 가까운 (이전) 줄까지 복사할 수 있다. 전체 라인을 원하지 않는다면 검색 이동을 (명령문으로) 여전히 사용하고, 마크를 지정하고 이전에 설명한 `x 명령을 사용할 수 있다.

“동사” 및 “대상” 말고도 vi는 “목적어(objects)”(용어의 문법적 의미로)도 있다. 지금까지 익명 레지스터의 사용만 설명했다. 그러나 "(이중 따옴표 수정자)를 접두사로 하는(prefixing) “목적어” 참조를 26개의 “이름(named)” 레지스터도 사용할 수 있다. "adda 레지스터에 현재 줄을 잘라내거나, "by/foo로 현재부터 “foo”가 포함된 다음 줄까지 텍스트를 ‘b’ 레지스터로 복사할 수 있다. 레지스터에서 붙여넣으려면 같은 수정자를 앞에 붙이면 된다: "ap는 ‘a’ 레지스터의 내용을 커서 뒤에 붙여넣고 "bP는 현재 줄 앞에 ‘b’ 레지스터의 내용을 붙여넣는다.

“접두어(prefixes)”라는 개념은 텍스트 조작 “언어”에 “형용사(adjectives)”와 “부사(adverbs)”와 문법적으로 비슷한 것을 추가한다. 대부분의 명령(동사)과 동작(상황에 따라 동사 또는 목적어)은 숫자 접두사를 사용할 수도 있다. 3J는 “다음 3줄을 합침”을 의미하고 d5}는 “현재 줄에서 밑으로 5번째 단락의 끝까지 삭제”를 뜻한다.

이것은 모두 중급 레벨 vi이다. Vim에만 국한되는 것은 아니며, 배우려고만 하면 훨씬 높은 중급 기술이 있다. 텍스트 조작 언어가 에디터의 “기본” 언어를 사용하여 작업 대부분을 쉽게 수행할 수 있도록 매우 간결하고 표현력이 풍부하므로, 이러한 중급 개념을 숙달하면 매크로를 작성할 필요가 거의 없음을 알 수 있다.

A sampling of more advanced tricks:

많은 : 명령이 있지만, 특히 :% s/foo/bar/g 전역 대체(substitution) 기술이 있다. (이것은 중급은 아니다. 그러나 다른 : 명령은 그럴 수 있다) 역사적으로 전체 : 명령 집합은 vi의 이전 버전인 ed(line editor)와 나중에 나온 ex(extended line editor) 유틸리티에서 상속되었다. 실제로 vi는 ex의 시각적 인터페이스(visual interface)이기 때문에 그렇게 이름 지었다.

: 명령은 보통 텍스트의 라인에 걸쳐 동작한다. ed와 ex는 터미널 화면이 흔하지 않고 많은 터미널이 “텔레 타입”(TTY) 장치였던 시대에 작성되었다. 따라서 텍스트의 인쇄본에서 작업하는 것이 일반적이었고 매우 간결한 인터페이스를 통해 명령어를 사용하였다. (일반적인 연결 속도는 초당 110 baud 또는 대략 초당 11문자였다 - 빠른 타이피스트보다 느리다; 정체(lags. 렉 먹는 것)은 다중 사용자 인터랙티브 세션에서는 일반적이었다; 또한 종이로 보존하려는 약간의 동기가 있었다)

그래서 대부분의 : 명령어의 문법은 명령 뒤에 주소와 주소 범위(줄 번호)를 포함한다. :127,215 s/foo/bar는 127과 215 사이의 줄에서 처음 만나는 “foo”를 “bar로 대치한다. 현재 줄은 ., 마지막 줄은 $로 약어를 쓸 수도 있다. 현재 줄의 뒤로는 +, 앞으로는 -를 상대적인 파생(offset) 접두어로 사용할 수 있다. 그래서 :.,$j는 현재 줄부터 마지막 줄까지 모두 한 줄로 합친다”를 뜻한다. :%는 (모든 줄) :1,$와 같다.

:... g:... v 명령은 대단히 강력하다. :... g는 패턴에 매칭되는 모든 줄에 후속 명령을 “전역적으로(globally)” 적용하는 접두어이고, :... v(conVerse(반대))는 패턴에 일치하지 않는 모든 줄에 적용한다. 다른 ex 명령처럼 이것들은 주소/범위 참조를 접두할 수 있다. :.,+21g/foo/d는 “현재 줄부터 다음 21줄에 걸쳐 “foo”를 포함하는 모든 줄을 지우는 것”을 뜻하고, :.,$v/bar/d는 “현재 줄부터 파일의 끝까지 “bar”를 포함하지 않는 모든 줄을 지우는 것”을 뜻한다.

유닉스 명령어 grep이 ex 명령에서 실제로 영감을 받았다는 것은 흥미롭다. ex 명령 :g/re/p(grep)은 “정규식(regular expression)” (re)를 포함하는 줄을 “전역적으로(globally)” “인쇄(print)”하는 법을 문자화한 것이다. ed와 ex가 사용될 때 :p 명령은 첫 번째로 배우는 것 중 하나였고 파일을 편집할 때 자주 사용되는 첫 번째 명령이었다. 현재 내용(보통 :.,+25p와 같이 사용하여 한 번에 한 페이지 전체)을 인쇄하는 방법이었다.

:% g/.../d 혹은 (reVerse/conVerse 짝인) :% v/.../d이 가장 일반적인 사용 패턴이다. 기억할만한 다른 ex 명령 짝도 있다.

줄을 근처로 옮길(move) 때 m을, 줄을 합칠(join) 때 j를 사용할 수 있다. 예로 목록에서 매칭되는(또는 반대로(conversely) 매칭되지 않는) 모든 것을 지우지 않고 분리하길 원하면 :% g/foo/m$와 같이 사용할 수 있다. 모든 “foo”(를 포함하는) 줄은 파일의 끝으로 옮겨질 것이다. (scratch(낙서) 공간으로 파일의 끝을 사용하는 다른 팁도 알아둬라) 목록에서 빼낸 모든 “foo” 줄은 상대적인 순서로 된다.(1G!GGmap!Ggrep foo<ENTER>1G:1,'a g/foo'/d (파일 끝으로 파일을 복사하고, grep으로 필터링하고, 나머지 모든 헤드 부분을 지우기)를 실행하는 것과 같다)

줄을 합치기 위해 합치려는 모든 줄에서 패턴을 찾을 수 있다. (예로, 글 머리 기호 목록에서 “^*“보다 “^ “로 시작하는 모든 줄) 그럴 때는 (일치하는 모든 줄에서 한줄 올리고 합치도록) :% g/^ /-1j을 사용한다. (부언하면: 글머리 기호 줄을 찾아 다음 줄을 합치는 것은 두 가지 이유로 동작하지 않는다… 하나의 글머리 기호 줄은 다른 것과 합칠 수 있다. 이어지는 모든 줄에 글머리 기호 줄을 합치지 않을 것이다; 쌍으로 인접하는 것에만 동작한다.)

s(substitute)를 gv (global/converse-global) 명령과 사용할 수 있음은 물론이다. 보통 그럴 필요가 없다. 그러나 다른 패턴과 일치하는 줄에만 대체하려고 하는 경우를 생각해보자. 종종 복잡한 패턴을 사용할 수 있고, 변경을 원하지 않는 줄 부분을 보존하기 위해 역참조를 사용한다. 그러나, 대체에서 일치하는 부분을 분리하는 것이 더 쉬울 것이다: :% g/foo/s/bar/zzz/g – “foo”를 포함하는 모든 줄에서 모든 “bar”를 “zzz”로 대치. (:% s/\(.*foo.*\)bar\(.*\)/\1zzz\2/g와 같은 것은 같은 줄에 “foo”가 먼저 오는 “bar”의 사례에만 동작할 것이다; 이미 매우 꼴사납다. 그리고 “bar”가 “foo”보다 앞서는 모든 경우를 잡기 위해 더 망가져야 할 것이다)

주요점은 ex 명령어 세트에 p, s, d보다 더 많은 것이 있다는 것이다.

: 주소도 마크를 참조할 수 있다. 그래서 ‘a’와 ‘b’ 마크 사이의 줄에서 foo 문자열을 포함하는 모든 줄을 합치려면 :'a,'bg/foo/j를 사용할 수 있다. (그렇다, 앞의 ex 명령 예제의 모든 경우에서 이러한 종류의 주소 표현을 접두해서 파일의 줄 서브셋을 한정할 수 있다)

나도 지난 15년간 몇 번 정도 사용했다. 그러나, 올바른 주문을 생각할 시간이 있다면 훨씬 효과적으로 수행될, 반복적이고 인터렉티브하게 일을 한다.

또 다른 매우 유용한 vi 혹은 ex 명령은 다른 파일의 내용을 읽어오는(read) :r이다. :r foo는 현재 줄에 “foo”라는 파일의 내용을 삽입한다.

:r!은 더욱 강력하다. 이것은 명령의 결과를 읽는다. vi 세션을 일시적으로 정지하고, 명령을 수행하고, 임시 파일에 출력을 리다이렉트하고, vi 세션을 다시 실행하여 임시 파일에서 내용을 읽어오는 것과 같다.

!(bang)과 :... !(ex bang) 명령은 훨씬 더 강력하다. 이것도 외부 명령을 실행하고 현재 텍스트 안으로 결과를 읽어온다. 그러나, 특정 명령으로 텍스트의 선택을 필터링할 수도 있다! 1G!Gsort(G는 vi의 “goto” 명령이다; 기본적으로는 파일의 마지막 줄로 간다, 그러나 1(첫 번째 줄)과 같이 줄 넘버를 접두할 수 있다)을 사용하여 파일의 모든 줄을 정렬할 수 있다. ex 버전인 :1,$!sort와 같다. 작가들이 종종 사용한다! Unix fmt 또는 fold 유틸리티를 사용하여 텍스트의 “줄 바꿈(word wrapping)” 또는 다시 포맷한다. 일반적인 매크로는 {!}fmt(현재 단락을 다시 포맷)이다. 프로그래머는 들여쓰기(indent) 또는 다른 코드 리포매팅 도구를 사용하여 코드를 실행하거나 코드 일부만 실행하기도 한다.

:r! 그리고 ! 명령을 사용하는 것은 외부 유틸리티나 필터를 에디터의 확장(extension)으로 취급할 수 있음을 의미한다. 데이터베이스에서 데이터를 가져오는 스크립트와 사용하거나 웹 사이트에서 데이터를 가져오는 wget, lynx 명령이나 원격 시스템에서 데이터를 가져오는 ssh 명령과 함께 가끔 사용한다.

또 다른 유용한 ex 명령은 :so(:source의 준말)이다. 이것은 파일 내용을 일련의 명령으로 읽는다. 보통 vi를 시작하면 암시적으로 ~/.exinitrc 파일에서 :source를 실행한다. (그리고 Vim은 보통 ~/.vimrc 파일을 사용한다) 이 기능을 사용하면 새로운 매크로 세트, 약어 및 에디터 설정을 읽어 들여(sourcing) 에디터 프로필을 변경할 수 있다. 파일에 따라 적용하는 ex 편집 명령의 시퀀스를 저장하는 트릭으로 사용할 수도 있다.

예를 들어 wc로 파일을 실행하는 일곱 줄의 파일 (36자)을 가지고 있고, 파일의 맨 위에 단어 개수 데이터를 C 스타일 주석으로 삽입하려 한다. vim + 'so mymacro.ex' ./mytarget과 같은 명령을 사용하여 “매크로”를 파일에 적용할 수 있다.

(vi와 Vim의 + 명령행 옵션은 보통 주어진 행 번호에서 편집 세션을 시작하는 데 사용한다. 그러나 +에 “source” 같이 유효한 ex 명령/표현식이 뒤따를 수 있다는 사실은 거의 알려지지 않았다; 간단한 예로 다음 스크립트가 있다: vi +'/foo/d|wq!' ~/.ssh/known_hosts는 SSH known hosts 파일에서 비대화식으로 항목을 제거하고 서버 집합을 다시 이미징한다)

대개 Perl, AWK, sed(ed 명령에서 실제 영감을 얻은 유틸리티인 grep과 같은)를 사용하여 “매크로”를 작성하기가 훨씬 쉽다.

@ 명령은 아마도 가장 모호한 vi 명령일 것이다. 10년 가까이 고급 시스템 관리 교육 과정을 가끔 가르치면서 사용해본 사람을 거의 만나지 못했다. @은 레지스터 내용을 vi나 ex 명령인 것처럼 실행합니다. 예 : :r!locate ...를 사용하여 시스템에서 파일을 찾고 이름을 문서로 읽어 들인다. 그리고 관심 있는 파일에 대한 전체 경로만 남겨둔 채 불필요한 것은 지운다. 경로의 구성마다 <Tab> 완성하는 것보다 낫다(탭 완성을 지원하지 않는 vi가 있는 머신을 만날 때는 더욱 힘들다): 다음을 사용한다.

  1. 0i:r (현재 줄을 유효한 :r 명령으로 만든다)
  2. cdd (“c” 레지스터에 그 줄을 잘라내고)
  3. @c로 명령을 실행한다.

단지 10개의 키 입력이다. (그리고 "cdd @c는 효과적으로 핑거(손이 기억하는) 매크로이다. 그래서 보통 6글자의 단어처럼 빠르게 타이핑할 수 있다)

A sobering thought

vi 파워의 표면만 훑었다. 여기에 설명한 것은 vim의 이름이 된 “improvements”의 일부분도 아니다! 여기서 설명한 모든 내용은 20, 30년 전의 vi에서도 동작한다.

내가 사용한 것보다 훨씬 많은 vi 파워를 사용해온 사람들이 있다.

추가링크

Share this:

comments powered by Disqus