社内イベント: エディタについて語る会で Vim script と ISO8583 の話をしました

エンジニアの佐野です。最近記事を書いていなかったので小ネタです。先日、菅原企画の社内イベント、エディタについて語る会が催されました。職種にもよりますがカンムでは多くの従業員はオンラインで業務を行っています。たまにはオフラインで交流も...ということで来れる人はオフィスに集まってエディタの話をしつつ軽食を楽しむというコンセプトです。

当日は Vim, Emacs, Visual Studio Code, nano... と様々なエディタのゆるい話から熱い話が語られました。私は VimVim script について話したので今日はそれを記事化します。


0. 私とエディタ

私は長らく Vim をエディタとして使っています。「エディタ」というものを意識したのは大学生の頃でしょうか。機械工学系だったのですがソフトウェア工学や C や C++ がカリキュラムにあり自分もそれらを履修しました。それらの演習では Microsoft Visual Studio (Visual C++だったかな...?)やメモ帳を使っていました。それにしてもなぜメモ帳だったのか...。

大学入学は2000年でした。ちょうどその頃に一般家庭にもインターネットが普及し始めました。IT革命という言葉が世間に踊った時期だった気がします。住んでいたアパートにもインターネット回線を導入して私はインターネットにどっぷりハマっていきました。インターネットに触れることでコンピュータに興味が出てきた私は Perl を独学したり PC を自作してサーバ構築の真似事のようなことを始めました。コードを書くときに前述の Visual Studio を使いたかったのですが有償(だったと思う)で手が出しにくい、メモ帳では機能が微妙すぎる、何かないだろうか?と本屋を物色していたら Vi (Vim), Emacs あたりの本を見つけました。どうやら Vi (Vim) というのがシンプルで良いらしいということで Vi を使い始めました。

使い始めた当初は「モードって何?」「なんでバックスペースで削除できないんだ?」という状態でした。普通の人からするとそんなものだと思います。設定ファイルの存在も知らず、毎回起動しては :set nu (行番号を表示する)を打っていました。

サラリーマンになって Java を書いていたときは Eclipse を使っていましたが、それ以外のプログラミング、サーバでのオペレーション、ファイル編集などはずっと Vim でした。これはほとんどのマシンに標準で入っていたというのと、大学からの流れで手に馴染んでいる、というのが理由でしょうか。

その後、新しいエディタやIDEが出る度に乗り換えを試みますが、結局は Vim に戻ってきました。私はロールとして長らくサーバサイドのアプリケーションエンジニアとインフラエンジニアをやっています。サーバに ssh してマシン上で作業したり手元でコードを書いたり...業務中は端末を操作している時間が長いため、多くの作業をターミナルで完結させたくなります。他のエディタを起動して時にターミナルに切り替えたり...というのがちょっとしたストレスになり、結局はVim に(ターミナルに)戻ってきてしまいます。

最近では GoLand や VSCode への乗り換えを試みましたがやはりダメでした。

1. Vim の設定

思い出話が長くなりました。学生の頃から使っていた Vim ですが、初期のころはいろいろなプラグインを入れたり設定をこねくり回したりして盆栽のように設定を育てていました。今はプラグインはだいぶ減って以下のもののみになっています。

これらのプラグインとともにいくつかの設定を施しています。 Vim を知らない人向けに少し書くと、Vim の設定ファイルは .vimrc で基本的な配置位置は $HOME/.vimrc になります。Vim のカスタマイズはこちらのファイルを編集することで行います。私の設定は以下のようになっています。

"=========================================================
" Basic Configuration
"=========================================================

set notitle
set nocompatible        "vi互換をoff
set nobackup            "バックアップファイルを作らない
set noswapfile          "スワップファイルを作らない
set number              "行番号を表示
set laststatus=2        "ステータスを常に表示
set showmode            "モードを表示する
set showcmd             "コマンドを表示
set noshowmatch
set display=uhex        "謎の文字を16進数で表示
set wildmenu            "補間候補を表示する
set wrap                "自動折返しを有効
set expandtab           "タブをスペースに変換
set tabstop=2           "タブをスペース4つ分として表示する
set shiftwidth=4        "シフトで移動する文字幅
set softtabstop=2       "タブキーを押したときに挿入する半角スペースの数
set noincsearch         "インクリメンタルサーチはしない
set wrapscan            "最後まで検索したら先頭へ戻る
set ignorecase          "大文字小文字無視
set smartcase           "検索文字列に大文字が含まれている場合は区別して検索する
set hlsearch            "検索文字をハイライト
set splitbelow          "新しいウィンドウを下に開く
set splitright          "新しいウィンドウを右に開く
set nocursorline        "カーソルのある行をハイライトしない
set nocursorcolumn      "カーソルのある列をハイライトしない
augroup Cursor
  autocmd WinLeave * setlocal nocursorline "カレントウィンドウから離れたらカーソルハイライトを消す
  highlight ZenkakuSpace cterm=underline ctermfg=lightblue guibg=#666666 "全角スペースを見えるようにする
  autocmd BufNewFile,BufRead * match ZenkakuSpace / /
augroup END

filetype plugin indent on

"=========================================================
" Private
"=========================================================

let mapleader = "\<Space>"
nnoremap <Leader>r :reg<CR>

"=========================================================
" Encode
"=========================================================
"表示するときの文字コード(ターミナルの設定と同じ)
set encoding=utf-8
"保存するときの文字コード
set fileencoding=utf-8
"文字コード自動判別の候補とする文字コード種を列挙
set fileencodings=iso-2022-jp,euc-jp,cp932,utf-8

"=========================================================
" Plugin management
"=========================================================

" プラグインインストールディレクトリ
let s:dein_dir = expand('~/.cache/dein')
let s:dein_repo_dir = s:dein_dir . '/repos/github.com/Shougo/dein.vim'

" dein.vim がなければ取得
if &runtimepath !~# '/dein.vim'
  if !isdirectory(s:dein_repo_dir)
    execute '!git clone https://github.com/Shougo/dein.vim' s:dein_repo_dir
  endif
  execute 'set runtimepath^=' . fnamemodify(s:dein_repo_dir, ':p')
endif

if dein#load_state(s:dein_dir)
  call dein#begin(s:dein_dir)

  let g:rc_dir    = expand('~/.vim/rc')
  let s:toml      = g:rc_dir . '/dein.toml'
  let s:lazy_toml = g:rc_dir . '/dein_lazy.toml'

  call dein#load_toml(s:toml,      {'lazy': 0})
  call dein#load_toml(s:lazy_toml, {'lazy': 1})

  call dein#end()
  call dein#save_state()
endif

if dein#check_install()
  call dein#install()
endif

"=========================================================
" Plugin configuration
"=========================================================
" gitgutter
set updatetime=250
let g:gitgutter_max_signs = 500

" goimports
let g:goimports = 1

" vim-quickhl
nmap <Space>m <Plug>(quickhl-manual-this)
xmap <Space>m <Plug>(quickhl-manual-this)
nmap <Space>M <Plug>(quickhl-manual-reset)
xmap <Space>M <Plug>(quickhl-manual-reset)

"=========================================================
" Color
"=========================================================
colorscheme cyberspace
set background=light

" 256色
set t_Co=256
" 背景色
set signcolumn=yes
hi SignColumn ctermbg=black
hi SignColumn guibg=black

"=========================================================
" Popup color
"=========================================================
hi NormalFloat guifg=#ffffff guibg=#191970
hi Pmenu guifg=#ffffff guibg=#191970

"=========================================================
" status line color
"=========================================================
set noshowmode
let g:lightline = { 'colorscheme': 'wombat' }

syntax enable

前半部分に set xxxx という設定が羅列されていますが、プラグインマネージャ dein の箇所には変数 ( let ... )や関数呼び出し ( call ... )、外部コマンドの呼び出し (execute '!git ...)、条件分岐 ( if ... ) が表れたりします。 set xxxx も含めこれらは Vim script と呼ばれます。つまり Vim の設定ファイルやプラグインの正体は Vim script の塊です。

ちなみに Vim script はオブジェクト指向プログラミングもサポートしています。 Vim script は単なる設定ではなくれっきとしたプログラミング言語です。

2. Vim script を書く

ではその Vim script を書いてみます。Vim の設定というよりは Hello, world のような簡単なプログラムを書いてみます。

Vim script の始め方は簡単で、 Vim を起動したらコマンドラインモードでそのまま Vim script が書けます。次の例ではテキストの2行目から4行目を取得して echo で結果を出力しています。結果はそのままステータスラインに表示されます。

map() などの便利な組み込み関数もあります。似たようなコードですが、2行目から4行目の数値をリストで取得して出力、続いてそれを map 関数を通して倍にして出力しています。

一連の処理は .vimrc に関数として定義することで call FuncName() で利用できます。次の関数は数値のリストを受け取ってそれを加算するものです。

3. ISO8583

ここまでで Vim script で普通のプログラミングができることを示しました。さて、カンムと言えば ISO8583 らしいのですが...先日の GoCon オフィスアワーで ISO8583 を Go で Parse してみましょうという出題がありました。これを Vim script で倒してみます。

ISO8583 自体の説明や GoCon の問題の説明や解説は下記のエントリを参照していただけると幸いです。が、少しだけ解説します。ISO8583というのはクレジットカードのデータ通信時に使われるプロトコルで、店頭でカードを切ったときにこのプロトコルにのっとってデータが飛んできます。カード会社はこのプロトコルを捌く必要があり、カンムでもこれを処理するサーバとアプリケーションが元気に稼働しています。これは Vim script ではなくは Go で書かれています。

tech.kanmu.co.jp

3.1 問題のファイルを読む

問題を解くにあたりまずやるべきことは問題のバイナリデータを読むことです。Vim script でもファイルシステムからファイルを読むことができます。次のようにファイルをバイナリモードで読み込んで1バイトずつ処理できます。

let inputfile = "/path/to/github.com/kanmu/gocon-2022-spring/message.bin"
for b in readfile(inputfile, 'B')
  " ISO8583 Processing
endfor

3.2 結果を送信する

さらに問題を解くには結果を送信する必要があるのですが Vim script でも当然できます。ソケットを開いてそこに HTTP を書き込むことで HTTP 通信ができます

let channel = ch_open("168.138.192.92:80",
            \ {"callback": "Callback", "mode": "raw", "waittime": "1000ms"})
let body = printf("{\"input\":{\"Type\":%d,\"PrimaryAccountNumber\":%x,\"ProcessingCode\":%x,\"  AmountTransaction\":%x,\"ExpirationDate\":%x}}",
            \ str2nr(mt), pan, pc, amount, ed)
let header = printf("POST /v1/data/iso8583/validation HTTP/1.1\r\nHost: 168.138.192.92\r\nConte  nt-Type: application/json\r\nContent-Length: %d\r\n\r\n",
            \ len(body))
let payload = header . body
call ch_sendraw(channel, payload)

3.3 ビット演算をする

ISO8583 を処理するにはビット演算を活用する必要があります。こちらについては詳細は上述したとおり GoCon の問題解説のエントリを見てください。and()論理積がとれます。 こちらはビットが立っている場所を調べて、立っていたら処理を行う...というコードになります。

" PAN
if and(bitmap, 0b0100000000000000000000000000000000000000000000000000000000000000) != 0
  if i == 10
    let panbyte = b/2
    let i = i + 1
    continue
  endif
  if i <= 10+panbyte
    let shift = 8 * (panbyte - (i - 10))
    let pan =  pan + b * float2nr(pow(2, shift))
    let i = i + 1
    continue
 endif
endif
" Processing Code
if and(bitmap, 0b0010000000000000000000000000000000000000000000000000000000000000) != 0
  if i <= 10+panbyte+pcbyte
    let shift = 8 * (pcbyte - (i - (10 + panbyte)))
    let pc =  pc + b * float2nr(pow(2, shift))
    let i = i + 1
    continue
  endif
endif

3.4 ビットシフトする

バイナリを処理する傍らビットシフトしたくなるんですが Vim script でも当然それはでき....なかった! Vim script にシフト演算子はありません(たぶん...)!よってここは2のべき乗で対応します。

if i <= 10+panbyte
  let shift = 8 * (panbyte - (i - 10))
  let pan =  pan + b * float2nr(pow(2, shift))
  let i = i + 1
  continue
endif

これは何をやっているのかというと、例えばカード番号 4019-2499-9999-9999 は ISO8583 では次のように BCD で表現されています。

0x40, 0x19, 0x24, 0x99, 0x99, 0x99, 0x99, 0x99

1バイトずつ処理していくため、まず 0x40 を左に7バイトシフトして 0x4000000000000000 とする、次に 0x19 を左に6バイトシフトして 0x19000000000000 とする... を行い、

 0x4000000000000000
   0x19000000000000
     0x240000000000
     ...
+
------------------------
4019249999999999

といった形でカード番号を取り出しています。

4. ISO8583() 関数を書く

重要な処理はだいたいここまでです。あとは一連の流れを組み立てていきます。結果、ISO8583 を処理する関数を書くと次のようになります。

function! ISO8583()
  let inputfile = "/path/to/github.com/kanmu/gocon-2022-spring/message.bin"

  let mt = ""
  let bitmap = ""
  let bitmapbyte = 8

  let panbyte = 0
  let pan = ""

  let pc = ""
  let pcbyte = 3

  let amount = ""
  let amountbyte = 6

  let ed = ""
  let edbyte = 2

  let i = 0
  for b in readfile(inputfile, 'B')
    " message type
    if i <= 1
      let mt = mt . printf("%02s", b)
    endif

    " bitmap
    if i >= 2 && i <= 9
      let shift = 8 * (bitmapbyte - (i - 1))
      let bitmap =  bitmap + b * float2nr(pow(2, shift))
    endif

    " data element
    if i >= 10
      " PAN
      if and(bitmap, 0b0100000000000000000000000000000000000000000000000000000000000000) != 0
        if i == 10
          let panbyte = b/2
          let i = i + 1
          continue
        endif
        if i <= 10+panbyte
          let shift = 8 * (panbyte - (i - 10))
          let pan =  pan + b * float2nr(pow(2, shift))
          let i = i + 1
          continue
        endif
      endif
      " Processing Code
      if and(bitmap, 0b0010000000000000000000000000000000000000000000000000000000000000) != 0
        if i <= 10+panbyte+pcbyte
          let shift = 8 * (pcbyte - (i - (10 + panbyte)))
          let pc =  pc + b * float2nr(pow(2, shift))
          let i = i + 1
          continue
        endif
      endif
      " Amount
      if and(bitmap, 0b0001000000000000000000000000000000000000000000000000000000000000) != 0
        if i <= 10+panbyte+pcbyte+amountbyte
          let shift = 8 * (amountbyte - (i - (10 + panbyte + pcbyte)))
          let amount =  amount + b * float2nr(pow(2, shift))
          let i = i + 1
          continue
        endif
      endif
      " Expiration date
      if and(bitmap, 0b0000000000000100000000000000000000000000000000000000000000000000) != 0
        if i <= 10+panbyte+pcbyte+amountbyte+edbyte
          let shift = 8 * (edbyte - (i - (10 + panbyte + pcbyte + amountbyte)))
          let ed =  ed + b * float2nr(pow(2, shift))
          let i = i + 1
          continue
        endif
      endif
    endif
    let i = i + 1
  endfor

  echo "--------------------"
  echo printf("Message Type: %s", mt)
  echo printf("Bitmap: %b", bitmap)
  echo printf("PAN: %x", pan)
  echo printf("ProcessingCode: %x", pc)
  echo printf("Amount Transaction: %x", amount)
  echo printf("Expiration Date: %x", ed)
  echo "--------------------"

  let channel = ch_open("168.138.192.92:80",
              \ {"callback": "Callback", "mode": "raw", "waittime": "1000ms"})
  let body = printf("{\"input\":{\"Type\":%d,\"PrimaryAccountNumber\":%x,\"ProcessingCode\":%x,\"AmountTransaction\":%x,\"ExpirationDate\":%x}}",
              \ str2nr(mt), pan, pc, amount, ed)
  let header = printf("POST /v1/data/iso8583/validation HTTP/1.1\r\nHost: 168.138.192.92\r\nContent-Type: application/json\r\nContent-Length: %d\r\n\r\n",
              \ len(body))
  let payload = header . body
  call ch_sendraw(channel, payload)
  echo payload
endfunction

function! Callback(handle, msg)
  echo a:msg
endfunction

ここで :call ISO8583() を呼び出してみます。クリアできました。

最後に

あまりドスの効いていない社内イベントの紹介記事になってしまいましたが...。Vim やカンムの社内イベントに興味を持っていただけたら幸いです。