シェルスクリプト
2010/07/24新規 | 2010/10/25更新 | 2014/01/28更新 | 2014/01/30更新 | 2014/01/31更新 | 2014/02/03更新 | 2014/02/08 更新
筆者が書いた sh
スクリプトのうち、もしかしたら他の人にも有用かもしれないものを、何かのついでに紹介します。
Contents
同名のファイルを探す「samenames.sh
」
「同名のファイルをリストアップしたい」。そんな欲求が生じる時はないでしょうか。目的のファイル名がわかっているときは、find . -name name
すれば事足りますが、例えば、ファイル群を一カ所にすべて移動させようとする際に、既に同名のファイルがあると、移動できないか、上書きの恐れがあります。よって、予め同名のファイルをリストアップしてみるのがよさげです。以下のように使います。
$ samenames.sh . -type f
ちなみに、コマンドライン引数に与えているのは、単に、find
のための引数です。このようにすると、カレントディレクトリ配下のファイル群にて、同名のファイルが表示されます。
そのシェルスクリプト「samenames.sh
」は以下の通りです。
samenames.sh
#!/bin/sh
me="`basename \"$0\"`"
if [ $# -lt 1 ]; then
cat <<EOF
$me - find same named files
usage:
$me find_pathname ... [find_expression ...]
author(s):
Taiji Yamada <taiji@aihara.co.jp>
EOF
exit
fi
c=0
find "$@" -print | while read p; do
dir="`dirname \"$p\"`"
base="`basename \"$p\"`"
echo "$base $dir"
done | sort | while IFS=" " read base1 dir1; do
if [ "$c" = 0 ]; then
base0="$base1"
dir0="$dir1"
flag=false
else
if [ "$base1" = "$base0" ]; then
[ "$c" = 1 ] &&
echo "$dir0/$base0"
echo "$dir1/$base1"
flag=:
else
base0="$base1"
dir0="$dir1"
c=0
$flag && echo
flag=false
fi
fi
c=`expr $c + 1`
done
これにより、探したい場所のファイルそれぞれが互いに同名をもつか調べる事が出来ます。このシェルスクリプトの要は、find で列挙したパス名をベース名を行頭に列挙し直し、それを sort
で並べ直した上で、同一のベース名が複数行に続く行のみを表示していることにあります。
[2011/10/20] 「while read
」の箇所を「while IFS=" " read
(空白はタブ)」に修正し、スペースを含むファイル名に対応しました。
同名のファイルで内容が異なるファイルを探す「samenamescmp.sh
」
先の発展で、同名のファイルで内容が異なるファイルを探しましょう(使い方も同じくします)。そのためのシェルスクリプト「samenamescmp.sh
」は以下の通りです。
samenamescmp.sh
#!/bin/sh
me="`basename \"$0\"`"
if [ $# -lt 1 ]; then
cat <<EOF
$me - find same named and different files/timestamps for same contents
usage:
$me find_pathname ... [find_expression ...]
author(s):
Taiji Yamada <taiji@aihara.co.jp>
EOF
exit
fi
case "$me" in
*time*) timestamp=:;;
*) timestamp=false;;
esac
c=0
n=0
find "$@" ! -type d -print | while read p; do
dir="`dirname \"$p\"`"
base="`basename \"$p\"`"
echo "$base $dir"
done | sort | while IFS=" " read base1 dir1; do
if [ "$c" = 0 ]; then
base0="$base1"
dir0="$dir1"
flag=false
else
if [ "$base1" = "$base0" ]; then
cmp -s "$dir0/$base0" "$dir1/$base1" || flag=:
if $flag; then
[ "$n" = 0 ] &&
echo "$dir0/$base0"
echo "$dir1/$base1 differ in contents"
elif $timestamp; then
{ test "$dir0/$base0" -nt "$dir1/$base1" ||
test "$dir0/$base0" -ot "$dir1/$base1"; } && flag=:
if $flag; then
[ "$n" = 0 ] &&
echo "$dir0/$base0"
echo "$dir1/$base1 differ in timestamp"
fi
fi
$flag && n=`expr $n + 1`
else
base0="$base1"
dir0="$dir1"
c=0
[ "$n" != 0 ] && echo
n=0
flag=false
fi
fi
c=`expr $c + 1`
done
[2011/10/20] 「while read
」の箇所を「while IFS=" " read
(空白はタブ)」に修正し、スペースを含むファイル名に対応しました。
同名のファイルで内容が等しいがタイムスタンプが異なるファイルを探す「samenamescmptime.sh
」
さらに発展で、同名のファイルで内容は等しいがタイムスタンプが異なるファイルを探しましょう(使い方も同じくします)。そのためのシェルスクリプト「samenamescmptime.sh
」は先の「samenamescmp.sh
」のシンボリックリンクにするとそのように動作します。このシェルスクリプトの要は、先の例と同様に find で列挙したパス名をベース名を行頭に列挙し直し、それを sort
で並べ直した上で、同一のベース名が複数行に続く行について、cmp
や test
で内容の同一性やタイムスタンプを検証している点にあります。
ディレクトリ内で大文字小文字を無視して同名のファイルを探す「samenameicase.sh
」
これは、大文字小文字を区別するファイルシステムのアイテムを区別しないファイルシステムへ移行するに先立ち、大文字小文字を無視すると同名になってしまうファイル名を検索するためのシェルスクリプトです。以下のように使います。
$ samenameicase.sh ~/bin ~/lib
そのシェルスクリプト「samenameicase.sh
」は以下の通り。
samenameicase.sh
#!/bin/sh
me="`basename \"$0\"`"
if [ $# -lt 1 ]; then
cat <<EOF
$me - find case-insensitive same named files non-recursively
usage:
$me pathname ...
author(s):
Taiji Yamada <taiji@aihara.co.jp>
EOF
exit
fi
while [ "$1" != "" ]; do
find "$1" -maxdepth 1 ! -name "`basename \"$1\"`" -print | while read p; do
dir="`dirname \"$p\"`"
base="`basename \"$p\"`"
echo "$base $dir"
done | while IFS=" " read b d; do
find "$d" ! -path "$d" -maxdepth 1 -iname "$b" ! -name "$b" -print
done | sort -f
shift
done
UnixのファイルシステムからMacのファイルシステム等にファイルをコピーする際だけでなく、他人にファイルを渡す前に、余計なトラブルを避けるためにこれでチェックするとよいでしょう。このシェルスクリプトの要は、find
を「while read
」を介して二段に渡って利用していることにあります。
ディレクトリ内で大文字小文字を無視して同名のファイルを再帰的に探す「samenamesicase.sh
」
上記とほぼ同様ですが、ディレクトリを再帰的にすべて探したい場合はこちらを以下のように使います。
$ samenamesicase.sh ~/
そのシェルスクリプト「samenamesicase.sh
」は以下の通り。先のと微妙に異なるだけです。
samenamesicase.sh
#!/bin/sh
me="`basename \"$0\"`"
if [ $# -lt 1 ]; then
cat <<EOF
$me - find case-insensitive same named files recursively
usage:
$me pathname ...
author(s):
Taiji Yamada <taiji@aihara.co.jp>
EOF
exit
fi
while [ "$1" != "" ]; do
find "$1" -type d -print | while read q; do
find "$q" -maxdepth 1 ! -name "`basename \"$q\"`" -print | while read p; do
dir="`dirname \"$p\"`"
base="`basename \"$p\"`"
echo "$base $dir"
done | while IFS=" " read b d; do
find "$d" ! -path "$d" -maxdepth 1 -iname "$b" ! -name "$b" -print
done | sort -f
done
shift
done
UnixのファイルシステムからMacのファイルシステムにホームディレクトリを移行するに先立ち、これでチェックするとよいでしょう。このシェルスクリプトの要は、find
を「while read
」を介して三段に渡って利用していることにあります。
一意なファイル
一意にファイル命名「uv.sh
」
例えば、文書をダウンロードしたけど「どうも前にも同じようなものダウンロードしてたみたいだけど、それが微妙に変更されているのか」、同一かもしれない文書ファイルが重複して溜まってるかもしれず、以下のような欲求がでてきます:
重複してるなら読まないので、そのファイルは消したい
重複してないなら違うものとして、そのファイルは読みたい、取り置きたい
ダウンロードする前にすでに保持しているものと同一かどうか調べればよいのでしょうが、現実的には困難です。よって、ダウンロードした後、どうすればよいか考えてみます。
ひとつの方法は、内容によって「一意のファイル名」を命名することです。「命名しようとした時に既に存在するファイル名があるなら警告」そういったシェルスクリプトがあると便利ではないでしょうか。以下のように使います。
$ cd ~/Downloads
$ ls article1.pdf # たった今ダウンロードしたファイル
$ uv.sh -s .pdf *.pdf # カレントディレクトリにある *.pdf ファイルを一意なファイル名+拡張子「.pdf」にリネイム(mv)してしまう
既に同一のファイル名が存在するならリネイムが失敗して、その旨が表示されます。このシェルスクリプト「uv.sh
」は以下の通りです。
uv.sh
#!/bin/sh
me="`basename \"$0\"`"
if [ "$#" -lt 1 ]; then
cat <<EOF
$me - rename files by message digest
usage:
$me [-m md5|sha1] [-s suf] file(s)
author(s):
Taiji Yamada <taiji@aihara.co.jp>
EOF
exit
fi
mdc=sha1
endoptions=false
keepsuffix=:
suf=
while [ "$1" != "" ]; do
$endoptions ||
case "$1" in
--) endoptions=:; shift; continue;;
-m) shift; mdc="$1"; shift; continue;;
-s) keepsuffix=false; shift; suf="$1"; shift; continue;;
esac
$keepsuffix && suf="`echo \"$1\" | sed -ne 's/.*\(\.[^.][^.]*\)$/\1/p'`"
mds="`openssl \"$mdc\" \"$1\" | sed -ne 's/.*= \(.*\)/\1/p'`"
dst="`dirname \"$1\"`"/"$mds$suf"
if [ ! -f "$dst" ]; then
echo mv "$1" "$dst"
mv "$1" "$dst" &&
case "$me" in
*ln*)
echo ln -s "$dst" "$1"
ln -s "$dst" "$1"
;;
esac
else
echo "already exists $dst for $1"
fi
shift
done
このシェルスクリプトを活用すれば、内容が同一のファイルを保持しないように心がける事が簡単になります。
ちなみに、このシェルスクリプトファイル名を「uvln.sh
」のようにシンボリックリンクしたりすると、mv
するのではなく ln -s
するように動作を変えるようになっています。オリジナルのファイル名を保持したい場面で有用かと思います。このシェルスクリプトの要は、openssl(1) を使ってファイルのダイジェストを生成し、その文字列を新たなファイル名とすることにあります。
一意でないファイルを探す「samefiles.sh
」
上述のシェルスクリプトだと、ファイルをリネームしてしまうので、「単に内容が同一のファイルを探したいだけなのに」という場面もあるかと思います。
先のシェルスクリプトよりも計算量は掛かりますが、内容が同一のファイルを表示する事も可能です。以下のように使います。
$ cd ~/Downloads
$ samefiles.sh . -name '*.pdf'
ちなみに、コマンドライン引数に与えているのは、単に、find
のための引数です。このようにすると、カレントディレクトリ配下のファイル群にて、ファイル名が '*.pdf'
にマッチするファイルから、内容が同一のファイルが表示されます。
そのシェルスクリプト「samefiles.sh
」は以下の通りです。
samefiles.sh
#!/bin/sh
me="`basename \"$0\"`"
if [ $# -lt 1 ]; then
cat <<EOF
$me - find same files by message digest
usage:
$me find_pathname ... [find_expression ...]
author(s):
Taiji Yamada <taiji@aihara.co.jp>
EOF
exit
fi
mdc=md5
c=0
find "$@" -type f ! -type l -print | while read file; do
mds="`openssl \"$mdc\" \"$file\" | sed -ne 's/.*= \(.*\)/\1/p'`"
echo $mds "$file"
done | sort | while read mds1 file1; do
if [ "$c" = 0 ]; then
mds0="$mds1"
file0="$file1"
flag=false
else
if [ "$mds1" = "$mds0" ]; then
[ "$c" = 1 ] &&
echo "$file0"
echo "$file1"
flag=:
else
mds0="$mds1"
file0="$file1"
c=0
$flag && echo
flag=false
fi
fi
c=`expr $c + 1`
done
これにより、探したい場所のファイルそれぞれが互いに内容が同一か否か調べる事が出来ます。このシェルスクリプトの要は、openssl(1) を使ってファイルのダイジェストを生成し、その文字列とパス名を sort で並べ直した上で、同一のダイジェストが複数行に続く行についてのみパス名を表示していることにあります。
ファイルの慎重な複製・移動・更新
ファイルの慎重な複製「cp.sh
」
ファイルを cp
しようとしたとき、同名のファイルが存在したなら、複製を続行するか否かは cp -i
で対話的に決めることが出来ますが、その判断をする際に、ファイルの内容が同一なのか、ファイルのタイムスタンプやサイズ等で決定したい場合があります。
以下のシェルスクリプト cp.sh
は、ターゲットファイルがソースファイルと同一なら何もせず、さもなくば、双方のファイルの諸情報を表示して対話的に cp
を続行します。ターゲットファイルが存在しなければ、単に複製を行ないます。
cp.sh
#!/bin/sh
me="`basename \"$0\"`"
if [ "$#" -lt 2 ]; then
cat <<EOF
$me - copy file to another or copy file(s) into directory
usage:
$me file file
$me file(s) directory
author(s):
Taiji Yamada <taiji@aihara.co.jp>
EOF
exit
fi
copy_one_to_dst(){
f="$1"
b="`basename \"$f\"`"
[ ! -d "$f" ] || return 1
[ -f "$f" ] || return 1
if [ -d "$dst" ]; then
if [ -f "$dst/$b" ]; then
if cmp "$f" "$dst/$b" > /dev/null 2>&1; then
return 0
fi
ls -Tl "$f" "$dst/$b"
echo "$me: overwrite: $f $dst/$b"
cp -i -p "$f" "$dst"
return 0
fi
echo cp -p "$f" "$dst"
cp -p "$f" "$dst"
else
if [ -f "$dst" ]; then
if cmp "$f" "$dst" > /dev/null 2>&1; then
return 0
fi
ls -Tl "$f" "$dst"
echo "$me: overwrites: $f $dst"
cp -i "$f" "$dst"
return 0
fi
echo cp -p "$f" "$dst"
cp -p "$f" "$dst"
fi
return 0
}
eval dst="\${$#}"
if [ ! -d "$dst" ]; then
if [ $# -ne 2 ]; then
echo "$me: error: [ ! -d \"$dst\" ] && \$#($#) != 2"
else
copy_one_to_dst "$1"
fi
else
i=1
while [ $i -lt $# ]; do
eval src="\${$i}"
copy_one_to_dst "$src"
i=`expr $i + 1`
done
fi
ファイルの慎重な移動「mv.sh
」
ファイルを mv
しようとしたとき、同名のファイルが存在したなら、複製を続行するか否かは mv -i
で対話的に決めることが出来ますが、その判断をする際に、ファイルの内容が同一なのか、ファイルのタイムスタンプやサイズ等で決定したい場合があります。
以下のシェルスクリプト mv.sh
は、ターゲットファイルがソースファイルと同一なら対話的にソースファイルの消去を促し、さもなくば、双方のファイルの諸情報を表示して対話的に mv
を続行します。ターゲットファイルが存在しなければ、単に移動を行ないます。
mv.sh
#!/bin/sh
me="`basename \"$0\"`"
if [ "$#" -lt 2 ]; then
cat <<EOF
$me - move file to another or move file(s) into directory
usage:
$me file file
$me file(s) directory
author(s):
Taiji Yamada <taiji@aihara.co.jp>
EOF
exit
fi
move_one_to_dst(){
f="$1"
b="`basename \"$f\"`"
[ ! -d "$f" ] || return 1
[ -f "$f" ] || return 1
if [ -d "$dst" ]; then
if [ -f "$dst/$b" ]; then
if cmp "$f" "$dst/$b" > /dev/null 2>&1; then
echo "$me: remove: $f"
rm -i "$f"
return 0
fi
ls -Tl "$f" "$dst/$b"
echo "$me: overwrite: $f $dst/$b"
mv -i "$f" "$dst"
[ ! -f "$f" ] || {
echo "$me: remove: $f"
rm -i "$f"
}
return 0
fi
echo mv "$f" "$dst"
mv "$f" "$dst"
else
if [ -f "$dst" ]; then
if cmp "$f" "$dst" > /dev/null 2>&1; then
echo "$me: remove: $f"
rm -i "$f"
return 0
fi
ls -Tl "$f" "$dst"
echo "$me: overwrites: $f $dst"
mv -i "$f" "$dst"
[ ! -f "$f" ] || {
echo "$me: remove: $f"
rm -i "$f"
}
return 0
fi
echo mv "$f" "$dst"
mv "$f" "$dst"
fi
return 0
}
eval dst="\${$#}"
if [ ! -d "$dst" ]; then
if [ $# -ne 2 ]; then
echo "$me: error: [ ! -d \"$dst\" ] && \$#($#) != 2"
else
move_one_to_dst "$1"
fi
else
i=1
while [ $i -lt $# ]; do
eval src="\${$i}"
move_one_to_dst "$src"
i=`expr $i + 1`
done
fi
ファイルの慎重な更新「ow.sh
」
上記 cp
, mv
と非常に似通っていますが、ターゲットファイルが存在する場合のみ、対話的に複製を行ないたい場合があります。
以下のシェルスクリプト ow.sh
は、ターゲットファイルがソースファイルと同一なら何もせず、さもなくば、双方のファイルの諸情報を表示して対話的に cp
を続行します。ターゲットファイルが存在しなければ、何もしません。
ow.sh
#!/bin/sh
me="`basename \"$0\"`"
if [ "$#" -lt 2 ]; then
cat <<EOF
$me - overwrite file to another or overwrite file(s) into directory
usage:
$me file file
$me file(s) directory
author(s):
Taiji Yamada <taiji@aihara.co.jp>
EOF
exit
fi
overwrite_one_to_dst(){
f="$1"
b="`basename \"$f\"`"
[ ! -d "$f" ] || return 1
[ -f "$f" ] || return 1
if [ -d "$dst" ]; then
if [ -f "$dst/$b" ]; then
if cmp "$f" "$dst/$b" > /dev/null 2>&1; then
return 0
fi
ls -Tl "$f" "$dst/$b"
echo "$me: overwrite: $f $dst/$b"
cp -i -p "$f" "$dst"
return 0
fi
else
if [ -f "$dst" ]; then
if cmp "$f" "$dst" > /dev/null 2>&1; then
return 0
fi
ls -Tl "$f" "$dst"
echo "$me: overwrites: $f $dst"
cp -i "$f" "$dst"
return 0
fi
fi
return 0
}
eval dst="\${$#}"
if [ ! -d "$dst" ]; then
if [ $# -ne 2 ]; then
echo "$me: error: [ ! -d \"$dst\" ] && \$#($#) != 2"
else
overwrite_one_to_dst "$1"
fi
else
i=1
while [ $i -lt $# ]; do
eval src="\${$i}"
overwrite_one_to_dst "$src"
i=`expr $i + 1`
done
fi
これらのシェルスクリプトの要は、cmp
の返り値によって挙動を制御していることに加えて、cp -i
, rm -i
, mv -i
の対話モードを有効利用していることにあります。
リモートのファイルとの diff を見る「rdiff.sh
」
rsync で複数のマシンで rsync -aunv * remote:path_to/
してると rsync で diff のようなファイル毎の差異を見たい時があります。逆に言えば、diff に rsync のような使い勝手が欲しい時があります。
「リモート diff」でウェブサイトを検索してみると、たくさん要望はあるようなのですが、どれもこちらの要望に微妙に合いません。よって、自前でシェルスクリプトを書くことにしました。以下のように使います。
$ rdiff.sh -u .xinitrc remote_host:
$ rdiff.sh --reverse -u ~/bin/* remote_host:bin/ | less
"--reverse
" オプションは diff コマンドの二つのファイル並びを入れ替えることになるオプションで、diff と GNU patch のノリで使い手が意図してる新旧と、rsync のノリでコマンドラインの末尾にリモートホストを指定しなければならない制約の双方を満たすためのものです。それ以外の後に続くオプションはそのまま diff コマンドに渡されます。
まず実装にあたって、素のシェルでは煩雑になってしまうことが予想されるので、POSIX準拠の ksh の拡張機能を使い、bash や zsh でも意図した動作をすることを目標にしました。それが以下のシェルスクリプトです。
rdiff.sh
#!/bin/ksh
reverse=false
while [ ! -z "$1" ]; do
case "$1" in
--reverse) reverse=:;;
*) break;;
esac
shift
done
rdiff(){
rdiff_args=("$@")
files=("${rdiff_args[${#rdiff_args[@]}-2]}" "${rdiff_args[${#rdiff_args[@]}-1]}")
$reverse && files=("${rdiff_args[${#rdiff_args[@]}-1]}" "${rdiff_args[${#rdiff_args[@]}-2]}")
for ((i=0; i<${#rdiff_args[@]}-2; i++)); do
rdiff_opts[$i]="${rdiff_args[$i]}"
done
equiv_path=
for i in 0 1; do
case "${files[$i]}" in
*:) equiv_path=$(echo "${files[($i+1)%2]}" | sed 's/^.*://');;
esac
done
for i in 0 1; do
case "${files[$i]}" in
*/)
case "${files[($i+1)%2]}" in
*/) ;;
*) files[$i]+=$(basename "${files[($i+1)%2]}");;
esac
esac
done
for i in 0 1; do
case "${files[$i]}" in
*:*)
eval $(echo "${files[$i]}" | sed -e 's|\([^:]*\):\([^:]*\)|host=\1;path=\2|')
if [ -z "$path" -a -n "$equiv_path" ]; then
path="$equiv_path"
fi
[ -z "$equiv_path" ] || case "$path" in
[^/]*)
prefix=$(pwd | sed "s|^$HOME/||")
path="$prefix/$path"
;;
esac
if [ -z "$host" ]; then
files[$i]="$path"
else
files[$i]="<(ssh $host cat \"$path\" 2> /dev/null)"
fi
;;
esac
done
echo diff "${rdiff_opts[@]}" "${files[@]}"
eval diff "${rdiff_opts[@]}" "${files[@]}"
}
args=("$@")
case "${args[${#args[@]}-1]}" in
*:|*/)
for ((i=0; i<${#args[@]}; i++)); do
case "${args[$i]}" in
-*) opts[$i]="${args[$i]}";;
*) break;;
esac
done
for ((j=i; j<${#args[@]}-1; j++)); do
rdiff "${opts[@]}" "${args[$j]}" "${args[${#args[@]}-1]}"
done
;;
*)
rdiff "$@";;
esac
一行目の #!/bin/ksh
は bash の場合は #!/bin/bash
に置き換えることが出来、zsh の場合は、#!/bin/zsh -o KSH_ARRAYS
に置き換えれば同様に動作します。
このシェルスクリプトの要は、シェルコマンド言語 では規定されていない配列変数 やプロセス置換 を積極的に利用していることにあります。
ファイル名の交換
例えば、何かファイルをストリームエディタsed(1)等で編集し、
$ sed -e 's/ *$//' main.c > main.c~
編集内容を確認:
$ diff -u main.c main.c~
それで問題なければ、これらのファイル名を交換したい、そういう場合、単純作業ながらちょっとだけ面倒で、万が一間違えるとファイルが失われてしまいます。以下の二つのシェルスクリプトはどちらもこのようなファイル名の交換の手間を、以下のように軽減するためのものです。
$ swapnames main.c main.c~
$ rotatenames main.c main.c~
ファイル名を逆順で交換「swapnames
」
以下のシェルスクリプト swapnames
は、A, B, C, D
というファイルをコマンドライン引数に指定した場合、その内容はこの順でそのままで、順に D, C, B, A
というファイル名に変更します。
swapnames
#!/bin/sh
swapapair(){
if [ "$1" = "$2" ]; then
:
elif [ -f "$1" -a -f "$2" ]; then
tmpname="$1.tmp"
# name="`basename $1`"; tmpname="`mktemp \"./$name.XXXXXX\"`"
mv "$1" $tmpname
mv "$2" "$1"
mv $tmpname "$2"
elif [ -f "$1" -a ! -f "$2" ]; then
mv "$1" "$2"
elif [ ! -f "$1" -a -f "$2" ]; then
mv "$2" "$1"
fi
}
l=1
u=$#
if [ $# = 0 ]; then
me="`basename \"$0\"`"
cat <<EOF
$me - swap filename(s)
usage:
$me file1 file2 ...
EOF
exit
fi
while [ $l -lt $u ]; do
eval lfile="\${$l}"
eval ufile="\${$u}"
echo swapapair "$lfile" "$ufile"
swapapair "$lfile" "$ufile"
l=`expr $l + 1`
u=`expr $u - 1`
done
連番でファイル名を付けたけど、やっぱり逆順の方がいいや、というときに便利です(そんなことは稀かとは思いますが…)。
ファイル名を回転で交換「rotatenames
」
以下のシェルスクリプト rotatenames
は、A, B, C, D
というファイルをコマンドライン引数に指定した場合、その内容はこの順でそのままで、順に D, A, B, C
というファイル名に変更します。
rotatenames
#!/bin/sh
swapapair(){
if [ "$1" = "$2" ]; then
:
elif [ -f "$1" -a -f "$2" ]; then
# tmpname="$1.tmp"
name="`basename $1`"; tmpname="`mktemp \"./$name.XXXXXX\"`"
mv "$1" $tmpname
mv "$2" "$1"
mv $tmpname "$2"
elif [ -f "$1" -a ! -f "$2" ]; then
mv "$1" "$2"
elif [ ! -f "$1" -a -f "$2" ]; then
mv "$2" "$1"
fi
}
n=1
while [ "$1" != "" ]; do
case "$1" in
--) shift; break;;
-n) shift; n="$1";;
*) break;
esac
shift
done
i=1
if [ $# = 0 ]; then
me="`basename \"$0\"`"
cat <<EOF
$me - rotate filename(s)
usage:
$me [-n number] file1 file2 ...
EOF
exit
fi
sed_script=
while [ $i -lt $# ]; do
j=`expr \( $i - 1 + $# + $n \) % $# + 1`
eval ifile="\${$i}"
eval jfile="\${$j}"
ifile="`echo \"$ifile\" | sed -e \"$sed_script\"`"
echo swapapair "$ifile" "$jfile"
swapapair "$ifile" "$jfile"
i=`expr $i + 1`
sed_script="${sed_script}s:^$jfile\$:$ifile:;"
done
オプションに -n 3 を指定し、
A, B, C, D
というファイルをコマンドライン引数に指定した場合、その内容はこの順でそのままで、順に B, C, D, A
というファイル名に変更します。
連番でファイル名を付けたけど、1つずつずれちゃった、というときに便利です(そんなことは稀かとは思いますが…)。
これらのシェルスクリプトの要は、eval
で任意の添字のコマンドライン引数の変数展開を行なっている点にあります。
各種書庫の変換・展開・格納
Unix には tar
をはじめ、cpio
, pax
, xar
などの書庫形式があり、さらに、圧縮形式も gzip
, bzip2
, xz
など様々です。また、Windows 系では lha
, zip
, 7zip, rar などの圧縮兼書庫形式があり、コマンドラインで扱う場合にはオプションの指定の仕方がそれぞれ微妙に異なるので、あまり使わない書庫などでは使い方を覚えるのは苦痛を伴います。
そこで、ラッパーとして多くの書庫に対応したシェルスクリプトを書きました。まずは、そのヘルプを見てみます。
usage:
rear.sh [-h|--help] [-l] [-f art] -t art archive.art [...]
unar.sh [-h|--help] [-l] [-f art] archive.art [...]
ar.sh [-h|--help] [-l] -t art directory [...]
requirement(s):
tar, compress, uncompress, lha, gzip, unzip, zip, bzip2,
lzma, xz, cpio, pax, xar, unrar and
rpmtocpout (see rpmtocpout.c) for RPM format
source archive type:
lzh, zip, tar, taz, tgz, tbz2, tlz, txz, 7z, t7z, cpio, cpz, cpgz, cpbz2, cplz, cpxz, pax, paz, pgz, pbz2, plz, pxz, xar, rpm, rar, ..
target archive type:
lzh, zip, tar, taz, tgz, tbz2, tlz, txz, 7z, t7z, cpio, cpz, cpgz, cpbz2, cplz, cpxz, pax, paz, pgz, pbz2, plz, pxz, xar, ..
author(s):
Taiji Yamada <taiji@aihara.co.jp>
art
に指定するのは一部は短縮した書庫の拡張子です。RPM と RAR を除き、展開・伸長と格納・圧縮の双方に対応しています。具体的な使い方の前に、このシェルスクリプトのソースコードを紹介します。
rear.sh
#!/bin/sh
prefix(){
echo "$1" | sed -e 's|\(\.[^./]*\)$||'
}
suffix(){
echo "$1" | sed -ne 's|^\(.*\)\(\.[^./]*\)$|\2|p'
}
me="`basename \"$0\"`"
unar_types='
lzh
zip
tar
taz
tgz
tbz2
tlz
txz
7z
t7z
cpio
cpz
cpgz
cpbz2
cplz
cpxz
pax
paz
pgz
pbz2
plz
pxz
xar
rpm
rar
'
ar_types='
lzh
zip
tar
taz
tgz
tbz2
tlz
txz
7z
t7z
cpio
cpz
cpgz
cpbz2
cplz
cpxz
pax
paz
pgz
pbz2
plz
pxz
xar
'
usage(){
cat <<EOF
usage:
rear.sh [-h|--help] [-l] [-f art] -t art archive.art [...]
unar.sh [-h|--help] [-l] [-f art] archive.art [...]
ar.sh [-h|--help] [-l] -t art directory [...]
requirement(s):
tar, compress, uncompress, lha, gzip, unzip, zip, bzip2,
lzma, xz, cpio, pax, xar, unrar and
rpmtocpout (see rpmtocpout.c) for RPM format
source archive type:
`for art in $unar_types; do printf "%s, " $art; done; echo ..`
target archive type:
`for art in $ar_types; do printf "%s, " $art; done; echo ..`
author(s):
Taiji Yamada <taiji@aihara.co.jp>
EOF
}
presuffix(){
pre="`prefix \"$1\"`"
suf="`suffix \"$1\"`"
pre0="$pre"
suf0="$suf"
while [ "$suf0" != "" ]; do
pre1="`prefix \"$pre0\"`"
suf1="`suffix \"$pre0\"`$suf"
[ "$suf0" = "$suf1" ] && break
case "$suf1" in
.tar.Z|.tar.gz|.tar.bz2|.tar.lzma|.tar.xz|.tar.7z) pre="$pre1"; suf="$suf1"; break ;;
.cpio.Z|.cpio.gz|.cpio.bz2|.cpio.lzma|.cpio.xz) pre="$pre1"; suf="$suf1"; break ;;
.pax.Z|.pax.gz|.pax.bz2|.pax.lzma|.pax.xz) pre="$pre1"; suf="$suf1"; break ;;
esac
pre0="$pre1"
suf0="$suf1"
done
}
preferedsuffix(){
case "$1" in
.taz) echo .tar.Z ;;
.tgz) echo .tar.gz ;;
.tbz2) echo .tar.bz2 ;;
.tlz) echo .tar.lzma ;;
.txz) echo .tar.xz ;;
.t7z) echo .tar.7z ;;
.cpz) echo .cpio.Z ;;
.cpgz) echo .cpio.gz ;;
.cpbz2) echo .cpio.bz2 ;;
.cplz) echo .cpio.lzma ;;
.cpxz) echo .cpio.xz ;;
.paz) echo .pax.Z ;;
.pgz) echo .pax.gz ;;
.pbz2) echo .pax.bz2 ;;
.plz) echo .pax.lzma ;;
.pxz) echo .pax.xz ;;
*) echo "$1" ;;
esac
}
check_ar_types(){
for t in $ar_types; do
[ "$t" = "$1" ] && break
done
}
art_lzh(){ lha "$1"; }
art_zip(){ unzip -Z "$1"; }
art_tar(){ tar tf "$1"; }
art_taz(){ uncompress "$1" | tar tf -; }
art_tgz(){ gzip -dc "$1" | tar tf -; }
art_tbz2(){ bzip2 -dc "$1" | tar tf -; }
art_tlz(){ lzma -dc "$1" | tar tf -; }
art_txz(){ xz -dc "$1" | tar tf -; }
art_7z(){ 7z l "$1"; }
art_t7z(){ 7z x -so "$1" | tar tf -; }
art_cpio(){ cpio -it --quiet -I "$1"; }
art_cpZ(){ cpio -it --quiet -I "$1"; }
art_cpgz(){ cpio -it --quiet -I "$1"; }
art_cpbz2(){ cpio -it --quiet -I "$1"; }
art_cplz(){ cpio -it --quiet -I "$1"; }
art_cpxz(){ cpio -it --quiet -I "$1"; }
#art_pax(){ pax -f "$1"; }
art_pax(){ cpio -it --quiet -I "$1"; }
art_paz(){ cpio -it --quiet -I "$1"; }
art_pgz(){ cpio -it --quiet -I "$1"; }
art_pbz2(){ cpio -it --quiet -I "$1"; }
art_plz(){ cpio -it --quiet -I "$1"; }
art_pxz(){ cpio -it --quiet -I "$1"; }
art_xar(){ xar -tf "$1"; }
art_rpm(){ rpmtocpout "$1" | cpio -it --quiet; }
art_rar(){ unrar l "$1"; }
unar_lzh(){ lha xqf "$1"; }
unar_zip(){ unzip -qo "$1"; }
unar_tar(){ tar xf "$1"; }
unar_taz(){ uncompress "$1" | tar xf -; }
unar_tgz(){ gzip -dc "$1" | tar xf -; }
unar_tbz2(){ bzip2 -dc "$1" | tar xf -; }
unar_tlz(){ lzma -dc "$1" | tar xf -; }
unar_txz(){ xz -dc "$1" | tar xf -; }
unar_7z(){ 7z x "$1"; }
unar_t7z(){ 7z x -so "$1" | tar xf -; }
unar_cpio(){ cpio -idm --quiet -I "$1"; }
unar_cpz(){ cpio -idm --quiet -I "$1"; }
unar_cpgz(){ cpio -idm --quiet -I "$1"; }
unar_cpbz2(){ cpio -idm --quiet -I "$1"; }
unar_cplz(){ cpio -idm --quiet -I "$1"; }
unar_cpxz(){ cpio -idm --quiet -I "$1"; }
#unar_pax(){ pax -r -f "$1"; }
unar_pax(){ cpio -idm --quiet -I "$1"; }
unar_paz(){ cpio -idm --quiet -I "$1"; }
unar_pgz(){ cpio -idm --quiet -I "$1"; }
unar_pbz2(){ cpio -idm --quiet -I "$1"; }
unar_plz(){ cpio -idm --quiet -I "$1"; }
unar_pxz(){ cpio -idm --quiet -I "$1"; }
unar_xar(){ xar -xf "$1"; }
unar_rpm(){ rpmtocpout "$1" | cpio -idm --quiet; }
unar_rar(){ unrar x "$1"; }
ar_lzh(){ lha cq "$@"; }
ar_zip(){ zip -rq "$@"; }
ar_tar(){ tar cf "$@"; }
ar_taz(){ f="$1" && shift && tar cf - "$@" | compress -c > "$f"; }
ar_tgz(){ f="$1" && shift && tar cf - "$@" | gzip -c > "$f"; }
ar_tbz2(){ f="$1" && shift && tar cf - "$@" | bzip2 -c > "$f"; }
ar_tlz(){ f="$1" && shift && tar cf - "$@" | lzma -c > "$f"; }
ar_txz(){ f="$1" && shift && tar cf - "$@" | xz -c > "$f"; }
ar_7z(){ 7z a "$@"; }
ar_t7z(){ f="$1" && shift && tar cf - "$@" | 7z a -si "$f"; }
ar_cpio(){ f="$1" && shift && find "$@" -print0 | cpio -o0 --quiet -O "$f"; }
ar_cpz(){ f="$1" && shift && find "$@" -print0 | cpio -o0 --quiet -ZO "$f"; }
ar_cpgz(){ f="$1" && shift && find "$@" -print0 | cpio -o0 --quiet -zO "$f"; }
ar_cpbz2(){ f="$1" && shift && find "$@" -print0 | cpio -o0 --quiet -jO "$f"; }
#ar_cplz(){ f="$1" && shift && find "$@" -print0 | cpio -o0 --quiet --lzma -O "$f"; }
ar_cplz(){ f="$1" && shift && find "$@" -print0 | cpio -o0 --quiet | lzma -c > "$f"; }
#ar_cpxz(){ f="$1" && shift && find "$@" -print0 | cpio -o0 --quiet -JO "$f"; }
ar_cpxz(){ f="$1" && shift && find "$@" -print0 | cpio -o0 --quiet | xz -c > "$f"; }
ar_pax(){ pax -w -f "$@"; }
ar_paz(){ f="$1" && shift && find "$@" -print0 | cpio -o0 --quiet -H ustar -ZO "$f"; }
ar_pgz(){ pax -w -zf "$@"; }
ar_pbz2(){ pax -w -jf "$@"; }
#ar_plz(){ f="$1" && shift && find "$@" -print0 | cpio -o0 --quiet -H ustar --lzma -O "$f"; }
ar_plz(){ f="$1" && shift && find "$@" -print0 | cpio -o0 --quiet -H ustar | lzma -c > "$f"; }
#ar_pxz(){ f="$1" && shift && find "$@" -print0 | cpio -o0 --quiet -H ustar -JO "$f"; }
ar_pxz(){ f="$1" && shift && find "$@" -print0 | cpio -o0 --quiet -H ustar | xz -c > "$f"; }
ar_xar(){ xar -cf "$@"; }
setunar(){
[ "$fart" = "" ] || { unar="unar_${fart}"; presuffix "$1"; return; }
unar="7z"
presuffix "$1"
[ "$suf" = "" ] || {
case "$suf" in
.jar) unar=zip ;;
.tar.Z) unar=taz ;;
.tar.gz) unar=tgz ;;
.tar.bz2|.tbz) unar=tbz2 ;;
.tar.lzma) unar=tlz ;;
.tar.xz) unar=txz ;;
.tar.7z) unar=t7z ;;
.cpio.Z) unar=cpz ;;
.cpio.gz) unar=cpgz ;;
.cpio.bz2) unar=cpbz2 ;;
.cpio.lzma) unar=cplz ;;
.cpio.xz) unar=cpxz ;;
.pax.Z) unar=paz ;;
.pax.gz) unar=pgz ;;
.pax.bz2) unar=pbz2 ;;
.pax.lzma) unar=plz ;;
.pax.xz) unar=pxz ;;
*) unar="`echo \"$suf\" | sed -e 's|^\.||'`" ;;
esac
}
art="art_${unar}"
unar="unar_${unar}"
}
listing=false
fart="`basename \"$0\" .sh | sed -n 's/\(.*\)to\(.*\)/\1/p'`"
tart="`basename \"$0\" .sh | sed -n 's/\(.*\)to\(.*\)/\2/p'`"
while [ "$1" != "" ]; do
case "$1" in
--) shift; break ;;
-h|--help) usage; exit 0 ;;
-f) shift; fart="$1" ;;
-t) shift; tart="$1" ;;
-l) listing=: ;;
--list)
for unar in $unar_types; do
for ar in $ar_types; do
[ "$unar" != "$ar" ] || continue
echo "${unar}to${ar}".sh
done
done
exit 0
;;
*) break ;;
esac
shift
done
if $listing; then
while [ "$1" != "" ]; do
[ ! -d "$1" ] || { find "$1" -print; shift; continue; }
setunar "$1"
"$art" "$1"
shift
done
exit 0
fi
case "$me" in
unar.sh)
while [ "$1" != "" ]; do
setunar "$1"
b="`basename \"$pre\"`"
mkdir -p "$b" && (
cd "$b" && "$unar" ../"$1"
) &&
echo unarchived from "$1" to "$b"/
shift
done
;;
ar.sh)
check_ar_types "$tart" && ar="ar_${tart}" || { usage; exit 1; }
x="`preferedsuffix \".$tart\"`"
while [ "$1" != "" ]; do
[ -d "$1" ] || continue
b="`basename \"$1\"`"
d="$b$x"
"$ar" "$d" "$1" &&
echo archived from "$1"/ to "$d"
shift
done
;;
rear.sh|*)
check_ar_types "$tart" && ar="ar_${tart}" || { usage; exit 1; }
x="`preferedsuffix \".$tart\"`"
t="`mktemp -d tmp_\"$me\"-XXXXXX`"
while [ "$1" != "" ]; do
setunar "$1"
b="`basename \"$pre\"`"
d="$pre$x"
mkdir -p "$t/$b" && (
cd "$t/$b" && "$unar" ../../"$1" && "$ar" ../../"$d" .
) &&
touch -r "$1" "$d"
echo rearchived from "$1" to "$d"
rm -rf "$t/$b"
shift
done
rm -rf "$t"
;;
esac
各種書庫の変換「rear.sh
」
では使い方を見てみましょう。以下は .tar
を .pax
へ変換した例です。
$ rear.sh -t pax filename.tar
rearchived from filename.tar to filename.pax
$ file filename.pax
filename.pax: POSIX tar archive
「-l
」オプションで内容物を確認することが出来ます。
$ rear.sh -l filename.pax
:
また、次は .pax
を .7z
へ変換した例です。
$ rear.sh -t 7z filename.pax
:
rearchived from filename.pax to filename.7z
但し、7z
は Unix のオーナー・グループ情報を保存しないので、以下のように .t7z
へ変換した方が望ましいです。
$ rear.sh -t t7z filename.pax
:
rearchived from filename.pax to filename.tar.7z
各種書庫の展開「unar.sh
」
また、この rear.sh
は ln -s rear.sh unar.sh
のように別名 unar.sh
で動作させることにより、書庫の展開(伸長)に用いることが出来ます。
では使い方を見てみましょう。以下は、.tar.7z
をディレクトリ filename/
へ展開した例です。
$ unar.sh filename.tar.7z
:
unarchived from filename.tar.7z to filename/
$ ls -F1
filename/
filename.7z
filename.pax
filename.tar
filename.tar.7z
「-l
」オプションで内容物を確認することが出来ます。
$ unar.sh -l filename.tar.7z
:
各種書庫の格納「ar.sh
」
さらに、この rear.sh
は ln -s rear.sh ar.sh
のように別名 ar.sh
で動作させることにより、書庫の格納(圧縮)に用いることが出来ます。
では使い方を見てみましょう。以下は、.tar.xz
にディレクトリ filename/
を格納した例です。
$ ar.sh -t txz filename
archived from filename/ to filename.tar.xz
$ file filename.tar.xz
filename.tar.xz: xz compressed data
「-l
」オプションで内容物を確認することが出来ます。
$ ar.sh -l filename
:
このシェルスクリプトの要は、いたるところで直前のコマンドの返り値「$!
」を非明示的に AND-OR リスト「&&
, ||
」で処理を制御していることにあります。
テキスト処理
シェルそのものは正規表現の機能がなく、sed
等を組み合わせることによって、perl
等を使わなくてもある程度のテキスト処理ができます。
sed とその正規表現について以下にまとめつつあるので、そちらも参考にして下さい。
sed で力不足なら perl
の前に Awk という選択肢もあります。Awk とその正規表現についても以下にまとめつつあるので、そちらも参考にして下さい。
正規表現全般については、以下にまとめつつあります。
しかし、「Awk は方言が多い」、「正規表現内の後方参照ができない」、「正規表現による置換における後方参照が GNU Awk の gensub
じゃないとできない」、「文字列の最初の文字の添字が 1 から始まる」、「特殊文字のエスケープがシェルのそれよりも怪奇である」などなど、Awk を使うなら perl
を使った方がまだマシだと思いますが、「機能過多でない」、「消極的実装ならコードの長寿命が期待できる」などのメリットはあり得ると思われます。以下は、sed で可能な範囲ですので Awk は登場してませんが、上記に多数の実例を記してあります。
コードから HTML への変換「code2html.sh
」
簡単な例として、ソースコードを HTML にペーストできるように「<
」から「<
」への変換等を行い「<pre>
〜</pre>
」で括るようなシェルスクリプトは以下のようになります。
code2html.sh
#!/bin/sh
#
# code2html.sh
#
# Copyright (C) 2007,2014 Taiji Yamada <taiji@aihara.co.jp>
#
me="`basename \"$0\"`"
usage(){
cat <<EOF
$me - converts plain text to HTML text, <pre>stdin</pre>, <fieldset><legend>filename</legend><pre>file</pre></fieldset> and so on
usage:
$me -c string [...]
$me - < pathname
$me pathname [...]
author(s):
Taiji Yamada <taiji@aihara.co.jp>
EOF
}
code2text(){
LC_ALL=C sed 's|&|\&|g;s|<|\<|g;s|>|\>|g'
}
code2pre(){
cat <<EOF
<pre>`code2text`
</pre>
EOF
}
code2fieldset(){
cat <<EOF
<fieldset>
<legend><code>$1</code></legend>
`cat "$1" | code2pre`
</fieldset>
EOF
}
while [ "$1" != "" ]; do
case "$1" in
-h|--help) usage; exit 0 ;;
-) code2pre; exit ;;
-c) shift; echo "$@" | code2text; exit ;;
*) code2fieldset "$1" ;;
esac
shift
done
これは以下のように使うとコマンドライン引数を「<
」から「<
」への変換等行ないます。
$ code2html.sh -c 'Taiji Yamada <taiji@aihara.co.jp>'
Taiji Yamada <taiji@aihara.co.jp>
これは以下のように使うと入力ファイルを「<pre>
〜</pre>
」で括ります。
$ code2html.sh - < filename.c
<pre>
:
</pre>
また、以下のように使うとさらに「<fieldset>
〜</fieldset>
」で括ります。
$ code2html.sh filename.c
<fieldset>
<legend><code>filename.c</code></legend>
<pre>
:
</pre>
</fieldset>
このスクリプトの要は、単純な sed
のワンライナーとシェルスクリプトの関数の用途に応じた呼び出しにあります。
HTML(5) の見出しから目次の章を作成する「content-html.sh
」
HTML の「<h2><a name="…"></a>〜</h2>
」のような見出しから目次の章を自動的に作成するシェルスクリプトは以下のようになります。
content-html.sh
#!/bin/ksh
#
# content-html.sh
#
# Copyright (C) 2007,2014 Taiji Yamada <taiji@aihara.co.jp>
#
printindent(){
[ ! "$1" -gt 0 ] || printf '%+*c' `expr "$1" \* 2` ' '
}
n=0
LC_ALL=C sed -ne 's/^[ ]*<h\([2-9]\)><a name="\([^"][^"]*\)"[ ]*><\/a>\(.*\)<\/h\1>.*$/\1 \2 \3/p;s/^[ ]*<h\([2-9]\)[ ][ ]*id="\([^"][^"]*\)"[ ]*>\(.*\)<\/h\1>.*$/\1 \2 \3/p' "$1" |
while IFS=' ' read level name title; do
eval contents_${n}_level='${level}'
eval contents_${n}_name='${name}'
eval contents_${n}_title='${title}'
n=`expr $n + 1`
done
echo "<h2>Contents</h2>"
l=1
i=0
while [ $i -lt $n ]; do
eval level="\$contents_${i}_level"
eval name="\$contents_${i}_name"
eval title="\$contents_${i}_title"
while [ $l -lt $level ]; do
indent=`expr $l - 2 + 1`
printindent $indent
echo "<ul>"
l=`expr $l + 1`
done
while [ $l -gt $level ]; do
indent=`expr $l - 2`
printindent $indent
echo "</ul>"
if [ $l -gt 2 ]; then
printindent $indent
echo "</li>"
fi
l=`expr $l - 1`
done
indent=`expr $l - 2 + 1`
printindent $indent
printf "<li><a href=\"#%s\"\t>%s</a>" "$name" "$title"
i=`expr $i + 1`
if [ $i -lt $n ] && eval next_level="\$contents_${i}_level" && [ $level -lt $next_level ]; then
echo
else
echo "</li>"
fi
done
while [ $l -gt 1 ]; do
indent=`expr $l - 2`
printindent $indent
echo "</ul>"
if [ $l -gt 2 ]; then
printindent $indent
echo "</li>"
fi
l=`expr $l - 1`
done
これは以下のように使います。
$ content-html.sh index.html
これは、HTML5 の「<h2 id="…">〜</h2>
」のような見出しにも対応しています。
このページのソースもこのシェルスクリプトを利用しており、冒頭の「Contents」セクションを見れば具体例がわかるでしょう。
そして、このシェルスクリプトのコードの要は、eval
で変数を動的に定義していることと expr
で簡単な整数演算をしていることにあります。
目次の章から HTML(5) の見出しを作成する「html-content.sh
」
逆に先のシェルスクリプトで作成されたような「<ul><li><a href="#…">〜</a>
」のような見出しへのリンクから見出しを作成するシェルスクリプトは以下のようになります。
html-content.sh
#!/bin/ksh
#
# html-content.sh
#
# Copyright (C) 2007,2014 Taiji Yamada <taiji@aihara.co.jp>
#
me="`basename \"$0\"`"
LC_ALL=C sed -ne '/^[ ]*<h2>Contents<\/h2>/,/^[ ]*<h\([2-9][^>]*\)>\(.*\)<\/h\1>/{
/^[ ]*<h\([2-9][^>]*\)>\(.*\)<\/h\1>/{
/^[ ]*<h2>Contents<\/h2>/!q
d
}
/^[ ]*<ul>/{
g
s/$/\./
h
d
}
/^[ ]*<\/ul>/{
g
s/\.$//
h
d
}
/^[ ]*<li><a href="#\([^"]*\)"[ ]*>\([^<]*\).*$/{
s/^[ ]*<li><a href="#\([^"]*\)"[ ]*>\([^<]*\).*$/\1 \2/
G
s/^\(.*\)\n\([^\n]*\)$/\2 \1/
p
}
}' "$1" | while IFS=' ' read level name title; do
level="$level "
level=${#level}
case "$me" in
*html5-*)
printf "<h%u id=\"%s\"\t>%s</h%u>\n\n" $level "$name" "$title" $level;
;;
*)
printf "<h%u><a name=\"%s\"\t></a>%s</h%u>\n\n" $level "$name" "$title" $level;
;;
esac
done
これは以下のように使います。
$ html-content.sh index.html
また、ln -s html-content.sh html5-content.sh
のような別名を作成しておけば、以下のように HTML5 の「<h2 id="…">〜</h2>
」のような見出しを出力します。
$ html5-content.sh index.html
このシェルスクリプトのコードの要は、sed
において、ホールドスペースをカウンタとして用い、つまり、ホールドスペースに詰め込んだ「.
」の文字数をセクションのネストレベルに対応させていることにあります。
HTML の外部へのリンクから参考文献の章を作成する「biblio-html.sh
」
HTML の「<a href="http://...">---</a>
」のような外部へのリンクから参考文献の章を自動的に作成するシェルスクリプトは以下のようになります。
biblio-html.sh
#!/bin/ksh
#
# biblio-html.sh
#
# Copyright (C) 2014 Taiji Yamada <taiji@aihara.co.jp>
#
me="`basename \"$0\"`"
i=0
n=0
LC_ALL=C sed -ne '/<a[ ][ ]*href="\([-a-z][-a-z]*:\/\/[^"][^"]*\)"[ ]*>\([^<]*\)<\/a>/{
s/^/#/
s/<a[ ][ ]*href="\([-a-z][-a-z]*:\/\/[^"][^"]*\)"[ ]*>\([^<]*\)<\/a>/\
\1 \2\
#/g
p
}
' "$1" | LC_ALL=C sed -e '/^#/d' |
while IFS=' ' read url title; do
echo "$i $url $title"
i=`expr $i + 1`
done | LC_ALL=C sort -k 2 | LC_ALL=C uniq -f 1 | LC_ALL=C sort -n | while IFS=' ' read i url title; do
eval biblios_${n}_url="\$url"
eval biblios_${n}_title="\$title"
n=`expr $n + 1`
done
[ "$n" != 0 ] || exit 0
case "$me" in
*-html5*)
cat <<EOF
<h2 id="bibliography" >Bibliography</h2>
EOF
;;
*)
cat <<EOF
<h2><a name="bibliography" ></a>Bibliography</h2>
EOF
;;
esac
cat <<EOF
<ol>
EOF
i=0
while [ $i -lt $n ]; do
eval url="\$biblios_${i}_url"
eval title="\$biblios_${i}_title"
echo " <li><a href=\"$url\" >$title</a></li>"
i=`expr $i + 1`
done
cat <<EOF
</ol>
EOF
これは以下のように使います。
$ biblio-html.sh index.html
このページのソースもこのシェルスクリプトを利用しており、末尾の「Bibliography」セクションを見れば具体例がわかるでしょう。
また、ln -s biblio-html.sh biblio-html5.sh
のような別名を作成しておけば、以下のように HTML5 の「<h2 id="…">〜</h2>
」のような見出しを出力します。
$ biblio-html5.sh index.html
このシェルスクリプトのコードの要は、sed
において、一行内で複数マッチする可能性のあるリンクを改行つきで出力、マッチしない部分文字列をコメント行「#〜
」として出力、次のパイプラインの sed
でコメント行を除去して、最終的に一行にひとつのリンク情報を出力している点、さらに、シェルスクリプトにおいて、「sort -k | uniq -f 1
」で重複しないユニークなリンク情報のみを抽出し、「sort -n
」で元の並びに戻していることにあります。
ここで、二つの sed
をパイプラインで行なっているのは、マッチしない始めの部分を除去するのが正規表現では難しいからですが、ホールドスペースと 'D' コマンドを使えば、少々難しくなるものの、一つの sed
で実現は可能です。以下に変更箇所のみ示します。
biblio-html.sh
:
LC_ALL=C sed -ne '
/<a[ ][ ]*href="\([-a-z][-a-z]*:\/\/[^"][^"]*\)"[ ]*>\([^<]*\)<\/a>\(.*\)$/{
/^<a[ ][ ]*href="\([-a-z][-a-z]*:\/\/[^"][^"]*\)"[ ]*>\([^<]*\)<\/a>\(.*\)$/{
h
s/<a[ ][ ]*href="\([-a-z][-a-z]*:\/\/[^"][^"]*\)"[ ]*>\([^<]*\)<\/a>\(.*\)$/\1 \2/p
x
s/<a[ ][ ]*href="\([-a-z][-a-z]*:\/\/[^"][^"]*\)"[ ]*>\([^<]*\)<\/a>\(.*\)$/\3/
}
/^<a[ ][ ]*href="\([-a-z][-a-z]*:\/\/[^"][^"]*\)"[ ]*>\([^<]*\)<\/a>\(.*\)$/!{
s/<a[ ][ ]*href="\([-a-z][-a-z]*:\/\/[^"][^"]*\)"[ ]*>\([^<]*\)<\/a>\(.*\)$/\
&/
}
D
}
' "$1" |
:
プレーンテキストから HTML の生成「a2html.sh
」
次は、本格的な sed
プログラミングの例を紹介します。
例題としては AsciiDoc のように、プレーンテキストから HTML を生成する sed
を作成します。但し、AsciiDoc のようにキチンとしたものでなく、相当な制限があることを承知でこのテーマを取り上げます。
ところで、POSIX sed
の正規表現の実装には主に2種類ありまして、基本正規表現(BRE: Basic Regular Expressions) と拡張正規表現(ERE: Extended Regular Expressions) です。sed
ではよく前者が使われますが、実装されていれば必要なら perl
等で馴染み深い現代的な後者が使えます。ここでは必要なので、後者を採用することにします。
しかし、sed
の ERE を利用するには、GNU sed の実装では「-r
」を必要としますが、POSIX の実装では「-E
」が必要です。よって、シェルスクリプトでは以下のようにするとよいでしょう。
#!/bin/ksh
sed --posix < /dev/null > /dev/null 2>&1 && sed_flags='-r --posix' || sed_flags=-E
sed $sed_flags
:
さて、プレーンテキストから HTML を生成する sed
を使ったシェルスクリプトは、かなり長いですが、以下のとおりです。
a2html.sh
これは以下のように使います。
$ a2html.sh sed-howto-1.txt > sed-howto-1.html
入力プレーンテキスト:
出力 HTML:
少し異なりますが、以下のように使う例も見てみましょう。
$ a2html.sh sed-howto-2.txt > sed-howto-2.html
入力プレーンテキスト:
出力 HTML:
さらに異なりますが、以下のように使う例も紹介します。
$ a2html.sh sed-howto-0.txt > sed-howto-0.html
入力プレーンテキスト:
出力 HTML:
タグのネスト等は考慮していないので実用的ではないし、sed
にここまで求めると相当苦労することがわかると思います。
このシェルスクリプトの要は、1段目の sed
において先読みを使って AsciiDoc の複数行タイプのセクションを一行タイプのセクションに変換し、2段目の sed
では HTML の開いたタグを閉じる為に、ホールドスペースをスタックのように使っていることにあります。
Bibliography
sh
find
sort
test
read
cmp
cp
rm
mv
シェルコマンド言語
配列変数
プロセス置換
eval
sed
expr
uniq
AsciiDoc
基本正規表現(BRE: Basic Regular Expressions)
拡張正規表現(ERE: Extended Regular Expressions)
GNU sed
Written by Taiji Yamada