Redirecting Contents of File to Standard Input

1行シェルスクリプトEmacsで極力物事を済ます方針になって久しいのですが、コンテスト中は時間がないので多少のスクリプトを用意しています。先日コンテスト中にこんなスクリプトがあったらいいなあ、と考えついたので早速作成しました。

  1. Emacsでテキストを加工する。具体的には「入力」「空行」「出力」「空行」...という形式とする
  2. コマンドを適用すると、「入力」「出力」がsample1.txt、answer1.txt、...と出力される

例えば以下のようなファイルを用意します。

3 2
1 2 3
1 2
3 2

5

4 3
1 2 3 4
1 2
3 4
3 4

8

これにコマンドを適用すると、以下のようにファイルに格納されるようにしたいのです。

sample1.txt:
3 2
1 2 3
1 2
3 2

answer1.txt:
5

sample2.txt:
4 3
1 2 3 4
1 2
3 4
3 4

answer2.txt:
8

こういう処理はシェルスクリプトでさっさと書いてしまいましょう。

unpack.sh:

#!/bin/sh

i=1

bodyname="sample"

test -f "$bodyname$i.txt" && rm -f "$bodyname$i.txt"

while read l; do
  if ! echo "$l" | grep '^ *$' > /dev/null; then
    echo $l >> "$bodyname$i.txt"
  else
    if [ "$bodyname" == "sample" ]; then
      bodyname="answer"
    else
      bodyname="sample"

      i=`expr $i + 1`
    fi

    test -f "$bodyname$i.txt" && rm -f "$bodyname$i.txt"
  fi
done

早速テストして...いい感じですね。内容もよいようです。

> ls
a.txt
> unpack.sh < a.txt
> ls
a.txt       answer2.txt sample1.txt sample3.txt
answer1.txt answer3.txt sample2.txt

さて、これを自分の環境にリリースしたのですが、どうも自分の操作と相性が合わないのです。どうしてだろう? と観察してみたのですが、Emacs diredから!でファイル操作ができないのが原因でした。Emacs diredから!とするにはシェルスクリプトがファイル名を引数として得て処理できなければなりません。

これを解決するのは簡単で、以下の1行を代入するだけでした。

           :
test -f "$bodyname$i.txt" && rm -f "$bodyname$i.txt"

test -f "$1" && exec <"$1"   # この行を挿入

while read l; do
           :

つまるところ、引数としてファイルが与えられていた場合、ファイルの内容を標準入力にリダイレクトしてしまえばよいのです。

さて、これでEmacs diredから快適にコマンドを使うことができるようになりました。今から使う機会が楽しみです。

少しばかりの続き

さて、このスクリプトであると空行が2つ以上続くとよくない結果となります。

> ls
a.txt
> cat a.txt
1  # sample1.txtに書き込まれるはず

2  # answer1.txtに書き込まれるはず


3  # sample2.txtに書き込まれるはず

4  # answer2.txtに書き込まれるはず
> unpack < a.txt
> ls # sample2.txtがない? sample3.txtがある??
a.txt       answer1.txt answer2.txt sample1.txt sample3.txt

空白毎に出力するファイルを更新しているのが原因です。これを防ぐために、予めsedを適用して調整しています。

           :
test -f "$1" && exec <"$1"

sed -ne '
s/^  *$//
H
$ {
  g
  s/^\n*//g
  s/\n*$//g
  s/\(\n\n\)\n*/\1/g
  p
}
' | while read l; do
           :

このsedスクリプトも面白いので興味があれば是非処理を追ってみてください。具体的には、「全て読み込んでから連続する改行を取り除く。ただし、各行が空白を含む空行であればこれも処理する」といったことをしています。また、最新のスクリプトこのGistで管理しています。