M.Hiroi's Home Page

Go Language Programming

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

[ PrevPage | Golang | NextPage ]

書式付き入出力関数

標準ライブラリ fmt に用意されているC言語ライクな書式付き入出力関数は、拙作のページ ファイル入出力 で簡単に紹介しました。今回はもう少しだけ詳しく書式付き入出力関数について説明します。

●書式付き出力関数

パッケージ fmt の Printf のように、データを整形して出力する関数のことを書式付き出力関数といいます。Printf 以外にも次の関数が用意されています。

func Printf(format string, a ...interface{}) (n int, err error)
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error)
func Sprintf(format string, a ...interface{}) string

Printf は標準出力に、Fprintf は io.Writer に整形した結果を出力します。返り値 n は出力したバイト数で、エラーが発生した場合は返り値 err でエラーを報告します。Sprintf は整形した結果を文字列にして返します。

引数の文字列 format を書式文字列といい、出力に関する様々な指定を行います。書式文字列はそのまま文字列として扱われますが、文字列の途中にパーセント % が表れると、その後ろの文字を変換指示子として解釈し、引数に与えられたデータをその指示に従って表示します。Go 言語では % + 変換指示子 を verb といいます。

主な verb を以下に示します。

%v   デフォルト
%#v  Go 言語の構文表現
%T   データ型の構文表現
%%   記号 % を表す
%t   真偽値 (true, false)
%b, %d, %o, %x
     整数 (2, 10, 8, 16 進数)
%c   文字 (Unicode)
%U   文字 (Unicode 形式, U+16進数)
%e, %f, %g
     浮動小数点数、複素数
%s   文字列
%p   ポインタ

それから、% と変換指示子の間にオプションでいろいろな設定を行うことができます。

  1. フラグ
  2. フィールド幅
  3. 精度
  4. 引数インデックス

これらのオプションは verb によって動作が異なる場合があります。簡単な例を示しましょう。

リスト : 整数の出力 (fmt01.go)

package main

import "fmt"

func main() {
    fmt.Printf("%d, %x, %o\n", 100, 100, 100)
    fmt.Printf("[%d]\n", 10)
    fmt.Printf("[%4d]\n", 10)
    fmt.Printf("[%4d]\n", 100000)
    fmt.Printf("[%4d]\n", 123456)
    fmt.Printf("[%-8d]\n", 123456)
    fmt.Printf("[%08d]\n", 123456)
}
$ go run fmt01.go
100, 64, 144
[10]
[  10]
[100000]
[123456]
[123456  ]
[00123456]

% の次の文字 d, x, o が変換指示子です。これらの指示子は整数値を表示する働きをします。例が示すように、d は 10 進数、x は 16 進数、o は 8 進数で表示します。verb の個数と与えるデータの数が合わないとエラーになるので注意してください。記号 % を出力したい場合は %% と続けて書きます。

整数値を表示する verb は、データを表示するフィールド幅を指定することができます。最初の例がフィールド幅を指定しない場合で、次の例がフィールド幅を 4 に指定した場合です。10 ではフィールド幅に満たないので、右詰めに出力されています。もし、フィールド幅に収まらない場合は、指定を無視して数値を出力します。フィールド幅を 0 で埋めたい場合は、フラグに 0 を指定します。左詰めにしたい場合は、フラグに - を指定します。

%s は文字列を表示します。%s の場合でも、フィールド幅を指定することができます。簡単な例を示しましょう。

リスト : 文字列の表示 (fmt02.go)

package main

import "fmt"

func main() {
    a := "hello, world";
    fmt.Printf("[%s]\n", a);
    fmt.Printf("[%20s]\n", a);
    fmt.Printf("[%-20s]\n", a);
}
$ go run fmt02.go
[hello, world]
[        hello, world]
[hello, world        ]

浮動小数点数や複素数を表示するには %e, %f, %g を使います。

小数点の右側に印字される桁数は、%e と %f ではデフォルトで 6 桁になります。これを変更するには精度を使います。精度はピリオド ( . ) のあとに数字を指定します。たとえば、"%.14f" とすると、小数点は 14 桁で表示されます。%g は最大有効桁数で表示します。%g で精度を指定すると有効桁数が変更されます。小数点数の桁ではないので注意してください。

簡単な例を示します。

リスト : 浮動小数点数の表示 (fmt03.go)

package main

import (
    "fmt"
    "math"
)

func main() {
    pi := math.Pi
    fmt.Printf("%e\n", pi)
    fmt.Printf("%.14e\n", pi)
    fmt.Printf("%f\n", pi)
    fmt.Printf("%.14f\n", pi)
    fmt.Printf("%g\n", pi)
    fmt.Printf("%.6g\n", pi)
}
$ go run fmt03.go
3.141593e+00
3.14159265358979e+00
3.141593
3.14159265358979
3.141592653589793
3.14159

このほかにも、書式付き出力関数にはいろいろな機能があります。詳細は Go 言語のマニュアル パッケージ fmt をお読みください。

●Go 言語の文字

Go 言語の文字列は byte の配列 (スライス) として扱われます。Go 言語のプログラム (ソースファイル) は UTF-8 でエンコードすることが規定されているので、文字列の長さはバイト数と文字数で異なる場合があります。当然ですが、文字列に添字を適用すると byte が返されるので、文字単位で値を取り出すことはできません。この場合、for 文の range を使うと文字列から文字を取り出していくことができます。

簡単な例を示しましょう。

リスト : 文字列から文字を取り出す (char01.go)

package main

import "fmt"

func main() {
    a := "hello, world"
    b := "こんにちは世界"
    fmt.Printf("len(a) = %d\n", len(a))
    fmt.Printf("len(b) = %d\n", len(b))
    for i := 0; i < len(a); i++ {
        fmt.Printf("%x ", a[i])
    }
    fmt.Println("")
    for i := 0; i < len(b); i++ {
        fmt.Printf("%x ", b[i])
    }
    fmt.Println("")
    for i, c := range a {
        fmt.Printf("%d: [%c]\n", i, c)
    }
    fmt.Println("")
    for i, c := range b {
        fmt.Printf("%d: [%c]\n", i, c)
    }
    fmt.Println("")
}
$ go run char01.go
len(a) = 12
len(b) = 21
68 65 6c 6c 6f 2c 20 77 6f 72 6c 64
e3 81 93 e3 82 93 e3 81 ab e3 81 a1 e3 81 af e4 b8 96 e7 95 8c
0: [h]
1: [e]
2: [l]
3: [l]
4: [o]
5: [,]
6: [ ]
7: [w]
8: [o]
9: [r]
10: [l]
11: [d]

0: [こ]
3: [ん]
6: [に]
9: [ち]
12: [は]
15: [世]
18: [界]

Go 言語の場合、文字を表すデータ型として rune が用意されています。rune は int32 の別名で、文字を表す Unicode の値になります。文字リテラルは 'a' や 'あ' のように ' で囲んで記述します。文字数は標準ライブラリ unicode/utf8 の関数 RuneCountInString で数えることができます。

func RuneCountInString(s string) (n int)

ファイルなどから文字単位でデータを読み込みたい場合は、標準ライブラリ bufio の関数 ReadRune を使うと便利です。

func (b *Reader) ReadRune() (r rune, size int, err error)

返り値 r が文字、size がバイト数、err がエラーを表します。

簡単な使用例と実行例を示します。

リスト : 文字単位での読み込み (char02.go)

package main

import (
    "fmt"
    "io"
    "os"
    "bufio"
)

func main() {
    rd := bufio.NewReader(os.Stdin)
    for {
        r, s, err := rd.ReadRune()
        if s == 0 {
            if err != io.EOF {
                fmt.Printf("oops! %v\n", err)
            }
            break
        } else {
            fmt.Printf("%c\n", r)
        }
    }
}
$ go run char02.go
hello, world
h
e
l
l
o
,

w
o
r
l
d


こんにちは世界
こ
ん
に
ち
は
世
界


●書式付き入力関数

ファイルからデータを読み込む場合、データをバッファに格納するだけではなく、数値や他のデータ型に変換できると便利です。Go 言語の場合、書式付き入力関数を使うと、入力データの変換を行うことができます。

func Scanf(format string, a ...interface{}) (n int, err error)
func Fscanf(r io.Reader, format string, a ...interface{}) (n int, err error)
func Sscanf(str string, format string, a ...interface{}) (n int, err error)

Scanf は標準入力 (os.Stdin)、Fscanf は io.Reader (r)、Sscanf は文字列 (str) からデータを読み込みます。返り値 n は読み込んだデータ数で、失敗した場合は返り値 err でエラーを報告します。書式文字列 format のあとの引数には、読み込んだデータを格納する変数をポインタで指定します。

簡単な例を示しましょう。

リスト : Sscanf の使用例 (fmt04.go)

package main

import "fmt"

func main() {
    var n int
    var d float64
    var s string
    fmt.Sscanf("5678abcd", "%d", &n)
    fmt.Printf("%d\n", n)
    fmt.Sscanf("5678abcd", "%o", &n)
    fmt.Printf("%o\n", n)
    fmt.Sscanf("5678abcd", "%x", &n)
    fmt.Printf("%x\n", n)
    fmt.Sscanf("1.23456789", "%f", &d)
    fmt.Printf("%f\n", d)
    fmt.Sscanf("hello, world", "%s", &s)
    fmt.Printf("%s\n", s)
}
$ go run fmt04.go
5678
567
5678abcd
1.234568
hello,

%d はデータを 10 進整数に変換します。入力データが 5678abcd の場合、5678 まで読み込んで整数に変換します。このとき、入力データ abcd は残っていることに注意してください、%o は 8 進整数に、%x は 16 進整数に変換します。%f はデータを浮動小数点数 (float64) に変換します。%s は空白以外の文字列を読み込みます。空白文字 (タブや改行も含む) は区切り記号として使われます。

書式文字列の中では複数の verb を指定することができます。入力データの空白文字は区切り記号として扱われるので、"%d%d%d" は入力データ "123 456 789" とマッチングします。なお、先頭にある空白文字は読み飛ばされますが、読み飛ばさない変換指示子 (たとえば %c など) もあります。

●Scanf の簡単な使用例

それでは簡単な使用例として、ファイルに数値データがありそれを全部足し算するプログラムを作ってみましょう。次のリストを見てください。

リスト : 数値データの総和 (fmt05.go)

package main

import (
    "fmt"
    "io"
    "os"
)

func main() {
    a, c, n := 0, 0, 0
    for {
        m, err := fmt.Scanf("%d", &n)
        if m == 0 {
            if err == io.EOF {
                fmt.Printf("入力数 %d, 合計 = %d\n", c, a)
                break
            } else {
                fmt.Printf("%d 番目のデータに誤りがあります: %v\n", c + 1, err)
                os.Exit(1)
            }
        }
        a += n
        c++
    }
}

書式文字列には区切り文字が指定されていませんが、先頭の空白文字は読み飛ばされるので問題ありません。ファイルが終了した場合、Scanf は 0 と io.EOF を返します。m と err をチェックしてファイルが終了したならば、入力されたデータ数 (変数 c) と合計値 (変数 a) を Printf で表示します。

err が io.EOF でない場合、データの読み込みに失敗しています。Printf でデータの番号とエラーの種類を表示して処理を終了します。それ以外の場合はデータを正常に読み込むことができました。a に n の値を加算して c の値をインクリメントします。

簡単な実行例を示します。

$ cat input.txt
 28236
 21625
 22672
 30646
  8110
 30976
$ go run fmt05.go < input.txt
入力数 6, 合計 = 142265
$ cat inputerr.txt
 28236
 21625
 22672
 30646
 abcde
  8110
 30976
$ go run fmt05.go < inputerr.txt
5 番目のデータに誤りがあります: expected integer
exit status 1

●単語のカウント

最後に簡単な例題として、ファイルのバイト数と行数と単語をカウントするプログラムを作ります。単語は空白文字で区切られた文字列とします。なお、Unix 系 OS には同等の機能を持つコマンド wc があります。次のリストを見てください。

リスト : 単語のカウント

package main

import (
    "os"
    "io"
    "bufio"
    "fmt"
    "unicode"
)

func wc(file *bufio.Reader) (int, int, int) {
    b, l, w := 0, 0, 0    // バイト数, 行数, 単語数
    inword  := false
    for {
        c, s, err := file.ReadRune()
        if err == io.EOF { return b, l, w }
        if c == unicode.ReplacementChar {
            fmt.Fprintf(os.Stderr, "%d バイト目で不正なコードを検出しました\n", b + 1)
            os.Exit(1)
        } else if unicode.IsSpace(c) {
            inword = false
            if c == '\n' { l++ }
        } else if !inword {
            inword = true
            w++
        }
        b += s
    }
}

func main() {
    b, l, w := wc(bufio.NewReader(os.Stdin))
    fmt.Printf("%d %d %d\n", l, w, b)
}

ファイルは標準入力 (os.Stdin) から読み込みます。実際の処理は関数 wc で行います。変数 b, l, c はそれぞれバイト数、行数、単語数をカウントします。文字の読み込みは bufio のメソッド ReadRune を使います。

func (b *Reader) ReadRune() (r rune, size int, err error)

返り値は文字、バイト数、エラーです。文字が正しくない場合、U+FFFD と読み込んだバイト数 1 を返します。U+FFFD は標準ライブラリ unicode に定数 ReplacementChar として定義されています。空白文字は関数 unicode.IsSpace で判定することができます。これは ASCII コードだけではなく Unicode の空白文字もチェックしてくれます。unicode には文字種別を判定する関数が用意されています。

文字 c が空白文字であれば変数 inword を false にセットします。そして、inword が false で c が空白文字以外であれば、inword を true にセットして変数 w を +1 します。これで単語をカウントすることができます。行数は c が空白文字かつ改行文字 '\n' であれば変数 l の値を +1 します。バイト数は ReadRune の返り値 s を変数 b に加算するだけです。

それでは実行してみましょう。

$ go run wc.go
hello, world
こんにちは 世界 さようなら
<- CTRL-D を入力
2 5 56
$ go run wc.go < wc.go
38 109 643
$ wc < wc.go
 38 109 643

正常に動作しているようですね。

ご参考までに、bufio.Scanner を使ったプログラムを示します。

リスト : 単語のカウント (2)

package main

import (
    "os"
    "bufio"
    "fmt"
    "unicode"
    "unicode/utf8"
)

func wc(s *bufio.Scanner) (int, int, int) {
    b, l, w := 0, 0, 0    // byte 数, line 数, word 数
    inword  := false
    s.Split(bufio.ScanRunes)
    for s.Scan() {
        c, s := utf8.DecodeRune(s.Bytes())
        if c == utf8.RuneError {
            fmt.Fprintf(os.Stderr, "%d バイト目で不正なコードを検出しました\n", b + 1)
            os.Exit(1)
        } else if unicode.IsSpace(c) {
            inword = false
            if c == '\n' { l++ }
        } else if !inword {
            inword = true
            w++
        }
        b += s
    }
    return b, l, w
}

func main() {
    b, l, w := wc(bufio.NewScanner(os.Stdin))
    fmt.Printf("%d %d %d\n", l, w, b)
}

関数 Split で分割関数を ScanRunes に設定します。分割関数は Scan を実行する前に設定してください。トークンは文字列またはバイト列でしか取り出すことができません。バイト列を文字に変換するには標準ライブラリ unicode/utf8 の関数 DecodeRune を使います。

func DecodeRune(p []byte) (r rune, size int)

DecodeRune は引数のバイト列 p の先頭から 1 文字取り出します。返り値 r が取り出した文字、size がバイト数です。文字が正しくない場合、文字 utf8.RuneError (U+FFFD) を返します。あとのプログラムは同じなので説明は割愛させていただます。興味のある方は実際に動かしてみてください。

●参考 URL

  1. パッケージ fmt - Go 言語

コマンドラインフラグの解析

近年の情報機器 (パソコンや携帯など) は 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

Copyright (C) 2022 Makoto Hiroi
All rights reserved.

[ PrevPage | Golang | NextPage ]