今回は Go 言語の例題として、テキストを操作する簡単なコマンドを作成してみましょう。Unix 系 OS のコマンドを参考にしていますが、簡易バージョンなので難しい機能は実装していません。それだけプログラムは簡単になります。作成するコマンドは次の通りです。
// // cat.go : ファイルの連結 // // Copyright (C) 2022 Makoto Hiroi // package main import ( "fmt" "os" "bufio" "flag" ) // オプション var bflag bool var nflag bool var sflag bool // ファイルの表示 func cat(file *os.File, n int) int { s := bufio.NewScanner(file) empty := false for s.Scan() { l := s.Text() if l != "" { if bflag || nflag { fmt.Printf("%6d %s\n", n, l) n++ } else { fmt.Println(l) } empty = false } else { if sflag && empty { continue } if nflag { fmt.Printf("%6d \n", n) n++ } else { fmt.Println("") } empty = true } } return n } func main() { // フラグの設定 flag.BoolVar(&bflag, "b", false, "空行以外に番号を付ける") flag.BoolVar(&nflag, "n", false, "行に番号を付ける") flag.BoolVar(&sflag, "s", false, "連続した空行を一つにまとめる") 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() } } }
$ ./cat test01.txt abcd ABCD efghi EFGHI end of file $ ./cat -n test01.txt 1 abcd 2 3 ABCD 4 efghi 5 6 7 EFGHI 8 end of file $ ./cat -b test01.txt 1 abcd 2 ABCD 3 efghi 4 EFGHI 5 end of file $ ./cat -b -s test01.txt 1 abcd 2 ABCD 3 efghi 4 EFGHI 5 end of file
// // cut.go : 行から n 番目のフィールドを切り出す // // Copyright (C) 2022 Makoto Hiroi // package main import ( "fmt" "bufio" "os" "strings" "strconv" "flag" ) // フラグ var dflag string var fflag string var oflag string func cut(file *os.File, fn []int) { s := bufio.NewScanner(file) for s.Scan() { l := s.Text() ss := strings.Split(l, dflag) for i, n := range fn { if 1 <= n && n <= len(ss) { fmt.Printf("%s", ss[n - 1]) } else { fmt.Print("") } if i < len(fn) - 1 { fmt.Print(oflag) } } fmt.Println("") } } func main() { // フラグの設定 flag.StringVar(&dflag, "d", "\t", "区切り文字を指定する") flag.StringVar(&fflag, "f", "", "フィールド番号を指定する") flag.StringVar(&oflag, "o", "", "出力時の区切り文字を指定する") flag.Parse() if fflag == "" { fmt.Fprintln(os.Stderr, "フィールド番号がありません") os.Exit(1) } if oflag == "" { oflag = dflag } fs := strings.Split(fflag, ",") fn := make([]int, len(fs)) for i, s := range fs { n, err := strconv.Atoi(s) if err != nil || n <= 0 { fmt.Fprintf(os.Stderr, "-f には 1 以上の数値を指定してください: %v\n", err) os.Exit(1) } fn[i] = n } if flag.NArg() == 0 { // 標準入力 cut(os.Stdin, fn) } else { for _, name := range flag.Args() { file, err := os.Open(name) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } cut(file, fn) file.Close() } } }
$ cat test02.txt abcd 1234 ABCD efghi 56789 EFGHI jkl 987 JKL mnopqrs 6543210 MNOPQRS $ ./cut -f 1 test02.txt abcd efghi jkl mnopqrs $ ./cut -f 3 test02.txt ABCD EFGHI JKL MNOPQRS $ ./cut -f 2,3 test02.txt 1234 ABCD 56789 EFGHI 987 JKL 6543210 MNOPQRS $ ./cut -o , -f 2,3 test02.txt 1234,ABCD 56789,EFGHI 987,JKL 6543210,MNOPQRS
// // paste.go : 行単位でファイルを連結する // // Copyright (C) 2022 Makoto Hiroi // package main import ( "fmt" "os" "bufio" "flag" ) // オプション var dflag string // 行の連結 func paste(files []*bufio.Scanner) { buff := make([]string, len(files)) for { n := 0 for i, s := range files { if s.Scan() { buff[i] = s.Text() n++ } else { buff[i] = "" } } if n == 0 { break } for i, l := range buff { if i == len(buff) - 1 { fmt.Printf("%s\n", l) } else { fmt.Printf("%s%s", l, dflag) } } } } func main() { // フラグの設定 flag.StringVar(&dflag, "d", "\t", "区切り文字の指定") flag.Parse() if flag.NArg() == 0 { // 標準入力 ss := []*bufio.Scanner {bufio.NewScanner(os.Stdin)} paste(ss) } else { files := make([]*os.File, flag.NArg()) ss := make([]*bufio.Scanner, flag.NArg()) for i, name := range flag.Args() { file, err := os.Open(name) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } files[i] = file ss[i] = bufio.NewScanner(file) } paste(ss) for _, file := range files { file.Close() } } }
$ ./cut -f 1 test02.txt > test03.txt $ ./cut -f 2 test02.txt > test04.txt $ ./cut -f 3 test02.txt > test05.txt $ paste test03.txt test04.txt test05.txt abcd 1234 ABCD efghi 56789 EFGHI jkl 987 JKL mnopqrs 6543210 MNOPQRS $ paste -d , test03.txt test04.txt test05.txt abcd,1234,ABCD efghi,56789,EFGHI jkl,987,JKL mnopqrs,6543210,MNOPQRS
// // haed.go : ファイルの先頭を表示する // // Copyright (C) 2022 Makoto Hiroi // package main import ( "fmt" "os" "bufio" "flag" ) func head(file *os.File, n uint) { s := bufio.NewScanner(file) var i uint for i = 0; i < n; i++ { if !s.Scan() { break } fmt.Println(s.Text()) } } func main() { // フラグの設定 nflag := flag.Uint("n", 10, "ファイルの先頭 n 行表示する") qflag := flag.Bool("q", false, "ファイルヘッダを表示しない") vflag := flag.Bool("v", false, "ファイルヘッダを常に表示する") flag.Parse() if flag.NArg() == 0 { // 標準入力 if *vflag { fmt.Println("==>標準入力<==") } head(os.Stdin, *nflag) } else { for i, name := range flag.Args() { file, err := os.Open(name) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } if !(*qflag) && (*vflag || flag.NArg() > 1) { fmt.Printf("==>%s<==\n", name) } head(file, *nflag) if !(*qflag) && i < flag.NArg() - 1 { fmt.Println("") } file.Close() } } }
$ ./head -n 3 test03.txt abcd efghi jkl $ ./head -n 3 test03.txt test04.txt test05.txt ==>test03.txt<== abcd efghi jkl ==>test04.txt<== 1234 56789 987 ==>test05.txt<== ABCD EFGHI JKL $ ./head -q -n 3 test03.txt test04.txt test05.txt abcd efghi jkl 1234 56789 987 ABCD EFGHI JKL
// // tail.go : ファイルの末尾を表示する // // Copyright (C) 2022 Makoto Hiroi // package main import ( "fmt" "os" "bufio" "flag" ) func tail(file *os.File, n uint) { s := bufio.NewScanner(file) queue := make([]string, n) var r, w, c uint = 0, 0, 0 for s.Scan() { l := s.Text() if c >= n { c-- r++ if r >= n { r = 0 } } queue[w] = l c++ w++ if w >= n { w = 0 } } // 表示 for ; c > 0; c-- { fmt.Println(queue[r]) r++ if r >= n { r = 0 } } } func main() { // フラグの設定 nflag := flag.Uint("n", 10, "ファイルの末尾 n 行表示する") qflag := flag.Bool("q", false, "ファイルヘッダを表示しない") vflag := flag.Bool("v", false, "ファイルヘッダを常に表示する") flag.Parse() if flag.NArg() == 0 { // 標準入力 if *vflag { fmt.Println("==>Stdin<==") } tail(os.Stdin, *nflag) } else { for i, name := range flag.Args() { file, err := os.Open(name) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } if !(*qflag) && (*vflag || flag.NArg() > 1) { fmt.Printf("==>%s<==\n", name) } tail(file, *nflag) if !(*qflag) && i < flag.NArg() - 1 { fmt.Println("") } file.Close() } } }
$ ./tail -n 3 test03.txt efghi jkl mnopqrs $ ./tail -n 3 test03.txt test04.txt test05.txt ==>test03.txt<== efghi jkl mnopqrs ==>test04.txt<== 56789 987 6543210 ==>test05.txt<== EFGHI JKL MNOPQRS $ ./tail -q -n 3 test03.txt test04.txt test05.txt efghi jkl mnopqrs 56789 987 6543210 EFGHI JKL MNOPQRS
// // wc.go : 単語のカウント // // Copyright (C) 2022 Makoto Hiori // package main import ( "os" "io" "bufio" "fmt" "flag" "unicode" ) // フラグ var cflag bool var mflag bool var lflag bool var wflag bool var nflag int func wc(file *os.File) (int, int, int, int) { rd := bufio.NewReader(file) c, l, w, m := 0, 0, 0, 0 // バイト数, 行数, 単語数, 文字数 inword := false for { r, s, err := rd.ReadRune() if s == 0 { if err == io.EOF { return c, l, w, m } else { fmt.Fprintf(os.Stderr, "%d バイト目で不正なコードを検出しました\n", c + 1) os.Exit(1) } } else if unicode.IsSpace(r) { inword = false if r == '\n' { l++ } } else if !inword { inword = true w++ } c += s m++ } } func printCount(c, l, w, m int, name string) { if lflag { fmt.Printf("%[1]*[2]d ", nflag, l) } if wflag { fmt.Printf("%[1]*[2]d ", nflag, w) } if mflag { fmt.Printf("%[1]*[2]d ", nflag, m) } if cflag { fmt.Printf("%[1]*[2]d ", nflag, c) } if !lflag && !wflag && !cflag && !mflag { fmt.Printf("%[1]*[2]d %[1]*[3]d %[1]*[4]d ", nflag, l, w, c) } fmt.Println(name) } func main() { // フラグの設定 flag.BoolVar(&cflag, "c", false, "バイト数を表示する") flag.BoolVar(&mflag, "m", false, "文字数を表示する") flag.BoolVar(&lflag, "l", false, "行数を表示する") flag.BoolVar(&wflag, "w", false, "単語数を表示する") flag.IntVar(&nflag, "n", 6, "数のフィールド幅を指定する") flag.Parse() if flag.NArg() == 0 { // 標準入力 c, l, w, m := wc(os.Stdin) printCount(c, l, w, m, "") } else { ca, la, wa, ma := 0, 0, 0, 0 // 合計値 for _, name := range flag.Args() { file, err := os.Open(name) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } c, l, w, m := wc(file) ca += c la += l wa += w ma += m printCount(c, l, w, m, name) file.Close() } if flag.NArg() > 1 { printCount(ca, la, wa, ma, "合計") } } }
$ ./wc wc.go 101 310 2280 wc.go $ ./wc -m wc.go 2080 wc.go $ ./wc -c -m wc.go 2080 2280 wc.go $ ./wc -l -w -c -m wc.go 101 310 2080 2280 wc.go $ ./wc -l -w -c -m wc.go head.go tail.go 101 310 2080 2280 wc.go 54 140 1079 1209 head.go 69 193 1299 1425 tail.go 224 643 4458 4914 合計
// // grep.go : 正規表現による文字列の検索 // // Copyright (C) 2022 Makoto Hiroi // package main import ( "fmt" "flag" "bufio" "os" "regexp" ) // オプション var cflag bool var qflag bool var iflag bool var nflag bool var vflag bool func printLine(l, name string, n int) { if !qflag && name != "" { fmt.Printf("%s:", name) } if nflag { fmt.Printf("%d:", n) } fmt.Println(l) } func grep(file *os.File, re *regexp.Regexp, name string) { s := bufio.NewScanner(file) n := 1 c := 0 for s.Scan() { l := s.Text() if re.MatchString(l) { c++ if !vflag && !cflag{ printLine(l, name, n) } } else if vflag && !cflag { printLine(l, name, n) } n++ } if cflag { if !qflag && name != "" { fmt.Printf("%s:", name) } fmt.Println(c) } } func main() { // オプションの設定 flag.BoolVar(&cflag, "c", false, "パターンと一致した行数を求める") flag.BoolVar(&qflag, "q", false, "ファイル名を表示しない") flag.BoolVar(&iflag, "i", false, "英大小文字を区別しない") flag.BoolVar(&nflag, "n", false, "行数を表示する") flag.BoolVar(&vflag, "v", false, "パターンと一致しない行を表示する") flag.Parse() // 検索パターンのコンパイル if flag.NArg() == 0 { fmt.Fprintln(os.Stderr, "検索パターンが必要です") os.Exit(1) } args := flag.Args() pattern := args[0] if iflag { pattern = `(?i)` + pattern } re, err := regexp.Compile(pattern) if err != nil { fmt.Fprintf(os.Stderr, "%v\n", err) os.Exit(1) } // 入力ファイルの取得 if flag.NArg() == 1 { grep(os.Stdin, re, "") } else { for _, name := range args[1:] { file, err := os.Open(name) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } if flag.NArg() == 2 { grep(file, re, "") } else { grep(file, re, name) } file.Close() } } }
$ ./grep '[a-d]+' test02.txt test03.txt test05.txt test02.txt:abcd 1234 ABCD test03.txt:abcd $ ./grep -n '[a-d]+' test02.txt test03.txt test05.txt test02.txt:1:abcd 1234 ABCD test03.txt:1:abcd $ ./grep -n -i '[a-d]+' test02.txt test03.txt test05.txt test02.txt:1:abcd 1234 ABCD test03.txt:1:abcd test05.txt:1:ABCD $ ./grep -v -n -i '[a-d]+' test02.txt test03.txt test05.txt test02.txt:2:efghi 56789 EFGHI test02.txt:3:jkl 987 JKL test02.txt:4:mnopqrs 6543210 MNOPQRS test03.txt:2:efghi test03.txt:3:jkl test03.txt:4:mnopqrs test05.txt:2:EFGHI test05.txt:3:JKL test05.txt:4:MNOPQRS $ ./grep -c '[a-d]+' test02.txt test03.txt test05.txt test02.txt:1 test03.txt:1 test05.txt:0 $ ./grep -c -i '[a-d]+' test02.txt test03.txt test05.txt test02.txt:1 test03.txt:1 test05.txt:1
// // gres.go : 正規表現による文字列の置換 // // Copyright (C) 2022 Makoto Hiroi // package main import ( "fmt" "flag" "bufio" "os" "regexp" ) // オプション var iflag bool func main() { // オプションの設定 flag.BoolVar(&iflag, "i", false, "英大小文字を区別しない") flag.Parse() // 検索パターンのコンパイル if flag.NArg() < 2 { fmt.Fprintln(os.Stderr, "検索パターンと置換パターンが必要です") os.Exit(1) } args := flag.Args() pattern := args[0] repl := args[1] if iflag { pattern = `(?i)` + pattern } re, err := regexp.Compile(pattern) if err != nil { fmt.Fprintf(os.Stderr, "%v\n", err) os.Exit(1) } // 置換 s := bufio.NewScanner(os.Stdin) for s.Scan() { fmt.Println(re.ReplaceAllString(s.Text(), repl)) } }
$ ./gres '[a-z]+' '<$0>' < test02.txt <abcd> 1234 ABCD <efghi> 56789 EFGHI <jkl> 987 JKL <mnopqrs> 6543210 MNOPQRS $ ./gres -i '[a-z]+' '<$0>' < test02.txt <abcd> 1234 <ABCD> <efghi> 56789 <EFGHI> <jkl> 987 <JKL> <mnopqrs> 6543210 <MNOPQRS> $ ./gres -i $'\t' ',' < test02.txt abcd,1234,ABCD efghi,56789,EFGHI jkl,987,JKL mnopqrs,6543210,MNOPQRS $ ./gres -i $'\t' '' < test02.txt abcd1234ABCD efghi56789EFGHI jkl987JKL mnopqrs6543210MNOPQRS
// // sort.go : テキストファイルのソート // // Copyright (C) 2022 Makoto Hiroi // package main import ( "fmt" "flag" "os" "bufio" "strings" "unicode" "sort" ) // フラグ var kflag int // フィールド指定 (0 は行全体とする) var bflag bool // 先頭の空白文字を無視する var iflag bool // 英大小文字を区別しない var nflag bool // 文字列を数値としてソートする var rflag bool // 降順にソート var tflag string // デリミタ (デフォルトは空白文字) // 文字列のソート func strSort(file *os.File) { s := bufio.NewScanner(file) buff := make([][]string, 0) n := 0 for s.Scan() { var k string l := s.Text() xs := strings.Split(l, tflag) if len(xs) < kflag { fmt.Fprintf(os.Stderr, "%d: %s, フィールド %d がありません\n", n + 1, l, kflag) os.Exit(1) } if kflag == 0 { k = l } else { k = xs[kflag - 1] } if bflag { k = strings.TrimLeftFunc(k, unicode.IsSpace) } if iflag { k = strings.ToLower(k) } buff = append(buff, []string {l, k}) n++ } if rflag { sort.SliceStable(buff, func(i, j int) bool { return buff[i][1] > buff[j][1] }) } else { sort.SliceStable(buff, func(i, j int) bool { return buff[i][1] < buff[j][1] }) } for _, ls := range buff { fmt.Println(ls[0]) } } // 数値のソート func strToNum(s string, l int) int { var n int _, err := fmt.Sscanf(s, "%d", &n) if err != nil { fmt.Fprintf(os.Stderr, "%d: %s, %v\n", l + 1, s, err) os.Exit(1) } return n } func numSort(file *os.File) { s := bufio.NewScanner(file) strBuff := make([]string, 0) numBuff := make([][]int, 0) n := 0 for s.Scan() { var k int l := s.Text() xs := strings.Split(l, tflag) if len(xs) < kflag { fmt.Fprintf(os.Stderr, "%d: %s, フィールド %d がありません\n", n + 1, l, kflag) os.Exit(1) } if kflag == 0 { k = strToNum(l, n) } else { k = strToNum(xs[kflag - 1], n) } strBuff = append(strBuff, l) numBuff = append(numBuff, []int {n, k}) n++ } if rflag { sort.SliceStable(numBuff, func(i, j int) bool { return numBuff[i][1] > numBuff[j][1] }) } else { sort.SliceStable(numBuff, func(i, j int) bool { return numBuff[i][1] < numBuff[j][1] }) } for _, ls := range numBuff { fmt.Println(strBuff[ls[0]] ) } } func main() { // フラグの設定 flag.IntVar(&kflag, "k", 0, "フィールドを指定する") flag.StringVar(&tflag, "t", "\t", "区切り文字を指定する") flag.BoolVar(&bflag, "b", false, "先頭の空白文字を無視する") flag.BoolVar(&iflag, "i", false, "英大小文字を区別しない") flag.BoolVar(&nflag, "n", false, "文字列を数値としてソートする") flag.BoolVar(&rflag, "r", false, "降順にソートする") flag.Parse() if flag.NArg() == 0 { // 標準入力 if nflag { numSort(os.Stdin) } else { strSort(os.Stdin) } } else { file, err := os.Open(flag.Args()[0]) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } if nflag { numSort(file) } else { strSort(file) } file.Close() } }
$ ./cat test06.txt abcd 1234 ABCD efghi 56789 EFGHI jkl 987 JKL mnopqrs 6543210 MNOPQRS DCBA 4321 abcd IHGFE 98765 efghi LKJ 789 jkl SRQPONM 123456 mnopqrs $ ./sort < test06.txt DCBA 4321 abcd IHGFE 98765 efghi LKJ 789 jkl SRQPONM 123456 mnopqrs abcd 1234 ABCD efghi 56789 EFGHI jkl 987 JKL mnopqrs 6543210 MNOPQRS $ ./sort -k 2 test06.txt abcd 1234 ABCD SRQPONM 123456 mnopqrs DCBA 4321 abcd efghi 56789 EFGHI mnopqrs 6543210 MNOPQRS LKJ 789 jkl jkl 987 JKL IHGFE 98765 efghi $ ./sort -k 3 test06.txt abcd 1234 ABCD efghi 56789 EFGHI jkl 987 JKL mnopqrs 6543210 MNOPQRS DCBA 4321 abcd IHGFE 98765 efghi LKJ 789 jkl SRQPONM 123456 mnopqrs $ ./sort -i -k 3 test06.txt abcd 1234 ABCD DCBA 4321 abcd efghi 56789 EFGHI IHGFE 98765 efghi jkl 987 JKL LKJ 789 jkl mnopqrs 6543210 MNOPQRS SRQPONM 123456 mnopqrs $ ./sort -n -k 2 test06.txt LKJ 789 jkl jkl 987 JKL abcd 1234 ABCD DCBA 4321 abcd efghi 56789 EFGHI IHGFE 98765 efghi SRQPONM 123456 mnopqrs mnopqrs 6543210 MNOPQRS