初歩のシェルスクリプトで遊ぶ[フィルタコマンドを繰り返し実行する『竹輪』]

フィルタコマンドを繰り返し実行するスクリプト『竹輪』

初歩のシェルスクリプトで遊ぶ[md5チェックサムのファイル書式を変換] - HatenaDiary id:Narr
先日のスクリプトのように、標準入力からデータを受け取って、加工し、標準出力からデータを出力するようなスクリプトのことを、フィルタとかフィルタコマンドとかいうようです。

個人的には、コマンドの類をフィルタと言うのは変な気がするのだけど……フィルタってのは、ローパスフィルタとかコーヒーフィルタとか換気扇のフィルタみたいなものであって、出てくる量が増えるならミキサーというのではなかろうか。


それは置いといて、フィルタコマンドでファイルひとつを変換処理するスクリプトを書いても、そのまま実行するだけではファイルは作られません。シェルから入力するファイルと作成するファイルを指定して、コマンドを実行しなきゃならない。

command 0< input-file1 1>output-file1

ファイルひとつなら手間ではありませんが、複数ファイルでは繰り返し処理を書かなきゃなりません。そのたびにスクリプトに繰り返し処理を追記するのも、ワンライナーで繰り返し実行するのも、面倒です。

初歩のシェルスクリプトで遊ぶ[ぬかみそフォントの制作サポート(20)] - HatenaDiary id:Narr
TTEditから書き出したcsvファイルをsvg画像へ変換するスクリプトですが、これも一文字分はフィルタコマンドです。
しかし、全文字分を変換処理するために、繰り返し処理やファイル名の処理を書いていました。

端末用ユーザインターフェースで簡単に繰り返し実行する

『竹輪』は、フィルタコマンドを登録すると、繰り返し実行の機能を提供します。
登録にスクリプトの追加は必要ありません。
コマンドラインでは、PATHの通った場所にスクリプトファイルをコピーすれば使えます。
端末用のUIでは、設定したディレクトリにスクリプトファイルをコピーします。シンボリックリンクでもよい。

command 0< input-file1 1>output-file1
command 0< input-file2 1>output-file2
command 0< input-file3 1>output-file3
……

というような処理ができます。
コマンドがもしcatならば、単純にファイルをコピーする処理になります。

標準入出力でファイルひとつずつを処理するコマンドであれば、テキストデータ以外でも使えます。
たとえば、たくさんのpng画像のファイルを、すべてjpeg画像に一括変換することもできます。このときスクリプトには一枚分の処理だけ書けばよく、繰り返し処理やファイル名の処理は書く必要がありません。というか、ファイル名は標準入出力なので設定できません。
ファイル名は『竹輪』側が決めます。初期設定では、入力ファイルと同じ名前で出力ファイルが作られます。拡張子を変更するオプションを使うと、画像ファイルの変換のときなどに、出力の画像形式に合わせた拡張子でファイルを作れます。


スクリプトは「フォント以外のダウンロード」のところに「renkon_chikuwa.7z」で置いてます。
『renkon』ってのは、私用で非公開の類似スクリプトです。こっちのほうが多機能なんですが、新しくコマンドをUIに登録するとき、多少はスクリプトを書かなきゃならない仕様なのです。使用頻度の低い機能は、あまり追加したくない。
そこで、『renkon』の機能の一部を代替する、機能は限定的だが設定が簡単なスクリプトを、というのが、この『chikuwa』です。

たとえばテキストファイルの文字コード変換機能であれば、2行で済みます。

#!/bin/sh
iconv -f cp932 -t utf8

WindowsシフトJISのテキストファイルを、utf8に変換するスクリプトなら、これだけです。ファイル名の処理も、繰り返し処理も、書く必要がありません。それらは『chikuwa』側が定形処理します。

機能スクリプト

スクリーンショットはユーザインターフェースのやつですが、機能の中心のスクリプトは分離して、独立実行できます。手入力では使いにくいですけど。

#!/bin/sh
#set -x
# ==================================================================================================
# ■ 「竹輪」-標準入出力でデータ処理するコマンドを繰り返し実行する_ver3
# 
# ▼ 基本動作
# 機能コマンド 0< 入力データ 1> 出力データ書き込み先
# 
# ${1} --cp ファイル作成のみ  --rm 入力ファイルを削除する
# ${2} 実行するフィルタコマンド
# ${3} 出力書き込み先ディレクトリ
# ${4} 出力ファイル名拡張子
# ${5}以降 入力ファイルパス
# 
# 
# ▼ 並列実行するとき xargs
# {
# for LL in *
# do
# printf '%s\000' "${LL}"
# done
# } | xargs -0 -n 1 -P "${xargsP}" \
# chikuwa.sh "${inplaceMode}" "${commandXX}" "${writeDir}" "${newExt}"
# 
# 
# 
# ==================================================================================================

# 端末操作時の簡易説明
# UIはCoreの存在確認に使う
if test "${#}" -eq 0 ; then
  echo "${0##*/}" '--cp|--rm filterCommand writeDir ext inputFile1 inputFile2 ...' 1>&2
  exit 0
fi

inplaceMode="${1:---cp}"
commandXX="${2:?}"
writeDir="${3:?}"
newExt="${4}"
# ${5} 以降 入力ファイルパス

if ! shift 4
then exit 255
fi
if test "${#}" -ge 1
then true
else exit 1
fi

# xargsから渡される引数の数を記録
#if test "${#}" -ge 2 ; then echo '${#}='"${#}" > `mktemp` ; fi # 【debug】

filterWrapFunc(){
# ${1} --cp ファイル作成のみ  --rm 入力ファイルを削除する
# ${2} コマンド
# ${3} ファイル作成先ディレクトリ
# ${4} 新規拡張子 空文字で拡張子変更なし
# ${5} 入力ファイルパス

local lo_inplaceMode lo_commandXX lo_outDir lo_outExt lo_inFile outFile lo_outTemp
lo_inplaceMode="${1}"
lo_commandXX="${2}"
lo_outDir="${3}"
lo_outExt="${4}"
lo_inFile="${5}"


# ---- 拡張子指定処理 ---------------------------------------
# 入力ファイルパスの拡張子を入れ替えたパスを作成する

extConvFunc(){
# ${1} 値を返すシェル変数名
# ${2} ファイルパス
# ${3} もし文字列があれば拡張子と見なして、${2}の拡張子をこれに入れ替える

if test -z "${3}"
then
  eval ${1}'="${2}"'
  return 0
fi

#        var  NewExt  dirname   basename
set -- "${1}" "${3}" "${2%/*}" "${2##*/}"
#echo "${@}" && return 1 # 【debug】
#                            拡張子削除
set -- "${1}" "${2}" "${3}" "${4%.*}"
#echo "${@}" && return 1 # 【debug】
eval ${1}'="${3}/${4}${2}"'
return 0
}

if ! extConvFunc lo_outFile "${lo_inFile}" "${lo_outExt}"
then return 0
fi

# ---- 出力先ファイルパス作成 ------------------------------
lo_outFile="${lo_outDir%/}/${lo_outFile##*/}"
#echo "${lo_outFile}" && return 0 # 【debug】


# ---- コマンド実行 ---------------------------------------
lo_outTemp="`mktemp`"
#echo  'if ! '"${lo_commandXX}"' 0< '"${lo_inFile}"' 1> '"${lo_outTemp}"' ; then return 1 ; fi' && return 0 # 【debug】

case "${lo_inplaceMode}" in
( '--cp' )
  if ! "${lo_commandXX}" 0< "${lo_inFile}" 1> "${lo_outTemp}" ; then return 1 ; fi

;;
( '--rm' )
  if ! "${lo_commandXX}" 0< "${lo_inFile}" 1> "${lo_outTemp}" ; then return 1 ; fi
# 入力ファイルを削除
# if ! rm -v -i -- "${lo_inFile}"  ; then return 1 ; fi # 【debug】
  if ! rm -v    -- "${lo_inFile}"  ; then return 1 ; fi

;;
( * )
  return 2

;;
esac


  if test -e "${lo_outFile}"                       ; then return 1
elif ! mv -n    -- "${lo_outTemp}" "${lo_outFile}" ; then return 1
else return 0
fi


# mv -n は、上書き先ファイルが存在したとき、終了ステータス0を返す。

}




while test "${#}" -ge 1
do

if test -f "${1}" && test -r "${1}"
then
  true
else
  echo 'skip:'"${1}" 1>&2
  continue
fi


#echo 'filterWrapFunc' "${inplaceMode}" "${commandXX}" "${writeDir}" "${newExt}" "${1}" # 【debug】
#shift 1 ; continue # 【debug】

if filterWrapFunc "${inplaceMode}" "${commandXX}" "${writeDir}" "${newExt}" "${1}"
then
  true
else
  printf '\033[7m%s\033[0m\n' "ERR:${commandXX} ${1} ${writeDir}" 1>&2
  exit 255
fi
shift 1

done

exit 0

本体の中核は、シェル関数の部分です。
繰り返し処理は単純なwhileループで行っています。
このwhileループは、順次実行のループです。ファイルひとつの処理が終わるまでは、次のファイルの処理は実行しません。
もし画像変換などで並列実行をしたいときは、xargs -P と組み合わせます。UIでは、xargsでの簡単な並列実行も設定できます。