入出力クラス。
成果物
情報源
IO
基本的な入出力機能のためのクラスです。
File::Constants は、File から IO へ移動しました。
多言語化と IO のエンコーディング
Rubyはエンコードが面倒そうなイメージ。OS間の差異を簡単に吸収できるのだろうか?
影響を受けるメソッドでは、IO のエンコーディングに従い読み込まれた文字列のエンコーディングが決定されます。また IO のエンコーディングを適切に設定することにより、読み込み時・書き込み時に文字列のエンコーディングを変換させることもできます。
エンコーディングの影響を受けるメソッドと受けないメソッド
IO の読み込みメソッドは2種類存在します。テキスト読み込みメソッドとバイナリ読み込みメソッドです。
テキスト読み込みメソッドは IO のエンコーディングの影響をうけます。詳しくは「IO のエンコーディングとエンコーディングの変換」を参照して下さい。以下がテキスト読み込みメソッドです。
IO.foreach IO.readlines IO#each_line IO#gets IO#getc IO#ungetc IO#read IO#readchar IO#readline IO#readlines
バイナリ読み込みメソッドは IO のエンコーディングの影響を受けません。返す文字列のエンコーディングは常に ASCII-8BIT になります。以下がバイナリ読み込みメソッドです。
IO#read(size) IO#read_nonblock IO#readpartial IO#sysread
また書き込みメソッド IO#write も IO のエンコーディングの影響を受けます。 IO のすべての書き込みメソッドは内部で IO#write を呼びますので、書き込みメソッドはすべて IO のエンコーディングの影響を受けます。
IOの書き込みAPIはwrite
のみってことか?
シーク関連のメソッドはエンコーディングの影響を受けません。常に1バイトを単位として動作します。
例:
f = File.open('t.txt', 'r+:euc-jp') p f.getc.encoding #=> Encoding::EUC_JP p f.read(1).encoding #=> Encoding::ASCII_8BIT
IO のエンコーディングとエンコーディングの変換
IO オブジェクトは外部エンコーディングと内部エンコーディングを持ちます。外部エンコーディングとは IO が表すファイルなどの文字エンコーディングです。内部エンコーディングとは IO から読み込まれた文字列、あるいは IO の書き込みメソッドへ渡す文字列の文字エンコーディングです。
面倒くさすぎる。
以下の三通りがあります。
IO のエンコーディングが指定されていない場合
IO からテキスト読み込みメソッドによって読み込まれた文字列のエンコーディングは Encoding.default_external に設定されます。このとき実際の文字エンコーディングは検査されず、変換もされません。
外部エンコーディングのみが指定されている場合
IO からテキスト読み込みメソッドによって読み込まれた文字列のエンコーディングは外部エンコーディングに設定されます。このとき実際の文字エンコーディングは検査されず、変換もされません。
IO へ書き込まれる文字列は外部エンコーディングへと変換されます。外部エンコーディングへの変換方法が分からない場合は例外が発生します。
外部エンコーディングと内部エンコーディング(あるいは default_internal)が指定されている場合
IO からテキスト読み込みメソッドによって読み込まれた文字列は、外部エンコーディングから内部エンコーディング(あるいは default_internal)へと変換されます。指定された文字エンコーディングと実際の文字エンコーディングが違っていた場合、例外が発生します。内部エンコーディングと Encoding.default_internal が両方とも指定されている場合は、内部エンコーディングが優先されます。
IO へ書き込まれる文字列は外部エンコーディングへと変換されます。外部エンコーディングへの変換方法が分からない場合は例外が発生します。
IO に対してエンコーディングを指定する方法には、生成時に IO.open や File.open に渡すモードとともに指定するものと生成後に IO#set_encoding を使って指定するものの二通りがあります。詳しくはそれぞれのメソッドの項を参照して下さい。通常は前者の方法を使います。
例1:
f = File.open('file1') p f.getc.encoding #=> Encoding::EUC_JP
例2:
f = File.open('t.txt', 'w+:shift_jis:euc-jp') f.write "\xB4\xC1\xBB\xFA" # 文字列 "漢字" の EUC-JP リテラル f.rewind s = f.read(4) puts s.dump #=> "\x8A\xBF\x8E\x9A" # エンコーディングがSJISへ変換されていることが分かる。
まとめ
以上をまとめると以下の表のようになります。Encoding.default_external は常に設定されているので、省略してあります。
読み込んだ文字列のエンコーディング
状態 バイナリ読み込みメソッド テキスト読み込みメソッド -------------------------------------------------------------------------------- 指定無し ASCII-8BIT default_external default_internal のみ ASCII-8BIT default_internal 外部エンコーディングのみ ASCII-8BIT 外部エンコーディング 内部エンコーディング指定あり ASCII-8BIT 内部エンコーディング 内部エンコーディングと default_internal 両方 ASCII-8BIT 内部エンコーディング
エンコーディングの変換
バイナリ読み込みメソッド テキスト読み込みメソッド 書き込みメソッド --------------------------------------------------------------------------------------------------------------------- 指定無し 変換なし 変換なし 変換なし 外部エンコーディングのみ 変換なし 変換なし 変換あり default_internal のみ 変換なし 変換あり 変換あり 内部エンコーディングのみ 変換なし 変換あり 変換あり 外部エンコーディングと内部エンコーディング 変換なし 変換あり 変換あり 外部エンコーディングと default_internal 変換なし 変換あり 変換あり
デフォルトの外部エンコーディングの指定
Encoding.default_external はコマンドオプション -E で指定します。 -E が指定されなかった場合は次のような優先順位で決定されます。
-E (最優先) > -K > locale
つまりスクリプトは以下のように実行するのが安全ということか。
ruby -E UTF_8 main.rb
あれ? RubyはデフォルトがUTF_8でいいんだよね? マジックコメントの影響は? 忘れたのでググった。
スクリプトエンコーディングは、ソースコードに書かれたリテラルの文字コードである。
マジックコメントはスクリプトエンコーディングを設定する。デフォルトはUTF-8。
# encode: utf-8
magic comment(最優先) > -K > RUBYOPTの-K > shebang
上のどれもが指定されていない場合、通常のスクリプトなら UTF-8、-e や stdin から実行されたものなら locale がスクリプトエンコーディングになります。 -K オプションが複数指定されていた場合は、後のものが優先されます。
エンコーディング種別 | 影響範囲 | 設定方法 | 参照方法 |
---|---|---|---|
スクリプトエンコーディング | ソースコード内リテラル | マジックコメント | __ENCODING__ |
外部エンコーディング | ファイル、STDIN等 | ruby -E |
Encoding.default_external |
内部エンコーディング | ファイル、STDIN等 | ruby -E |
Encoding.default_internal |
ファイル名のエンコーディング
ファイル名の文字エンコーディングはプラットフォームに依存します。ファイル名の文字エンコーディングが固定されているプラットフォーム(Win, Mac)では、エンコーディングは暗黙に変換されます(予定)。UNIX では変換されずそのままシステムコールに渡されます。
怖すぎる。もしかしてRubyってクロスプラットフォームに書けないの?
Dir.glob, Dir.foreach などが返すファイル名のエンコーディングも同様にプラットフォーム依存です。 UNIX では ASCII-8BIT です。
なぜロケールを使わないのか。それならUTF-8なのに。日本語アウトじゃん。マジか。本当に? ひどくない?
いや、試しに日本語名のファイルを作成して確認したけど問題ないっぽい。どゆこと? ASCII-8BITって何者? まあいいや。
p Dir.glob('*')
["Ruby.docs.ruby.lang.org.20211028091759", "memo", "日本語"]
バイナリモード
Windows の IO にはテキストモードとバイナリモードという2種類のモードが存在します。これらのモードは上で説明した IO のエンコーディングとは独立です。改行の変換にしか影響しません。
EOF での読み込みメソッドの振る舞いの違い
空ファイルや EOF での各読み込みメソッドの振る舞いは以下のとおりです。ただし、length を指定できるメソッドに関しては、length に nil または 0 を指定した場合、 EOF であっても常に空文字列 "" を返します。
メソッド 空のファイルに対して IO.read(空ファイル) "" IO.read(空ファイル, length) nil IO.readlines(空ファイル) [] IO.foreach(空ファイル) 何もしない
メソッド 既にEOFだったら IO#each_byte 何もしない IO#getc nil IO#gets nil IO#read() "" IO#read(length) nil IO#read_nonblock EOFError IO#readchar EOFError IO#readline EOFError IO#readlines [] IO#readpartial EOFError IO#sysread EOFError IO#bytes 通常どおり IO#lines 通常どおり
これは酷い。6パターンもある。絶対に覚えられない。テストケースでテストしておかないと想定外の実行時エラーが起きるバグになりそう。たぶんそのメソッドにおける最適な戻り値なんだろうけど慣れないとハマりそう。
- 空文字
""
nil
- 空配列
[]
- なにもしない
- 通常どおり(ってなんだよ)
EOFError
メンバ抜粋
特異メソッド
binread binwrite copy_stream for_fd foreach new open pipe popen read readlines select sysopen try_convert write
インスタンスメソッド
<< advise autoclose= autoclose? binmode binmode? clone close close_on_exec= close_on_exec? close_read close_write closed? dup each each_byte each_char each_codepoint each_line eof eof? external_encoding fcntl fdatasync fileno flush fsync getbyte getc gets internal_encoding ioctl isatty lineno lineno= pid pos pos= pread print printf putc puts pwrite read read_nonblock readbyte readchar readline readlines readpartial reopen rewind seek set_encoding set_encoding_by_bom stat sync sync= sysread sysseek syswrite tell to_i to_io tty? ungetbyte ungetc write write_nonblock
定数
SEEK_CUR SEEK_DATA SEEK_END SEEK_HOLE SEEK_SET
読込
単位 | イテレータ | 配列 | 文字列 | 整数 |
---|---|---|---|---|
バイト | each_byte |
read |
getbyte ,readbyte |
|
字 | each_char |
getc ,readchar |
||
行 | each , each_line |
readlines |
gets ,readline |
each
IO.write("testfile", "This is line one,\nThis is line two,\nThis is line three,\nAnd so on...") f = File.new("testfile") f.each { |line| p "#{f.lineno}: #{line}" }
APIパターンが多い。
each(rs = $/, chomp: false) {|line| ... } -> self each(limit, chomp: false) {|line| ... } -> self each(rs, limit, chomp: false) {|line| ... } -> self each(rs = $/, chomp: false) -> Enumerator each(limit, chomp: false) -> Enumerator each(rs, limit, chomp: false) -> Enumerator each_line(rs = $/, chomp: false) {|line| ... } -> self each_line(limit, chomp: false) {|line| ... } -> self each_line(rs, limit, chomp: false) {|line| ... } -> self each_line(rs = $/, chomp: false) -> Enumerator each_line(limit, chomp: false) -> Enumerator each_line(rs, limit, chomp: false) -> Enumerator
chomp
は各行末から改行コード削除。limit
は最大バイト数。
書込
print
printf
putc
puts
pwrite
write
puts
IO.write("testfile2", "") f = File.new("testfile2", 'r+') f.puts 'putsのテスト出力。' f.puts '二行目。' #f.flush #f.fsync f.each { |line| p "#{f.lineno}: #{line}" } # 位置が末尾のため何も出力されない f.seek IO::SEEK_SET # 位置を先頭に戻す f.each { |line| p "#{f.lineno}: #{line}" } # 出力される
ファイルポインタの位置を意識している必要がある。書き込みが終了したときは末尾である。なので一旦先頭にシークしてやることで、再び先頭から読み取ることができるようになる。
所感
文字コードとか面倒くさそうだな。内部コードだけでよろしくやってほしい。指定しないときはUTF-8だけでいいよ。
対象環境
- Raspbierry pi 4 Model B
- Raspberry Pi OS buster 10.0 2020-08-20 ※
- bash 5.0.3(1)-release
- Ruby 3.0.2
$ uname -a Linux raspberrypi 5.10.52-v7l+ #1441 SMP Tue Aug 3 18:11:56 BST 2021 armv7l GNU/Linux