How to spend the terminal

技術ブログでさえない

Go言語でnkfみたいなやつ

nkfという今年で30周年になる古いソフトがあります。nkfでは文字コードを変換することができます。 nkfには様々な機能がありますが、次の3点を満たすものをGo言語で作ってみました。

文字コードを変換する

文字コードの変換は以下の記事を参考にしました。というか丸コピ

qiita.com

文字コードを判定する

文字コードの判定はchardetを使用しました。chardetは文字コードを判定するためのライブラリです。 判定に失敗した時にはエラーを返します。

改行コードを変換する

改行コードの変換は正規表現を使用しました。

コード

コードは以下のようになりました。

main.go

package main

import (
    "flag"
    "fmt"
    "log"
    "os"
)

var (
    inJIS     = flag.Bool("J", false, "Input JIS(ISO2022JP)")
    inEUC     = flag.Bool("E", false, "Input EUCJP")
    inSJIS    = flag.Bool("S", false, "Input Shift_JIS")
    inUTF8    = flag.Bool("W", false, "Input UTF-8")
    outJIS    = flag.Bool("j", false, "Output JIS(ISO2022JP)")
    outEUC    = flag.Bool("e", false, "Output EUCJP")
    outSJIS   = flag.Bool("s", false, "Output Shift_JIS")
    outUTF8   = flag.Bool("w", false, "Output UTF-8")
    unix      = flag.Bool("Lu", false, "LF(UNIX) Newline")
    windows   = flag.Bool("Lw", false, "CRLF(Windows) Newline")
    macintosh = flag.Bool("Lm", false, "CR(Macintosh) Newline")
    override  = flag.Bool("override", false, "Override")
    guess     = flag.Bool("g", false, "Detect Charset")
)

func setIn() string {
    switch {
    case *inJIS:
        return "ISO2022JP"
    case *inEUC:
        return "EUCJP"
    case *inSJIS:
        return "ShiftJIS"
    case *inUTF8:
        return "UTF8"
    default:
        return ""
    }
}

func setOut() string {
    switch {
    case *outJIS:
        return "ISO2022JP"
    case *outEUC:
        return "EUCJP"
    case *outSJIS:
        return "ShiftJIS"
    case *outUTF8:
        return "UTF8"
    default:
        return "ISO2022JP"
    }
}

func setNl() string {
    switch {
    case *unix:
        return "UNIX"
    case *windows:
        return "WINDOWS"
    case *macintosh:
        return "MACINTOSH"
    default:
        return ""
    }
}

func main() {
    var fp *os.File
    var err error
    flag.Parse()
    if len(flag.Args()) < 1 {
        fp = os.Stdin
    } else {
        fp, err = os.Open(flag.Args()[0])
        if err != nil {
            log.Fatal(err)
        }
        defer fp.Close()
    }

    in := setIn()
    out := setOut()
    nl := setNl()
    if *guess {
        charset, _ := Guess(fp)
        fmt.Println(charset)
    } else {
        str, err := Convert(fp, in, out, nl)
        if err != nil {
            log.Fatal(err)
        }
        if *override && !(len(flag.Args()) < 1) {
            fp.Close()
            fp, err = os.OpenFile(flag.Args()[0], os.O_WRONLY, 0666)
            if err != nil {
                log.Fatal(err)
            }
            defer fp.Close()
            fmt.Fprint(fp, str)
        } else {
            fmt.Fprint(os.Stdout, str)
        }
    }
}

convert.go

package main

import (
    "errors"
    "io/ioutil"
    "os"
    "regexp"
    "strings"

    "github.com/saintfish/chardet"
    "golang.org/x/text/encoding/japanese"
    "golang.org/x/text/transform"
)

var errDet = errors.New("Couldn't detect")

func charDet(b []byte) (string, error) {
    d := chardet.NewTextDetector()
    res, err := d.DetectBest(b)
    if err != nil {
        return "", err
    }
    switch res.Charset {
    case "Shift_JIS":
        return "ShiftJIS", nil
    case "UTF-8":
        return "UTF8", nil
    case "EUC-JP":
        return "EUCJP", nil
    case "ISO-2022-JP":
        return "ISO2022JP", nil
    default:
        return res.Charset, errDet
    }
}

func toUtf8(str string, in string) (string, error) {
    var u8 []byte
    var err error
    switch in {
    case "ISO2022JP":
        u8, err = ioutil.ReadAll(transform.NewReader(strings.NewReader(str), japanese.ISO2022JP.NewDecoder()))
    case "EUCJP":
        u8, err = ioutil.ReadAll(transform.NewReader(strings.NewReader(str), japanese.EUCJP.NewDecoder()))
    case "ShiftJIS":
        u8, err = ioutil.ReadAll(transform.NewReader(strings.NewReader(str), japanese.ShiftJIS.NewDecoder()))
    case "UTF8":
        u8, err = []byte(str), nil
    default:
        u8, err = ioutil.ReadAll(transform.NewReader(strings.NewReader(str), japanese.ShiftJIS.NewDecoder()))
    }
    if err != nil {
        return "", err
    }
    return string(u8), err
}

func toSjis(str string) (string, error) {
    sjis, err := ioutil.ReadAll(transform.NewReader(strings.NewReader(str), japanese.ShiftJIS.NewEncoder()))
    if err != nil {
        return "", err
    }
    return string(sjis), err
}

func toEuc(str string) (string, error) {
    euc, err := ioutil.ReadAll(transform.NewReader(strings.NewReader(str), japanese.EUCJP.NewEncoder()))
    if err != nil {
        return "", err
    }
    return string(euc), err
}

func toJis(str string) (string, error) {
    jis, err := ioutil.ReadAll(transform.NewReader(strings.NewReader(str), japanese.ISO2022JP.NewEncoder()))
    if err != nil {
        return "", err
    }
    return string(jis), err
}

func nlRep(str string, nl string) string {
    rep := regexp.MustCompile(`\r\n|\r|\n`)
    switch nl {
    case "UNIX":
        return rep.ReplaceAllString(str, "\n")
    case "WINDOWS":
        return rep.ReplaceAllString(str, "\r\n")
    case "MACINTOSH":
        return rep.ReplaceAllString(str, "\r")
    }
    return str
}

/*
Only detect Character encoding
*/
func Guess(file *os.File) (string, error) {
    input, err := ioutil.ReadAll(file)
    if err != nil {
        return "", err
    }
    det, err := charDet(input)
    return det, err
}

/*
Convert Character encoding
*/
func Convert(file *os.File, in string, out string, nl string) (string, error) {
    input, err := ioutil.ReadAll(file)
    if err != nil {
        return "", err
    }
    if in == "" {
        in, err = charDet(input)
        if err != nil {
            return "", err
        }
    }
    u8, err := toUtf8(string(input), in)
    if err != nil {
        return "", err
    }
    if nl != "" {
        u8 = nlRep(u8, nl)
    }
    var output string
    switch out {
    case "ISO2022JP":
        output, err = toJis(u8)
    case "ShiftJIS":
        output, err = toSjis(u8)
    case "EUCJP":
        output, err = toEuc(u8)
    case "UTF8":
        output = u8
    }
    return output, err
}

まとめ

EUC-JPの判定はあまりうまくいかないのでEオプションをつけるといいと思います。 chardetはShift_JISEUC-JPで誤判定が発生する可能性があるため、overrideオプションを使う時は入力オプションを使うことをおすすめします。 ここまで長くなるとコードをまるまる載せるのではなく、GitHubで公開したほうがいいと思いました。 GitHubに公開しました(2017/07/14)

github.com