HHeLiBeXの日記 正道編

日々の記憶の記録とメモ‥

各言語での1行読み込み時に得られる文字列の違い

手元にある各言語で、「1行」を読み込んだ時の文字列の違い、とりわけ改行コードの扱いについて調べてみたメモ。

環境

手元にあるものということで、環境は以下のものに限定する。

繰り返すが、環境は「CentOS 7」。ということで、改行コードは「LF」(一般に"\n"で表されることが多い)が標準となっている環境。

入力ファイル

od -cした結果で示す。

0000000   h   o   g   e  \r  \n   f   o   o  \r   b   a   r  \n   e   n
0000020   d
0000021

改行コードとして一般的に知られている3つのパターンを含み、かつファイルの末尾には改行コードが無いというファイルである。

  • "hoge"の後には、Windows系のOSで標準とされる改行コード「CR LF」
  • "foo"の後には、昔の(?)MacOS系のOSで標準とされる改行コード「CR」
  • "bar"の後には、UNIX/Linux系のOSで標準とされる改行コード「LF」
  • "end"の後には、いきなりEOF

プログラムでの処理

各言語で書くプログラムは、入力の末端に到達したと判定されるまで「1行」を読み込み、先頭にスラッシュ('/')を付加して「1行」の区切りが分かるようにしたうえでそのまま出力。その際に追加の改行コードが出力されないような方法で出力する。

出力パターン

答えを先に言ってしまう形になるが、試したケースで計6パターンの出力が得られた。 以下のような感じである。

  • パターン1:いわゆる改行コードはすべて認識し、得られる文字列からは取り除かれる
0000000   /   h   o   g   e   /   f   o   o   /   b   a   r   /   e   n
0000020   d
0000021
  • パターン2:「LF」が改行コードと認識され、かつ得られる文字列に改行コードが含まれる
0000000   /   h   o   g   e  \r  \n   /   f   o   o  \r   b   a   r  \n
0000020   /   e   n   d
0000024
  • パターン3:「LF」が改行コードと認識され、得られる文字列からは取り除かれる
0000000   /   h   o   g   e  \r   /   f   o   o  \r   b   a   r   /   e
0000020   n   d
0000022
  • パターン4:「CR LF」と「LF」が改行コードと認識され、得られる文字列からは取り除かれる
0000000   /   h   o   g   e   /   f   o   o  \r   b   a   r   /   e   n
0000020   d
0000021
  • パターン5:「LF」が改行コードと認識され、得られる文字列からは取り除かれ、さらに改行コードの前にEOFが来ると末端と認識されてしまう
0000000   /   h   o   g   e  \r   /   f   o   o  \r   b   a   r
0000016
  • パターン6:「CR LF」と「LF」が改行コードと認識され、得られる文字列からは取り除かれ、さらに改行コードの前にEOFが来ると末端と認識されてしまう
0000000   /   h   o   g   e   /   f   o   o  \r   b   a   r
0000015

Java

念のため、BufferedReader版とScanner版の両方を試した。

BufferedReader版

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;

public class Main {
    public static void main(String[] args) {
        try (BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
            PrintWriter out = new PrintWriter(System.out)
        ) {
            String buf;
            while ((buf = in.readLine()) != null) {
                out.print('/');
                out.print(buf);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Scanner版

import java.io.PrintWriter;
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        try (Scanner in = new Scanner(System.in);
            PrintWriter out = new PrintWriter(System.out)
        ) {
            while (in.hasNextLine()) {
                String buf = in.nextLine();
                out.print('/');
                out.print(buf);
            }
        }
    }
}

C

#include <stdio.h>

int main(int argc, char** argv) {
    char buf[1024];

    while (fgets(buf, sizeof(buf), stdin)) {
        printf("/");
        printf("%s", buf);
    }

    return 0;
}

C++

念のため、cin.getlineとstringヘッダのgetlineの両方を試した。

cin.getline

#include <iostream>

using namespace std;

int main(int argc, char** argv) {
    char buf[1024];

    while (cin.getline(buf, sizeof(buf))) {
        cout << '/';
        cout << buf;
    }

    return EXIT_SUCCESS;
}

stringヘッダのgetline

#include <iostream>
#include <string>

using namespace std;

int main(int argc, char** argv) {
    string buf;

    while (getline(cin, buf)) {
        cout << '/';
        cout << buf;
    }

    return EXIT_SUCCESS;
}

PHP

<?php

$lines = file('php://stdin');

foreach ($lines as $line) {
    echo '/' . $line;
}

Python 2

import sys

while True:
    line = sys.stdin.readline()
    if line == '':
        break
    sys.stdout.write('/')
    sys.stdout.write(line)

print文を使うと、末尾に勝手に改行コードが挿入されるので、sys.stdout.writeを使う。

Python 3

import sys

while True:
    line = sys.stdin.readline()
    if line == '':
        break
    sys.stdout.write('/')
    sys.stdout.write(line)

print文を使うと、末尾に勝手に改行コードが挿入されるので、sys.stdout.writeを使う。

Ruby

while line = STDIN.gets
    print '/'
    print line
end

Perl

while (my $line = readline(STDIN)) {
    print '/';
    print $line;
}

Go

package main

import (
    "bufio"
    "fmt"
    "io"
    "os"
)

func ReadLine(reader *bufio.Reader) (str string, err error) {
    prefix := false
    buf := make([]byte, 0, 1024)
    line := make([]byte, 0, 1)
    for {
        line, prefix, err = reader.ReadLine()
        if err == io.EOF {
            return
        }
        buf = append(buf, line...)
        if prefix {
            continue
        }
        str = string(buf)
        return
    }
}

func main() {
    stdin := bufio.NewReader(os.Stdin)

    for {
        line, err := ReadLine(stdin)
        if err == io.EOF {
            break
        }

        fmt.Print("/")
        fmt.Print(line)
    }
}

bash

環境変数IFSの値によって挙動が変わるだろうということで、3パターン試した。

IFS=$'\n'

#! /bin/bash

while IFS=$'\n' read line ; do
    echo -n /
    echo -n ${line}
done

IFS=$'\r'

#! /bin/bash

while IFS=$'\r' read line ; do
    echo -n /
    echo -n ${line}
done

IFS=

#! /bin/bash

while IFS= read line ; do
    echo -n /
    echo -n ${line}
done

Awk

{
    printf("/");
    printf("%s", $0);
}

sed

言語なのかという突っ込みがありそうだが、行処理なら任せろというツールの一つであるので外せないということで。

s|^|/|

行頭にスラッシュを挿入するという単純な正規表現

まとめると‥

  • パターン1:いわゆる改行コードはすべて認識し、得られる文字列からは取り除かれる
  • パターン2:「LF」が改行コードと認識され、かつ得られる文字列に改行コードが含まれる
  • パターン3:「LF」が改行コードと認識され、得られる文字列からは取り除かれる
  • パターン4:「CR LF」と「LF」が改行コードと認識され、得られる文字列からは取り除かれる
  • パターン5:「LF」が改行コードと認識され、得られる文字列からは取り除かれ、さらに改行コードの前にEOFが来ると末端と認識されてしまう
  • パターン6:「CR LF」と「LF」が改行コードと認識され、得られる文字列からは取り除かれ、さらに改行コードの前にEOFが来ると末端と認識されてしまう
言語 1 2 3 4 5 6
Java(BufferedReader) O
Java(Scanner) O
C O
C++(cin.getline) O
C++(getline) O
PHP O
Python 2 O
Python 3 O
Ruby O
Perl O
Go O
bash(IFS=$'\n') O
bash(IFS=$'\r') O
bash(IFS=) O
Awk O
sed O

bashで「end」が出力されないというのは予想外だった。

bashを除くと、Go言語がちょっと特殊。