Actionlintで出力されたShellcheckのエラーの見にくさをEmacsでかわす

しむどん 2026-01-24

Actionlintで出力されたShellcheckのエラーは問題の位置や理由が特定しにくい。それを解消するために、YAMLの一部を指定してShellcheckを実行するEmacs Lispを書いた。本稿はその備忘録である。

Actionlintが出力するShellcheckの結果の問題箇所の特定しにくさ

Actionlintを使ってGitHub ActionsのYAMLをチェックすると、さまざまなチェックが行われるがShellcheckが入っていれば、Shellcheckも実行される。YAMLの書き方にもよるが、このActionlint経由のShellcheckのエラーはどこがエラーしているのか分かりにくいことがある。

たとえば次のようなYAMLを書いたとする。

on:
  workflow_call: {}

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Run build
        run: |
          if [ $(date +"%m-%d") = "01-01" ]
          then
            echo $MESSAGE_NEW_YEAR
          else
            echo $MESSAGE_NORMAL
          fi
        env:
          MESSAGE_NEW_YEAR: "Happy new year"
          MESSAGE_NORMAL: "Hello"

test_bad.yml

このYAMLファイルをActionlintでチェックしてみよう。

actionlint test_bad.yml
test_bad.yml:9:9: shellcheck reported issue in this script: SC2046:warning:1:6: Quote this to prevent word splitting [shellcheck]
  |
9 |         run: |
  |         ^~~~
test_bad.yml:9:9: shellcheck reported issue in this script: SC2086:info:3:8: Double quote to prevent globbing and word splitting [shellcheck]
  |
9 |         run: |
  |         ^~~~
test_bad.yml:9:9: shellcheck reported issue in this script: SC2086:info:5:8: Double quote to prevent globbing and word splitting [shellcheck]
  |
9 |         run: |
  |         ^~~~

ActionlintはShellcheckで問題があると判定した箇所の内容を出力している。エラーメッセージにはYAMLの中のどのrunに問題があるか、そのrunの先頭から数えて何行目に問題があるのかは出力してくれている。しかし本来Shellcheckが本来出力してくれている詳細な説明は省略されてしまっており、またrunの先頭から数えて何行目かという表示も、ファイル全体だと何行目か分からないため、問題箇所が特定しづらい。

このエラーの分かりにくさについて思う事

こういう分かりにくさがあると、GitHub Actionsの修正も億劫になってしまうし、そういうものが積み重なる事でリンターのようなツールを無視しがちになってしまう事もあるだろう。

ただ落ち着いて考えると、ActionlintがSchellcheckをラップして動作してしまっているので、分かりにくくなっているだけであり、Schellcheckを実行できれば分かりやすくて特定しやすいメッセージを見られるはずだ。問題なのはSchellcheckにYAMLをそのまま渡す事はできない事だ。この程度の問題はすぐに解決できそうだった。

YAMLの一部分だけShellcheckする処理を実装する

Emacsには shell-command-on-region という関数がある。この関数は指定したコマンドをbash等のシェル経由で実行するが、その時にリージョンで選択している領域をコマンド実行しているプロセスの標準入力として渡す。 shellcheck コマンドはチェックするコードを標準入力から受け取る事もできるため、これを使用すればYAMLの一部分に対してチェックを行える。また行数が分からなくなる問題についても、Emacsの narrow-to-region 関数を使えば、リージョンで選択している領域以外の部分を一時的に非表示にし、表示部分を先頭行として行番号を表示させる事ができる。なにやらすぐに実装できそうだったので、Emacs Lispで処理を実装する事にした。

;;;###autoload
(defun my-shellcheck-on-region (beg end)
  (interactive "r")
  (shell-command-on-region beg end "shellcheck --exclude=SC2148 -" "*shellcheck*")
  (narrow-to-region beg end)
  (display-line-numbers-mode 1)
  (display-buffer "*shellcheck*"))

このEmacs Lispを評価し、YAML内のシェルスクリプトをリージョンで選択し、 M-x my-shellcheck-on-region でチェックを実行する。出力は次のようになる。

In - line 1:
          if [ $(date +"%m-%d") = "01-01" ]
               ^--------------^ SC2046 (warning): Quote this to prevent word splitting.


In - line 3:
            echo $MESSAGE_NEW_YEAR
                 ^---------------^ SC2086 (info): Double quote to prevent globbing and word splitting.

Did you mean:
            echo "$MESSAGE_NEW_YEAR"


In - line 5:
            echo $MESSAGE_NORMAL
                 ^-------------^ SC2086 (info): Double quote to prevent globbing and word splitting.

Did you mean:
            echo "$MESSAGE_NORMAL"

For more information:
  https://www.shellcheck.net/wiki/SC2046 -- Quote this to prevent word splitt...
  https://www.shellcheck.net/wiki/SC2086 -- Double quote to prevent globbing ...

実行するとYAMLはナローイング(一時的にファイルの一部を非表示にする)され、シェルスクリプトの部分だけが表示される。 (display-line-numbers-mode 1) を指定しているので、強制的に行番号表示が有効になり、表示されているYAML内のシェルスクリプトの先頭行は1として表示されるため、Shellcheckの出力とも行番号の整合性が取れる。修正が終われば M-x widen を実行し、ナローイングを解除する事でYAML全体が表示される。

感想

ActionlintでのShellcheckの出力の分かりにくさを回避できたと思う。本質的な改善ではないけれど、こういう柔軟な対応で、簡素な改善ができる所がEmacsの良さだなとあらためて感じた。