さて、bash, zsh の dirstack の例を、まず、示します。
taiji@Tagiri ~ % for d in ~/Documents ~/tmp ~/Sites; do pushd "$d"; done ~/Documents ~ ~/tmp ~/Documents ~ ~/Sites ~/tmp ~/Documents ~ taiji@Tagiri Sites % dirs ~/Sites ~/tmp ~/Documents ~ taiji@Tagiri Sites % pushd ~/Downloads ~/Downloads ~/Sites ~/tmp ~/Documents ~ taiji@Tagiri Sites % popd ~/Sites ~/tmp ~/Documents ~
このスタック操作を「ページ記述言語 PostScript」風に対応してしまおうというのが、本節の目的です。例えば、以下のように。
taiji@Tagiri Sites % pstack ~ ~/Documents ~/tmp ~/Sites taiji@Tagiri Sites % exch ~ ~/Documents ~/Sites ~/tmp taiji@Tagiri tmp % roll 4 -1 ~/Documents ~/Sites ~/tmp ~ taiji@Tagiri ~ % roll 4 1 ~ ~/Documents ~/Sites ~/tmp taiji@Tagiri ~ % push ~/Downloads ~ ~/Documents ~/Sites ~/tmp ~/Downloads taiji@Tagiri ~ % pop ~ ~/Documents ~/Sites ~/tmp
bash, zsh の dirs コマンドとは逆順の表示になっていることに注意。
以下の bash, zsh 用の PostScript 風 PS Dir. Stack 制御は、元々古い bash 用に作成してあったものを、現代の bash, zsh 用に ChatGPT に移植してもらったモノです。
#
# PS Dir. Stack for bash
#
pstack() {
local -a st
local line
local i
st=()
while IFS= read -r line; do
st+=( "$line" )
done < <(dirs -l -p)
# dirs は top/current を先頭に表示するので、
# PostScript スタック風に「底から上へ」表示する。
for (( i=${#st[@]}-1; i>=0; i-- )); do
printf '%s' "${st[$i]}"
(( i > 0 )) && printf ' '
done
printf '\n'
}
push() {
if (( $# != 1 )); then
printf 'usage: push DIR\n' >&2
return 2
fi
builtin pushd -- "$1" > /dev/null || return
pstack
}
pop() {
builtin popd > /dev/null || return
pstack
}
roll() {
if (( $# != 2 )); then
printf 'usage: roll M N\n' >&2
return 2
fi
if ! [[ $1 =~ ^[0-9]+$ && $2 =~ ^-?[0-9]+$ ]]; then
printf 'roll: arguments must be integers\n' >&2
return 2
fi
local -i m=$1
local -i j=$2
local -i r times
local -i i k t
local -a st
local -a new
local line
st=()
while IFS= read -r line; do
st+=( "$line" )
done < <(dirs -l -p)
if (( m < 1 || m > ${#st[@]} )); then
printf 'roll: stack has only %d entries\n' "${#st[@]}" >&2
return 2
fi
# PostScript の roll と同じ向き。
# 例: roll 2 1 は exch。
r=$(( j % m ))
(( r < 0 )) && r=$(( r + m ))
# top-first 配列なので、正の roll は逆向きに見える。
times=$(( (m - r) % m ))
for (( t=0; t<times; t++ )); do
new=()
# 上から m 番目をトップへ移動
new+=( "${st[$((m-1))]}" )
for (( k=0; k<m-1; k++ )); do
new+=( "${st[$k]}" )
done
for (( k=m; k<${#st[@]}; k++ )); do
new+=( "${st[$k]}" )
done
st=( "${new[@]}" )
done
# st[0] が新しいカレントディレクトリ
builtin cd -- "${st[0]}" || return
# directory stack を再構成
dirs -c
# pushd -n は「現在のディレクトリを変えずに」スタックへ積む。
# 望む順序にするため、末尾から積み直す。
for (( k=${#st[@]}-1; k>=1; k-- )); do
builtin pushd -n -- "${st[$k]}" > /dev/null || return
done
pstack
}
alias exch='roll 2 1'
PS Dir. Stack は、シェルのディレクトリスタックを PostScript の operand stack 風に扱うための小さな関数群です。pstack, push, pop, roll, exch を提供し、zsh と bash の両方で使えます。
#
# PS Dir. Stack for zsh
#
pstack() {
emulate -L zsh
local -a st
local i
# zsh の $dirstack には「現在のディレクトリ」は含まれないので、
# PWD を先頭、dirstack をその下に置く。
st=( "$PWD" "${dirstack[@]}" )
# PostScript のスタック表示風に、底から上へ表示する。
for (( i=${#st}; i>=1; i-- )); do
printf '%s' "${st[$i]}"
(( i > 1 )) && printf ' '
done
printf '\n'
}
push() {
emulate -L zsh
if (( $# != 1 )); then
print -u2 -- "usage: push DIR"
return 2
fi
builtin pushd -- "$1" > /dev/null || return
pstack
}
pop() {
emulate -L zsh
builtin popd > /dev/null || return
pstack
}
roll() {
emulate -L zsh
if (( $# != 2 )); then
print -u2 -- "usage: roll M N"
return 2
fi
if ! [[ $1 =~ '^[0-9]+$' && $2 =~ '^-?[0-9]+$' ]]; then
print -u2 -- "roll: arguments must be integers"
return 2
fi
local -i m=$1
local -i j=$2
local -i r times k t
local -a st new
st=( "$PWD" "${dirstack[@]}" )
if (( m < 1 || m > ${#st} )); then
print -u2 -- "roll: stack has only ${#st} entries"
return 2
fi
# PostScript の roll と同じ向きにする。
# 例: roll 2 1 は exch。
r=$(( j % m ))
(( r < 0 )) && r=$(( r + m ))
times=$(( (m - r) % m ))
for (( t=0; t<times; t++ )); do
# 上から m 番目の要素をトップへ移動する。
new=( "${st[$m]}" )
for (( k=1; k<m; k++ )); do
new+=( "${st[$k]}" )
done
for (( k=m+1; k<=${#st}; k++ )); do
new+=( "${st[$k]}" )
done
st=( "${new[@]}" )
done
# st[1] が新しいカレントディレクトリ。
builtin cd -- "${st[1]}" || return
# 残りを zsh の directory stack に戻す。
dirstack=()
for (( k=2; k<=${#st}; k++ )); do
dirstack+=( "${st[$k]}" )
done
pstack
}
alias exch='roll 2 1'
昔の bash なら、以下のコードでも動いたのですが、これでは現代ではダメでした。
# for very old bash
pstack(){
i=`expr ${#DIRSTACK[@]} - 1`; while expr $i '>=' 0 > /dev/null; do echo -n "${DIRSTACK[$i]} "; i=`expr $i - 1`; done; echo
}
roll(){
m=$1; if (($2>0)); then n=$((m-$2)); else n=$((-$2)); fi; i=0; while expr $i '<' $n > /dev/null; do DIR="`dirs -l +$(($m-1))`"; popd +$(($m-1)) > /dev/null; pushd "$DIR" > /dev/null; i=`expr $i + 1`; done; pstack
}
pop(){
popd > /dev/null
pstack
}
push(){
pushd "$1" > /dev/null
pstack
}
alias exch='roll 2 1'
筆者は PostScript の経験があるので、このスタック操作で俄然快適なのです。
しかし、ChatGPT が書き下したコードは親切ですが、長いですね。検討してみます。
省サイズ技法の開発 ‒ 重複実体を symlink 化する $ ls 2008/ 2009/ 2010/ 2011/ 2012/ 2013/ 2014/ 2015/ 2016/ 2017/ 2018/ 2019/ 2020/ 2021/ 2022/ 2023/ 2024/ 2025/ 2026/ 2027/ $ dedup-to-symlink-roll.sh 20*
まず、「=」は同名のファイルやディレクトリ配下の内容が等しいことを表す。 一方、「≠」は同名のファイルやディレクトリ配下の内容が等しくはないことを表す。 2008 └ A = ‥/2026/A ファイルがそれと等しい └ B/ = ‥/2026/B/ ディレクトリ配下がそれと等しい └ C ≠ ‥/2026/C ファイルがそれと等しくない └ D/ ≠ ‥/2026/D/ ディレクトリ配下が一部それと等しくない └ 1 = ‥/2026/D/1 ディレクトリ配下のファイルがそれと等しい └ 2 ≠ ‥/2026/D/2 ディレクトリ配下のファイルがそれと等しくない 2009 同上 └ A = ‥/2026/A └ B/ = ‥/2026/B/ └ C ≠ ‥/2026/C └ D/ ≠ ‥/2026/D/ └ 1 = ‥/2026/D/1 └ 2 ≠ ‥/2026/D/2 中略 2026 最新 └ A └ B/ └ C └ D/ └ 1 └ 2 以下のように、「→」は同名のファイルへのシンボリックリンクを表す。 そして、「=」「≠」は同名のファイルやディレクトリ配下の実体を表す。 また、そのようなシンボリックリンクを施すシェルスクリプトとなる。 2008 └ A → ‥/2026/A ファイルがそれと等しい、のでシンボリックリンク └ B/ → ‥/2026/B/ ディレクトリ配下がそれと等しい、のでシンボリックリンク └ C ≠ ‥/2026/C ファイルがそれと等しくない └ D/ ≠ ‥/2026/D/ ディレクトリ配下が一部それと等しくない └ 1 = ‥/2026/D/1 ディレクトリ配下のファイルがそれと等しい、が、シンボリックリンクしない └ 2 ≠ ‥/2026/D/2 ディレクトリ配下のファイルがそれと等しくない 2009 同上 └ A → ‥/2026/A └ B/ → ‥/2026/B/ └ C ≠ ‥/2026/C └ D/ ≠ ‥/2026/D/ └ 1 = ‥/2026/D/1 └ 2 ≠ ‥/2026/D/2 中略 2026 最新 └ A └ B/ └ C └ D/ └ 1 └ 2 次に、2027 ディレクトリが rsync で増えたとする。 2008 2027 以外は、同上 └ A → ‥/2026/A └ B/ → ‥/2026/B/ └ C ≠ ‥/2026/C └ D/ ≠ ‥/2026/D/ └ 1 = ‥/2026/D/1 └ 2 ≠ ‥/2026/D/2 2009 └ A → ‥/2026/A └ B/ → ‥/2026/B/ └ C ≠ ‥/2026/C └ D/ ≠ ‥/2026/D/ └ 1 = ‥/2026/D/1 └ 2 ≠ ‥/2026/D/2 中略 2026 └ A = ‥/2027/A └ B/ = ‥/2027/B/ └ C ≠ ‥/2027/C └ D/ ≠ ‥/2027/D/ └ 1 = ‥/2027/D/1 └ 2 ≠ ‥/2027/D/2 2027 最新 └ A └ B/ └ C └ D/ └ 1 └ 2 以下のように、「→」は同名のファイルへのシンボリックリンクを表す。 また、「赤」のシンボリックリンクは現状では実現できていない技術である。 また、そのようなシンボリックリンクを施すシェルスクリプトとなる。 2008 └ A → ‥/2027/A └ B/ → ‥/2027/B/ └ C ≠ ‥/2027/C └ D/ ≠ ‥/2027/D/ └ 1 = ‥/2027/D/1 └ 2 ≠ ‥/2027/D/2 2009 └ A → ‥/2027/A └ B/ → ‥/2027/B/ └ C ≠ ‥/2027/C └ D/ ≠ ‥/2027/D/ └ 1 = ‥/2027/D/1 └ 2 ≠ ‥/2027/D/2 中略 2026 └ A → ‥/2027/A └ B/ → ‥/2027/B/ └ C → ‥/2027/C └ D/ ≠ ‥/2027/D/ └ 1 = ‥/2027/D/1 └ 2 ≠ ‥/2027/D/2 2027 最新 └ A └ B/ └ C └ D/ └ 1 └ 2 よって、この省サイズ技法によりストレージの浪費が避けられた。
以下がそのシェルスクリプト dedup-to-symlink.sh:
#!/bin/ksh
md5d() {
[ -d "$1" ] && {
find "$1" -type f -exec md5 {} \; | sort
} || {
md5 "$1"
}
}
tac() {
tail -r
}
set -- $(printf "%s\n" "$@" | tac)
base="$1"
while [ "$2" != '' ]; do
for p in "$base"/*; do
b="$(basename "$p")"
if [[ -e "$2/$b" ]]; then
diff -q <(cd "$base" && md5d "$b") <(cd "$2" && md5d "$b") > /dev/null && {
echo "$base/$b" "$2/$b" matched
(cd "$2" &&
rm -rf "$b" &&
ln -s ../"$base/$b" .
)
} || {
:; # echo "$base/$b" and "$2/$b" differ
}
else
:; # echo "$2/$b" not found
fi
done
shift
done
以下がそのシェルスクリプト dedup-to-symlink-roll.sh:
#!/bin/ksh
tac() {
tail -r
}
basedir="$(dirname "$0")"
while [ "$2" != '' ]; do
echo "$basedir"/dedup-to-symlink.sh "$@"
"$basedir"/dedup-to-symlink.sh "$@"
set -- $(printf "%s\n" "$@" | tac)
shift
set -- $(printf "%s\n" "$@" | tac)
done
まず、職場のリモートホストに以下のように接続します。
$ ssh -fND 11080 bar.foo.ac.jp $こうすることで、sshがlocalhostのポート11080でSOCKS4サーバを立ててくれます。そこで、自宅のローカルホストのNetscape|Mozillaの「メニュー」→「編集」→「設定」→「詳細」→「プロキシ」の設定で「手動でプロキシを設定する」、「SOCKSホスト」をlocalhost、「ポート」を11080で「SOCKSv4」にチェックを付けて「OK」とします。これで、このブラウザ設定ではあたかも職場からアクセスしたようにブラウズできるようになります。
用が済んだら、そのsshのプロセスをkillして、ブラウザの設定を元に戻しておきましょう。さもないと、自宅からのプライベートなアクセスも職場からアクセスしたことになってしまいますよ。
exec ssh-agent $SHELLこの$SHELLのかわりにstartxでもよいでしょう。この$SHELLやstartxから立ち上がった子プロセスの端末上で、
$ ssh-addとして、パスフレーズを打ち込みます。これで、このssh-agentが呼び出している子プロセスの存命中はパスフレーズを入力する必要がありません。
さて、このエージェントを多段のマシンで連鎖させたい場合は、以下のように他ホストへsloginします。
$ slogin -XAC foo.bar.ac.jp %これで次のマシンへのsloginもパスフレーズを入力する必要がありません。
% slogin -XAC bar.foo.ac.jp $
注意すべきは、ssh-agentプロセスが消えるべきときにきちんと消えていること。意図せず生き残ったssh-agentは百害あって一利なし。
$ ssh -fNL 13389:remotehost:3389 foo.bar.ac.jp $リモートデスクトップ接続したいマシンの3389ポートをlocalhostへフォワーディングさせ、あとは、ローカルのWinXPマシンから「スタート」→「すべてのプログラム」→「アクセサリ」→「通信」→「リモートデスクトップ接続」で、「localhost:13389」へ接続すると、そのマシンでリモートデスクトップのサービスが動いていれば、所謂ログオン画面が出てきます。あとはそのアカウントがリモートデスクトップ権限を持ってさえいればそのマシンにログオンできるでしょう。ちなみにその為の設定は、WinXP Proだと、「コントロールパネル」→「システム」、「システムのプロパティ」→「リモート」の「リモートデスクトップ」のところで設定できます。用が済んだらオフにしておきましょう。
here$ slogin -XAC foo.example.org foo$ slogin -XAC bar.example.org bar$しかし、以下のようにするとあたかも直接ホスト bar.example.org へ slogin することができます。
slogin -XAC -o 'ProxyCommand=ssh foo.example.org nc -w 10 %h %p' bar.example.orgこれの便利なところは、scp でも直接ホスト bar.example.org とファイルのやり取りが出来る事です。
scp -o 'ProxyCommand=ssh foo.example.org nc -w 10 %h %p' bar.example.org:filename.txt .いちいち ProxyCommand を指定したくなければ下記の参考文献のように ~/.ssh/config へ bar.example.org 向け設定を書いておくと良いでしょう。
find . -type f -perm +=x -exec ls -l {} \; | less
findの-permはファイルモード及びアクセス制御リストを条件にファイルを探すためのオプションです。「+」ではなく-perm -=xとすると、ユーザ(u:user)、グループ(g:group)、その他(o: other)のすべて(a:all)に実行権がついたファイルを探すという意味になります。「=」の前はugo,aを指定できますが、省略する場合でも=は必要なようです。また、-typeはディレクトリ、通常のファイルなどのファイルタイプを条件にファイルを探すオプション。よって、-type fがないと実行権が付いてて構わないディレクトリにもマッチしてしまいますので注意です。
さて、確認してそれらの実行権を外しても問題ないなら、以下のように一気に直せます(但し、本当に実行するときは途中のechoを取って下さい)。
find . -type f -perm +=x -exec echo chmod -x {} \;
findを使って以下のようにします。find . -depth -type d -empty -printここで、空のディレクトリを消す場合、空のディレクトリを消した結果、その親ディレクトリが空のディレクトリになり、それも消したいものとします。
findはデフォルトでは「幅優先探索」なので、それだと親ディレクトリは探索済みで消す機会が失われています。よって、「-depth」オプションにより「深さ優先探索」させると、空になった親ディレクトリも消すことが出来ます。find . -depth -type d -empty -exec rmdir {} \;
逆に、空になった親ディレクトリを消したくない場合には、「-depth」オプションを外せばよいのです。
% ls abc-1.01/ abc-1.01.tar.gzしかし、このabc-1.01.tar.gzは作業ディレクトリabc-1.01/にある内容よりも本当に新しいのだろうか?
そのような場合、以下のようにfindして何も見つからなければ、アーカイブしたものが開いてあるディレクトリの内容よりも新しいことがわかります。
% find abc-1.01/ -newer abc-1.01.tar.gz -printfindの-newerは、指定されたファイルよりも新しいファイルを探すためのオプション。
そのような場合、X11に付属のlndirが便利。
% mkdir abc-1.01-doc % cd abc-1.01-doc % lndir ~foo/tex/abc-1.0-docこうすると、ディレクトリ構造を保存したまま再帰的にすべてのファイルがシンボリックリンクされます。(2002/11/18注)ハードディスクの価格が安くなって広大なディスクスペースが使えるようになった現在、この方法で省スペース化を計るのは、かなり時代錯誤になりつつあるでしょう。よって、以上の方法でシンボリックリンクしたディレクトリツリーを通常のディレクトリツリーに戻す方法を紹介します。
% cd abc-1.01-doc
% find . -lname ~foo/tex/abc-1.0-doc/\* -exec rm {} \;
% rsync -auv ~foo/tex/abc-1.0-doc/ ./
ここでは、後述のrsyncを用いています。とはいえ、作業ディレクトリを引き継ぐのではなく「共同作業」をするのであれば、cvsとかsvnとかの利用を検討すべきでしょう。
% rsync -n -au /home/webmaster/public_html/ /usr/local/etc/httpd/htdocs/ % rsync -n -au /usr/local/etc/httpd/htdocs/ /home/webmaster/public_html/同期元、同期先のディレクトリ指定の最後の「/」はディレクトリを意味しているので、必ずつけること。 -nオプションは同期すべきファイルを実際にはコピーせず表示だけする時に使う。-aはアーカイブモード、-uはタイムスタンプを比較して配布先より新しいもののみを更新したい場合に使う。以上のように確認したら、-nオプションを外して再実行し、実際にディレクトリを同期すればよい。また、--deleteオプションをつければ、配布先にあって、配布元にないゴミファイルを消してくれる。
rsyncに関してはUNIX Magazine 7, 2000にわかりやすい解説あり。
以下はfooの~/tex/paper-2000とbarの~/tex/paper-2000を同期させる例。
foo% rsync -e ssh -au ~/tex/paper-2000/ bar:tex/paper-2000/ foo% rsync -e ssh -au bar:tex/paper-2000/ ~/tex/paper-2000/もし、foo上で削除したファイルをbarでも反映させたい場合は、以下のようにすべき。
foo% rsync -e ssh -au --delete ~/tex/paper-2000/ bar:tex/paper-2000/いずれにせよ、-nオプションでまず確認した方がよろしいかと。
$ du -ha | lessなどとすれば、ホームのすべてのディレクトリ及びファイルのサイズとその総和を表示してくれます。ここで-hオプションでファイルサイズの単位を読み易い形式に指定しています。けれども、その表示は見やすいとは言えません。そこで、du2ps を使って PostScript 形式で視覚的に判り易い出力を得る事ができます。
$ du -a | du2ps | gv -但し、この du2ps はかなりレガシーなプログラムなので、昨今のラージファイルサイズには対応してませんし、du の-hオプションに相当する機能も欲しいところです。そのような機能を加えるパッチを用意したのでここに紹介します。
$ du -a | du2ps -h | gv - $ du -k | du2ps -K -h | gv -ちなみに、du -hを入力としても、誤差は避けられないもののうまく処理してくれます。
$ du | du2pdf -h > du2.pdf $ open -a /Applications/Adobe\ Reader\ 8/Adobe\ Reader.app du2.pdf但し、Adobe CMap の UniJIS-UTF8-H を使っているので、Mac OS X Snow Leopard までのプレビューでは、残念ながら表示できないようです。
% cd abc-1.01 % configure --prefix=/usr/local % make # su # make installをした後は、以下のコマンドを習慣づけてみるのはどうかな?
% find /usr/local /usr/X11R6 /etc ! -type d -newer Makefile | tee ../abc-1.01.installedすると、新規ファイルや改変されたファイルのリストが得られるので、アンインストールも比較的簡単。
% xmkmf -a; make % su # make install install.manでも同様です。Makefileを生成しないものはコンパイル前に何かファイルをtouchしておくのも良いかと。
但し、アンインストールと言っても、やみくもにrm -f `cat abc-1.01.installed`とやると、まれに消してはいけないファイル(/usr/local/info/dir等)も消してしまう可能性があるので気をつけよう。逆にまれに、cp -pでインストールさせるのもあるのでこの方法では完璧なインストールリストを作成しているわけではないことに注意。そういったものを求めている人は、 Makefileをよく眺めてインストール時のルートディレクトリを変更可能か確認した上で別のディレクトリにインストールし、そのリストを保持しておくとか、素直に各種パッケージの流儀に沿った管理をしましょう。
$ stat -f '%Sm' -t '%Y%m%d' index.html 20071109のようにファイルのタイムスタンプを好みのフォーマットで得られます。しかし、このコマンドはPOSIX準拠のコマンドではなく Solaris に無いので困ってしまいます。
fstrftime(){ # $0 [-F=format] file [...]
perl -s -e 'use POSIX; for $arg (@ARGV) {
($mtime=(stat($arg))[9]) &&
print strftime(($F?$F:"%Y%m%d")."\n",localtime($mtime));
}' -- "$@"
}
ほとんどは以上で十分だと思いますが、この程度のことでPerlやオペレーティングシステムに依存したりするのは好ましくないかもしれません。よってプログラミング言語Cで必要十分な機能を書いてしまいました。$ fstrftime --help usage: fstrftime [+format|-l] [-a|-m|-c] [-L|-P] [-p|-f] [--] file [...] $ fstrftime index.html 20071109意外と便利だと思うのですが如何でしょうか。# mv index.html index.html-`fstrftime index.html`とかやってしまいます。
$ stat -f '%Su.%Sg' index.html taiji.taijiのようにファイルのオーナー情報を得るのは簡単なのですが、やはり、このコマンドはPOSIX準拠ではないので、どのシステムにもあるとは限りません。
fwhose(){ # $0 [-u|-g] [-n] file [...]
perl -s -e 'for $arg (@ARGV) { if (($un,$gn)=(stat($arg))[4,5]) {
($un,$gn)=((getpwuid($un))[0],(getgrgid($gn))[0]) if (!$n);
if ((!$u&&!$g)||($u&&$g)) { printf "%s.%s\n", $un, $gn; }
elsif ($u) { printf "%s\n", $un; }
else { printf "%s\n", $gn; }
}}' -- "$@"
}
そもそもの用途が限られているので以上で十分かどうかはなんとも言えませんが、やはりこの程度のことでPerlやオペレーティングシステムに依存したりするのは好ましくありません。よってプログラミング言語Cで必要十分な機能を書いてしまいました。$ fwhose --help usage: fwhose [-u|-g] [-n|-N] [-L|-P] [-p|-f] [--] file [...] $ fwhose index.html taiji.taiji例えばrootで動作するスクリプト内で、root権限でそのまま実行せずにファイルの所有者権限へ降格してから実行する際などに有用かと思われます。
$ fwho | less
$ fwho -C -l /pathto/lastlog -u /pathto/utmp -w /pathto/wtmp.{4,3,2,1,0}.gz /pathto/wtmp | less
gzip で圧縮されたファイルを指定してもオッケーです。また、-C オプションでエンディアン変換をします。ユーザのログイン情報をサクッと見たいときは、かなり便利です。[2009/4/3追記] 以前のは、BSD 系, Mac OS X Tiger しか考慮していませんでしたが、Linux および、Solaris の utmpx に対応してみました。$ ./logdo -s 'mailx foo@example.com' \ -w 86400 '(Request|userpath|Interpolated|Connection from)' /var/log/git-daemon.log \ -w 1800 -v '(Did not receive identification string|Invalid user|reverse mapping checking)' ' sshd' /var/log/secure.logさらに、このような長大なコマンドライン引数が煩わしいなら、以下のように「-C config -q」オプションを末尾に加えれば設定ファイルが生成されますので、
$ ./logdo 〃 -C config.xml -q次からは以下のようにすれば上例と同じ監視ができます。
$ ./logdo -c config.xml実はこの logdo はプログラミング言語 C++ で書かれており、以下のように Boost C++ ライブラリを積極的に活用しているので、たったの200行足らずのソースコードです。
$ ./write_large_data -s `echo '1024*1024*1024' | bc` test.bin 100% / 1GB(0.1Gbps) 1024MB(78.3Mbps) 1048576KB(80181.6Kbps)Mac OS X 32/64bit と Solaris 32/64bit で動作確認してあります
#!/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" # $base の後の空白はタブ
done | while IFS=" " read b d; do # IFS= のあとの空白はタブ
find "$d" ! -path "$d" -maxdepth 1 -iname "$b" ! -name "$b" -print
done | env LC_ALL=C sort -f
done
shift
done
ちなみに、最初の find に -L オプションをつけると、シンボリックリンクも辿るようになります。しかし、これではとても効率が悪いですね。そこで、ここでは C および boost C++ によるものを書いてみました。
まず C によるコード c/samenamesicase.c:
/*
samenamesicase.c - search case-insensitive same named files recursively
Copyright (C) 2013 Taiji Yamada <taiji@aihara.co.jp>
This program is distributed under the BSD 2-clause license.
See http://opensource.org/licenses/BSD-2-Clause
*/
#include <stdio.h>
#include <stdlib.h>
#include <stddef.h> /* offsetof() */
#include <string.h>
#include <sys/types.h> /* dirent.h for the legacy */
#include <dirent.h> /* struct dirent, etc */
#include <unistd.h> /* pathconf(, _PC_NAME_MAX), etc */
#include <limits.h> /* PATH_MAX */
#include <sys/stat.h> /* S_ISLNK(), etc */
#include <errno.h>
int pathname_ignore_case_cmp(const void *a, const void *b)
{
return strcasecmp(*(const char **)a, *(const char **)b);
}
int directory_tree(const char *pathname, int follow_symlinks)
{
DIR *d;
char *p, **v;
struct dirent *entry, *result;
size_t c = 0, i = 0, j, k, l;
long name_max;
if ((d = opendir(pathname))) {
if (!((name_max = pathconf(pathname, _PC_NAME_MAX)) > 0))
name_max = PATH_MAX;
if (!(entry = malloc(offsetof(struct dirent, d_name) + name_max + 1))) {
fprintf(stderr, "error: entry = malloc()\n");
exit(1);
}
while (readdir_r(d, entry, &result) == 0 && result) {
if (strcmp(entry->d_name, ".") == 0 ||
strcmp(entry->d_name, "..") == 0)
continue;
++c;
}
rewinddir(d);
k = c;
v = malloc(sizeof(char **)*c);
while (readdir_r(d, entry, &result) == 0 && result) {
if (strcmp(entry->d_name, ".") == 0 ||
strcmp(entry->d_name, "..") == 0)
continue;
v[i] = malloc(strlen(entry->d_name) + 1);
strcpy(v[i], entry->d_name);
++i;
}
rewinddir(d);
qsort(v, c, sizeof(char *), pathname_ignore_case_cmp);
for (i=0, j=1; j<c; ++i, ++j) {
if (pathname_ignore_case_cmp(&v[i], &v[j]) == 0) {
if (k != i) printf("%s/%s\n", pathname, v[i]);
printf("%s/%s\n", pathname, v[(k=j)]);
}
else if (k != c) {
printf("\n");
k = c;
}
}
if (k != c)
printf("\n");
for (i=0; i<c; ++i)
free(v[i]);
free(v);
if (!(p = malloc((l=strlen(pathname)) + 1))) {
fprintf(stderr, "error: p = malloc(strlen(pathname) + 1)\n");
exit(1);
}
while (readdir_r(d, entry, &result) == 0 && result) {
if (strcmp(entry->d_name, ".") == 0 ||
strcmp(entry->d_name, "..") == 0)
continue;
if (!(p = realloc(p, l + 1 + strlen(entry->d_name) + 1))) {
fprintf(stderr, "error: p = realloc(p, l + 1 + strlen(entry->d_name) + 1)\n");
exit(1);
}
strcpy(p, pathname);
strcat(p, "/");
strcat(p, entry->d_name);
if (!follow_symlinks) {
struct stat sb;
if (lstat(p, &sb) == 0 && S_ISLNK(sb.st_mode))
continue;
}
directory_tree(p, follow_symlinks);
}
closedir(d);
free(p);
free(entry);
}
else {
switch (errno) {
case ENOTDIR: break;
default:
fprintf(stderr, "error: %s: \"%s\"\n", strerror(errno), pathname);
break;
}
}
return 0;
}
int usage(int argc, char *argv[], FILE *fp)
{
(void)argc;
fprintf(fp, "%s [-h|--help] [-L] pathname [...]\n", argv[0]);
return (fp == stderr) ? 1 : 0;
}
int main(int argc, char *argv[])
{
int rv = 0, a;
int follow_symlinks = 0;
if (argc < 2) return usage(argc, argv, stderr);
for (a=1; a<argc; a++)
if (strcmp(argv[a], "-h") == 0 || strcmp(argv[a], "--help") == 0)
return usage(argc, argv, stdout);
else if (strcmp(argv[a], "-L") == 0)
follow_symlinks = !0;
else
rv = directory_tree(argv[a], follow_symlinks);
return rv;
}
次は C++ によるコード samenamesicase.cc:
/*
samenamesicase.cc - search case-insensitive same named files recursively
Copyright (C) 2013 Taiji Yamada <taiji@aihara.co.jp>
This program is distributed under the BSD 2-clause license.
See http://opensource.org/licenses/BSD-2-Clause
*/
#include <iostream>
#include <vector>
#include <boost/algorithm/string.hpp> // to_upper(), etc
#include <boost/filesystem.hpp>
namespace fs = boost::filesystem;
inline
bool filename_ignore_case_less(const fs::path &a, const fs::path &b)
{
std::string A(a.filename().string()), B(b.filename().string());
boost::to_upper(A);
boost::to_upper(B);
return A < B;
}
inline
bool filename_ignore_case_equal_to(const fs::path &a, const fs::path &b)
{
return !filename_ignore_case_less(a, b) &&
!filename_ignore_case_less(b, a);
}
int directory_tree(const fs::path &p, bool follow_symlinks)
{
try {
if (!fs::exists(p)) return 1;
if (fs::is_directory(p)) {
std::vector<fs::path> v;
copy(fs::directory_iterator(p), fs::directory_iterator(),
back_inserter(v));
std::sort(v.begin(), v.end(), filename_ignore_case_less);
std::vector<fs::path>::const_iterator it, jt, kt = v.end();
for (it=v.begin(), jt=it+1; jt!=v.end(); ++it, ++jt) {
if (filename_ignore_case_equal_to(*it, *jt)) {
if (kt!=it) std::cout << it->string() << std::endl;
std::cout << (kt=jt)->string() << std::endl;
}
else if (kt != v.end()) {
std::cout << std::endl;
kt = v.end();
}
}
if (kt != v.end())
std::cout << std::endl;
for (it=v.begin(); it!=v.end(); ++it) {
if (!follow_symlinks && is_symlink(*it))
continue;
directory_tree(*it, follow_symlinks);
}
}
}
catch (const fs::filesystem_error &ex) {
std::cerr << ex.what() << std::endl;
return 1;
}
return 0;
}
int usage(int argc, char *argv[], std::ostream &os)
{
(void)argc;
os << argv[0] << " [-h|--help] [-L] pathname [...]" << std::endl;
return os == std::cerr ? 1 : 0;
}
int main(int argc, char *argv[])
{
int rv = 0, a;
bool follow_symlinks = false;
if (argc < 2) return usage(argc, argv, std::cerr);
for (a=1; a<argc; ++a)
if (std::string(argv[a]) == "--help")
return usage(argc, argv, std::cout);
else if (std::string(argv[a]) == "-L")
follow_symlinks = true;
else
rv = directory_tree(fs::path(argv[a]), follow_symlinks);
return rv;
}
いずれも、-L オプションをつけると、シンボリックリンクも辿ります。
シェルスクリプトの方は、find -type d でディレクトリを辿りつつ、すべてのディレクトリエントリ名について同ディレクトリ内の大文字小文字区別なしの同名を find -maxdepth 1 -iname name で探していますが、C および C++ の方は、ディレクトリを辿るのは同様ですが、すべてのディレクトリエントリ名を大文字小文字区別なしで並び変えて、そこから大文字小文字区別なしの同名をリストアップしていおり、これよりも効率的なものは思いつかないです。