ファイルを二分割する


レギュレーション

  • ファイルを受け取り、それを二分割して二つのファイルを生成せよ。
    • 分割する条件は行数とする(32768行目で分割、等)。
    • 入力はメモリに乗りきらないことも考慮せよ。
  • awk(1), perl(1) 等のワンライナーを使ってはならない。それで実現可能なのは自明であり面白くない。
  • bash(1), zsh(1) 等の拡張機能を使ってはならない。シェルはdash(1)とする。

テストファイル

$ yes yes | head -c 40000 > input.txt
$ wc input.txt
10000 10000 40000 input.txt
$

不合格な回答

split

split(1) といういかにもな名前のプログラムはこの用途には使えない。

$ split -l 4096 input.txt output
$ wc output*
 4096  4096 16384 outputaa
 4096  4096 16384 outputab
 1808  1808  7232 outputac
10000 10000 40000 total

望まないファイル outputac が生成されており要求仕様を満たさない。(ただし下記を見よ)

head; cat

(head -n ${x} > output1.txt; cat > output2.txt) < input.txt

この問題をググると出て来る回答 https://superuser.com/a/624240 これはNG。なんでかというとhead(1)がバッファリングするからデータが欠ける。

$ (head -n 7 > output1.txt; cat > output2.txt) < input.txt
$ wc output*
    7     7    28 output1.txt
 8976  8976 35904 output2.txt
 8983  8983 35932 total

7行出力するのに4096バイト読んじゃったから、足しても元に戻らない。

なお、head(1)の実装によっては頑張って無駄な読み込みを避けようとするものもあるようだ。が、絶対保証できるかというとパイプがseekで読み戻せないと厳しい気がするのでkernel依存の気がする。

間違いではないが難のある回答

head -n 負の数

head -n  ${x} input.txt > output1.txt
head -n -${x} input.txt > output2.txt

これはcoreutilsの拡張機能だ。POSIXを逸脱しており、移植性に問題がある。

wc; head; tail

x=1024
y=$(wc -l input.txt)
z=$((${x} - ${y}))
head -n ${x} input.txt > output1.txt
tail -n ${z} input.txt > output2.txt 

これは入力ファイルを3回スキャンしている。入力がめちゃくちゃ大きい時にペナルティが大きい。

split その2

split -l ${x} input.txt output
mv outputaa output1.txt
cat output?? > output2.txt
rm output??

上の方で split(1) は要求仕様を満たさないとしたが、切りすぎてるだけなので、あとからまた混ぜればいいじゃんという発想はあり得る。

上記のようにcatで後半を混ぜればレギュレーション通りの結果は得られる。ただし入力を二回スキャンすることになり、無駄ではある。

q

q -k "SELECT * FROM input.txt LIMIT ${x}" > output1.txt
q -k "SELECT * FROM input.txt WHERE ROWID > ${x}" > output2.txt

q(1)は、処理中にインメモリDBを作っているので、メモリに乗りきらない入力に対して不適切である。

あとawkがNGってレギュレーションなのにSQLがOKとするというのはちょっとずるい。

文句のない回答

csplit

csplit -f output input.txt $((${x} + 1))

csplit(1)はPOSIXに古くからあるコマンドであり移植性が高い。普通はファイルを正規表現で切るという使い方で解説されていることが多いが、行番号で切ることもできる。ただ+1する必要があるのがポイントか。

シェルのreadで

n=0
x=1024
while IFS= read -r line || [ -n "${line}" ]; do
    if [ ${n} -lt ${x} ]; then
        output=output1.txt
    else
        output=output2.txt
    fi
    n=$((${n} + 1))
    echo ${line} >> ${output}
done < input.txt

いくらdashの機能が貧弱とはいえこのていどのシェルスクリプトは実現可能である。