Tips on Unix コマンドライン

ここでは Unixで役立つちょっとしたコマンドの秘訣をメモしてあります。これは筆者が、「こんな使い方があるんだあ〜」とか「なんか、こうしたら出来た〜」とか思ったUnixの使い方の中でも、特にコマンドラインの秘訣について紹介されています。「もっとこうやった方がいいよ」とかがたぶんあると思うので、ご教示頂ければ幸甚です。(Mac OS X 関係はこちら)

Contents

一般

OpenSSHの秘訣

あたかも職場からウェブアクセスしているかのように自宅のブラウザで閲覧したい

職場のIPアドレスからだとアクセスが許されているのに、一般にはアクセス制限されているサイトってありますよね。日曜でも自宅で研究しているような仕事の虫?には、自宅からでもあたかも職場からアクセスしているかのようにウェブブラウズしたいものです。もちろん、X11が使えるならslogin -Xで職場にログインしてリモートのブラウザを開いてもいいのですが、レスポンスも遅いのであまり実用的ではないでしょう。そういうときは、sshの-D オプション、ダイナミックポートフォワーディングとNetscape|Mozillaのプロキシ設定が使えます。

まず、職場のリモートホストに以下のように接続します。

$ ssh -fND 11080 bar.foo.ac.jp
$ 
こうすることで、sshがlocalhostのポート11080でSOCKS4サーバを立ててくれます。そこで、自宅のローカルホストのNetscape|Mozillaの「メニュー」→「編集」→「設定」→「詳細」→「プロキシ」の設定で「手動でプロキシを設定する」、「SOCKSホスト」をlocalhost、「ポート」を11080で「SOCKSv4」にチェックを付けて「OK」とします。これで、このブラウザ設定ではあたかも職場からアクセスしたようにブラウズできるようになります。

用が済んだら、そのsshのプロセスをkillして、ブラウザの設定を元に戻しておきましょう。さもないと、自宅からのプライベートなアクセスも職場からアクセスしたことになってしまいますよ。

パスフレーズの入力を極力少なくしたい

cvsやrsyncをssh経由で使用していたり、多段にマシンにsloginしていたりすると、どうしてもパスフレーズの入力が億劫になってきます。これを横着する由緒あるやり方ってなんでしょうか。それは、ssh-agentと sshの-Aオプション、ForwardAgent機能です。例えばあなたのログインスクリプトで以下のようにssh-agentを実行させます。
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は百害あって一利なし。

ファイアーウォール内のPCを急遽リモートから面倒みてあげなきゃいけなくなった

そのPCがUnix系ならそこへslogin出来ればなんとかなろうものですが、Win系だと「リモートデスクトップ接続」が可能であれば一番やりやすい場合もあるかと思います。ローカルのCygwin環境等を使ってslogin可能なマシンへ以下のように接続し、
$ ssh -fNL 13389:remotehost:3389 foo.bar.ac.jp
$ 
リモートデスクトップ接続したいマシンの3389ポートをlocalhostへフォワーディングさせ、あとは、ローカルのWinXPマシンから「スタート」→「すべてのプログラム」→「アクセサリ」→「通信」→「リモートデスクトップ接続」で、「localhost:13389」へ接続すると、そのマシンでリモートデスクトップのサービスが動いていれば、所謂ログオン画面が出てきます。あとはそのアカウントがリモートデスクトップ権限を持ってさえいればそのマシンにログオンできるでしょう。ちなみにその為の設定は、WinXP Proだと、「コントロールパネル」→「システム」、「システムのプロパティ」→「リモート」の「リモートデスクトップ」のところで設定できます。用が済んだらオフにしておきましょう。

ファイアーウォール内のsshdへログインしたい

ホスト foo.example.org からホスト bar.example.org へは slogin でき、外部からは foo.example.org へ直接 slogin できるが、bar.example.org へは直接 slogin できないような状況があります。例えば、bar.example.org がプライベートアドレス空間であるような場合です。ちょっとした作業なら以下のように多段で slogin するでしょう。
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 向け設定を書いておくと良いでしょう。
(参考文献: ncある限りぼくはどこまででもいけるッ! - (ひ)メモ)

実行権のついたファイルを探したい

例えば、他の環境でアーカイブしたファイルをUnixで展開したりしたときに、普通のファイルに何故か意味なく実行権が付いてたりします。放っておいても構わないでしょうが、結構目障りです。直すとしても、そのようなファイルがディレクトリ階層的にたくさんあると、直すのもひと苦労です。まずは以下のようにfindでそのようなファイルを確認する事ができます。
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 -print
find-newerは、指定されたファイルよりも新しいファイルを探すためのオプション。

人の仕事を部分的に引き継ぎたい

fooさんのTeXディレクトリ~foo/tex/abc-1.0-docを自分で引き継いで作業しなければならなくなった。しかし、画像ファイルとかいろいろサイズが大きいので、まるごとコピーするのは辛い。

そのような場合、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にわかりやすい解説あり。

ネットワーク経由でディレクトリを同期したい

上記の話題に関連して、ネットワーク経由でディレクトリの同期、もしくは同期の確認をしたい場合、これもrsyncを使いましょう。でも、セキュリティ上、rshのポートなんか閉じてますよね。でも大丈夫。rsyncはsshの利用も想定されている。

以下は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(1)があります。例えば、自分のホームで
$ 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を入力としても、誤差は避けられないもののうまく処理してくれます。

また、UTF-8なファイルシステム、例えば Mac OS X では、この du2pdf スクリプトを用意して以下のようにすれば、日本語等のファイル名も表示してくれます。
$ 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 までのプレビューでは、残念ながら表示できないようです。

ところで、同種のプログラムに xdu というのもあります。

管理者

configure; make; make installしたけど、果たしてどのファイルが新しくインストールされたのか…

例えば以下のようなインストール作業、
% 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をよく眺めてインストール時のルートディレクトリを変更可能か確認した上で別のディレクトリにインストールし、そのリストを保持しておくとか、素直に各種パッケージの流儀に沿った管理をしましょう。

プログラム

シェルスクリプト内でファイルのタイムスタンプを得たい

NetBSD由来のstat(1)コマンドを使えば、
$ stat -f '%Sm' -t '%Y%m%d' index.html
20071109
のようにファイルのタイムスタンプを好みのフォーマットで得られます。しかし、このコマンドはPOSIX準拠のコマンドではなく Solaris に無いので困ってしまいます。

では、どの環境にでもインストールされていることが現在ではほぼ期待できるPerlで、こうしたファイルのタイムスタンプを得るコードをシェルスクリプト内で書いてみると、以下のようになります。
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`とかやってしまいます。

シェルスクリプト内でファイルのオーナー情報を得たい

前述の問題とほとんど同様なのですが、NetBSD由来のstat(1)コマンドで
$ stat -f '%Su.%Sg' index.html 
taiji.taiji
のようにファイルのオーナー情報を得るのは簡単なのですが、やはり、このコマンドはPOSIX準拠ではないので、どのシステムにもあるとは限りません。

やはり、どの環境にでもインストールされていることが現在ではほぼ期待できるPerlで、こうしたファイルのタイムスタンプを得るコードをシェルスクリプト内で書いてみると、以下のようになります。
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権限でそのまま実行せずにファイルの所有者権限へ降格してから実行する際などに有用かと思われます。

lastlog, utmp, wtmp を全部みたい、指定したい

last(1), who(1) などを使えば utmp, wtmp などに書いてある情報が見れますが、ローテイトされているファイル等、直接ファイルを指定してみたい場合があります。last(1) の -f オプションで utmp, wtmp 形式は指定出来ますが、lastlog 形式は指定出来ませんし、Mac OS X Tiger のように圧縮されているといちいち伸長するのも面倒です。また、エンディアンが異なるマシンで見ようとすると時刻が狂ってしまいます。なので、必要最低限のコマンドをサクッと書いてしまいました。
$ 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 に対応してみました。

ログファイルを定期的に監視し、何か異常がある時にはメールを送る等のコマンドを実行したい

このような目的には、http://logcheck.org/ の logcheck や http://www.logwatch.org/ の logwatch などいろいろあります。私は長らくはhttp://www.aihara.co.jp/~junt/tools/logcheck/ の logcheck で事足りていたのですが、他も同様に煩雑さや柔軟性の無さの問題から、もっとシンプルで柔軟性のあるコマンドを自作するに至りました。それが、logdo です。
例えば以下のようにすると、24時間毎に /var/log/git-daemon.log にて正規表現「(Request|userpath|Interpolated|Connection from)」にマッチする行を、30分毎に /var/log/secure.log で正規表現「 sshd」にマッチし「(Did not receive identification string|Invalid user|reverse mapping checking)」にマッチしない行が mailx コマンドを使ってメールで foo@example.com に通達されます。
$ ./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行足らずのソースコードです。
  1. 正規表現処理 … Boost Xpressive
  2. 複数のログファイルの並列監視 … Boost Thread
  3. XML形式コンフィグファイル入出力 … Boost Serialization
よって、Boost プログラミングの好例にもなっていると思います。ちなみに、tarball にはおまけとして他のプログラムもありますが、
の順に diff コマンド等で眺めてみると、完成に至る過程がよく判るようになっています。

検査用に大きなサイズのファイルを書く

レガシーなシステムだと2GB以上のファイルの扱いが特殊だったり、対応してなかったり、プログラムのビルド時に別途定義が必要だったりします。また、ある程度のサイズの書き込みをするとなんらかの問題が生じることを再現したり、ストレージの書き込み速度を測定したりしたい場合があります。以下のプログラムは、以上の目的で速攻でっち上げたプログラムです。
但し現状、dd コマンドと SIGINFO シグナルの送信でほぼ代替できる程度のものです。
$ ./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 で動作確認してあります

大文字小文字区別ありのファイルシステムで大文字小文字区別なしで同名のファイルを探す

例えば、UFS から HFS+ のファイルシステムへ移行する際、ついでなので Mac OS X 規定の大文字小文字区別なしだと失われてしますファイルを予め調査しておきたい場合があるかと思います。つまり、大文字小文字区別ありのファイルシステムで大文字小文字区別なしで同名のファイルを探す、まずはシェルスクリプトを作成してみました。
#!/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++ の方は、ディレクトリを辿るのは同様ですが、すべてのディレクトリエントリ名を大文字小文字区別なしで並び変えて、そこから大文字小文字区別なしの同名をリストアップしていおり、これよりも効率的なものは思いつかないです。

Copyright (C) 1998,2000,2003,2007,2009,2013 Taiji Yamada