我々がシェルを操作している時間のうち、たぶん8割くらいはcd
とls
を打っているんじゃないかとすら、私は思っています。
世の中には様々なシェルハックが溢れている昨今ですが、ls
は置いといて、cd
のコストを減らせれば、そこそこの効率化が図れるのではないでしょうか。そんなことを考えて色々な改善を考えてみたことをまとめた記事です。
私はBashユーザなのでBashの話ですが、Zshでも多少の改変で応用が効くのではないかと思います。
ここでの試行錯誤は2014年3月現在のものです。興味深い改造があれば都度書き足していきます。
##cdのオプション
まず man bash
から cd
の説明を観てみましょう。
cd [-L|-P] [dir]
Change the current directory to dir. The variable HOME is the
default dir. The variable CDPATH defines the search path for
the directory containing dir. Alternative directory names in
CDPATH are separated by a colon (:). A null directory name in
CDPATH is the same as the current directory, i.e., ``.''. If
dir begins with a slash (/), then CDPATH is not used. The -P
option says to use the physical directory structure instead of
following symbolic links (see also the -P option to the set
builtin command); the -L option forces symbolic links to be fol-
lowed. An argument of - is equivalent to $OLDPWD. If a non-
empty directory name from CDPATH is used, or if - is the first
argument, and the directory change is successful, the absolute
pathname of the new working directory is written to the standard
output. The return value is true if the directory was success-
fully changed; false otherwise.
cd
コマンド、世界中のユーザに日々相当使われているけど、オプションは相当貧弱だなと思います。-L
も -P
も使いどころがそれほどない。これらのオプション、日々使っている人いるんでしょうか。
せめてウェブブラウザでいうような「進む」「戻る」「ブックマーク」「検索」といったインターフェースがあればいいのにと思って、色々作ってみました。
ちなみに、cd -
とすれば直前にいたディレクトリに戻ることができますが、履歴をたどって戻り続けるといったようなことはできません。cd -
を連続で打っても、2つのディレクトリ間を行き来するだけです。
そこで私なりのcd
ハックを以下でご紹介してみようと思います。面白そうな機能があれば取り入れていただければ幸いです。
##pushdとpopd、そしてdirs
まずはpushd
とpopd
について解説しておかなければなりません。
Bashにはpushd
とpopd
という組み込みコマンドがあるのですが、あまり使われている場面に出くわしたことがありません。pushd
は、カレントディレクトリをスタックに保存して与えれたディレクトリに移動する、もう一つのcd
コマンドといったものです。pushd 移動したいディレクトリ
といった感じで、cd
コマンドと同様に使えます。
スタックに積まれたディレクトリで一番新しいディレクトリに移動したい場合にはpopd
コマンドを使います。これは引数無し。スタックに積まれた一番新しいディレクトリに移動して、スタックからその情報を削除します。いわゆるLIFOですね。
スタック一覧を見たい場合にはdirs
コマンドを使います。スタックを削除したり表示方法を替えたりといったオプションもありますが、詳しくは man bash
の中から dirs
の項目を参照下さい。
dirs [-clpv] [+n] [-n]
##cdをpushdで再定義する
pushd
が cd
の代わりとして使えるとはいえ、cd
の方が打つのに慣れているし、pushd
って5文字もあるしで、恒常的に pushd
を使うのは面倒です。であれば cd
の定義を pushd
を使ったもので置き換えてしまおうと考えました。
普段から cd
と pushd
を使い分けている人は、この設定はスタックの履歴が混ざってしまうので問題となりますが、「普段のシェル作業で pushd
なんて使わないよ」という人は、この設定にしてしまって問題無いと思います。私は数年間この設定で作業していますが、特段パフォーマンス的だったりといった問題が出たことはありません。
# 履歴を記録する cd の再定義
function cd {
if [ -z "$1" ] ; then
# cd 連打で余計な $DIRSTACK を増やさない
test "$PWD" != "$HOME" && pushd $HOME > /dev/null
elif ( echo "$1" | egrep "^\.\.\.+$" > /dev/null ) ; then
cd $( echo "$1" | perl -ne 'print "../" x ( tr/\./\./ - 1 )' )
else
pushd "$1" > /dev/null
fi
}
これらの関数は ~/.bashrc
に書くか、~/.bashrc
から source
(.
) コマンドで読まれるファイルに書く必要があります。
定義の説明ですが、まず cd
コマンドを引数無しで打ったときにホームディレクトリに戻ることができる機能があって、ここでもpushd
でこれを再現していますが、これをホームディレクトリで打ったときに pushd
が余計なスタックを増やさないような配慮をしています。
またオマケ機能として、cd ..
で1つ上のディレクトリに戻れる機能の拡張として、cd ...
と打つと2つ上のディレクトリに戻れ、$n$ 個のドットで $n-1$ つ上のディレクトリに戻れる機能を書いてあります。シェル力が足りなかったので、egrep
で検査した上で必要があればPerlを起動するように書いていますが、ここのパフォーマンスが特段気になる方は削ってもよいと思います。ここの then
節で表れる cd
はこの関数の再起呼び出しとして解釈されます(無限ループには注意して書いたつもりです)。
そうでなければシンプルに pushd
に引数を渡してカレントディレクトリを変更しています。pushd
はいちいちスタックに入れたディレクトリについての報告を標準出力にしてくるので、>/dev/null
しておきます。
この定義は、一部の人が使い慣れた cd -
を潰したりはしません。
事情があって組み込みのcd
コマンドを使いたい場合は builtin cd 移動したいディレクトリ
というコマンドを使います。
##事前指定したディレクトリへ素早く移動する
ブラウザでいう「ブックマーク」のような機能が欲しいと思って、色々考えました。
cd
コマンドには CDPATH
という環境変数があって、PATH
のように、ここにコロン区切りでパス群を入れておくと、カレントディレクトリがどこであろうと、このパス群からディレクトリを探すという機能があるのですが、私には使い勝手が悪く感じました。込み入った設定や状況にしておくと、意図しないディレクトリに飛んでしまったりとか。
pushd
は CDPATH
を見るのかとか調べる以前に、意図しないことが起こらないように、これは別コマンドにしてしまったほうがいいなと感じました。最初は cdjump
という名前だったのですが、長いので cdj
という名前にしました。
# ショートカットキーで移動するcd
function cdj {
### cdjはCDJ_DIR_MAPという環境変数の配列の定義が必要です
# CDJ_DIR_MAP配列の例は以下です。ディレクトリのエイリアスと実ディレクトリのパスを空白区切りでペアで書いていきます
# export CDJ_DIR_MAP=(
# dbox ~/Dropbox
# cvs ~/cvs
# etc /etc
# );
test -n "$DEBUG" && echo "DEBUG: dir arg=$arg #CDJ_DIR_MAP=${#CDJ_DIR_MAP[*]}"
declare arg=$1 \
subarg=$2 \
dir i key value warn
if [ -z "$arg" -o "$arg" = "-h" ] || [ "$arg" = "-v" -a -z "$subarg" ] ; then
### help and usage mode
echo "Usage: $FUNCNAME <directory_alias>"
echo " $FUNCNAME [-h|-v|-l <directory_alias>]"
echo "-h: help"
echo "-l: list defined lists"
echo "-v <directory_alias>: view path specify alias."
return
elif [ "$arg" = "-v" -o "$arg" = "-l" ] ; then
### view detail mode
for (( i=0; $i<${#CDJ_DIR_MAP[*]}; i=$((i+2)) )) ; do
key="${CDJ_DIR_MAP[$i]}"
value="${CDJ_DIR_MAP[$((i+1))]}"
if [ "$arg" = "-l" ] ; then
if [ ! -d "$value" ] ; then
warn=" ***NOT_FOUND***"
else
warn=""
fi
printf "%8s => %s%s\n" "$key" "$value" "$warn"
elif [ "$arg" = "-v" ] ; then
if [ "$key" = "$subarg" ] ; then
echo $value
return
fi
fi
done
return
fi
### change directory mode
for (( i=0; $i<${#CDJ_DIR_MAP[*]}; i=$((i+2)) )) ; do
key="${CDJ_DIR_MAP[$i]}"
value="${CDJ_DIR_MAP[$((i+1))]}"
test -n "$DEBUG" && echo "$key => $value"
if [ "$key" = "$arg" ] ; then
if [ -n "$subarg" ] ; then
dir="$value/$subarg"
else
dir="$value"
fi
cd "$dir"
return
fi
done
echo "directory alias \"$arg\" is not found"
return 1
}
大昔に書いた自分も忘れてしまった部分もあるのですが、以下のような仕掛けです。
まず CDJ_DIR_MAP
という配列を定義します。これがいわゆる cdj
コマンドでの「ブックマーク」に相当します。上のコメントで書いたような例で書きます。いくらでも書けます。
最近のBashでは連想配列がサポートされたという話も聞いたり聞かなかったりするのですが、世間の新しくないBashに合わせて配列にキーバリューのペアで記録する形になっています。
前述の例であれば、cdj dbox
と打てば cd ~/Dropbox
と同じ意味になります。長く深いディレクトリであればあるほど、この恩恵は大きいです。
ヘルプを表示する -h
、現在の設定を確認する -l
、与えられたキーに対する実ディレクトリを表示する -v
といったオプションも付けました。
cd
コマンドは標準でディレクトリのタブ補完が効きます。cdj
コマンドもタブ補完ができるようにしましょう。function cdj
で cdj
コマンドを定義した後で以下のような記述を書きます。
type cdj >/dev/null 2>&1 &&
_cdj()
{
local cur prev opts i
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
opts=$(for i in $(seq 0 2 $((${#CDJ_DIR_MAP[*]}-2)) ) ; do echo ${CDJ_DIR_MAP[$i]} ; done ; echo "-h -v -l")
#echo ""
#echo "> cur=$cur prev=$prev COMP_CWORD=$COMP_CWORD"
#echo "> opts=$opts"
#echo "> COMPWORDS=${COMP_WORDS[*]}"
case ${prev} in
-l)
opts=$(for i in $(seq 0 2 $((${#CDJ_DIR_MAP[*]}-2)) ) ; do echo ${CDJ_DIR_MAP[$i]} ; done)
COMPREPLY=( $(compgen -W "$opts" -- ${cur} ))
return 0
;;
*)
;;
esac
COMPREPLY=( $(compgen -W "$opts" -- ${cur} ))
return 0
} &&
complete -F _cdj cdj
bash_completion の色々な書き方を真似て CDJ_DIR_MAP
を見るようにしてみただけです。
私はbash_completionを導入していますが、もしかしたら事前にbash_completionを導入しておかないといけないかもしれません。それぞれのOS/ディストリビューション事に導入方法が違いますので、検索してみてください。
##取得したディレクトリ移動履歴を利用する
前述のように cd
コマンドを再定義したことで、pushd
のおかげでディレクトリ履歴が取られるようになりました。
であれば、その履歴を番号選択して移動ができる cdhist
、cd -
とは違い、打ち続けると履歴をたどっていける popd
を利用した cdback
コマンドを定義してみましょう。
# 最近の cd によって移動したディレクトリを選択
function cdhist {
local dirnum
#dirs -v | head -n $(( LINES - 3 ))
dirs -v | sort -k 2 | uniq -f 1 | sort -n -k 1 | head -n $(( LINES - 3 ))
read -p "select number: " dirnum
if [ -z "$dirnum" ] ; then
echo "$FUNCNAME: Abort." 1>&2
elif ( echo $dirnum | egrep '^[[:digit:]]+$' > /dev/null ) ; then
cd "$( echo ${DIRSTACK[$dirnum]} | sed -e "s;^~;$HOME;" )"
else
echo "$FUNCNAME: Wrong." 1>&2
fi
}
function cdback {
#popd $1 >/dev/null
local num=$1 i
if [ -z "$num" -o "$num" = 1 ] ; then
popd >/dev/null
return
elif [[ "$num" =~ ^[0-9]+$ ]] ; then
for (( i=0 ; i<num ; i++ )) ; do
popd >/dev/null
done
return
else
echo "cdback: argument is invalid." >&2
fi
}
##カレントディレクトリにある、打つのが面倒なディレクトリへ移動
特に最近では日本語ディレクトリが多くなってきている印象です。日本語のファイル名やディレクトリ名は利用すれば便利なのですが、それはGUIでの話であって、CUIではcd
のときに打つのが面倒だったりといった問題があります。
そこで日本語変換をオンにせずとも、カレントディレクトリの一覧を番号付きで出して、番号を打つだけで移動ができる cdlist
コマンドを定義してみましょう。
# 現在のディレクトリの中にあるディレクトリを番号指定で移動
function cdlist {
local -a dirlist opt_f=false
local i d num=0 dirnum opt opt_f
while getopts ":f" opt ; do
case $opt in
f ) opt_f=true ;;
esac
done
shift $(( OPTIND -1 ))
dirlist[0]=..
# external pipe scope. array is established.
for d in * ; do test -d "$d" && dirlist[$((++num))]="$d" ; done
# TODO: Is seq installed?
for i in $( seq 0 $num ) ; do printf "%3d %s%b\n" $i "$( $opt_f && echo -n "$PWD/" )${dirlist[$i]}" ; done
read -p "select number: " dirnum
if [ -z "$dirnum" ] ; then
echo "$FUNCNAME: Abort." 1>&2
elif ( echo $dirnum | egrep '^[[:digit:]]+$' > /dev/null ) ; then
cd "${dirlist[$dirnum]}"
else
echo "$FUNCNAME: Something wrong." 1>&2
fi
}
##名前でディレクトリ名を検索して移動する
例えば "qiita" という文字列が入ったディレクトリを大文字小文字区別せず検索して、その検索結果から移動したい場合。よくありそうですね。
find / -iname \*qiita\* -type d
これで出てきた結果を目視して、コピーアンドペーストをして移動…。面倒な感じがします。そもそもfind
コマンド自体が検索に時間がかかります。
Linuxにはlocate
、Macにはmdfind
という、事前に作成したインデックスファイルを利用して高速検索ができるコマンドがあるので、find /
するくらいなら、そちらを利用したほうがよさそうです。
大体の場合、移動したいディレクトリが、カレントディレクトリ以下にあるのか、ホームディレクトリ以下にあるのか、それとも /
以下にあるのか、いちいち区別するのは不便です。
そこで書いてみたのが mdfind
を利用した cdmdfind
です。
# cd shortcut by mdfind (Mac OS X Spotlight CLI)
type mdfind >/dev/null 2>&1 && \
function cdmdfind {
local arg="$1" path i=0 j selnum selpath OUTPUT
declare -a pathes
if [ -z "$arg" ] || [ "$arg" = "-h" ] ; then
echo "Usage:"
echo " $FUNCNAME STRING"
return
fi
# mdfind search is case insensitive
for path in $(mdfind -name "$arg" | sed -e 's/ /+/g') ; do
path=$(echo "$path" | sed -e 's/\+/ /g')
test -d "$path" || continue
i=$((i+1))
pathes[$i]="$path"
done
if [ -z "${pathes[1]}" ] ; then
# Nothing search result.
return
fi
if [ $i -ge $LINES ] ; then
OUTPUT=$PAGER
test -z "$OUTPUT" && OUTPUT=cat
else
OUTPUT=cat
fi
for j in $(seq 1 $i) ; do
printf "%2d: %s\n" $j "${pathes[$j]}"
done | $OUTPUT
read -p "select number: " selnum
selpath="${pathes[$selnum]}"
if [ -z "$selpath" ] ; then
echo "$FUNCNAME: select is wrong." 1>&2
return 1
fi
cd "$selpath"
}
検索を実行すると、メニューが出て…といったところは前述の cdlist
をそのまま真似ました。
さらに cdmdfind
を真似て cdlocate
を書きました。
# cd shortcut by locate
type locate >/dev/null 2>&1 && \
function cdlocate {
local arg="$1" path i=0 j selnum selpath OUTPUT
declare -a pathes
if [ -z "$arg" ] || [ "$arg" = "-h" ] ; then
echo "Usage:"
echo " $FUNCNAME STRING"
return
fi
# mdfind search is case insensitive
for path in $(locate "$arg" | grep -i -E "/[^/]*$arg[^/]*$" | sed -e 's/ /+/g') ; do
path=$(echo "$path" | sed -e 's/\+/ /g')
test -d "$path" || continue
i=$((i+1))
pathes[$i]="$path"
done
if [ -z "${pathes[1]}" ] ; then
# Nothing search result.
return
fi
if [ $i -ge $LINES ] ; then
OUTPUT=$PAGER
test -z "$OUTPUT" && OUTPUT=cat
else
OUTPUT=cat
fi
for j in $(seq 1 $i) ; do
printf "%2d: %s\n" $j "${pathes[$j]}"
done | $OUTPUT
read -p "select number: " selnum
selpath="${pathes[$selnum]}"
if [ -z "$selpath" ] ; then
echo "$FUNCNAME: select is wrong." 1>&2
return 1
fi
cd "$selpath"
}
ほぼ cdmdfind
そのままです。ただ、locate
の出力が、mdfind -name
とは違い、パス全体の中からの一致で非常に多くの検索が出てくるので grep -i -E "/[^/]*$arg[^/]*$"
で検索結果を適切なように絞っています。
##他にもアイデアが…
この記事を書くにあたって、他にもcd
を効率化することができるか考えたのですが、いくつか考えついたものがあります。指定ディレクトリ以下に .git/
があるディレクトリを一覧してGitワークディレクトリを選択させるコマンドとか…。完成したら随時この記事に追記していこうとおもいます。
もしあなたにもアイデアがあれば、ぜひこの記事へのコメントや Twitter @xtetsuji へ教えていただければ幸いです。
##参考
私の GitHub リポジトリの dotfiles にはこれらの設定例が書かれています。ただし、~/.bash_secret
だけは秘密のファイルとして公開はしていません。