web小説礼賛記

さらなる俺 TUEE をあなたに!

シェル入門 #7: 基本操作の作成

シェルマッチョになったため全面改稿予定

 目次の閲覧など、基本的な操作をn-xxxというコマンドにし、シェルから利用できることにします。これらのコマンドを、n-シリーズと名付けます。n-シリーズは~/binに置くことにします。

 早く動かしたいため、n-commands sub-commandという形式は止めました。サブコマンドを使う場合、サブコマンド間で実装の共有などができるのがメリットです。

 mobaはシェルスクリプトの本を読んでいないため、いい加減なコードを書きます。御免なさい。とにかく動けばOKだと割り切っています。

n-seriesの作成

前提

 これらのコマンドは、小説ディレクトリの上で実行されるものとします。エラーチェックは、ほどほどにしかやりません。

 ディレクトリ構成は、

$ tree -d -L 1
.
├── build
├── gnome
└── src
  • src/に原稿が入り、build/に原稿の変換結果が入ります。gnome/に設定ファイルを入れます。

  • 原稿の名前はすべてn.txtで、一行目にタイトルが書いてあるとします。

作成用 Creaters

n-new

 touch -aとするだけで、既存ファイルのmodification timeの更新を避けることができます。

 これもコマンドにします(~/bin/n-new)。また、小説データのルートディレクトリから使用し、番号のみを与えて使うことにします。

#!/bin/bash
echo "$@" | xargs -I{} touch -a 'src/{}.txt'

 n-new {0..24}のように使えます。スクリプトの作成後は、chmod +x ~/bin/*で実行許可を与える必要があります(x: execute)。n-newを実行し、ファイルが作られることを確認しましょう。

stdinを受け取れるようにする

 先ほどのコマンドは、引数しか使用しませんでした。標準引数も入力として扱えるようにします。

 この辺り、全く『正しい』方法なのか分かりません。特にシェルは、確信を持つのが難しい言語ですね。検索では、肝心な部分はヒットしませんし 笑

# cat は stdin が空の場合フリーズするため、分岐が必要
if [ -p /dev/stdin ] ; then
    echo "$(cat) $@"
else
    echo "$@"
fi | tr ' ' '¥n' | grep -v '^$' |
while read n; do
    # 注意: このとき、nは一つの引数であるが
    #     任意の文字が許されており、番号ではない可能性がある
    touch -a "src/$n.txt";
done
  • echo "$@" は全ての引数をスペース区切りにします。
  • tr (translate) は文字の置換を行います。
  • grep -v '^$' は空行を削除します。

 何度も書く部分なので、以後どのファイルからも、while read 以前は省略します。

番号 -> 原稿ファイルのパス を抽象化

 後でn-pathコマンドを作り、n-newでもn-pathコマンドを使うようにします。

    touch -a "$(n-path $n)"
エラーチェックを追加

 飛ばして行きますね。

# 省略: stdin と引数を改行区切りで連結する処理
..
while read n; do
    # n が整数でなければ、エラー出力をして次のnへ
    [[ ! n =~ ^0$|^[1-9][0-9]*$ ]] || {
        echo "not given valid number: $n" 1>&2
        continue
    }
    touch -a "$(n-path $n)"
done
echo .. 1>&2

 stderr2に出力しています。#3でも書きました。

[[]]=~

 正規表現でnを0以上の整数とマッチさせています。(マッチしなければリスト{}を実行)。

 [[]]は強力なテストです。man bashの329行目に書いてあります。=~正規表現とマッチングできます。

  • 正規表現をクオーツで囲うとエラーなので注意してください(bash)。
  • 0を使うとあ0あみたいなものにも部分的にマッチする(結果、[[]]の終了ステータスが、マッチ成功の0になる)ので、^0$とすることで完全一致を検出します。`
  • [0-9]\d(d: digit)とは書けません。文字クラス:digit:`はありますが、使いたくないですね。

 [[ .. =~ // ]] の代わりに、grepの終了ステータス$?(マッチが無ければ1)を使うこともできます。

 grepで検証する場合、出力を (/dev/nullに) 捨てる手間があります。

 bashとは厄介な言語ですね。このコマンドが書ければ、他のコマンドも十分書けます。

閲覧用 Viewers

目次生成 n-table

 #5でもやりました。sortはfindの直後にやってもいいです。

#!/bin/bash
# -d: ディレクトリが存在しなければ終了(エラー)
[ -d src/ ] || exit 1
find src/* -type f | while read f; do
    printf '%2d %s\n'  "$(basename "$f" .txt)" "$(sed -n 1p "$f")"
done | sort -n

 ひとまず、話数が2桁の前提で整形して書きました。もしくは、多めの桁数でフォーマットしておけば安全です。

 テストコマンド[]中の表現-については、man [を参照してください。制御文字&&については、man bashを参照してさい。

文字数カウントするオプション

 -cもしくは--countオプションが指定されていた場合、文字数も表示する処理に切り替えます。エイリアスでデフォルトにしてもいいでしょう。

 目次や中間データに対して列を足していくスタイルもアリです。今は3つしかありませんが、列の組み合わせは二項分布で増えていくので。

 オプションの分解(パース)は、今回は単に$1を調べれば良いです。

if ! ( [ "$1" == '-c' ] || [ "$1" == '--count' ] ) ; then
    # 文字数カウント無し
    find src/* -type f | while read f; do
        printf '%2d %s\n' "$(basename "$f" .txt)" "$(sed -n 1p "$f")"
    done
else
    # 文字数カウント有り
    find src/* -type f | while read f; do
        printf '%2d %4d %s\n'       \
            "$(basename "$f" .txt)" \
            "$(cat "$f" | wc -m)"   \
            "$(sed -n 1p "$f")"
    done
fi | sort -n
wcの出力を使う

 wcの出力が、閲覧用に整えられているのが厄介です。頭に四つの空白がありますが、printfが数値として評価し、空白を消してくれました。

 wcの出力を空白区切りと見て、フィールドを抜き出す方法もあります。awkが無難です: wc -m <file> | awk -F ' ' '{print $1}'

 cutでフィールドを抜き出す場合、空のフィールドを無視しないため、5番目のフィールドにアクセスして、.. | cut -d ' ' -f 5となります(実装によるかもしれませんが)。先に、sedで行頭の空白を消し、連続する空白を圧縮しておくと確実です。

総文字数

 総文字数は、wcの出力を使うなら、wc -m src/* | tail -1。もしくは、文字数カウント付きの目次に対し、awk '{sum+=$2; print $0} END {print " " sum " chars in total."}'などとして得られます。

後から整形する

 タブ文字\t区切りのデータを作り、最後に空白の整形をします。awkを使うのが基本だと思います。

#!/bin/bash
..
# -F: field separator
fi | sort -n | awk -F '\t' '{printf "format", paramA, paramB, .. }'

 左寄せでよければcolumn -t -s '\t'も使えます。(t: table, s: separator)

確実な配列 ensureing alignment

 tab文字区切りの目次にスペースを入れて、列ごとに右寄せや左寄せにします。自動的にやってくれるコマンドは無いようです(table化するツールは? 要改稿)。

 配列用のコマンド(もしくは一般的なテーブリングコマンド)を作っても良いかもしれません。案としては、データの最大値(もしくは最大桁数)を記憶しておいて、awk内のprintfで配列(整形)できます。

専用の文字数カウントを使う

 空白文字をカウントしない、タイトルを文字数に含めない、などの配慮をしてくれるコマンドを使用します。(#8で作るn-count)

 front matter付きのファイルにする場合を考えても、専用コマンドでカウントすべきです。

| columnの自動化
エイリアスを設定する
# .bashrc
n-table() { command n-table "$@" | column ; }
パイプ先なら出力に対し| columnを行う

 多分こんな感じ。要改稿。

if [ -p /dev/stdout ] ; then
    cat | column
else
    cat
fi

n話目の内容をコピー n-pc

 pbcopyは、stdinをpaste board(クリップボード)にコピーするコマンドです。

 mobaはpcエイリアスを作っています。

 n-pcにより、n話目をpbcopyします。n-build自体は、#8で作ります。

#!/bin/bash
# TODO: 出力先ディレクトリの設定

# 0以上の整数が与えられなければ終了
[[ "$1" =~ ^0$|^[1-9][0-9]*$ ]] || {
    echo 'error: not given number.' 1>&2
    exit 1
}
# ファイルが存在しなければ終了
[ -f "build/$1.txt" ] || {
    echo "error: number $1 doesn't exist in your build." 1>2&
    exit 1
}
cat "build/$1.txt" | pbcopy

 n-build関連の部分はコメントアウトしておきました。実際、コピーする度にbuildするのは、安全ですが重いです。ユーザがbuildを忘れないようにする方針で行きます。

編集用 Editors

 ちょっとしたコマンドの有無で大違いです。

通し番号 -> 原稿のパス n-path

 基本的な道具から作ります。スペース区切りの番号を、改行区切りのsrc/n.txtに変えます。

#!/bin/bash
# 通し番号 -> 原稿のパス の変換を行う

# stdin と引数を改行区切りで連結する処理 |
while read n; do
    if [[ ! "$n" =~ ^0$|^[1-9][0-9]*$ ]] ; then
        echo "invalid arugument: $n" 1>&2
        continue
    fi
    echo "src/$n.txt"
done

 動作を確認します。

$ echo '-2' | n-path {-1..0*
invalid arugument: -2
invalid arugument: -1
src/0.txt
$ echo '-2' n-path {-1..0} 1>/dev/null
invalid arugument: -2
invalid arugument: -1

タイトルの変更 n-title

 つまり原稿の一行目を差し替えます。n-title number titleという形で使います。

一行目を消してから入力内容を挿入する

 行の削除は、seddコマンドで行います。ファイルの上書きは、tee経由なら、リダ入れクションよりも確実でしょう。

#!/bin/bash

# 原稿が存在しなければ終了
[ -f "$(n-path "$1")" ] || {
    echo "error: not given number: '$1'" 1>&2
    exit 1
}

# "$1"話目の原稿の一行目を"$2"に差し替える
cat "$(n-path "$1")"         |
    sed '1d'                 |
    printf "$2\n%s" "$(cat)" |
    tee "$(n-path "$1")" > /dev/null

 gsed -i "1s/.*/$2/g"とか、BSD系のsedsed -i '' "1s/.*/$2/g"としてもいいです。

話の入れ替え n-swap

 シェルコマンドには、ファイルのswap(入れ替え)が無いのですね。

#!/bin/bash

[ ! -f n-path "$1" ] && [ ! -f n-path "$2" ] || {
    echo 'not given two valid numbers.' 1>&2
    exit 1
}

# swapping
mv "$(n-path $1)"{,.cptemp}
mv "$(n-path $2)" "$(n-path $1)"
mv "$(n-path $1).cptemp" "$(n-path $2)"

 要改稿: mktempコマンドを利用し、安全に入れ替える

 簡単にテストしておきます。

~/Dropbox/n/narou/ms $ n-table
 0 プロローグ Prologue      13
 1 悪の製法 Making of Evils  14
 2 タイトル            15
 3 あああ           16
 4 いいい           17
 5                 18
<省略>
$ n-swap 3 4
$ n-table
 0 プロローグ Prologue      13
 1 悪の製法 Making of Evils  14
 2 タイトル            15
 3 いいい           16
 4 あああ           17
 5                 18
<省略>

※ターミナルではきちんと揃って表示されています。

n-edit

 エディタを開くコマンドを用意します。今回はパスが空白を含まないため、シェルの単語分解に任せてvim $(n-path "$@")とすることもできます。(Vimを使う場合)。

 いくつか方法を試しましたが、『Vim: Warning: Input is not from a terminal』というエラーが出ることが多いです。厄介な仕様があるようです。

 これだから、シェルは程々でいい気がします……。

 BSD系のxargsだと話が早いです。

#!/bin/bash
n-path "$@" | tr '\n' '\0' | xargs -0o vim

 この素晴らしい回答を参考に、gxargsでも書いてみました。

n-path "$@" | gxargs -d '\n' sh -c '</dev/tty vim "$@"' zero
# ヌル文字区切り版(BSD系のxargsでも使えてベター)
n-path "$@" | tr  '\n' '\0' | gxargs -0 sh -c '</dev/tty vim "$@"' zero

終わり

 お疲れ様でした! n-new, n-table, n-pc, n-path, n-count (現在はwc -m), n-title, n-swap, n-vim が用意できました。無敵に近づいた気がしませんか?(執筆力は上がりませんが)

 同じような操作を何度も書き過ぎました。まだ改善が見込めますが、それは後のお愉しみ。