How to spend the terminal

技術ブログでさえない

Goで作ったprintfでHello world

この記事は、SLP KBIT Advent Calendar 2017 12日目の記事です。

はじめに

予告していた通りGo言語でHello worldを書きます。 なぜHello worldを書くのかというと、アドベントカレンダーの記事の水準が上がってきていて、初心者が書きにくくなってきているからです。Hello worldを書いた程度の微笑ましい記事でもいいと私は思っています。

package main
import "fmt"
func main() {
    fmt.Printf("Hello world\n")
}

はい、Hello worldを書けました。これで終わりです・・・と本当に言ったら怒られるので本題に入ります。

printfを作る

*nixのシェルにはprintfコマンドが組み込まれています。printfコマンドはC言語のprintfのように引数を書式にしたがって変換し、出力します。

これを作ろうと思った理由は二つあって、Windowsで*nixシェルっぽいコマンドを作っていたのと、楽に作れそうだからです。 楽に作れそうだったのですが、なかなか大変でした。

fmt.Printfに投げて終わり?

はじめに考えていたのは次のようなプログラムでした。

package main

import (
    "fmt"
    "os"
)

func main() {
      if len(os.Args) < 2 {
        fmt.Fprintf(os.Stderr, "%s: not enough arguments\n", os.Args[0])
    } else if len(os.Args) == 2 {
        f := os.Args[1]
        fmt.Printf(f)
    } else {
        f := os.Args[1]
        s := os.Args[2:]
        fmt.Printf(f, s...)  // スライスを可変引数にして渡す
    }
}

os.Args[1](1番目の引数)がフォーマット(書式)で、os.Args[2:](2番目以降の引数)が引数となっています。 このまま実行できればそれで終わりだったのですが、数値の変換が行われません。 なぜそうなるかというと、os.Argsがstringの配列で、stringとして渡されるからです。

引数を変換する

fmt.Printf()の引数をGoDocで見てみましょう。

func Printf(format string, a ...interface{}) (n int, err error)

1番目の引数は書式で文字列、2番目以降の引数はinterface{}(任意の型)の可変引数です。つまり、2番目以降の引数を変換したものを[]interface{}(interface{}の配列)に入れておけばいいわけです。

では、どうやって変換すればいいでしょうか。例えば、99が整数であるか文字列であるか判断することはできません。そこでformatを参照して文字列であるか整数であるか実数であるかを判断します。

判断する関数を作る

使える書式

今回のprintfで使える書式は以下の書式です。

  • 文字列
    • %s
    • %q(ダブルクォーテーションをつける)
  • 整数
    • %d
    • %o
    • %x
    • %X
  • 実数
    • %f
    • %F
    • %e
    • %E
  • その他
    • %%(%を出力)

      どう解析するか

      1文字ずつ調べて、'%'を見つけたら書式フラグを立てます。 書式であることがわかれば文字列であれば文字列、整数であれば整数、実数であれば実数を配列に加えます。整数か実数が指定されているのにそうでない場合は0もしくは0.0を配列に加えます。 書式でない文字が出てきた場合か書式フラグが立ったまま解析が終わった場合、エラーを返します。

      コード

func StrToIF(f string, s []string) ([]interface{}, error) {
    a := []interface{}{}
    runes := []rune(f)
    argNum := 0
    percent := false
    verb := ""
    for _, r := range runes {
        if percent {
            verb += string(r)
            switch r {
            case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
            case '+', '-', '.', '#':
            case '%':
                if verb != "%%" {
                    return a, FormatError{verb}
                } else { // %%
                    percent = false
                    continue
                }
            case 's', 'q':
                if argNum < len(s) {
                    a = append(a, s[argNum])
                    argNum++
                } else {
                    a = append(a, "")
                }
                percent = false
            case 'd', 'o', 'x', 'X':
                var i int64
                var err error
                if argNum < len(s) {
                    i, err = strconv.ParseInt(s[argNum], 0, 64)
                    if err != nil {
                        i = 0
                    }
                    argNum++
                } else {
                    i = 0
                }
                a = append(a, i)
                percent = false
            case 'f', 'F', 'e', 'E':
                var fl float64
                var err error
                if argNum < len(s) {
                    fl, err = strconv.ParseFloat(s[argNum], 64)
                    if err != nil {
                        fl = 0.0
                    }
                    argNum++
                } else {
                    fl = 0.0
                }
                a = append(a, fl)
                percent = false
            default:
                return a, FormatError{verb}
            }
        }
        if r == '%' {
            if percent == false {
                percent = true
                verb = "%"
            }
        }
    }
    if percent {
        return a, FormatError{verb}
    }
    return a, nil
}

エスケープシーケンスの処理

書式に合わせた変換はできるようになりましたが、"Hello world\n"を入力すると改行されずにそのまま出力されます。これは"[\n]"が"[\][n]"といったようになるからです。 そのため、エスケープシーケンスに変換します。stringsパッケージを用いて泥臭く実装しました。

func formatReplace(f string) string {
    f = strings.Replace(f, "\\a", "\a", -1)
    f = strings.Replace(f, "\\b", "\b", -1)
    f = strings.Replace(f, "\\f", "\f", -1)
    f = strings.Replace(f, "\\n", "\n", -1)
    f = strings.Replace(f, "\\r", "\r", -1)
    f = strings.Replace(f, "\\t", "\t", -1)
    f = strings.Replace(f, "\\v", "\v", -1)
    f = strings.Replace(f, "\\'", "'", -1)
    f = strings.Replace(f, "\\\"", "\"", -1)
    return f
}

最後に加工した書式と[]interface{}を可変引数に展開してfmt.Printf()に投げて完成です。

まとめ

以上よりprintfを作ることができました。ソースコードGitHubに上げています。

github.com

標準ライブラリのみで使ったのでWindowsでも簡単に使うことができます。 これでprintfを行うことで誰でもシェル上でHello worldを行うことができます。 シェルならWindowsでもechoコマンドでできますが。