bashで行の追加について

bashは行指向のため、
行の追加は難しい。

前置きが長くなるが、
まずは、edコマンドの説明から入る。

edの使い方なんて分からなくて良いという人も
いるかもしれないが、
sed,vi,grepの役割を理解する上で重要、
かつ、今でも実践的なので、
ここに書く。


$ ed 開くファイル

と指定すると、
ファイルを開いて、操作できるようになる。
1pとすると
一行目が表示されるし、
2dとすると
二行目が削除される。
wでファイルを書き込むことになる。

コマンド、アドレスの指定の仕方は
以下のサイトに描かれているので参考にしてほしい。
http://www.uetyi.com/server-const/entry-271.html
SunOSのedのレファレンス
https://docs.oracle.com/cd/E19253-01/819-1210/ed-1/index.html

つまり、sedと違って、
ファイルを直接編集するコマンドである。

edのコマンドのうち
今でも重要なのは
挿入に関する
i  指定の行、文字列の前に挿入する
a  指定の行、文字列の後に挿入する

である。

なぜこのコマンドが重要かというと

まず、
壊れても良いファイルを用意して、
linuxで下のコマンド
を実行してほしい。

$ cat ファイル | sed '1i hellosed'

catせずにできるのでは?

当たり前だが、
元のファイルの一番上の行にhellosedと書かれた
文章が表示される。

また、
$ sed -i '1i hellosed' ファイル
とすると
ファイルが上書きされる。

ここまで来てこれは当たり前のことだと思うかもしれないが、

今度はBSD系統のOS(FreeBSD,OpenBSD等)でこれを実行してほしい。
(macを持っている人はmacでよい。)

おそらく実行できないはずだ。

なぜBSD系統だと実行できないか。

これは、BSDがLinuxに対して劣っているわけでなく、
sedはもともとedの置換の機能に特化したもので、
そもそも文字列を挿入するためのものでないからである。
他にもgrepはedのグローバル(g)で正規表現(re)を表示(p)するためものという
役割なので、grepはあのような動きを取るようにしている。

ここら辺の歴史的な経緯、
名前の由来は

sed&awk プログラミング改訂版
出版社 オライリージャパン
に描かれている

古い本だけど、
今でも十分通用する内容です。
(awkの部分に関しては、今はpython、ruby,Powershellを使ったほうが
良い部分もあるが)

BSDはこの歴史と、
UNIX思想由来の「ひとつの
コマンドはひとつのことだけうまくやらせる」
というルールを愚直に守っている。
そのため、sedがこのような仕様になっている

話が脱線するが、
コマンドがclassみたいになっていて、
コマンドのオプションがクラスの関数のように
働くのはUNIX思想に反する
ので注意しよう(UNIX思想について
全く気にしないならOK)。

なので、
一行目だけファイルに挿入したものを
表示するなら、下のようになる。
BSD系統のOS
cat << EOF | ed -s csvawk 2>/dev/null
> 1i
> helloed
> .
> ,p
> q
> q
> EOF



1iでファイルの一行目にhelloedと書き込んだあと、
,pでファイル全体を表示し、
qでエディタを終了させている。

cat << EOF
でなくて、
echo -e "1i¥nhelloed¥n.,q¥nq¥nq" | ed -s csvawk 2>/dev/null
でもよい。
-eは改行等特殊文字を展開するコマンド


edの-sオプションはファイルに書き込んだ文字数と
を表示しないようにしている。

/dev/nullで標準エラー出力を捨てているが、
これはファイルに書き込んだものをセーブしないときに
出る「?」というエラーを捨てるために行っている。


上書きを行う場合は下のようになる。

cat << EOF | ed -s csvawk
> 1i
> helloed
> .
> w
> q
> EOF


ファイルに対して挿入、上書きに関するスクリプトで、
BSDでもLinuxでも動かしたい場合は
sed,でなく、edで書いたほうが良いということになる。

ちなみにedは上書き用のコマンド
なので注意すること。

問題は、パイプの上流から、来た行を処理するときだ。
Linuxなら、
sed '行番号s 追加したい文字列'
で処理できるが、
BSDなら、
awk '{
    if(NR ==  指定の行){
         print "追加したい文字列"
    }
    print $0
}'
とする必要がある。
LinuxでもBSDでも動かすには、
後者の書き方で統一する必要があるが、
これはあまりにも不格好だ。

なので、筆者はコマンドを自作することを考えた。

GO言語で書くので、
golangをあらかじめインストールしてほしい。

$ mkdir  -p ~/mysrc/golang/insertrow/
として、
~/mysrc/golang/insertrow/
に作業用フォルダを作成し、

そこに
insertrow.goファイルを作成

package main

import (
    "bufio"
    "flag"
    "fmt"
    "io"
    "io/ioutil"
    "os"
)

//処理するまえにエラーがわかったら標準入力を捨てて、
//処理途中でエラーがでたら標準入力がそのままで処理する。
//パイプ上流から受け取らなかったらおうむ返しになるのはsedも同じなのでよい。
//最終行から挿入は標準入力 | tac | insert -n | tac //Linuxの場合
//最終行から挿入は標準入力 | tail -r | insert -n | tail -r
var (
    inrow = flag.Int("n", 1, "Insert row")
)

//標準入力を標準エラー入力にリダイレクト
func main() {

    //コマンドラインから持ってきたオプションのパース処理。これがないと正しく動かない。
    flag.Parse()

    if *inrow < 1 {
        fmt.Fprintln(os.Stderr, "-nオプションには1以上の数字を入力してください。")

        //golangはioutil.Discardを使って捨てることになる。
        io.Copy(ioutil.Discard, os.Stdin)

        os.Exit(1)
    }

    //フラグ処理をした後の最初の引数が入る。引数がない場合は空の文字列が入る。
    mes := flag.Arg(0)

    cin := bufio.NewReader(os.Stdin)

    var row []byte
    var err error

    //行を挿入したかどうかに必要
    isInserted := false
    row, _, err = cin.ReadLine()

    i := 1
    for ; err != io.EOF; row, _, err = cin.ReadLine() {

        if err != nil && err != io.EOF {

            fmt.Fprintln(os.Stderr, "正しく読み込めない行があります。")
        }
        if i == *inrow {

            fmt.Fprintln(os.Stdout, mes)

            isInserted = true
        }
        fmt.Fprintln(os.Stdout, string(row))
        i++
    }

    //標準入力を最後まで読んで指定された行数の方が多い場合はエラーになる。
    if *inrow > i {

        fmt.Fprintln(os.Stderr, "標準入力に入っている行数より多い行数を指定しました。")

        os.Exit(1)
        //標準入力の最後の行に追加する場合は下になる。
    } else if !isInserted && err == io.EOF {
        fmt.Fprintln(os.Stdout, mes)

        isInserted = true
    }

}



~/mysrc/golang/insertrow
に移動して
$ go build insertrow.go

コンパイルして、
バイナリファイルのinsertrowを作成

~/.bashrcに
~/binへの
パスを通して、

$cp ~/mysrc/golang/insertrow/insertrow ~/bin/insertrow

としてコマンドから実行できるようにする。

使い方

-nオプションで
行数指定。
指定しなければ、デフォルトでは一行目に挿入する

また、
文字列を渡すと、挿入する行に文字列を追加する。
文字列を指定しなければ空文字を入れる。


$ ls -1 / | insertrow -n 2
二行目に文字列が空白の行が挿入される。

$ls -1 / | insertrow "hello"
一行目にhelloが追加される。

下の行から2番目に追加
(BSDの場合。Linuxの場合はtail -r の代わりにtacを使う)
$ ls -1 / | tail -r | insertrow -n 2 "hello" | tail -r


まとめ
ファイルの一部に挿入して、表示,上書きは
Linuxだけならsed。
edを使うとBSD,Linuxでも動くようになる。

パイプの上流から来たものに関しては
Linuxだけならsed。
awkを使うとBSD,Linuxともに動くようになる。
しかし、不格好なので、コマンドを
自作することも考える。

また、edコマンドは
数少ない上書き用のコマンドのため、
覚えておくこと。
これがないと、いちいち一時ファイルを作成して、
それをcpコマンドで上書きする
みたいなコマンドを書くしかなくなる。


コメント

人気の投稿