M.Hiroi's Home Page

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

応用編 : 簡単な CLI ツールの作成


Copyright (C) 2022 Makoto Hiroi
All rights reserved.

はじめに

今回は Go 言語の例題として、テキストを操作する簡単なコマンドを作成してみましょう。Unix 系 OS のコマンドを参考にしていますが、簡易バージョンなので難しい機能は実装していません。それだけプログラムは簡単になります。作成するコマンドは次の通りです。

●cat

//
// 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

//
// 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

//
// 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

●head

//
// 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

//
// 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

//
// 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

//
// 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

//
// 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

//
// 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

初版 2022 年 1 月 29 日