Ideavim !:과 셸 스크립트 조합으로 초간단 플러그인 만들기

Ideavim !:과 셸 스크립트 조합으로 초간단 플러그인 만들기

시작하며

Ideavim에 :! 기능이!

안녕하세요. 카카오페이손해보험 고객서버개발팀 존그립입니다.

IntelliJ 전용 Vim 플러그인인 Ideavim은 매우 활발하게 업데이트가 진행되고 있어 가끔 기능을 확인할 때마다 흐뭇한 마음으로 놀라곤 합니다.

얼마 전에는 별다른 생각 없이 set-commands 문서를 읽다가 :!를 지원하는 shell set이 추가된 것을 보고 기분 좋게 깜짝 놀라고 말았습니다.

초창기의 Ideavim에서 지원하지 않았던 기능으로 기억하고 있습니다. 하지만 이제 기능을 사용할 수 있게 되었으니 다양한 활용 방안이 떠오릅니다.

set-commands 문서의 shell
set-commands 문서의 shell

이 기능을 사용하면 IntelliJ 내에서 :! 뒤에 shell 명령 문자열을 입력하는 방식으로 shell 명령을 사용할 수 있습니다.

간단한 예제: 정렬

간단한 예를 들어 보겠습니다.

다음과 같은 enum을 정의한 kotlin 소스코드가 있을 때…

예제용 enum
예제용 enum

enum 항목들을 선택하고 :! sort를 입력하면…

visual mode로 선택된 enum
visual mode로 선택된 enum

선택한 영역의 텍스트가 sort 명령에 stdin으로 제공되어, 아래와 같이 sort 명령의 실행 결과가 IntelliJ 에디터로 출력됩니다.

! 뒤에 오는 문자열을 셸로 전달하는 간단한 구조이기 때문에, 평소 터미널에서와 같이 |로 다른 명령을 연결해 사용하는 것도 가능합니다.

sort로 정렬된 enum
sort로 정렬된 enum

발전된 예제: snake_case를 camelCase로 변환하기

이제 이걸 좀 더 발전시켜 vim에서 하던 것처럼 좀 복잡한 명령을 셸 스크립트로 만들어서 실행하는 걸 생각해 볼 수 있습니다.

이번에는 snake_case를 camelCase로 변환하는 작은 명령을 사용해 보죠.

snake*case라는 이름을 갖는 변수를 하나 만들어 놓고, * 뒤에 따라오는 소문자 알파벳 한 글자를 대문자로 바꿔주는 perl 코드를 입력해 보겠습니다.

! perl -pe 's/_([a-z])/\U$1/g'

snake_case를 camelCase로 변환하기 위해 명령을 입력하는 장면
snake_case를 camelCase로 변환하기 위해 명령을 입력하는 장면

결과는 다음과 같습니다.

camelCase로 변환이 완료
camelCase로 변환이 완료

잘 작동하긴 하지만 매번 입력하는 것은 번거로울 테니 셸 스크립트로 만들어 보겠습니다.

작업하는 김에 여러 줄을 처리할 수 있도록 while read도 적용해 줍니다.

#!/bin/bash

while read line || [[ -n "$line" ]]; do
    perl -pe 's/_([a-z])/\U$1/g' <<< "$line"
done

이 코드를 어디에서나 실행할 수 있도록 PATH 경로에 위치한 디렉터리에 snake-to-camel 이라는 이름의 파일로 저장하고, chmod +x 명령을 사용해 실행권한을 부여합니다.

그러고 나서 IntelliJ 로 돌아가서 snake_case 문자열을 여러 개 만든 다음, !:를 통해 방금 만든 셸 스크립트를 실행해 보겠습니다.

여러 snake_case 문자열을 선택하고 명령어를 입력하는 장면
여러 snake_case 문자열을 선택하고 명령어를 입력하는 장면

실행 결과는 다음과 같습니다.

여러 snake_case 문자열의 camelCase 변환이 완료된 장면
여러 snake_case 문자열의 camelCase 변환이 완료된 장면

잘 작동하는군요! 이제 이걸 normal 모드에서도 실행할 수 있도록 vmap을 설정해 주겠습니다.

~/.ideavimrc에 다음과 같이 한 줄을 추가해 주면 됩니다.

vmap sc :! snake-to-camel<CR>

이제 IntelliJ에서 snake_case를 camelCase로 변환하고 싶은 라인을 여러 줄 선택한 다음에 sc를 연달아 누르면 케이스 변환이 됩니다. 참 쉽네요!

물론 이런 종류의 작업은 이미 IntelliJ의 플러그인들의 기능과 겹치는 것이긴 합니다.

그러나 IntelliJ에서 사용하기 위한 나만의 플러그인을 하나 개발하려면 프로젝트를 만들어야 하고 IntelliJ의 API 문서를 읽어야 하는 등 번거로운 점이 많습니다.

하지만 평소 터미널을 통해 늘 사용하는 명령을 겨우 몇 줄 밖에 안 되는 셸 스크립트로 저장해놓고 IntelliJ 에서 부를 수 있기 때문에 그 간편함은 어지간한 IntelliJ 플러그인들이 따라올 수 없는 장점이라 할 수 있습니다.

게다가 직접 만든 것이기 때문에 기능을 확장하거나 문제를 고치기도 쉽습니다.

좀 더 발전된 예제: create table SQL을 entity property로 변환하기

이번에는 더 나아가서 실제로 제가 최근 사용하고 있는 기능을 소개하고자 합니다.

create table SQL을 JPA Entity property로 변환하는 기능인데, 같은 기능을 IDE에서도 제공하지만 좀 더 가볍고 직접 기능 수정이 가능하다는 장점이 있습니다.

다음은 가상의 create table SQL의 칼럼 정의 하나를 만들어본 것입니다.

category_code VARCHAR(20) NOT NULL COMMENT '카테고리 코드',

순서대로 칼럼 이름, 타입, nullable, comment의 구조로 이루어져 있음을 알 수 있습니다.

각각의 항목을 추출하는 간단한 셸 명령을 먼저 만들어 봅시다.

그렇다면 이것도 snake_case 변환기와 비슷한 방식으로 작은 기능을 먼저 해결하고, 여러 줄을 처리하는 셸 스크립트로 확장해 나가는 분할 정복 방식을 사용할 수 있을 것입니다.

칼럼 이름

칼럼 이름은 가장 먼저 오는 snake_case 단어를 획득하면 될 것 같습니다.

echo "category_code VARCHAR(20) NOT NULL COMMENT '카테고리 코드'," \
  | sed -E 's/^([a-zA-Z0-9_]+) .*/\1/'

다만 케이스를 snake_case에서 camelCase로 변환해 줄 필요가 있겠군요.

echo "category_code VARCHAR(20) NOT NULL COMMENT '카테고리 코드'," \
  | sed -E 's/^([a-zA-Z0-9_]+) .*/\1/' \
  | perl -pe 's/_([a-z])/\U$1/g'

타입

타입은 칼럼 이름 다음에 스페이스를 두고 곧바로 이어지는 두 번째 단어를 획득하면 될 것입니다.

echo "category_code VARCHAR(20) NOT NULL COMMENT '카테고리 코드'," \
  | perl -pe 's/^([a-zA-Z0-9_]+) +([^ ]+).*/$2/'

작업하는 김에 추출한 타입을 편하게 다루고 싶으므로, 마지막에 \L을 넣어서 소문자로 변환해 주도록 하겠습니다.

echo "category_code VARCHAR(20) NOT NULL COMMENT '카테고리 코드'," \
  | perl -pe 's/^([a-zA-Z0-9_]+) +([^ ]+).*/\L$2/'

NULL 여부

null 허용인지 아닌지는 간단하게 not null을 포함하는지를 확인하면 되겠습니다.

echo "category_code VARCHAR(20) NOT NULL COMMENT '카테고리 코드'," \
  | perl -pe 's/^.+(not null).*/\L$1/i'

코멘트

코멘트는 쉽군요. comment 뒤에 오는 따옴표 내의 문자열을 가져오면 됩니다.

echo "category_code VARCHAR(20) NOT NULL COMMENT '카테고리 코드'," \
  | sed -E "s/.* comment +'([^']+)'.*/\1/i"

모두 모아 셸 스크립트로 완성

이제 위에서 만든 것들을 모두 모아 하나의 셸 스크립트로 작성해 보면 다음과 같이 됩니다.

#!/bin/bash

while read line || [[ -n "$line" ]]; do
    if ! echo "$line" | grep -qiE '^[a-z_]+ +(varchar|int|decimal|date(time)?|tinyint|text).*(comment)?'; then
        # 작업 대상으로 적합하지 않으므로 주석처리한다
        echo "// $line"
        continue
    fi

    column_name=$(echo "$line" | sed -E 's/^([a-zA-Z0-9_]+) .*/\1/')
    # snake_case를 camelCase 로 변환해서 필드 이름으로 쓴다
    field_name=$(echo "$column_name" | perl -pe 's/_([a-z])/\U$1/g')
    # data_type은 추출한 다음 소문자로 변환한다
    data_type=$(echo "$line" | perl -pe 's/^([a-zA-Z0-9_]+) +([^ ]+).*/\L$2/')
    not_null=$(echo "$line" | perl -pe 's/^.+(not null).*/\L$1/i')
    comment=$(echo "$line" | sed -E "s/.* comment +'([^']+)'.*/\1/i")

    case $data_type in
        varchar*|text)  kotlin_type="String" ;;
        int*)           kotlin_type="Int" ;;
        decimal*)       kotlin_type="BigDecimal" ;;
        datetime*|timestamp*) kotlin_type="LocalDateTime" ;;
        date)           kotlin_type="LocalDate" ;;
        tinyint*)       kotlin_type="Boolean" ;;
        *)              kotlin_type="String" ;;
    esac
    if [[ "$not_null" != "not null" ]]; then
        kotlin_type="$kotlin_type?"
    fi

    echo "@Column(name = \"$column_name\")"
    printf "val $field_name: $kotlin_type, "
    if [[ -n "$comment" ]]; then
        printf " // $comment $is_not_null"
    fi
    echo
done

위의 셸 스크립트 파일 convert-jpa는 create table SQL문을 읽어서 Kotlin 문법의 JPA Entity property로 변환해 줍니다.

다음은 예시를 위해 작성해 본 가상의 create SQL 입니다. 셸 스크립트가 잘 작동하는지 실험해 보겠습니다.

CREATE TABLE products (
    id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID',
    product_code VARCHAR(50) UNIQUE NOT NULL COMMENT '상품 관리 코드',
    product_name VARCHAR(200) NOT NULL COMMENT '상품명',
    category_code VARCHAR(20) NOT NULL COMMENT '카테고리 코드',
    brand_name VARCHAR(100) COMMENT '브랜드명',
    stock_quantity INT NOT NULL DEFAULT 0 COMMENT '재고수량',
    status VARCHAR(20) DEFAULT 'HIDDEN' COMMENT '상품상태',
    description TEXT COMMENT '상품 설명',
    created_datetime DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '입력일시',
    updated_datetime DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '수정일시',
    created_by VARCHAR(50) NOT NULL COMMENT '등록자 ID',
    updated_by VARCHAR(50) COMMENT '수정자 ID',
    is_tax_free TINYINT(1) DEFAULT 0 COMMENT '면세여부: 1=면세, 0=과세',
    sale_start_date DATETIME COMMENT '판매시작일',
    sale_end_date DATETIME COMMENT '판매종료일',
) COMMENT '예제용 상품 테이블';

다음과 같이 기초를 만들어 놓고…

스크립트를 사용하기 위해 만든 테스트 엔티티
스크립트를 사용하기 위해 만든 테스트 엔티티

작성해 둔 create table 문을 붙여 넣은 다음, 준비해 둔 셸 스크립트를 실행합니다.

:! convert-jpa

convert-jpa를 실행하기 위해 SQL을 붙여넣은 장면
convert-jpa를 실행하기 위해 SQL을 붙여넣은 장면

결과가 나왔습니다!

convert-jpa 실행 결과
convert-jpa 실행 결과

import를 수동으로 해주기만 하면 충분히 사용할 수 있을 것 같습니다.

하는 김에 @Column과 LocalDateTime을 import 해주었습니다.

import까지 마친 모습
import까지 마친 모습

물론 convert-jpa는 단순한 셸 스크립트이기 때문에 IntelliJ Ideavim 바깥에서도 쓸 수 있습니다.

create SQL을 저장해둔 파일의 출력을 convert-jpa로 단순하게 연결해 주기만 해도 같은 결과를 얻을 수 있습니다.

convert-jpa를 bash에서 사용하는 모습
convert-jpa를 bash에서 사용하는 모습

이제 이 기능을 IntelliJ 안에서 간단하게 실행할 수 있도록 vmap을 지정해 주도록 하겠습니다.

다음 라인을 ~/.ideavimrc에 추가하고 IntelliJ에서 :source ~/.ideavimrc를 하거나 IntelliJ를 재실행하면 됩니다.

vmap sj :!convert-jpa<CR>

이렇게 설정하면 create table 문을 JPA Entity로 변환하고 싶을 때 create 문을 복사해 넣고 영역을 선택한 다음 sj를 순서대로 누르면 실행됩니다.

마치며

Ideavim 사용자라면 만들어 보세요

Ideavim을 사용해서 여러분의 셸 스크립트도 IntelliJ 플러그인처럼 동작하게 만들어보는 것을 권하고 싶습니다.

앞에서도 이야기했지만 IntelliJ 플러그인을 만드는 것은 보람 있지만 번거로운 일입니다.

프로젝트를 만들고, 플러그인 형식에 맞게 설정 파일을 구성하고, IntelliJ API 스펙을 읽어야 하고, IntelliJ 버전 상향에도 대응해야 하는 등 타이핑해야 할 것도 많고 신경 쓸 것도 많습니다.

하지만 UNIX like 환경에서 대체로 잘 작동하는 간단한 셸 스크립트라면 만드는 데에도 그리 오래 걸리지 않기 때문에 필요한 기능이 생겼을 때 30분 정도만 투자해도 딱 일주일 동안만 사용이 필요한 기능을 뚝딱 만들어낼 수 있습니다.

Ideavim에서 :!를 지원하기 때문에 이제는 IntelliJ와 통합된 기능인 것처럼 사용하는 것도 가능해졌으니 자신만의 셸 스크립트가 있다면 연결해서 사용해 보는 것을 권해보고 싶습니다.

만들어둔 셸 스크립트가 없더라도 셸 명령 사용에 익숙하지 않았던 분이라면 새로 시작해 보는 것도 나쁘지 않을 것 같습니다.

아… 그리고 마지막 한 가지. 이 방법은 순수한 셸 스크립트이기 때문에 ChatGPT 같은 도구를 사용할 수 없는 망 분리 환경에서도 사용이 가능합니다.

johngrib.vim
johngrib.vim

카카오페이손해보험 고객서버개발팀 백엔드 개발자 존그립입니다. 독서와 탭볼, 터미널을 좋아합니다.