「z」と「z-fish」と「sh/ksh」版

今日は「z」というディレクトリ移動ツールのお話。
「z-fish」とは「z」の「fish」シェル版です。

「z」は、先に紹介した「autojump」と目的は同じですが、優先するディレクトリの判定方法に違いがあります。
曰く、今まで何回も訪れたディレクトリ同様、最近になって頻繁に訪れているディレクトリも重要だろうと。(たぶん)
「autojump」がpythonで実装されているのに対し、「z」はシェルスクリプトのみで実装しているところも違います。
この「z」が「bash」シェル「zsh」シェル対応なので、「fish」シェル用に書き直されたのが「z-fish」というわけです。

取り敢えず、インストール方法から。

「z」のインストールは簡単で、
$ brew install z
後は「.bashrc」なり「.zshrc」なりに
. `brew –prefix`/etc/profile.d/z.sh
この一行を書いて終わりです。

「z-fish」のインストールは、少し手間がかかります。
ホームディレクトリかどこかで、
$ git clone https://github.com/sjl/z-fish.git
とすると「z-fish」というディレクトリが作成されてファイルがダウンロードされます。
その中の「z.fish」をどこでも適当な場所に置きます。
(私は環境の都合上「/opt/local/etc」に置きました)

それから、「~/.config/fish/config.fish」に以下を書いておきます。

##
# Read Z extension
##
if test -f /opt/local/etc/z.fish
  . /opt/local/etc/z.fish
end

ディレクト名は適時書き換えてくださいね。
後は、「~/.config/fish/functions/fish_prompt.fish」に、

# for z.fish
 z --add "$PWD"

を書いておきます。
この辺りは、先の記事 をご覧ください。

使い方は、「autojump」の「j」コマンドと同じく「z」コマンドでディレクトリを移動します。
ただ単に「z」と入力した場合は、現在のデータベースの順位順にディレクトリを一覧表示します。
「j」も「z」もデータベースさえ育っていれば、
$ z u l b
で「/usr/local/bin」に移動するかもしれません。

「z」のデータベースは「~/.z」なのですが、ちと構成が理解しづらいところがあります。
私が「autojump」と併用している理由は、他のシェル用に展開するのにデータベースを利用しやすいから、というのと、「z」の真価をまだ評価中だからであって、普通はどちらかだけで良いと思います。

さて・・・

タイトルに書いた「sh/ksh」版を説明します。
最初に触れたように「z」はシェルスクリプトで書かれています。
だから「z-fish」というものも生まれたのですが、「ksh系」シェルや「dash」シェルにはどちらも使えません。
そこでこれらシェル用に、「autojump」と「z」と両方埋め込んだスクリプトを作りました。
本来は差分を取って・・・という事にしないといけないのだと思うのですが、ここでのやり方がまだわかっていませんし、「WTFPL license」(どうぞご勝手に)ということですので、以下そのまま掲載します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
# Copyright (c) 2009 rupa deadwyler under the WTFPL license

# maintains a jump-list of the directories you actually use
#
# INSTALL:
#   * put something like this in your .bashrc/.zshrc:
#     . /path/to/z.sh
#   * cd around for a while to build up the db
#   * PROFIT!!
#   * optionally:
#     set $_Z_CMD in .bashrc/.zshrc to change the command (default z).
#     set $_Z_DATA in .bashrc/.zshrc to change the datafile (default ~/.z).
#     set $_Z_NO_RESOLVE_SYMLINKS to prevent symlink resolution.
#     set $_Z_NO_PROMPT_COMMAND if you're handling PROMPT_COMMAND yourself.
#     set $_Z_EXCLUDE_DIRS to an array of directories to exclude.
#
# USE:
#   * z foo     # cd to most frecent dir matching foo
#   * z foo bar # cd to most frecent dir matching foo and bar
#   * z -r foo  # cd to highest ranked dir matching foo
#   * z -t foo  # cd to most recently accessed dir matching foo
#   * z -l foo  # list all dirs matching foo (by frecency)

# Modified for (K)SH by みろす on 02/Mar/2013

_z() {

 _z_local_datafile="${_Z_DATA:-$HOME/.z}"

 # bail out if we don't own ~/.z (we're another user but our ENV is still set)
 [ -f "$_z_local_datafile" -a ! -O "$_z_local_datafile" ] && return

 # add entries
 if [ "$1" = "--add" ]; then
  shift

  # $HOME isn't worth matching
  [ "$*" = "$HOME" ] && return

  # don't track excluded dirs
  for _z_local_exclude in "${_Z_EXCLUDE_DIRS[@]}"; do
   [ "$*" = "$_z_local_exclude" ] && return
  done

  # maintain the file
  _z_local_tempfile="$(mktemp $datafile.XXXXXX)" ¦¦ return
  while read line; do
   [ -d "${line%%\¦*}" ] && echo $line
  done < "$_z_local_datafile" ¦ awk -v path="$*" -v now="$(date +%s)" -F"¦" '
   BEGIN {
    rank[path] = 1
    time[path] = now
   }
   $2 >= 1 {
    if( $1 == path ) {
     rank[$1] = $2 + 1
     time[$1] = now
    } else {
     rank[$1] = $2
     time[$1] = $3
    }
    count += $2
   }
   END {
    if( count > 6000 ) {
     for( i in rank ) print i "¦" 0.99*rank[i] "¦" time[i] # aging
    } else for( i in rank ) print i "¦" rank[i] "¦" time[i]
   }
  '
2>/dev/null >¦ "$_z_local_tempfile"
  if [ $? -ne 0 -a -f "$_z_local_datafile" ]; then
   env rm -f "$_z_local_tempfile"
  else
   env mv -f "$_z_local_tempfile" "$_z_local_datafile"
  fi

 # tab completion
 elif [ "$1" = "--complete" ]; then
  while read line; do
   [ -d "${line%%\¦*}" ] && echo $line
  done < "$_z_local_datafile" ¦ awk -v q="$2" -F"¦" '
   BEGIN {
    if( q == tolower(q) ) nocase = 1
    split(substr(q,3),fnd," ")
   }
   {
    if( nocase ) {
     for( i in fnd ) tolower($1) !~ tolower(fnd[i]) && $1 = ""
    } else {
     for( i in fnd ) $1 !~ fnd[i] && $1 = ""
    }
    if( $1 ) print $1
   }
  '
2>/dev/null

 else
  # list/go
  unset _z_local_fnd
  unset _z_local_list
  unset _z_local_typ
  unset _z_local_last
  while [ "$1" ]; do case "$1" in
   -h) echo "z [-h][-l][-r][-t] args" >&2; return;;
   -l) _z_local_list=1;;
   -r) _z_local_typ="rank";;
   -t) _z_local_typ="recent";;
   --) while [ "$1" ]; do shift; _z_local_fnd="$_z_local_fnd $1";done;;
    *) _z_local_fnd="$_z_local_fnd $1";;
  esac; _z_local_last=$1; shift; done
  [ "$_z_local_fnd" ] ¦¦ _z_local_list=1

  # if we hit enter on a completion just go there
  case "$_z_local_last" in
   # completions will always start with /
   /*) [ -z "$_z_local_list" -a -d "$_z_local_last" ] && cd "$_z_local_last" && return;;
  esac

  # no file yet
  [ -f "$_z_local_datafile" ] ¦¦ return

  _z_local_cd="$(while read line; do
   [ -d "
${line%%\¦*}" ] && echo $line
  done < "
$_z_local_datafile" ¦ awk -v t="$(date +%s)" -v list="$_z_local_list" -v typ="$_z_local_typ" -v q="$_z_local_fnd" -F"¦" '
   function frecent(rank, time) {
    dx = t-time
    if( dx < 3600 ) return rank*4
    if( dx < 86400 ) return rank*2
    if( dx < 604800 ) return rank/2
    return rank/4
   }
   function output(files, toopen, override) {
    if( list ) {
     cmd = "
sort -n >&2"
     for( i in files ) if( files[i] ) printf "
%-10s %s\n", files[i], i ¦ cmd
     if( override ) printf "
%-10s %s\n", "common:", override > "/dev/stderr"
    } else {
     if( override ) toopen = override
     print toopen
    }
   }
   function common(matches) {
    # shortest match
    for( i in matches ) {
     if( matches[i] && (!short ¦¦ length(i) < length(short)) ) short = i
    }
    if( short == "
/" ) return
    # shortest match must be common to each match. escape special characters in
    # a copy when testing, so we can return the original.
    clean_short = short
    gsub(/[\(\)\[\]\¦]/, "
\\\\&", clean_short)
    for( i in matches ) if( matches[i] && i !~ clean_short ) return
    return short
   }
   BEGIN { split(q, a, "
"); oldf = noldf = -9999999999 }
   {
    if( typ == "
rank" ) {
     f = $2
    } else if( typ == "
recent" ) {
     f = $3-t
    } else f = frecent($2, $3)
    wcase[$1] = nocase[$1] = f
    for( i in a ) {
     if( $1 !~ a[i] ) delete wcase[$1]
     if( tolower($1) !~ tolower(a[i]) ) delete nocase[$1]
    }
    if( wcase[$1] && wcase[$1] > oldf ) {
     cx = $1
     oldf = wcase[$1]
    } else if( nocase[$1] && nocase[$1] > noldf ) {
     ncx = $1
     noldf = nocase[$1]
    }
   }
   END {
    if( cx ) {
     output(wcase, cx, common(wcase))
    } else if( ncx ) output(nocase, ncx, common(nocase))
   }
  ')"

  [ $? -gt 0 ] && return
  [ "$_z_local_cd" ] && cd "$_z_local_cd"
 fi
}

alias ${_Z_CMD:-z}='_z 2>&1'

[ "$_Z_NO_RESOLVE_SYMLINKS" ] ¦¦ _Z_RESOLVE_SYMLINKS="-P"

# for Autojump
j() {
  cd `autojump $*`
}

# Prompt for Z extension and Autojump
[ "$_Z_NO_PROMPT_COMMAND" ] ¦¦ {
 PS1='$(eval "_z --add $(pwd '$_Z_RESOLVE_SYMLINKS' 2>/dev/null) 2>/dev/null")$(autojump -a $PWD >/dev/null 2>&1)'$PS1
}

長いですが、ローカル変数の使用をやめたくらいで、実際はほとんど変わっていません。
プロンプトの関係で「autojump」と「z」を同居させた、というのが追加部分です。
これを、例えば「/opt/local/etc/jz.sh」みたいに保存して、「.kshrc」やその他「sh」で読み込む設定ファイルに、

# Read Autojump and Z extention for SH
. /opt/local/etc/jz.sh

とでも書いておけば、「dash」シェルや「ksh系」シェルで「j」コマンドと「z」コマンドが使えるようになります。

もし「autojump」が重複するようでしたら、ごめんなさい。
こちらを優先ということでお願いします。

残るは「csh」と「rc」なんだけどこれがなかなか・・・

 

コメントをどうぞ