はじめに
標準入力から文字列をつけとってなんやかんやする機会は多いと思います。Go で標準入力から文字列を受け取る方法の 1 つとして fmt.Scanf を使う方法があります。for ループを使った複数行の文字列を読み込んでいるときに Unix 系の OS と WindowsOS で挙動が異なることに気づきました。バグっぽい挙動を示しています。
問題になっているのは OS の差異による改行コードの違い、CRLF(\r\n
) と LF(\n
)によるものです。
公式リポジトリの Issue にもあがっていました。Open な Issue です。
サンプル
実際に fmt.Scanf を使う以下のような簡単な実装を考えてみます。
package main
import "fmt"
func main() {
var f, s string
fmt.Printf("Please enter first string:\n")
_, err := fmt.Scanf("%s", &f)
fmt.Println(err)
fmt.Printf("Please enter second string:\n")
_, err = fmt.Scanf("%s", &s)
fmt.Println(err)
}
Unix 系で go run main.go
として実行します。入力する文字列は hoge
, fuga
とします。
Unix系での実行
Please enter first string:
hoge
<nil>
Please enter second string:
fuga
<nil>
想定通りに読み込むことができました。
Windows系での実行
Windows 系で go run main.go
として実行します。Unix 系のときと同様に入力する文字列は hoge
, fuga
とします。
Please enter first string:
hoge
<nil>
Please enter second string:
unexpected newline
見ると 2 回目の fmt.Scanf でエラーとして unexpected newline
と出力されていることがわかります。これは想定外です。
詳細を見てみる
何が起きているか、詳細を確認します。デバッグ用の io.Reader を実装して確かめます。デバッグ用の io.Reader の実装は Issue からの引用です。
type myReader struct{}
func (r myReader) Read(p []byte) (n int, err error) {
n, err = os.Stdin.Read(p)
fmt.Fprintf(os.Stderr, "DEBUG: %q %v\n", p[:n], err)
return n, err
}
func main() {
input := myReader{}
var f, s string
fmt.Printf("Please enter first string:\n")
_, err := fmt.Fscanf(input, "%s", &f)
fmt.Println(err)
fmt.Printf("Please enter second string:\n")
_, err = fmt.Fscanf(input, "%s", &s)
fmt.Println(err)
}
- Unix系での実行
Please enter first string:
hoge
DEBUG: "h" <nil>
DEBUG: "o" <nil>
DEBUG: "g" <nil>
DEBUG: "e" <nil>
DEBUG: "\n" <nil>
<nil>
Please enter second string:
fuga
DEBUG: "f" <nil>
DEBUG: "u" <nil>
DEBUG: "g" <nil>
DEBUG: "a" <nil>
DEBUG: "\n" <nil>
<nil>
Process finished with exit code 0
- Windows系での実行
Please enter first string:
hoge
DEBUG: "h" <nil>
DEBUG: "o" <nil>
DEBUG: "g" <nil>
DEBUG: "e" <nil>
DEBUG: "\r" <nil>
<nil>
Please enter second string:
DEBUG: "\n" <nil>
unexpected newline
上記の結果から、Windows 系では 2 回目の読み込みの際に \n
を読み込もうとしていることがわかります。
で、これは Pob Pike 氏が以下のようにコメントしています。Oh...
The scanning code in fmt is cursed. cc @rsc
進行中のCL
レビュー中の CL は以下です。
暫定対応
暫定的には以下のように末尾に \n
を付与する ことで WindowsOS でも想定通りエラーにならずに動作します。(Unix 系の OS でも同様に動きます)
func main() {
var f, s string
fmt.Printf("Please enter first string:\n")
- _, err := fmt.Scanf("%s", &f)
+ _, err := fmt.Scanf("%s\n", &f)
fmt.Println(err)
fmt.Printf("Please enter second string:\n")
- _, err = fmt.Scanf("%s", &s)
+ _, err = fmt.Scanf("%s\n", &s)
fmt.Println(err)
}
Please enter first string:
hoge
<nil>
Please enter second string:
fuga
<nil>
参考: https://stackoverflow.com/questions/17401709/why-doesnt-fmt-scanf-in-go-wait-for-user-input/17403785
エラーにならずに文字列を読み込むことができました。