ハイパーマッスルエンジニア

Vim、ShellScriptについてよく書く

カーソル下の関数だけ実行できるVimスクリプトを作った

f:id:rasukarusan:20210620091652p:plain
 
vimでshellスクリプトを書いてるとき、ある関数だけ実行したい、ってのがよくある。そのとき毎回コメントアウトしていたのが面倒であった。例えば下記のような場合。

main() {
    echo 'main'
}
main2() {
    echo 'main2'
}

main
# main2 # mainだけ実行したいのでmain2はコメントアウト

逆にmain2を実行したいときは、mainをコメントアウトしてmain2のコメントアウトを外して実行、みたいな流れをしていた。
実行にはvim-quickrunを使用しているので\rで実行できて楽だが、いかんせんコメントアウトが面倒だった。

カーソル下、もしくは関数のブロック内でCtrl-Jを押したら実行されるようにしたい

vim-rest-consoleを触っていて、Ctrl-Jで実行できる操作感の良さを知った。こんな感じでshellスクリプトを書いているときも実行したい。

それで作ったのがこれ。

カーソル下または関数ブロック内でCtrl-jを押すと実行される

全体のソース

完全に自分仕様になっている。shellスクリプト限定だし、関数の宣言はxxx() {のように書かないといけないなど、自分以外は使えないかもしれん。

" =============================================
" カーソル下の関数を実行
" 関数ブロック内で実行も可能
" =============================================
nnoremap <silent><nowait><C-j> :call <sid>exec_this_method()<CR>
function! s:exec_this_method() abort
  let allowFileType = ['sh', 'bash', 'zsh']
  if match(allowFileType, &ft) == -1 | return | endif

  let targetScript = expand('%:p')
  " スクリプト内の関数を全て取得
  let methods = split(system('grep -P "\(\) {" ' . targetScript . ' | tr -d "() {"'), '\n')

  " 引数ありの場合に対応するため、<cword>ではなく現在行を取得して対象の関数を抽出する
  let currentLine = split(getline('.'), ' ')
  let targetMethod = len(currentLine) > 0 ? currentLine[0] : '' 

  " カーソル下の文字列が関数であるかを判定
  let index = match(methods, targetMethod)
  if index == -1 " 関数ではない場合(関数ブロック内で実行した場合)
    let line = line('.')
    while line > 0
      let line -= 1
      let matchLine = matchstr(getline(line), '.*() {')
      if matchLine != ''
        let targetMethod = substitute(matchLine, '() {', '', 'g')
        let index = match(methods, targetMethod)
        break
      endif
    endwhile
  endif
  " 探索しても見つからなかった場合、終了
  if targetMethod == ''
    echo 'target not found'
    return
  endif

  " 対象メソッドの実行のみを残したスクリプト文字列を生成
  call remove(methods, index)
  " 対象メソッド以外を除外するためのsed文を作成 ex) sed -e '/^main$/d' -e '/^main /d'
  let sed = 'sed'
  for method in methods
    let sed = sed . ' -e "/^' . method . '$/d"' . ' -e "/^' . method . ' /d"'
  endfor

  " 生成した文字列をスクリプトとして実行できるよう一時ファイルに保存
  let tempfile = tempname()
  call system(sed . ' ' . targetScript . ' > ' . tempfile)

  " 一時ファイルを実行
  execute ':QuickRun ' . &ft . ' -srcfile ' . tempfile
  echo 'exec ' . targetMethod
  call system('rm ' . tempfile)
endfunction

quickrunに実行と結果出力は任せる

quickrunは下記のようにファイルを指定してあげると、実行して結果を現在のバッファ内に出してくれる。

:QuickRun sh -srcfile test.sh

また、ファイルではなくとも文字列を渡すことで実行も可能。

:QuickRun sh -src "echo 'hoge'"

ヘルプが日本語で書かれているのでめちゃくちゃ理解しやすい。

ちなみに自分のquickrunの設定は下記。

let g:quickrun_config={
  \'_': {
  \  'split': ''
  \},
\}
set splitbelow
" \rで保存して実行、画面分割を下に出す
nnoremap \r :cclose<CR>:write<CR>:QuickRun -mode n<CR>
xnoremap \r :<C-U>cclose<CR>:write<CR>gv:QuickRun -mode v<CR>

今回作ったVimスクリプトは、「実行対象のメソッドを抽出し、quickrunが実行できる形にする」をしているだけ。

終わり

実行対象のメソッドを抽出するとき愚直にgrepでやってるけど他にいい方法がないか模索中。