M.Hiroi's Home Page

お気楽 Go 言語プログラミング入門

応用編 : コマンドラインフラグの解析


Copyright (C) 2022 Makoto Hiroi
All rights reserved.

はじめに

近年の情報機器 (パソコンや携帯など) は GUI (Graphical User Interface) が標準ですが、パソコンやサーバ向けの OS には CUI (Character User Interface または Comand Line Interface, CLI) 環境も標準で装備されています。GUI アプリケーションの作成は、ツールキットやフレームワークなどを使うことで、以前に比べれば容易になっているのですが、それでも初心者には敷居が高いと思います。

その点、小さな CLI ツールであれば初心者でも比較的簡単に作成することができます。Go 言語は CLI ツールの作成に適しているので、気軽にチャレンジしてみてください。今回は CLI ツールを作成するときに便利なコマンドラインフラグの解析について簡単に説明します。

●コマンドの形式

GUI でアプリケーションを実行する場合、たとえば Windows ではアイコンをダブルクリックすればいいですね。ところが CUI の場合、ユーザーはコンピュータに対する命令 (コマンド) をキーボードから入力します。この命令を処理するプログラムを「シェル (shell)」といいます。

シェルはユーザーの命令を OS に伝える橋渡しの役割を持っています。シェルには殻、亀の甲羅、外観、というような意味がありますが、OS 全体を包み込んでいてユーザーから直接見えるプログラムであることから、名づけられたのでしょう。Unix 系 OS の場合、いろいろなシェルがありますが、Linux では標準のシェルとして bash が搭載されています。Windows ではコマンドプロンプトや Power Shell を使用するのが一般的です。

コマンドは次のような形式で入力します。

コマンド名 引数1 引数2 ... 

最初はコマンド名で、その後ろに引数が必要になる場合もあります。コマンド名や引数の間は空白で区切ります。そして最後にリターンキーを入力すると、シェルがこのコマンドを実行します。

大抵のコマンドにはコマンドラインフラグ (またはコマンドラインオプション) が用意されていて、それを引数に渡すことができます。単にフラグとかオプションと呼ばれることもあります。フラグを指定することでコマンドの動作を指定したり変更することができます。

Windows では '/' の後ろのアルファベット 1 文字で表しますが、Unix 系 OS では '-' の後ろのアルファベット 1 文字で表すか、'--' の後ろの文字列で表すことが多いです。このほかにも引数でファイル名やディレクトリ名、あるいは特別なオプションを指定する場合もあります。これはコマンドによって異なります。

●フラグの解析

CLI ツールの作成で、ちょっと面倒な処理がコマンドラインフラグの解析です。Go 言語の場合、標準ライブラリ flag を使ってフラグの解析を簡単に行うことができます。flag で解析できるフラグの構文を以下に示します。

  1. -flagName
  2. -flagName=value
  3. -flagName value

flag は ('-' or '--') + 文字列 をフラグとして認識します。そして、そのあとにフラグの値 value を指定することができます。そして、フラグとして認識できない引数または終了記号 '--' に出会うと、フラグの解析処理を終了します。

value は文字列だけではなく、真偽値、数値 (整数と浮動小数点数)、日付などいろいろな値を指定することができます。なお、1 の形式で指定できるのは真偽値だけです。また、真偽値は 3 の形式で指定することはできません。1 または 2 の形式を使ってください。

簡単な例として、真偽値、整数、文字列を受け取るフラグを定義してみましょう。これらのフラグは次の関数を使って定義します。

func Bool(name string, value bool, usage string) *bool
func Int(name string, value int, usage string) *int
func String(name string, value string, usage string) *string

第 1 引数 name がフラグ名、第 2 引数 value がフラグのデフォルト値、第 3 引数 usage がフラグの説明文で、自動生成されるヘルプで使用されます。ヘルプはフラグ -h で表示することができます。返り値はフラグの値を格納した変数へのポインタです。

フラグの設定が終わったら関数 Parse でフラグを解析します。

func Parse()

解析されたフラグの値は変数に格納されます。フラグが指定されていない場合、変数はデフォルト値で初期化されます。フラグを取り除いたコマンドライン引数は関数 Args で求めることができます。

func Args() []string
func Arg(i int) string
func NArg() int
func NFlag() int

このほかに、i 番目のコマンドライン引数を返す関数 Arg、コマンドライン引数の個数を返す関数 NArg、設定されているフラグの個数を返す関数 NFlag もあります。

簡単なプログラムと実行例を示します。

リスト : flag の簡単な使用例 (test01.go)

package main
import (
    "fmt"
    "flag"
)

func main() {
    a := flag.Bool("a", false, "bool flag")
    b := flag.Int("b", 0, "int flag")
    c := flag.String("c", "oops!", "string flag")
    flag.Parse()
    fmt.Println(*a, *b, *c)
    fmt.Println(flag.Args())
}
$ go build test01.go
$ ls
test01  test01.go
$ ./test01
false 0 oops!
[]
$ ./test01 foo bar baz
false 0 oops!
[foo bar baz]
$ ./test01 -a -b 10 -c foo
true 10 foo
[]
$ ./test01 -a -b 10 -c foo bar baz
true 10 foo
[bar baz]
$ ./test01 -a=true -b=-123 -c=bar foo bar baz
true -123 bar
[foo bar baz]

bool flag は次の値を受け付けます。

1, 0, t, f, T, F, true, false, TRUE, FALSE, True, False

int flag は接頭辞に 0 (8 進数), 0x (16 進数), - 符号を受け付けます。

フラグ -h を指定するとヘルプが表示されます。フラグの解析に失敗した場合もエラーメッセージとともにヘルプが表示されます。

$ ./test01 -h
Usage of ./test01:
  -a    bool flag
  -b int
        int flag
  -c string
        string flag (default "oops!")
$ ./test01 -b 1.2345
invalid value "1.2345" for flag -b: parse error
Usage of ./test01:
  -a    bool flag
  -b int
        int flag
  -c string
        string flag (default "oops!")
$ ./test01 -abc
flag provided but not defined: -abc
Usage of ./test01:
  -a    bool flag
  -b int
        int flag
  -c string
        string flag (default "oops!")

最後の例のように -abc は一つのフラグとして認識され、それは定義されていないのでエラーとなります。たとえば、bool flag として -a -b -c が定義されていても、-abc とまとめて指定することはできないので注意してください。

ヘルプは次の関数で表示することもできます。

func PrintDefaults()

メッセージは標準エラーに出力されます。

●フラグの値を格納する変数を用意する

フラグの値を格納する変数をユーザーが用意して、フラグを定義する関数に渡す方法もあります。

func BoolVar(p *bool, name string, value bool, usage string)
func IntVar(p *int, name string, value int, usage string)
func StringVar(p *string, name string, value string, usage string)

今までの関数の後ろに Var を付けたものが関数名になります。第 1 引数が変数へのポインタで、返り値はありません。最初の例 (test01.go) を書き直すと次のようになります。

リスト : flag の簡単な使用例 (2)

package main
import (
    "fmt"
    "flag"
)

func main() {
    var a bool
    var b int
    var c string
    flag.BoolVar(&a, "a", false, "bool flag")
    flag.IntVar(&b, "b", 0, "int flag")
    flag.StringVar(&c, "c", "oops!", "string flag")
    flag.Parse()
    fmt.Println(a, b, c)
    fmt.Println(flag.Args())
}

実行例は同じなので省略します。

●ファイルの連結

それでは簡単な例題として、ファイルを連結するコマンド cat を Go 言語で作成してみましょう。プログラムを簡単にするため、サポートするフラグは -b と -n だけにします。-n は行の番号を表示し、-b は空行以外の行に番号を振ります。-n と -b を同時に指定した場合は -b を優先することにします。

cat は拙作のページ「ファイル入出力」で作成した cat.go を改造すると簡単です。プログラムは次のようになります。

リスト : ファイルの連結 (cat.go)

package main
import (
    "fmt"
    "os"
    "bufio"
    "flag"
)

// フラグ
var bflag bool
var nflag bool

// ファイルの表示
func cat(file *os.File, n int) int {
    s := bufio.NewScanner(file)
    for s.Scan() {
        l := s.Text()
        if (bflag && l != "") || nflag {
            fmt.Printf("%6d  %s\n", n, l)
            n++
        } else {
            fmt.Println(l)
        }
    }
    return n
}

func main() {
    // フラグの設定
    flag.BoolVar(&bflag, "b", false, "number nonempty output lines, overrides -n")
    flag.BoolVar(&nflag, "n", false, "number all output lines")
    flag.Parse()
    if bflag && nflag {
        nflag = false
    }
    // 行数
    n := 1
    if flag.NArg() == 0 {
        // 標準入力
        cat(os.Stdin, n)
    } else {
        for _, name := range flag.Args() {
            file, err := os.Open(name)
            if err != nil {
                fmt.Fprintln(os.Stderr, err)
                os.Exit(1)
            }
            n = cat(file, n)
            file.Close()
        }
    }
}

フラグの値は大域変数 bflag, nflag に格納します。関数 BoolVar でフラグを設定し、関数 Parse で解析します。bflag と nflag が共に真ならば、-n と -b が同時に指定されたので、nflag を false に書き換えます。行数は変数 n でカウントします。次に、関数 NArg の返り値をチェックして、それが 0 ならば引数 (ファイル名) の指定はないので、標準入力 (os.Stdin) からデータを読み込みます。

ファイル名の指定がある場合、関数 os.Open でファイルをオープンします。エラーが発生した場合はメッセージを表示して os.Exit で終了します。あとは関数 cat を呼び出してファイルを表示し、変数 n を cat の返り値で書き換えます。最後にオープンしたファイルを関数 Close でクローズします。

関数 cat は簡単です。引数 file がファイル、n が行数です。ファイルの読み込みは bufio.Scanner を使います。s.Scan() で 1 行ずつデータを読み込み、s.Text() で行を取り出して変数 l にセットしてます。bflag が真で行 l が "" ではない、または nflag が真ならば行番号 n を表示します。そうでなければ行 l をそのまま表示するだけです。

これでプログラムは完成です。それでは実際に試してみましょう。

$ go build cat.go
$ ./cat -h
Usage of ./cat:
  -b    number nonempty output lines, overrides -n
  -n    number all output lines
$ ./cat < test.txt
abcdefghijklmn
ABCDEFGHIJKLMN

abcdefghijklmn
ABCDEFGHIJKLMN
abcdefghijklmn

ABCDEFGHIJKLMN
$ ./cat -n test.txt test.txt
     1  abcdefghijklmn
     2  ABCDEFGHIJKLMN
     3
     4  abcdefghijklmn
     5  ABCDEFGHIJKLMN
     6  abcdefghijklmn
     7
     8  ABCDEFGHIJKLMN
     9  abcdefghijklmn
    10  ABCDEFGHIJKLMN
    11
    12  abcdefghijklmn
    13  ABCDEFGHIJKLMN
    14  abcdefghijklmn
    15
    16  ABCDEFGHIJKLMN
$ ./cat -b test.txt test.txt
     1  abcdefghijklmn
     2  ABCDEFGHIJKLMN

     3  abcdefghijklmn
     4  ABCDEFGHIJKLMN
     5  abcdefghijklmn

     6  ABCDEFGHIJKLMN
     7  abcdefghijklmn
     8  ABCDEFGHIJKLMN

     9  abcdefghijklmn
    10  ABCDEFGHIJKLMN
    11  abcdefghijklmn

    12  ABCDEFGHIJKLMN

正常に動作しているようです。興味のある方は他のフラグを追加するなど、プログラムをいろいろ改造してみてください。

●参考 URL

  1. flag package, (英語)
  2. パッケージ flag
  3. サンプルで学ぶ Go 言語:Command-Line Flags

初版 2022 年 1 月 9 日