Carpe Diem

備忘録

GoのCLIで標準入力とファイル読み込みの両方に対応する

概要

Goでは簡単にコマンドラインツールが作れますが、人によって引数やオプションといったインタフェースがバラバラになりがちです。

POSIX Utility Syntax Guidelinesというガイドラインがあるので、これに則るとUnixライクな統一されたインタフェースのCLIツールになります。

環境

インタフェースのイメージ

例えば文字列の入ったファイルがあり

$ cat sample.txt
hogefuga

入力文字列をすべて大文字にするコマンドcapitalizeを作る場合、以下の3つのインタフェースをサポートするイメージです。

$ cat sample.txt | capitalize
HOGEFUGA

$ cat sample.txt | capitalize -
HOGEFUGA

$ capitalize sample.txt
HOGEFUGA

具体的なコード

では具体的にどういった処理をするかをコードで説明します。

var filename string
if args := flag.Args(); len(args) > 0 {
        filename = args[0]
}

var r io.Reader
switch filename {
case "":
        if terminal.IsTerminal(int(syscall.Stdin)) {
                return nil, errors.New("usage: capitalize path")
        }
        r = os.Stdin
case "-":
        r = os.Stdin
default:
        f, err := os.Open(filename)
        if err != nil {
                return nil, err
        }
        defer f.Close()
        r = f
}
b, err := ioutil.ReadAll(r)
if err != nil {
        return nil, err
}
return b, nil

ロジックとポイント

ロジックは以下です。

  • flag.Args()コマンドライン引数をチェックする
  • 空の場合、terminal.IsTerminal()で入力漏れかパイプで渡しているかをチェック
    • パイプの場合は標準入力(os.Stdin)を使う
    • 入力漏れの場合はエラー扱い
  • -の場合、ガイドラインに則って標準入力(ただし標準出力として扱うケースもあります)
  • コマンドライン引数が存在する場合はそれを使う

ポイントとしては以下です。

コード全体

コード全体は以下のリポジトリにあります。

github.com

標準入力に対応してると何が嬉しい?

標準入力や標準出力に対応させているとどんなメリットがあるでしょうか。

Unix系コマンドでパイプを使って色んな組み合わせができる

Unix系コマンドは非常に洗練されたツールばかりです。
わざわざプログラムを書かずともUnix系コマンドを利用するだけで要件を満たせることも多々あります。

そんなUnix系コマンドと連携できるのは大きなメリットです。

$ cat sample.txt
aaa
bbb
ccc
ddd
eee
$ cat sample.txt | ./capitalize | grep -e "CC"
CCC

Macだとpbcopy/pbpasteと連携できる

標準出力対応していればpbcopyを使ってクリップボードに保存できます。

$ echo "abcde" | ./capitalize | pbcopy

逆にpbpasteを使えばクリップボードにあるデータを標準入力として使えるので、わざわざファイルとして用意せずともそのままコマンドに流せます。

$ pbpaste | ./capitalize
ABCDE

まとめ

CLIツールのインタフェースデザインの1つとして、標準入力とファイル読み込みに対応したケースを紹介しました。

参考