(Vim) 필요없는 공백제거하기

왠만한 편집기들은 파일을 저장하기 전에 각 라인의 뒤에 들어있는 쓸데없는 공백들을 제거해주는 기능을 제공한다. (이러한 trailing space들의 해악에 대해서는 따로 말할 필요가 없을 것 같다.) 물론 vim에서도 이걸 사용할 수 있다. :s 명령을 사용하면 각 줄에서 맨 뒤쪽 공백을 제거할 수 있다.

:%s/\s\+$//

간단히 설명하면 이 명령은 다음과 같이 동작한다.

  1. %으로 전체 라인에 대해서 반복한다.
  2. 패턴은 /\s\+$/ 으로 공백문자가 이어지다가 라인의 끝을 만나는 것이다.
  3. 이 패턴을 지운다.

이걸 :autocmd에 끼얹으면 손쉽게 파일 저장 이전에 실행될 수 있도록 할 수 있다. * 는 모든 파일이라는 의미이므로, *.py,*.js 처럼 바꾸면 파이썬, 자바스크립트 파일에 대해서만 실행할 수도 있다.

:autocmd BufWritePre * %s/\s\+$//

이렇게하면 파일을 저장하려 할 때마다 저 치환 동작을 수행해서 불필요한 공백을 제거할 수 있다. 극히 간단하게 적용할 수 있는 방법이고, 사실 인터넷을 뒤져봐도 워낙 간단한 방법이라 많이 소개된다. 다음과 같은 변형으로 키맵에 연결하는 경우도 있다. (<C-u>는 명령모드에서 프롬프트에 입력된 내용을 모두 지우는 키 동작이다.)

:nnoremap <F8> :<C-u>%s/\s\+$//<CR>

좀 더 들여다보기

그런데 이 방법은 엄청 간단한 대신에 몇 가지 부작용을 가지고 있다. 첫째로 가장 근본적인 문제는 %s 명령을 사용한다는 것 때문에 발생한다. 범위에 %를 주는 경우, 모든 라인을 따라 돌아가면서 명령을 반복하게 되는데, 이 때 커서가 같이 따라 움직이게 된다. 따라서 저장을 마치고 나면 편집하던 위치가 아니라 항상 파일의 맨 끝으로 가게 된다. (물론 이건 <C-o>로 되돌아가면 되긴 하다)

다른 문제는 :s 명령은 검색 레지스터를 사용한다는 것이다. 따라서 특정 단어를 검색하고, 해당 단어가 하이라이트 된 중에 파일을 저장했다면 찾고 있던 내역이 사라지는 것이다. 다만 autocmd는 항상 실행 전후에 검색 레지스터를 백업했다가 복구한다. 이 문제는 키맵으로 설정하는 경우에 다르게 처리해야 한다.

셋째로 이미 끝자리 공백이 모두 제거된 파일에 대해서는 치환이 실패할 것이므로 매번 E486: Pattern not found: \s\+$ 이라는 에러 메시지가 표시될 것이다.

이를 해결할 수 있는 실마리들은 다음과 같다.

  1. 치환 후 원래위치를 복원하기 : 현재 커서 위치의 라인과, 스크롤 상태 등을 따로 기억했다가 복원하는 여러 팁들이 있는데, 가장 간단한 방법은 winsaveview() 함수를 통해서 현재 화면의 상태를 얻고 나중에 winrestview() 함수를 통해서 복원하는 것이다.
  2. 역시 검색 레지스터는 스크립트에서 쓰기/읽기가 가능하므로 백업/복원이 가능하다.
  3. :s 명령의 플래그중에는 치환실패 메시지를 출력하지 않게하는 e 옵션이 있다.

이를 토대로 다음과 같이 코드를 수정할 수 있다. 다만 map 명령에서 키 시퀀스 부분을 여러 줄로 분할하여 입력할 수는 없으므로 엄청 긴 키맵을 입력해야하는 부담이 있다.

nnoremap <F8> :<C-u>let _x = @/<Bar>let _w = winsaveview()<Bar>%s/\s\+$//e<Bar>let @/ = _x<Bar>call winrestview(_w)<CR>

또 키맵이나 autocmd의 경우 변수를 추가로 노출하기 때문에 깔끔한 방법이 아니다. 따라서 다음과 같이 함수로 정의해서 저장 할 때, 키를 눌렀을 때 작동할 수 있도록 하자.

function! s:remove_trailing_spaces() abort
	let x = @/
	let w = winsaveview()
	silent! %s/\s\+$//e  "silent 명령으로 치환 결과를 표시 안함
	let @/ = x
	call winrestview(w)
endfunction

이 함수를 호출하는 사용자 정의 Ex 명령과 키맵을 작성한다. 그리고 적절한 키와 자동명령에서 호출되도록 해주면 된다.

" 명령으로 정의
command! -nargs=0 RemoveTrailingSpaces
			\ call <SID>remove_trailing_spaces()
" 저장시마다 발동
augroup TS
  au!
  au BufWritePre * RemoveTrailingSpaces
augroup END

" 키맵으로 정의해서 원할때마다 실행하려면
noremap <script><silent> <Plug>(remove_trailing_spaces)
			\ :<C-u>RemoveTrailingSpaces()<CR>
nmap <F8> <Plug>(remove_trailing_spaces)

이상의 코드를 vim파일로 작성해놓고 :source 명령으로 읽어들이면 자동으로 혹은 수동으로 실행할 수 있게 된다.

보너스 – 파일 끝의 빈줄 제거하기

줄 끝의 공백만큼이나 많이 생기는 것이 파일 끝의 빈 줄이다. 이것도 간단히 처리할 수 있다. 파일끝 공백은 개행문자로부터 시작하여 공백-개행-공백-개행-…-공백이 계속해서 이어진다. 결국 (개행공백*)의 반복이다. 그리고 그 끝은 파일의 끝이다. vim 검색 패턴에서 \%$는 파일의 끝을 의미하므로 다음과 같은 명령으로 파일 끝 빈 줄을 제거할 수 있다.

:s/\(\n\s\+\)\+\%$//e

참고로, 파일 끝 빈줄은 파일 내에서 한 번만 나올 것이기 때문에 %를 붙이지 않는다. (붙여도 상관은 없다만…) slient s/\(\n\s\+\)\+\%$//e 를 위에서 작성한 함수에 추가해주면 깔끔하게 정리 성공.