Emacs를 이용한 단순 반복 작업

내 이 세상 도처에서 쉴 곳을 찾아보았으나, 마침내 찾아낸, 컴퓨터가 있는 구석방보다 나은 곳은 없더라.

Emacs를 이용한 단순 반복 작업

며칠 전 아주 지겨워 보이는 작업을 하게 되었다. 엑셀 파일에 있는 정보를 참고해 설정 파일을 수정하는 일이었다. 설정 파일은 국가별 설정 정보를 담고 있는데 각 국가별 설정에 새로운 필드를 추가해야 하고 필드의 값은 엑셀 파일을 참조해 지정해야 했다.

딱 봐도 지루하고 짜증나는 일이었다. 설정 파일에서 국가 코드를 확인하고, 엑셀 파일에서 해당 국가 코드를 찾아 그 코드에 매칭되는 정보를 복사한 다음, 설정 파일에 새로운 필드를 추가하고 복사한 값을 붙여넣어야 한다. 설정 파일에는 약 250개의 국가 코드가 있었다. 국가 코드 하나 당 30초씩 걸린다면 30 \times 250 = 7,500 초, 125분이 걸릴 것이다. 익숙해지면 속도가 빨라지겠지만 처음에 버벅거릴 것을 고려하면 작업 시간을 두 시간 이상으로 잡는 게 타당할 것이다. 엑셀 파일과 설정 파일을 왔다갔다 하며 두 시간 넘게 작업할 생각을 하니 눈 앞이 노래졌다.

게다가 작업을 마친 후 중간에 실수가 없었다고 어떻게 장담할 수 있을까? 확인 작업이 필요하다면 시간도 늘어날 뿐 아니라 지겨운 작업을 또 해야 한다. 잠시 눈을 감고 생각해 보았다. 어떻게 하면 이 지겨운 작업을 회피할 수 있을까?

설정 파일의 각 국가 설정에 새로운 키를 추가하는 것은 비교적 쉬운 일이었다. 키보드 매크로를 이용하면 금방 끝낼 수 있다. 키에 대응될 값이 국가 코드에 따라 다르고 그 값은 엑셀 파일을 참고해야 한다는 점이 문제였다. 잠깐 국가 코드 몇 개를 엑셀 파일에서 찾아봤다. 이런 식이라면 정말 오랫동안 지겨운 작업을 해야 할게 뻔했다.

그러다 갑자기 'Emacs Lisp으로 함수를 만들어 작업을 해보면 어떨까?' 하는 생각이 떠올랐다. 국가 코드를 입력하면 그에 대응되는 값을 리턴하는 함수는 다음과 같은 식으로 만들 수 있을 것이다.

(defun f (cc)
  (cond ((equal cc "AT") "DE")
        ((equal cc "DE") "DE")
        ((equal cc "JP") "JP")
        ((equal cc "US") "US")
        ...
        ))

cond의 각 브랜치는 엑셀 포뮬러를 이용해 쉽게 만들 수 있었다.

=CONCATENATE("((equal cc """,C2,""")"," ", """",D2,""")")

포뮬러를 매핑 컬럼 전체에 적용해 생성한 cond 브랜치를 복사해 Emacs Lisp 함수에 붙여넣으면 된다. 이제 엑셀 파일과 설정 파일을 왔다갔다 하지 않아도 된다. 매핑 값을 넣어야 하는 곳에서 함수를 호출하면 된다. 그러나 몇 가지 고려 사항이 더 있었다.

  • 함수에 인자를 어떻게 전달할 것인가?
  • 함수의 리턴 값을 에디터에 어떻게 입력할 것인가?
  • 어떻게 키보드 입력(조작)을 최소화할 것인가?

함수에 전달해야 할 인자는 국가 코드였으므로 자동화가 가능해 보였다. 그러나 Emacs Lisp을 잘 아는 것도 아니었고 시간도 촉박했기 때문에, 인자 값은 키보드로 직접 입력하기로 했다. read-string을 이용하면 키보드로부터 값을 입력 받을 수 있다.

(defun f ()
  (interactive)
  (let ((cc (upcase (read-string "Country Code: "))))
    (cond ((equal cc "AT") "DE")
          ((equal cc "DE") "DE")
          ((equal cc "JP") "JP")
          ((equal cc "US") "US")
          ...)))

이 함수를 실행하면 리턴 값을 미니버퍼에 표시할 뿐 에디터에 입력하지는 않는다. 포인트에 함수 리턴 값을 입력하려면 insert를 사용해야 한다.

(defun f ()
  (interactive)
  (let ((cc (upcase (read-string "Country Code: "))))
    (insert (cond ((equal cc "AT") "DE")
                  ((equal cc "DE") "DE")
                  ((equal cc "JP") "JP")
                  ((equal cc "US") "US")
                  ...))))

이제 매핑 값을 입력할 위치에서 위 함수를 호출하고 해당 설정에 대한 국가 코드를 입력하면 된다. 함수 실행 후 다음 다음 입력 포인트로 자동으로 이동한다면 좀더 편할 것이다.

(defun f ()
  (interactive)
  (let ((cc (upcase (read-string "Country Code: "))))
    (insert (cond ((equal cc "AT") "DE")
                  ((equal cc "DE") "DE")
                  ((equal cc "JP") "JP")
                  ((equal cc "US") "US")
                  ...)))
  (forward-line 9)
  (backward-char 2)
  (f))

다행히 다음 입력 포인트로 이동하는 것은 단순했다. 아홉 줄 아래로 내려간 다음 앞으로 두 칸 이동하면 다음 입력 포인트가 되었다. 함수 마지막에서 다시 자신을 호출해 무한 루프를 만들었다. 그리고 위 함수에 적절한 단축키를 지정했다.

매핑 값을 입력해야 할 위치에서 단축키를 눌러 함수를 실행시키면 된다. 해당 섹션의 국가 코드를 입력하면 매핑 값이 입력되고 다음 입력 포인트로 커서가 이동한다. 계속 국가 코드만 입력하면 된다. 이렇게 해서 두 시간 동안 눈 빠지게 해야 할 작업을 조금 편하게 할 수 있었다.

여전히 실수가 개입할 여지가 있긴 하다. 국가 코드를 직접 입력해야 하기 때문이다. 내가 Emacs Lisp을 잘 알았다면 설정파일에서 해당 포인트의 국가 코드를 캡쳐해 사용하도록 할 수 있었을 것이다. 그러나 엑셀 파일과 설정 파일을 왔다갔다 하며 작업하는 것보다는 훨신 나아졌다.

작업 시간은 두 시간 정도 걸린 것 같았다. 처음 추정했던 작업 시간보다 줄어들지는 않았다. Emacs Lisp에 익숙하지 않아 함수를 작성하는 데 시간이 걸렸기 때문이다. 그러나 두 시간 동안 엑셀 파일과 설정 파일에서 오류를 발견해 수정했다. 일부 국가 코드는 엑셀 파일에만 있고 일부 국가 코드는 설정 파일에만 있는 것을 발견해 차이점을 보고했다. 무엇보다고 작업을 하면서 짜증이 나지 않았고 작업이 끝난 후 녹초가 되지도 않았다.