shiimaxx's blog

最も愛を大切に

コマンドの終了ステータスを操作するツールを作った - altercode

https://github.com/shiimaxx/altercode

コマンドの実行結果が正常(終了ステータスが0)の場合でも、特定の条件の場合は任意の終了ステータスにしたいというときに使えます。
複雑な条件でなければワンライナーシェルスクリプトでもできるとは思いますが、練習がてらGoで書きました。

とある環境のCIパイプラインで、WARNINGなどの警告は出ているがコマンドの終了ステータスが0であるためCIをパスしてしまっているものを強制的に失敗させたいという目的があって作りました。

使い方は次のとおりです。この場合、commadの標準出力に「warning」が含まれると終了ステータスが3になります。

altercode -contain warning -exit-code 3 -- command

現状はコマンドの標準出力に特定の文字列が含まれているかをチェックすることができます。
また、複数の条件を指定したい場合は設定ファイルを利用します。詳細はREADMEをご覧ください。

実装について

オプションのパース

引数に指定するコマンドはオプションを含むこともあると思いますので、--によってオプション部分の終了を明示できるようにしています。 --でオプションの終了を示す方法は、Bashのビルトインコマンドでも使われています。

man bash に以下の記載があります。

-- A -- signals the end of options and disables further option processing. Any arguments after the -- are treated as file- names and arguments. An argument of - is equivalent to --.

Goの標準パッケージでCLIツールのオプションのパースを行うflagでは、--をオプションの終了として解釈するため特別な実装は必要ありません。
FlagSet.Parse()でパースすると--より前をオプションとしてパースしてくれます。

flagパッケージのGoDocに以下の記載があります。

Flag parsing stops just before the first non-flag argument ("-" is a non-flag argument) or after the terminator "--".

引数に指定したコマンドの終了ステータスの取得

引数に指定したコマンドがエラーになり、終了ステータスが0以外の場合はそのステータスコードで終了します(コマンドの実行自体ができない場合は別です)。
そのため、コマンドの終了ステータスを取得する必要がありました。

実際のコードは次のようになっています。

    out, err := cmd.Output()
    if err != nil {
       if exitErr, ok := err.(*exec.ExitError); ok {
          fmt.Fprintf(c.errStream, string(exitErr.Stderr))
          if ws, ok := exitErr.ProcessState.Sys().(syscall.WaitStatus); ok {
             return ws.ExitStatus()
          }
          return ExitCodeError
       }
       :

execパッケージのCmd.Output()でコマンドを実行しています。
Cmd.Output()は内部的にCmd.Run()を実行しており、これはコマンドの実行に問題があった場合に*exec.ExitError型のエラーを返します。

コマンドの実行結果に関する情報は、*exec.ExitError型に埋め込まれている*os.ProcessStateSys()メソッドで取得します。その後、取得したものをsyscall.WaitStats型にキャストし、ExitStatus()メソッドを呼び出すことで終了ステータスを取得できます。

まとめ

ちょっとしたツールの紹介でした。
2019年は「コードを書く → アウトプットする」というサイクルをもっと増やそうと思っていまして、それの第1弾でした。

参考文献