シェル入門 #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
という形で使います。
一行目を消してから入力内容を挿入する
行の削除は、sedのd
コマンドで行います。ファイルの上書きは、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系のsedでsed -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 が用意できました。無敵に近づいた気がしませんか?(執筆力は上がりませんが)
同じような操作を何度も書き過ぎました。まだ改善が見込めますが、それは後のお愉しみ。