今回は 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