わさっきhb

大学(教育研究)とか ,親馬鹿とか,和歌山とか,とか,とか.

PATHの不要なパスを削除

Cygwin環境なのですが,PATHに入っているパス(コマンドサーチパス)に重複があります.

$ echo $PATH
/home/takehiko/Lib/ruby/bin:/cygdrive/c/Program Files/SlikSvn/bin:/cygdrive/c/Program Files/TortoiseSVN/bin:/home/takehiko/bin:/usr/local/bin:/usr/bin:/bin:/usr/X11R6/bin:/usr/local/bin:/usr/bin:/bin:/usr/X11R6/bin:/cygdrive/c/tex/bin:/cygdrive/c/Program Files/SlikSvn/bin/:/cygdrive/c/Windows/system32:/cygdrive/c/Windows:(以下略)

/usr/local/bin, /usr/bin, /binが重複しています.しかし,~/.zshrc他を見ても,どこで重複されているかはわかりません.
見苦しいし,ついでにzshの勉強を兼ねて,この重複を除去することにしました.
実は10年以上前に,重複を取り除くPerlスクリプトを書いたことがあります.

#!/usr/bin/perl

# pathpack : $path あるいは $PATH のうち、無駄なパス名を省く
# 使用方法 : set path=(`pathpack $path`)
#            PATH=`pathpack $PATH`

$sep = " ";			# パス名間の区切り子
@path = @ARGV;			# パス名配列
if ($#path == $[) {
	$_ = $path[$[];
	if (/:/) {
		@path = split(/:/, $_);
		$sep = ":";
	} elsif (/;/) {
		@path = split(/;/, $_);
		$sep = ";";
	}
}

%path = ();			# パス配列用の連想配列
for ($pathnum = $#path; $pathnum >= $[; $pathnum--) {
	$p = $path[$pathnum];
	$path{$p} = $pathnum;
}

@newpath = ();
for ($pathnum = $#path; $pathnum >= $[; $pathnum--) {
	$p = $path[$pathnum];
	if ($path{$p} == $pathnum) {
		unshift(@newpath, $p);
	}
}

$" = $sep;
print "@newpath\n";

exit;

パスの環境変数がPATHのsh系でも,pathのcsh系でも実行可能です(ちなみにzshでもうまくいきました).しかしPerlから離れてだいぶ過ぎまして,コードの保守もままなりません.
zshで書くにあたり,sedなどの外部コマンドは一切使用しないという制約を設けました.シェル起動時に実行しても,時間をかけないようにしたいのです.
それで,コードです.

function debug_mode() { return 1 }  # 0のときデバッグモード

function pathpack() {
    local q=':'
    for p in $path
    do
	debug_mode && echo $p
	[ "$q" '==' "${q/:$p:/}" ] && q="$q$p:"
	debug_mode && echo $q
    done
    q=${q#:}
    q=${q%:}
    debug_mode && echo $q
    debug_mode || PATH=$q
}

何をしているのかというと,forループで,パスを先頭から順に取り出して,初出のパスなら,文字列をとる変数qに付け加えています.初期値を ":" とし,追加する際には "パス:" を付け加えます.最終的には ":パス1:パス2:…:パス最後:" という形の文字列になります.先頭と末尾の ":" を取り除き,PATHに代入すれば,完了です.
パスの取り出しには,文字列の環境変数PATHからパラメータ分割するのではなく,配列のpathを使用しました.
「[ "$q" '==' "${q/:$p:/}" ]」が肝心なところですが,すでにパスpが登録されていれば,右辺ではqから該当箇所のパスを除去した文字列ができ,両辺が異なるため偽となります.パスpが登録されていなければ,除去できないので両辺の文字列は等しく,真となるので,&&以降の処理すなわちパス追加をします.なお,「==」をクォートしていなければ,「zsh: = not found」というエラーが出ます.また,「${q/:$p:/}」のところを「${q#:$p:}」としても,うまくいきませんでした.
最後に,debug_modeですが,デバッグモードを簡潔に表すために定義した関数です.以下の効果があります.

  • debug_modeがreturn 0のときに,「debug_mode && コマンド」はコマンドを実行する
  • debug_modeがreturn 0のときに,「debug_mode || コマンド」はコマンドを実行しない
  • debug_modeがreturn 1のときに,「debug_mode && コマンド」はコマンドを実行しない
  • debug_modeがreturn 1のときに,「debug_mode || コマンド」はコマンドを実行する

動作確認しときましょう.

$ echo $PATH
/home/takehiko/Lib/ruby/bin:/cygdrive/c/Program Files/SlikSvn/bin:/cygdrive/c/Program Files/TortoiseSVN/bin:/home/takehiko/bin:/usr/local/bin:/usr/bin:/bin:/usr/X11R6/bin:/usr/local/bin:/usr/bin:/bin:/usr/X11R6/bin:/cygdrive/c/tex/bin:/cygdrive/c/Program Files/SlikSvn/bin/:/cygdrive/c/Windows/system32:/cygdrive/c/Windows:(以下略)
$ echo $PATH | md5sum
40824a1dfb341d08282ce4fa4fa2b5c4 *-
$ perl ~/bin/pathpack "$PATH"
/home/takehiko/Lib/ruby/bin:/cygdrive/c/Program Files/SlikSvn/bin:/cygdrive/c/Program Files/TortoiseSVN/bin:/home/takehiko/bin:/usr/local/bin:/usr/bin:/bin:/usr/X11R6/bin:/cygdrive/c/tex/bin:/cygdrive/c/Program Files/SlikSvn/bin/:/cygdrive/c/Windows/system32:/cygdrive/c/Windows:(以下略)
$ perl ~/bin/pathpack "$PATH" | md5sum
6a37db66349e004af218f887f7040dca *-
$ function debug_mode() { return 0 }
$ pathpack | tail -n 1
$ echo $PATH
/home/takehiko/Lib/ruby/bin:/cygdrive/c/Program Files/SlikSvn/bin:/cygdrive/c/Program Files/TortoiseSVN/bin:/home/takehiko/bin:/usr/local/bin:/usr/bin:/bin:/usr/X11R6/bin:/cygdrive/c/tex/bin:/cygdrive/c/Program Files/SlikSvn/bin/:/cygdrive/c/Windows/system32:/cygdrive/c/Windows:(以下略)
$ pathpack | tail -n 1 | md5sum
6a37db66349e004af218f887f7040dca *-

最後に,~/.zshrcに,debug_modeとpathpackの定義のあとに,「pathpack」を書けば,設定完了です.