├── .github
└── FUNDING.yml
├── .gitignore
├── LICENSE
├── README.md
├── _zshz
├── img
├── demo.gif
├── mit_license.svg
└── zsh_4.3.11_plus.svg
└── zsh-z.plugin.zsh
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: agkozak
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | custom: [ "https://www.paypal.me/agkozak" ]
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.zwc
2 | .*.swp
3 | ._zplugin/
4 | ._zinit/
5 | zsdoc/data
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018-2025 Alexandros Kozak
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Zsh-z
2 |
3 | [](https://opensource.org/licenses/MIT)
4 | 
5 | [](https://github.com/agkozak/zsh-z/stargazers)
6 |
7 | 
8 |
9 | Zsh-z is a command-line tool that allows you to jump quickly to directories that you have visited frequently or recently -- but most often a combination of the two (a concept known as ["frecency"](https://en.wikipedia.org/wiki/Frecency)). It works by keeping track of when you go to directories and how much time you spend in them. Based on this data, it predicts where you want to go when you type a partial string. For example, `z src` might take you to `~/src/zsh`. `z zsh` might also get you there, and `z c/z` might prove to be even more specific -- it all depends on your habits and how long you have been using Zsh-z to build up a database. After using Zsh-z for a little while, you will get to where you want to be by typing considerably less than you would need to if you were using `cd`.
10 |
11 | Zsh-z is a native Zsh port of [`rupa/z`](https://github.com/rupa/z), a tool written for `bash` and Zsh that uses embedded `awk` scripts to do the heavy lifting. `rupa/z` was my most used command-line tool for a couple of years. I decided to translate it, `awk` parts and all, into pure Zsh script, to see if by eliminating calls to external tools (`awk`, `sort`, `date`, `sed`, `mv`, `rm`, and `chown`) and reducing forking through subshells I could make it faster. The performance increase is impressive, particularly on systems where forking is slow, such as Cygwin, MSYS2, and WSL. I have found that in those environments, switching directories using Zsh-z can be over 100% faster than it is using `rupa/z`.
12 |
13 | There is also a significant stability improvement. Race conditions have always been a problem with `rupa/z`, and users of that utility occasionally lose their `~/.z` databases. By having Zsh-z only use Zsh (`rupa/z` uses a hybrid shell code standard that works on `bash` as well), I have been able to implement a `zsh/system`-based file-locking mechanism similar to [the one @mafredri once proposed for `rupa/z`](https://github.com/rupa/z/pull/199). It is now nearly impossible to crash the database.
14 |
15 | There are other, smaller improvements which I document below in [Improvements and Fixes](#improvements-and-fixes). For instance, tab completions are now sorted by frecency by default rather than alphabetically (the latter behavior can be restored if you like it -- [see below](#settings)).
16 |
17 | Zsh-z is a drop-in replacement for `rupa/z` and will, by default, use the same database (`~/.z`, or whatever database file you specify), so you can go on using `rupa/z` when you launch `bash`.
18 |
19 | ## Table of Contents
20 | - [News](#news)
21 | - [Installation](#installation)
22 | - [Command Line Options](#command-line-options)
23 | - [Settings](#settings)
24 | - [Case Sensitivity](#case-sensitivity)
25 | - [`ZSHZ_UNCOMMON`](#zshz_uncommon)
26 | - [Making `--add` work for you](#making---add-work-for-you)
27 | - [Other Improvements and Fixes](#other-improvements-and-fixes)
28 | - [Migrating from Other Tools](#migrating-from-other-tools)
29 | - [`COMPLETE_ALIASES`](#complete_aliases)
30 | - [Known Bugs](#known-bugs)
31 |
32 | ## News
33 |
34 |
35 | Here are the latest features and updates.
36 |
37 | - August 24, 2023
38 | + Zsh-z will now run when `setopt NO_UNSET` has been enabled (props @ntninja).
39 | - August 23, 2023
40 | + Better logic for loading `zsh/files` (props @z0rc).
41 | - August 2, 2023
42 | + Zsh-z still uses the `zsh/files` module when possible but will fall back on the standard `chown`, `mv`, and `rm` commands in its absence.
43 | - April 27, 2023
44 | + Zsh-z now allows the user to specify the directory-changing command using the `ZSHZ_CD` environment variable (default: `builtin cd`; props @basnijholt).
45 | - January 27, 2023
46 | + If the database file directory specified by `ZSHZ_DATA` or `_Z_DATA` does not already exist, create it (props @mattmc3).
47 | - June 29, 2022
48 | + Zsh-z is less likely to leave temporary files sitting around (props @mafredri).
49 | - June 27, 2022
50 | + A bug was fixed which was preventing paths with spaces in them from being updated ([#61](https://github.com/agkozak/zsh-z/issues/61)).
51 | + If writing to the temporary database file fails, the database will not be clobbered (props @mafredri).
52 | - December 19, 2021
53 | + ZSH-z will now display tildes for `HOME` during completion when `ZSHZ_TILDE=1` has been set.
54 | - November 11, 2021
55 | + A bug was fixed which was preventing ranks from being incremented.
56 | + `--add` has been made to work with relative paths and has been documented for the user.
57 | - October 14, 2021
58 | + Completions were being sorted alphabetically, rather than by rank; this error has been fixed.
59 | - September 25, 2021
60 | + Orthographical change: "Zsh," not "ZSH."
61 | - September 23, 2021
62 | + `z -xR` will now remove a directory *and its subdirectories* from the database.
63 | + `z -x` and `z -xR` can now take an argument; without one, `PWD` is assumed.
64 | - September 7, 2021
65 | + Fixed the unload function so that it removes the `$ZSHZ_CMD` alias (default: `z`).
66 | - August 27, 2021
67 | + Using `print -v ... -f` instead of `print -v` to work around longstanding bug in Zsh involving `print -v` and multibyte strings.
68 | - August 13, 2021
69 | + Fixed the explanation string printed during completion so that it may be formatted with `zstyle`.
70 | + Zsh-z now declares `ZSHZ_EXCLUDE_DIRS` as an array with unique elements so that you do not have to.
71 | - July 29, 2021
72 | + Temporarily disabling the use of `print -v`, which was mangling CJK multibyte strings.
73 | - July 27, 2021
74 | + Internal escaping of path names now works with older versions of ZSH.
75 | + Zsh-z now detects and discards any incomplete or incorrectly formatted database entries.
76 | - July 10, 2021
77 | + Setting `ZSHZ_TRAILING_SLASH=1` makes it so that a search pattern ending in `/` can match the end of a path; e.g. `z foo/` can match `/path/to/foo`.
78 | - June 25, 2021
79 | + Setting `ZSHZ_TILDE=1` displays the `HOME` directory as `~`.
80 | - May 7, 2021
81 | + Setting `ZSHZ_ECHO=1` will cause Zsh-z to display the new path when you change directories.
82 | + Better escaping of path names to deal paths containing the characters ``\`()[]``.
83 | - February 15, 2021
84 | + Ranks are displayed the way `rupa/z` now displays them, i.e. as large integers. This should help Zsh-z to integrate with other tools.
85 | - January 31, 2021
86 | + Zsh-z is now efficient enough that, on MSYS2 and Cygwin, it is faster to run it in the foreground than it is to fork a subshell for it.
87 | + `_zshz_precmd` simply returns if `PWD` is `HOME` or in `ZSH_EXCLUDE_DIRS`, rather than waiting for `zshz` to do that.
88 | - January 17, 2021
89 | + Made sure that the `PUSHD_IGNORE_DUPS` option is respected.
90 | - January 14, 2021
91 | + The `z -h` help text now breaks at spaces.
92 | + `z -l` was not working for Zsh version < 5.
93 | - January 11, 2021
94 | + Major refactoring of the code.
95 | + `z -lr` and `z -lt` work as expected.
96 | + `EXTENDED_GLOB` has been disabled within the plugin to accommodate old-fashioned Windows directories with names such as `Progra~1`.
97 | + Removed `zshelldoc` documentation.
98 | - January 6, 2021
99 | + I have corrected the frecency routine so that it matches `rupa/z`'s math, but for the present, Zsh-z will continue to display ranks as 1/10000th of what they are in `rupa/z` -- [they had to multiply theirs by 10000](https://github.com/rupa/z/commit/f1f113d9bae9effaef6b1e15853b5eeb445e0712) to work around `bash`'s inadequacies at dealing with decimal fractions.
100 | - January 5, 2021
101 | + If you try `z foo`, and `foo` is not in the database but `${PWD}/foo` is a valid directory, Zsh-z will `cd` to it.
102 | - December 22, 2020
103 | + `ZSHZ_CASE`: when set to `ignore`, pattern matching is case-insensitive; when set to `smart`, patterns are matched case-insensitively when they are all lowercase and case-sensitively when they have uppercase characters in them (a behavior very much like Vim's `smartcase` setting).
104 | + `ZSHZ_KEEP_DIRS` is an array of directory names that should not be removed from the database, even if they are not currently available (useful when a drive is not always mounted).
105 | + Symlinked database files were having their symlinks overwritten; this bug has been fixed.
106 |
107 |
108 |
109 | ## Installation
110 |
111 | ### General observations
112 |
113 | This plugin can be installed simply by putting the various files in a directory together and by sourcing `zsh-z.plugin.zsh` in your `.zshrc`:
114 |
115 | source /path/to/zsh-z.plugin.zsh
116 |
117 | For tab completion to work, `_zshz` *must* be in the same directory as `zsh-z.plugin.zsh`, and you will want to have loaded `compinit`. The frameworks handle this themselves. If you are not using a framework, put
118 |
119 | autoload -U compinit; compinit
120 |
121 | in your `.zshrc` somewhere below where you source `zsh-z.plugin.zsh`.
122 |
123 | If you add
124 |
125 | zstyle ':completion:*' menu select
126 |
127 | to your `.zshrc`, your completion menus will look very nice. This `zstyle` invocation should work with any of the frameworks below as well.
128 |
129 | ### For [antigen](https://github.com/zsh-users/antigen) users
130 |
131 | Add the line
132 |
133 | antigen bundle agkozak/zsh-z
134 |
135 | to your `.zshrc`, somewhere above the line that says `antigen apply`.
136 |
137 | ### For [Oh My Zsh](http://ohmyz.sh/) users
138 |
139 | Zsh-z is now included as part of Oh My Zsh! As long as you are using an up-to-date installation of Oh My Zsh, you can activate Zsh-z simply by adding `z` to your `plugins` array in your `.zshrc`, e.g.,
140 |
141 | plugins=( git z )
142 |
143 | It is as simple as that.
144 |
145 | If, however, you prefer always to use the latest version of Zsh-z from the `agkozak/zsh-z` repo, you may install it thus:
146 |
147 | git clone https://github.com/agkozak/zsh-z ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-z
148 |
149 | and activate it by adding `zsh-z` to the line of your `.zshrc` that specifies `plugins=()`, e.g., `plugins=( git zsh-z )`.
150 |
151 | ### For [prezto](https://github.com/sorin-ionescu/prezto) users
152 |
153 | Execute the following command:
154 |
155 | git clone https://github.com/agkozak/zsh-z.git ~/.zprezto-contrib/zsh-z
156 |
157 | Then edit your `~/.zpreztorc` file. Make sure the line that says
158 |
159 | zstyle ':prezto:load' pmodule-dirs $HOME/.zprezto-contrib
160 |
161 | is uncommented. Then find the section that specifies which modules are to be loaded; it should look something like this:
162 |
163 | zstyle ':prezto:load' pmodule \
164 | 'environment' \
165 | 'terminal' \
166 | 'editor' \
167 | 'history' \
168 | 'directory' \
169 | 'spectrum' \
170 | 'utility' \
171 | 'completion' \
172 | 'prompt'
173 |
174 | Add a backslash to the end of the last line add `'zsh-z'` to the list, e.g.,
175 |
176 | zstyle ':prezto:load' pmodule \
177 | 'environment' \
178 | 'terminal' \
179 | 'editor' \
180 | 'history' \
181 | 'directory' \
182 | 'spectrum' \
183 | 'utility' \
184 | 'completion' \
185 | 'prompt' \
186 | 'zsh-z'
187 |
188 | Then relaunch `zsh`.
189 |
190 | ### For [zcomet](https://github.com/agkozak/zcomet) users
191 |
192 | Simply add
193 |
194 | zcomet load agkozak/zsh-z
195 |
196 | to your `.zshrc` (below where you source `zcomet.zsh` and above where you run `zcomet compinit`).
197 |
198 | ### For [zgen](https://github.com/tarjoilija/zgen) users
199 |
200 | Add the line
201 |
202 | zgen load agkozak/zsh-z
203 |
204 | somewhere above the line that says `zgen save`. Then run
205 |
206 | zgen reset
207 | zsh
208 |
209 | to refresh your init script.
210 |
211 | ### For [Zim](https://github.com/zimfw/zimfw)
212 |
213 | Add the following line to your `.zimrc`:
214 |
215 | zmodule https://github.com/agkozak/zsh-z
216 |
217 | Then run
218 |
219 | zimfw install
220 |
221 | and restart your shell.
222 |
223 | ### For [Zinit](https://github.com/zdharma-continuum/zinit) users
224 |
225 | Add the line
226 |
227 | zinit load agkozak/zsh-z
228 |
229 | to your `.zshrc`.
230 |
231 | Zsh-z supports `zinit`'s `unload` feature; just run `zinit unload agkozak/zsh-z` to restore the shell to its state before Zsh-z was loaded.
232 |
233 | ### For [Znap](https://github.com/marlonrichert/zsh-snap) users
234 |
235 | Add the line
236 |
237 | znap source agkozak/zsh-z
238 |
239 | somewhere below the line where you `source` Znap itself.
240 |
241 | ### For [zplug](https://github.com/zplug/zplug) users
242 |
243 | Add the line
244 |
245 | zplug "agkozak/zsh-z"
246 |
247 | somewhere above the line that says `zplug load`. Then run
248 |
249 | zplug install
250 | zplug load
251 |
252 | to install Zsh-z.
253 |
254 | ## Command Line Options
255 |
256 | - `--add` Add a directory to the database
257 | - `-c` Only match subdirectories of the current directory
258 | - `-e` Echo the best match without going to it
259 | - `-h` Display help
260 | - `-l` List all matches without going to them
261 | - `-r` Match by rank (i.e. how much time you spend in directories)
262 | - `-t` Time -- match by how recently you have been to directories
263 | - `-x` Remove a directory (by default, the current directory) from the database
264 | - `-xR` Remove a directory (by default, the current directory) and its subdirectories from the database
265 |
266 | ## Settings
267 |
268 | Zsh-z has environment variables (they all begin with `ZSHZ_`) that change its behavior if you set them. You can also keep your old ones if you have been using `rupa/z` (whose environment variables begin with `_Z_`).
269 |
270 | * `ZSHZ_CMD` changes the command name (default: `z`)
271 | * `ZSHZ_CD` specifies the default directory-changing command (default: `builtin cd`)
272 | * `ZSHZ_COMPLETION` can be `'frecent'` (default) or `'legacy'`, depending on whether you want your completion results sorted according to frecency or simply sorted alphabetically
273 | * `ZSHZ_DATA` changes the database file (default: `~/.z`)
274 | * `ZSHZ_ECHO` displays the new path name when changing directories (default: `0`)
275 | * `ZSHZ_EXCLUDE_DIRS` is an array of directories to keep out of the database (default: empty)
276 | * `ZSHZ_KEEP_DIRS` is an array of directories that should not be removed from the database, even if they are not currently available (useful when a drive is not always mounted) (default: empty)
277 | * `ZSHZ_MAX_SCORE` is the maximum combined score the database entries can have before they begin to age and potentially drop out of the database (default: 9000)
278 | * `ZSHZ_NO_RESOLVE_SYMLINKS` prevents symlink resolution (default: `0`)
279 | * `ZSHZ_OWNER` allows usage when in `sudo -s` mode (default: empty)
280 | * `ZSHZ_TILDE` displays the name of the `HOME` directory as a `~` (default: `0`)
281 | * `ZSHZ_TRAILING_SLASH` makes it so that a search pattern ending in `/` can match the final element in a path; e.g., `z foo/` can match `/path/to/foo` (default: `0`)
282 | * `ZSHZ_UNCOMMON` changes the logic used to calculate the directory jumped to; [see below](#zshz_uncommon`) (default: `0`)
283 |
284 | ## Case sensitivity
285 |
286 | The default behavior of Zsh-z is to try to find a case-sensitive match. If there is none, then Zsh-z tries to find a case-insensitive match.
287 |
288 | Some users prefer simple case-insensitivity; this behavior can be enabled by setting
289 |
290 | ZSHZ_CASE=ignore
291 |
292 | If you like Vim's `smartcase` setting, where lowercase patterns are case-insensitive while patterns with any uppercase characters are treated case-sensitively, try setting
293 |
294 | ZSHZ_CASE=smart
295 |
296 | ## `ZSHZ_UNCOMMON`
297 |
298 | A common complaint about the default behavior of `rupa/z` and Zsh-z involves "common prefixes." If you type `z code` and the best matches, in increasing order, are
299 |
300 | /home/me/code/foo
301 | /home/me/code/bar
302 | /home/me/code/bat
303 |
304 | Zsh-z will see that all possible matches share a common prefix and will send you to that directory -- `/home/me/code` -- which is often a desirable result. But if the possible matches are
305 |
306 | /home/me/.vscode/foo
307 | /home/me/code/foo
308 | /home/me/code/bar
309 | /home/me/code/bat
310 |
311 | then there is no common prefix. In this case, `z code` will simply send you to the highest-ranking match, `/home/me/code/bat`.
312 |
313 | You may enable an alternate, experimental behavior by setting `ZSHZ_UNCOMMON=1`. If you do that, Zsh-z will not jump to a common prefix, even if one exists. Instead, it chooses the highest-ranking match -- but it drops any subdirectories that do not include the search term. So if you type `z bat` and `/home/me/code/bat` is the best match, that is exactly where you will end up. If, however, you had typed `z code` and the best match was also `/home/me/code/bat`, you would have ended up in `/home/me/code` (because `code` was what you had searched for). This feature is still in development, and feedback is welcome.
314 |
315 | ## Making `--add` Work for You
316 |
317 | Zsh-z internally uses the `--add` option to add paths to its database. @zachriggle pointed out to me that users might want to use `--add` themselves, so I have altered it a little to make it more user-friendly.
318 |
319 | A good example might involve a directory tree that has Git repositories within it. The working directories could be added to the Zsh-z database as a batch with
320 |
321 | for i in $(find $PWD -maxdepth 3 -name .git -type d); do
322 | z --add ${i:h}
323 | done
324 |
325 | (As a Zsh user, I tend to use `**` instead of `find`, but it is good to see how deep your directory trees go before doing that.)
326 |
327 | ## Other Improvements and Fixes
328 |
329 | * `z -x` works, with the help of `chpwd_functions`.
330 | * Zsh-z is compatible with Solaris.
331 | * Zsh-z uses the "new" `zshcompsys` completion system instead of the old `compctl` one.
332 | * No error message is displayed when the database file has not yet been created.
333 | * Special characters (e.g., `[`) in directory names are now supported.
334 | * If `z -l` returns only one match, a common root is not printed.
335 | * Exit status codes are more logical.
336 | * Completions now work with options `-c`, `-r`, and `-t`.
337 | * If `~/foo` and `~/foob` are matches, `~/foo` is no longer considered the common root. Only a common parent directory can be a common root.
338 | * `z -x` and the new, recursive `z -xR` can now accept an argument so that you can remove directories other than `PWD` from the database.
339 |
340 | ## Migrating from Other Tools
341 |
342 | Zsh-z's database format is identical to that of `rupa/z`. You may switch freely between the two tools (I still use `rupa/z` for `bash`). `fasd` also uses that database format, but it stores it by default in `~/.fasd`, so you will have to `cp ~/.fasd ~/.z` if you want to use your old directory history.
343 |
344 | If you are coming to Zsh-z (or even to the original `rupa/z`, for that matter) from `autojump`, try using my [`jumpstart-z`](https://github.com/agkozak/jumpstart-z/blob/master/jumpstart-z) tool to convert your old database to the Zsh-z format, or simply run
345 |
346 | awk -F "\t" '{printf("%s|%0.f|%s\n", $2, $1, '"$(date +%s)"')}' < /path/to/autojump.txt > ~/.z
347 |
348 | ## `COMPLETE_ALIASES`
349 |
350 | `z`, or any alternative you set up using `$ZSH_CMD` or `$_Z_CMD`, is an alias. `setopt COMPLETE_ALIASES` divorces the tab completion for aliases from the underlying commands they invoke, so if you enable `COMPLETE_ALIASES`, tab completion for Zsh-z will be broken. You can get it working again, however, by adding under
351 |
352 | setopt COMPLETE_ALIASES
353 |
354 | the line
355 |
356 | compdef _zshz ${ZSHZ_CMD:-${_Z_CMD:-z}}
357 |
358 | That will re-bind `z` or the command of your choice to the underlying Zsh-z function.
359 |
360 | ## Known Bug
361 | It is possible to run a completion on a string with spaces in it, e.g., `z us bi` might take you to `/usr/local/bin`. This works, but as things stand, after the completion the command line reads
362 |
363 | z us /usr/local/bin.
364 |
365 | You get where you want to go, but the detritus on the command line is annoying. This is also a problem in `rupa/z`, but I am keen on eventually eliminating this glitch. Advice is welcome.
366 |
--------------------------------------------------------------------------------
/_zshz:
--------------------------------------------------------------------------------
1 | #compdef zshz ${ZSHZ_CMD:-${_Z_CMD:-z}}
2 | #
3 | # Zsh-z - jump around with Zsh - A native Zsh version of z without awk, sort,
4 | # date, or sed
5 | #
6 | # https://github.com/agkozak/zsh-z
7 | #
8 | # Copyright (c) 2018-2023 Alexandros Kozak
9 | #
10 | # Permission is hereby granted, free of charge, to any person obtaining a copy
11 | # of this software and associated documentation files (the "Software"), to deal
12 | # in the Software without restriction, including without limitation the rights
13 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14 | # copies of the Software, and to permit persons to whom the Software is
15 | # furnished to do so, subject to the following conditions:
16 | #
17 | # The above copyright notice and this permission notice shall be included in all
18 | # copies or substantial portions of the Software.
19 | #
20 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26 | # SOFTWARE.
27 | #
28 | # z (https://github.com/rupa/z) is copyright (c) 2009 rupa deadwyler and
29 | # licensed under the WTFPL license, Version 2.a
30 | #
31 | # shellcheck shell=ksh
32 |
33 | ############################################################
34 | # Zsh-z COMPLETIONS
35 | ############################################################
36 | emulate -L zsh
37 | (( ZSHZ_DEBUG )) &&
38 | setopt LOCAL_OPTIONS WARN_CREATE_GLOBAL NO_WARN_NESTED_VAR 2> /dev/null
39 |
40 | # TODO: This routine currently reproduces z's feature of allowing spaces to be
41 | # used as wildcards in completions, so that
42 | #
43 | # z us lo bi
44 | #
45 | # can expand to
46 | #
47 | # z /usr/local/bin
48 | #
49 | # but it also reproduces z's buggy display on the commandline, viz.
50 | #
51 | # z us lo /usr/local/bin
52 | #
53 | # Address.
54 |
55 | local completions expl completion
56 | local -a completion_list
57 |
58 | completions=$(zshz --complete ${(@)words:1})
59 | [[ -z $completions ]] && return 1
60 |
61 | for completion in ${(f)completions[@]}; do
62 | if (( ZSHZ_TILDE )) && [[ $completion == ${HOME}* ]]; then
63 | completion="~${(q)${completion#${HOME}}}"
64 | else
65 | completion="${(q)completion}"
66 | fi
67 | completion_list+=( $completion )
68 | done
69 |
70 | _description -V completion_list expl 'directories'
71 |
72 | if [[ $ZSHZ_COMPLETION == 'legacy' ]]; then
73 | compadd "${expl[@]}" -QU -- "${completion_list[@]}"
74 | else
75 | compadd "${expl[@]}" -QU -V zsh-z -- "${completion_list[@]}"
76 | fi
77 |
78 | compstate[insert]=menu
79 |
80 | return 0
81 |
82 | # vim: ft=zsh:fdm=indent:ts=2:et:sts=2:sw=2:
83 |
--------------------------------------------------------------------------------
/img/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agkozak/zsh-z/cf9225feebfae55e557e103e95ce20eca5eff270/img/demo.gif
--------------------------------------------------------------------------------
/img/mit_license.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/img/zsh_4.3.11_plus.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/zsh-z.plugin.zsh:
--------------------------------------------------------------------------------
1 | ################################################################################
2 | # Zsh-z - jump around with Zsh - A native Zsh version of z without awk, sort,
3 | # date, or sed
4 | #
5 | # https://github.com/agkozak/zsh-z
6 | #
7 | # Copyright (c) 2018-2025 Alexandros Kozak
8 | #
9 | # Permission is hereby granted, free of charge, to any person obtaining a copy
10 | # of this software and associated documentation files (the "Software"), to deal
11 | # in the Software without restriction, including without limitation the rights
12 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 | # copies of the Software, and to permit persons to whom the Software is
14 | # furnished to do so, subject to the following conditions:
15 | #
16 | # The above copyright notice and this permission notice shall be included in all
17 | # copies or substantial portions of the Software.
18 | #
19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25 | # SOFTWARE.
26 | #
27 | # z (https://github.com/rupa/z) is copyright (c) 2009 rupa deadwyler and
28 | # licensed under the WTFPL license, Version 2.
29 | #
30 | # Zsh-z maintains a jump-list of the directories you actually use.
31 | #
32 | # INSTALL:
33 | # * put something like this in your .zshrc:
34 | # source /path/to/zsh-z.plugin.zsh
35 | # * cd around for a while to build up the database
36 | #
37 | # USAGE:
38 | # * z foo cd to the most frecent directory matching foo
39 | # * z foo bar cd to the most frecent directory matching both foo and bar
40 | # (e.g. /foo/bat/bar/quux)
41 | # * z -r foo cd to the highest ranked directory matching foo
42 | # * z -t foo cd to most recently accessed directory matching foo
43 | # * z -l foo List matches instead of changing directories
44 | # * z -e foo Echo the best match without changing directories
45 | # * z -c foo Restrict matches to subdirectories of PWD
46 | # * z -x Remove a directory (default: PWD) from the database
47 | # * z -xR Remove a directory (default: PWD) and its subdirectories from
48 | # the database
49 | #
50 | # ENVIRONMENT VARIABLES:
51 | #
52 | # ZSHZ_CASE -> if `ignore', pattern matching is case-insensitive; if `smart',
53 | # pattern matching is case-insensitive only when the pattern is all
54 | # lowercase
55 | # ZSHZ_CD -> the directory-changing command that is used (default: builtin cd)
56 | # ZSHZ_CMD -> name of command (default: z)
57 | # ZSHZ_COMPLETION -> completion method (default: 'frecent'; 'legacy' for
58 | # alphabetic sorting)
59 | # ZSHZ_DATA -> name of datafile (default: ~/.z)
60 | # ZSHZ_EXCLUDE_DIRS -> array of directories to exclude from your database
61 | # (default: empty)
62 | # ZSHZ_KEEP_DIRS -> array of directories that should not be removed from the
63 | # database, even if they are not currently available (default: empty)
64 | # ZSHZ_MAX_SCORE -> maximum combined score the database entries can have
65 | # before beginning to age (default: 9000)
66 | # ZSHZ_NO_RESOLVE_SYMLINKS -> '1' prevents symlink resolution
67 | # ZSHZ_OWNER -> your username (if you want use Zsh-z while using sudo -s)
68 | # ZSHZ_UNCOMMON -> if 1, do not jump to "common directories," but rather drop
69 | # subdirectories based on what the search string was (default: 0)
70 | ################################################################################
71 |
72 | autoload -U is-at-least
73 |
74 | if ! is-at-least 4.3.11; then
75 | print "Zsh-z requires Zsh v4.3.11 or higher." >&2 && exit
76 | fi
77 |
78 | ############################################################
79 | # The help message
80 | #
81 | # Globals:
82 | # ZSHZ_CMD
83 | ############################################################
84 | _zshz_usage() {
85 | print "Usage: ${ZSHZ_CMD:-${_Z_CMD:-z}} [OPTION]... [ARGUMENT]
86 | Jump to a directory that you have visited frequently or recently, or a bit of both, based on the partial string ARGUMENT.
87 |
88 | With no ARGUMENT, list the directory history in ascending rank.
89 |
90 | --add Add a directory to the database
91 | -c Only match subdirectories of the current directory
92 | -e Echo the best match without going to it
93 | -h Display this help and exit
94 | -l List all matches without going to them
95 | -r Match by rank
96 | -t Match by recent access
97 | -x Remove a directory from the database (by default, the current directory)
98 | -xR Remove a directory and its subdirectories from the database (by default, the current directory)" |
99 | fold -s -w $COLUMNS >&2
100 | }
101 |
102 | # Load zsh/datetime module, if necessary
103 | (( ${+EPOCHSECONDS} )) || zmodload zsh/datetime
104 |
105 | # Global associative array for internal use
106 | typeset -gA ZSHZ
107 |
108 | # Fallback utilities in case Zsh lacks zsh/files (as is the case with MobaXterm)
109 | ZSHZ[CHOWN]='chown'
110 | ZSHZ[MV]='mv'
111 | ZSHZ[RM]='rm'
112 | # Try to load zsh/files utilities
113 | if [[ ${builtins[zf_chown]-} != 'defined' ||
114 | ${builtins[zf_mv]-} != 'defined' ||
115 | ${builtins[zf_rm]-} != 'defined' ]]; then
116 | zmodload -F zsh/files b:zf_chown b:zf_mv b:zf_rm &> /dev/null
117 | fi
118 | # Use zsh/files, if it is available
119 | [[ ${builtins[zf_chown]-} == 'defined' ]] && ZSHZ[CHOWN]='zf_chown'
120 | [[ ${builtins[zf_mv]-} == 'defined' ]] && ZSHZ[MV]='zf_mv'
121 | [[ ${builtins[zf_rm]-} == 'defined' ]] && ZSHZ[RM]='zf_rm'
122 |
123 | # Load zsh/system, if necessary
124 | [[ ${modules[zsh/system]-} == 'loaded' ]] || zmodload zsh/system &> /dev/null
125 |
126 | # Make sure ZSHZ_EXCLUDE_DIRS has been declared so that other scripts can
127 | # simply append to it
128 | (( ${+ZSHZ_EXCLUDE_DIRS} )) || typeset -gUa ZSHZ_EXCLUDE_DIRS
129 |
130 | # Determine if zsystem flock is available
131 | zsystem supports flock &> /dev/null && ZSHZ[USE_FLOCK]=1
132 |
133 | # Determine if `print -v' is supported
134 | is-at-least 5.3.0 && ZSHZ[PRINTV]=1
135 |
136 | ############################################################
137 | # The Zsh-z Command
138 | #
139 | # Globals:
140 | # ZSHZ
141 | # ZSHZ_CASE
142 | # ZSHZ_CD
143 | # ZSHZ_COMPLETION
144 | # ZSHZ_DATA
145 | # ZSHZ_DEBUG
146 | # ZSHZ_EXCLUDE_DIRS
147 | # ZSHZ_KEEP_DIRS
148 | # ZSHZ_MAX_SCORE
149 | # ZSHZ_OWNER
150 | #
151 | # Arguments:
152 | # $* Command options and arguments
153 | ############################################################
154 | zshz() {
155 |
156 | # Don't use `emulate -L zsh' - it breaks PUSHD_IGNORE_DUPS
157 | setopt LOCAL_OPTIONS NO_KSH_ARRAYS NO_SH_WORD_SPLIT EXTENDED_GLOB UNSET
158 | (( ZSHZ_DEBUG )) && setopt LOCAL_OPTIONS WARN_CREATE_GLOBAL
159 |
160 | local REPLY
161 | local -a lines
162 |
163 | # Allow the user to specify a custom datafile in $ZSHZ_DATA (or legacy $_Z_DATA)
164 | local custom_datafile="${ZSHZ_DATA:-$_Z_DATA}"
165 |
166 | # If a datafile was provided as a standalone file without a directory path
167 | # print a warning and exit
168 | if [[ -n ${custom_datafile} && ${custom_datafile} != */* ]]; then
169 | print "ERROR: You configured a custom Zsh-z datafile (${custom_datafile}), but have not specified its directory." >&2
170 | exit
171 | fi
172 |
173 | # If the user specified a datafile, use that or default to ~/.z
174 | # If the datafile is a symlink, it gets dereferenced
175 | local datafile=${${custom_datafile:-$HOME/.z}:A}
176 |
177 | # If the datafile is a directory, print a warning and exit
178 | if [[ -d $datafile ]]; then
179 | print "ERROR: Zsh-z's datafile (${datafile}) is a directory." >&2
180 | exit
181 | fi
182 |
183 | # Make sure that the datafile exists before attempting to read it or lock it
184 | # for writing
185 | [[ -f $datafile ]] || { mkdir -p "${datafile:h}" && touch "$datafile" }
186 |
187 | # Bail if we don't own the datafile and $ZSHZ_OWNER is not set
188 | [[ -z ${ZSHZ_OWNER:-${_Z_OWNER}} && -f $datafile && ! -O $datafile ]] &&
189 | return
190 |
191 | # Load the datafile into an array and parse it
192 | lines=( ${(f)"$(< $datafile)"} )
193 | # Discard entries that are incomplete or incorrectly formatted
194 | lines=( ${(M)lines:#/*\|[[:digit:]]##[.,]#[[:digit:]]#\|[[:digit:]]##} )
195 |
196 | ############################################################
197 | # Add a path to or remove one from the datafile
198 | #
199 | # Globals:
200 | # ZSHZ
201 | # ZSHZ_EXCLUDE_DIRS
202 | # ZSHZ_OWNER
203 | #
204 | # Arguments:
205 | # $1 Which action to perform (--add/--remove)
206 | # $2 The path to add
207 | ############################################################
208 | _zshz_add_or_remove_path() {
209 | local action=${1}
210 | shift
211 |
212 | if [[ $action == '--add' ]]; then
213 |
214 | # TODO: The following tasks are now handled by _agkozak_precmd. Dead code?
215 |
216 | # Don't add $HOME
217 | [[ $* == $HOME ]] && return
218 |
219 | # Don't track directory trees excluded in ZSHZ_EXCLUDE_DIRS
220 | local exclude
221 | for exclude in ${(@)ZSHZ_EXCLUDE_DIRS:-${(@)_Z_EXCLUDE_DIRS}}; do
222 | case $* in
223 | ${exclude}|${exclude}/*) return ;;
224 | esac
225 | done
226 | fi
227 |
228 | # A temporary file that gets copied over the datafile if all goes well
229 | local tempfile="${datafile}.${RANDOM}"
230 |
231 | # See https://github.com/rupa/z/pull/199/commits/ed6eeed9b70d27c1582e3dd050e72ebfe246341c
232 | if (( ZSHZ[USE_FLOCK] )); then
233 |
234 | local lockfd
235 |
236 | # Grab exclusive lock (released when function exits)
237 | zsystem flock -f lockfd "$datafile" 2> /dev/null || return
238 |
239 | fi
240 |
241 | integer tmpfd
242 | case $action in
243 | --add)
244 | exec {tmpfd}>|"$tempfile" # Open up tempfile for writing
245 | _zshz_update_datafile $tmpfd "$*"
246 | local ret=$?
247 | ;;
248 | --remove)
249 | local xdir # Directory to be removed
250 |
251 | if (( ${ZSHZ_NO_RESOLVE_SYMLINKS:-${_Z_NO_RESOLVE_SYMLINKS}} )); then
252 | [[ -d ${${*:-${PWD}}:a} ]] && xdir=${${*:-${PWD}}:a}
253 | else
254 | [[ -d ${${*:-${PWD}}:A} ]] && xdir=${${*:-${PWD}}:a}
255 | fi
256 |
257 | local -a lines_to_keep
258 | if (( ${+opts[-R]} )); then
259 | # Prompt user before deleting entire database
260 | if [[ $xdir == '/' ]] && ! read -q "?Delete entire Zsh-z database? "; then
261 | print && return 1
262 | fi
263 | # All of the lines that don't match the directory to be deleted
264 | lines_to_keep=( ${lines:#${xdir}\|*} )
265 | # Or its subdirectories
266 | lines_to_keep=( ${lines_to_keep:#${xdir%/}/**} )
267 | else
268 | # All of the lines that don't match the directory to be deleted
269 | lines_to_keep=( ${lines:#${xdir}\|*} )
270 | fi
271 | if [[ $lines != "$lines_to_keep" ]]; then
272 | lines=( $lines_to_keep )
273 | else
274 | return 1 # The $PWD isn't in the datafile
275 | fi
276 | exec {tmpfd}>|"$tempfile" # Open up tempfile for writing
277 | print -u $tmpfd -l -- $lines
278 | local ret=$?
279 | ;;
280 | esac
281 |
282 | if (( tmpfd != 0 )); then
283 | # Close tempfile
284 | exec {tmpfd}>&-
285 | fi
286 |
287 | if (( ret != 0 )); then
288 | # Avoid clobbering the datafile if the write to tempfile failed
289 | ${ZSHZ[RM]} -f "$tempfile"
290 | return $ret
291 | fi
292 |
293 | local owner
294 | owner=${ZSHZ_OWNER:-${_Z_OWNER}}
295 |
296 | if (( ZSHZ[USE_FLOCK] )); then
297 | # An unsual case: if inside Docker container where datafile could be bind
298 | # mounted
299 | if [[ -r '/proc/1/cgroup' && "$(< '/proc/1/cgroup')" == *docker* ]]; then
300 | print "$(< "$tempfile")" > "$datafile" 2> /dev/null
301 | ${ZSHZ[RM]} -f "$tempfile"
302 | # All other cases
303 | else
304 | ${ZSHZ[MV]} "$tempfile" "$datafile" 2> /dev/null ||
305 | ${ZSHZ[RM]} -f "$tempfile"
306 | fi
307 |
308 | if [[ -n $owner ]]; then
309 | ${ZSHZ[CHOWN]} ${owner}:"$(id -ng ${owner})" "$datafile"
310 | fi
311 | else
312 | if [[ -n $owner ]]; then
313 | ${ZSHZ[CHOWN]} "${owner}":"$(id -ng "${owner}")" "$tempfile"
314 | fi
315 | ${ZSHZ[MV]} -f "$tempfile" "$datafile" 2> /dev/null ||
316 | ${ZSHZ[RM]} -f "$tempfile"
317 | fi
318 |
319 | # In order to make z -x work, we have to disable zsh-z's adding
320 | # to the database until the user changes directory and the
321 | # chpwd_functions are run
322 | if [[ $action == '--remove' ]]; then
323 | ZSHZ[DIRECTORY_REMOVED]=1
324 | fi
325 | }
326 |
327 | ############################################################
328 | # Read the current datafile contents, update them, "age" them
329 | # when the total rank gets high enough, and print the new
330 | # contents to STDOUT.
331 | #
332 | # Globals:
333 | # ZSHZ_KEEP_DIRS
334 | # ZSHZ_MAX_SCORE
335 | #
336 | # Arguments:
337 | # $1 File descriptor linked to tempfile
338 | # $2 Path to be added to datafile
339 | ############################################################
340 | _zshz_update_datafile() {
341 |
342 | integer fd=$1
343 | local -A rank time
344 |
345 | # Characters special to the shell (such as '[]') are quoted with backslashes
346 | # See https://github.com/rupa/z/issues/246
347 | local add_path=${(q)2}
348 |
349 | local -a existing_paths
350 | local now=$EPOCHSECONDS line dir
351 | local path_field rank_field time_field count x
352 |
353 | rank[$add_path]=1
354 | time[$add_path]=$now
355 |
356 | # Remove paths from database if they no longer exist
357 | for line in $lines; do
358 | if [[ ! -d ${line%%\|*} ]]; then
359 | for dir in ${(@)ZSHZ_KEEP_DIRS}; do
360 | if [[ ${line%%\|*} == ${dir}/* ||
361 | ${line%%\|*} == $dir ||
362 | $dir == '/' ]]; then
363 | existing_paths+=( $line )
364 | fi
365 | done
366 | else
367 | existing_paths+=( $line )
368 | fi
369 | done
370 | lines=( $existing_paths )
371 |
372 | for line in $lines; do
373 | path_field=${(q)line%%\|*}
374 | rank_field=${${line%\|*}#*\|}
375 | time_field=${line##*\|}
376 |
377 | # When a rank drops below 1, drop the path from the database
378 | (( rank_field < 1 )) && continue
379 |
380 | if [[ $path_field == $add_path ]]; then
381 | rank[$path_field]=$rank_field
382 | (( rank[$path_field]++ ))
383 | time[$path_field]=$now
384 | else
385 | rank[$path_field]=$rank_field
386 | time[$path_field]=$time_field
387 | fi
388 | (( count += rank_field ))
389 | done
390 | if (( count > ${ZSHZ_MAX_SCORE:-${_Z_MAX_SCORE:-9000}} )); then
391 | # Aging
392 | for x in ${(k)rank}; do
393 | print -u $fd -- "$x|$(( 0.99 * rank[$x] ))|${time[$x]}" || return 1
394 | done
395 | else
396 | for x in ${(k)rank}; do
397 | print -u $fd -- "$x|${rank[$x]}|${time[$x]}" || return 1
398 | done
399 | fi
400 | }
401 |
402 | ############################################################
403 | # The original tab completion method
404 | #
405 | # String processing is smartcase -- case-insensitive if the
406 | # search string is lowercase, case-sensitive if there are
407 | # any uppercase letters. Spaces in the search string are
408 | # treated as *'s in globbing. Read the contents of the
409 | # datafile and print matches to STDOUT.
410 | #
411 | # Arguments:
412 | # $1 The string to be completed
413 | ############################################################
414 | _zshz_legacy_complete() {
415 |
416 | local line path_field path_field_normalized
417 |
418 | # Replace spaces in the search string with asterisks for globbing
419 | 1=${1//[[:space:]]/*}
420 |
421 | for line in $lines; do
422 |
423 | path_field=${line%%\|*}
424 |
425 | path_field_normalized=$path_field
426 | if (( ZSHZ_TRAILING_SLASH )); then
427 | path_field_normalized=${path_field%/}/
428 | fi
429 |
430 | # If the search string is all lowercase, the search will be case-insensitive
431 | if [[ $1 == "${1:l}" && ${path_field_normalized:l} == *${~1}* ]]; then
432 | print -- $path_field
433 | # Otherwise, case-sensitive
434 | elif [[ $path_field_normalized == *${~1}* ]]; then
435 | print -- $path_field
436 | fi
437 |
438 | done
439 | # TODO: Search strings with spaces in them are currently treated case-
440 | # insensitively.
441 | }
442 |
443 | ############################################################
444 | # `print' or `printf' to REPLY
445 | #
446 | # Variable assignment through command substitution, of the
447 | # form
448 | #
449 | # foo=$( bar )
450 | #
451 | # requires forking a subshell; on Cygwin/MSYS2/WSL1 that can
452 | # be surprisingly slow. Zsh-z avoids doing that by printing
453 | # values to the variable REPLY. Since Zsh v5.3.0 that has
454 | # been possible with `print -v'; for earlier versions of the
455 | # shell, the values are placed on the editing buffer stack
456 | # and then `read' into REPLY.
457 | #
458 | # Globals:
459 | # ZSHZ
460 | #
461 | # Arguments:
462 | # Options and parameters for `print'
463 | ############################################################
464 | _zshz_printv() {
465 | # NOTE: For a long time, ZSH's `print -v' had a tendency
466 | # to mangle multibyte strings:
467 | #
468 | # https://www.zsh.org/mla/workers/2020/msg00307.html
469 | #
470 | # The bug was fixed in late 2020:
471 | #
472 | # https://github.com/zsh-users/zsh/commit/b6ba74cd4eaec2b6cb515748cf1b74a19133d4a4#diff-32bbef18e126b837c87b06f11bfc61fafdaa0ed99fcb009ec53f4767e246b129
473 | #
474 | # In order to support shells with the bug, we must use a form of `printf`,
475 | # which does not exhibit the undesired behavior. See
476 | #
477 | # https://www.zsh.org/mla/workers/2020/msg00308.html
478 |
479 | if (( ZSHZ[PRINTV] )); then
480 | builtin print -v REPLY -f %s $@
481 | else
482 | builtin print -z $@
483 | builtin read -rz REPLY
484 | fi
485 | }
486 |
487 | ############################################################
488 | # If matches share a common root, find it, and put it in
489 | # REPLY for _zshz_output to use.
490 | #
491 | # Arguments:
492 | # $1 Name of associative array of matches and ranks
493 | ############################################################
494 | _zshz_find_common_root() {
495 | local -a common_matches
496 | local x short
497 |
498 | common_matches=( ${(@Pk)1} )
499 |
500 | for x in ${(@)common_matches}; do
501 | if [[ -z $short ]] || (( $#x < $#short )) || [[ $x != ${short}/* ]]; then
502 | short=$x
503 | fi
504 | done
505 |
506 | [[ $short == '/' ]] && return
507 |
508 | for x in ${(@)common_matches}; do
509 | [[ $x != $short* ]] && return
510 | done
511 |
512 | _zshz_printv -- $short
513 | }
514 |
515 | ############################################################
516 | # Calculate a common root, if there is one. Then do one of
517 | # the following:
518 | #
519 | # 1) Print a list of completions in frecent order;
520 | # 2) List them (z -l) to STDOUT; or
521 | # 3) Put a common root or best match into REPLY
522 | #
523 | # Globals:
524 | # ZSHZ_UNCOMMON
525 | #
526 | # Arguments:
527 | # $1 Name of an associative array of matches and ranks
528 | # $2 The best match or best case-insensitive match
529 | # $3 Whether to produce a completion, a list, or a root or
530 | # match
531 | ############################################################
532 | _zshz_output() {
533 |
534 | local match_array=$1 match=$2 format=$3
535 | local common k x
536 | local -a descending_list output
537 | local -A output_matches
538 |
539 | output_matches=( ${(Pkv)match_array} )
540 |
541 | _zshz_find_common_root $match_array
542 | common=$REPLY
543 |
544 | case $format in
545 |
546 | completion)
547 | for k in ${(@k)output_matches}; do
548 | _zshz_printv -f "%.2f|%s" ${output_matches[$k]} $k
549 | descending_list+=( ${(f)REPLY} )
550 | REPLY=''
551 | done
552 | descending_list=( ${${(@On)descending_list}#*\|} )
553 | print -l $descending_list
554 | ;;
555 |
556 | list)
557 | local path_to_display
558 | for x in ${(k)output_matches}; do
559 | if (( ${output_matches[$x]} )); then
560 | path_to_display=$x
561 | (( ZSHZ_TILDE )) &&
562 | path_to_display=${path_to_display/#${HOME}/\~}
563 | _zshz_printv -f "%-10d %s\n" ${output_matches[$x]} $path_to_display
564 | output+=( ${(f)REPLY} )
565 | REPLY=''
566 | fi
567 | done
568 | if [[ -n $common ]]; then
569 | (( ZSHZ_TILDE )) && common=${common/#${HOME}/\~}
570 | (( $#output > 1 )) && printf "%-10s %s\n" 'common:' $common
571 | fi
572 | # -lt
573 | if (( $+opts[-t] )); then
574 | for x in ${(@On)output}; do
575 | print -- $x
576 | done
577 | # -lr
578 | elif (( $+opts[-r] )); then
579 | for x in ${(@on)output}; do
580 | print -- $x
581 | done
582 | # -l
583 | else
584 | for x in ${(@on)output}; do
585 | print $x
586 | done
587 | fi
588 | ;;
589 |
590 | *)
591 | if (( ! ZSHZ_UNCOMMON )) && [[ -n $common ]]; then
592 | _zshz_printv -- $common
593 | else
594 | _zshz_printv -- ${(P)match}
595 | fi
596 | ;;
597 | esac
598 | }
599 |
600 | ############################################################
601 | # Match a pattern by rank, time, or a combination of the
602 | # two, and output the results as completions, a list, or a
603 | # best match.
604 | #
605 | # Globals:
606 | # ZSHZ
607 | # ZSHZ_CASE
608 | # ZSHZ_KEEP_DIRS
609 | # ZSHZ_OWNER
610 | #
611 | # Arguments:
612 | # #1 Pattern to match
613 | # $2 Matching method (rank, time, or [default] frecency)
614 | # $3 Output format (completion, list, or [default] store
615 | # in REPLY
616 | ############################################################
617 | _zshz_find_matches() {
618 | setopt LOCAL_OPTIONS NO_EXTENDED_GLOB
619 |
620 | local fnd=$1 method=$2 format=$3
621 |
622 | local -a existing_paths
623 | local line dir path_field rank_field time_field rank dx escaped_path_field
624 | local -A matches imatches
625 | local best_match ibest_match hi_rank=-9999999999 ihi_rank=-9999999999
626 |
627 | # Remove paths from database if they no longer exist
628 | for line in $lines; do
629 | if [[ ! -d ${line%%\|*} ]]; then
630 | for dir in ${(@)ZSHZ_KEEP_DIRS}; do
631 | if [[ ${line%%\|*} == ${dir}/* ||
632 | ${line%%\|*} == $dir ||
633 | $dir == '/' ]]; then
634 | existing_paths+=( $line )
635 | fi
636 | done
637 | else
638 | existing_paths+=( $line )
639 | fi
640 | done
641 | lines=( $existing_paths )
642 |
643 | for line in $lines; do
644 | path_field=${line%%\|*}
645 | rank_field=${${line%\|*}#*\|}
646 | time_field=${line##*\|}
647 |
648 | case $method in
649 | rank) rank=$rank_field ;;
650 | time) (( rank = time_field - EPOCHSECONDS )) ;;
651 | *)
652 | # Frecency routine
653 | (( dx = EPOCHSECONDS - time_field ))
654 | rank=$(( 10000 * rank_field * (3.75/( (0.0001 * dx + 1) + 0.25)) ))
655 | ;;
656 | esac
657 |
658 | # Use spaces as wildcards
659 | local q=${fnd//[[:space:]]/\*}
660 |
661 | # If $ZSHZ_TRAILING_SLASH is set, use path_field with a trailing slash for matching.
662 | local path_field_normalized=$path_field
663 | if (( ZSHZ_TRAILING_SLASH )); then
664 | path_field_normalized=${path_field%/}/
665 | fi
666 |
667 | # If $ZSHZ_CASE is 'ignore', be case-insensitive.
668 | #
669 | # If it's 'smart', be case-insensitive unless the string to be matched
670 | # includes capital letters.
671 | #
672 | # Otherwise, the default behavior of Zsh-z is to match case-sensitively if
673 | # possible, then to fall back on a case-insensitive match if possible.
674 | if [[ $ZSHZ_CASE == 'smart' && ${1:l} == $1 &&
675 | ${path_field_normalized:l} == ${~q:l} ]]; then
676 | imatches[$path_field]=$rank
677 | elif [[ $ZSHZ_CASE != 'ignore' && $path_field_normalized == ${~q} ]]; then
678 | matches[$path_field]=$rank
679 | elif [[ $ZSHZ_CASE != 'smart' && ${path_field_normalized:l} == ${~q:l} ]]; then
680 | imatches[$path_field]=$rank
681 | fi
682 |
683 | # Escape characters that would cause "invalid subscript" errors
684 | # when accessing the associative array.
685 | escaped_path_field=${path_field//'\'/'\\'}
686 | escaped_path_field=${escaped_path_field//'`'/'\`'}
687 | escaped_path_field=${escaped_path_field//'('/'\('}
688 | escaped_path_field=${escaped_path_field//')'/'\)'}
689 | escaped_path_field=${escaped_path_field//'['/'\['}
690 | escaped_path_field=${escaped_path_field//']'/'\]'}
691 |
692 | if (( matches[$escaped_path_field] )) &&
693 | (( matches[$escaped_path_field] > hi_rank )); then
694 | best_match=$path_field
695 | hi_rank=${matches[$escaped_path_field]}
696 | elif (( imatches[$escaped_path_field] )) &&
697 | (( imatches[$escaped_path_field] > ihi_rank )); then
698 | ibest_match=$path_field
699 | ihi_rank=${imatches[$escaped_path_field]}
700 | ZSHZ[CASE_INSENSITIVE]=1
701 | fi
702 | done
703 |
704 | # Return 1 when there are no matches
705 | [[ -z $best_match && -z $ibest_match ]] && return 1
706 |
707 | if [[ -n $best_match ]]; then
708 | _zshz_output matches best_match $format
709 | elif [[ -n $ibest_match ]]; then
710 | _zshz_output imatches ibest_match $format
711 | fi
712 | }
713 |
714 | # THE MAIN ROUTINE
715 |
716 | local -A opts
717 |
718 | zparseopts -E -D -A opts -- \
719 | -add \
720 | -complete \
721 | c \
722 | e \
723 | h \
724 | -help \
725 | l \
726 | r \
727 | R \
728 | t \
729 | x
730 |
731 | if [[ $1 == '--' ]]; then
732 | shift
733 | elif [[ -n ${(M)@:#-*} && -z $compstate ]]; then
734 | print "Improper option(s) given."
735 | _zshz_usage
736 | return 1
737 | fi
738 |
739 | local opt output_format method='frecency' fnd prefix req
740 |
741 | for opt in ${(k)opts}; do
742 | case $opt in
743 | --add)
744 | [[ ! -d $* ]] && return 1
745 | local dir
746 | # Cygwin and MSYS2 have a hard time with relative paths expressed from /
747 | if [[ $OSTYPE == (cygwin|msys) && $PWD == '/' && $* != /* ]]; then
748 | set -- "/$*"
749 | fi
750 | if (( ${ZSHZ_NO_RESOLVE_SYMLINKS:-${_Z_NO_RESOLVE_SYMLINKS}} )); then
751 | dir=${*:a}
752 | else
753 | dir=${*:A}
754 | fi
755 | _zshz_add_or_remove_path --add "$dir"
756 | return
757 | ;;
758 | --complete)
759 | if [[ -s $datafile && ${ZSHZ_COMPLETION:-frecent} == 'legacy' ]]; then
760 | _zshz_legacy_complete "$1"
761 | return
762 | fi
763 | output_format='completion'
764 | ;;
765 | -c) [[ $* == ${PWD}/* || $PWD == '/' ]] || prefix="$PWD " ;;
766 | -h|--help)
767 | _zshz_usage
768 | return
769 | ;;
770 | -l) output_format='list' ;;
771 | -r) method='rank' ;;
772 | -t) method='time' ;;
773 | -x)
774 | # Cygwin and MSYS2 have a hard time with relative paths expressed from /
775 | if [[ $OSTYPE == (cygwin|msys) && $PWD == '/' && $* != /* ]]; then
776 | set -- "/$*"
777 | fi
778 | _zshz_add_or_remove_path --remove $*
779 | return
780 | ;;
781 | esac
782 | done
783 | req="$*"
784 | fnd="$prefix$*"
785 |
786 | [[ -n $fnd && $fnd != "$PWD " ]] || {
787 | [[ $output_format != 'completion' ]] && output_format='list'
788 | }
789 |
790 | #########################################################
791 | # Allow the user to specify directory-changing command
792 | # using $ZSHZ_CD (default: builtin cd).
793 | #
794 | # Globals:
795 | # ZSHZ_CD
796 | #
797 | # Arguments:
798 | # $* Path
799 | #########################################################
800 | zshz_cd() {
801 | setopt LOCAL_OPTIONS NO_WARN_CREATE_GLOBAL
802 |
803 | if [[ -z $ZSHZ_CD ]]; then
804 | builtin cd "$*"
805 | else
806 | ${=ZSHZ_CD} "$*"
807 | fi
808 | }
809 |
810 | #########################################################
811 | # If $ZSHZ_ECHO == 1, display paths as you jump to them.
812 | # If it is also the case that $ZSHZ_TILDE == 1, display
813 | # the home directory as a tilde.
814 | #########################################################
815 | _zshz_echo() {
816 | if (( ZSHZ_ECHO )); then
817 | if (( ZSHZ_TILDE )); then
818 | print ${PWD/#${HOME}/\~}
819 | else
820 | print $PWD
821 | fi
822 | fi
823 | }
824 |
825 | if [[ ${@: -1} == /* ]] && (( ! $+opts[-e] && ! $+opts[-l] )); then
826 | # cd if possible; echo the new path if $ZSHZ_ECHO == 1
827 | [[ -d ${@: -1} ]] && zshz_cd ${@: -1} && _zshz_echo && return
828 | fi
829 |
830 | # With option -c, make sure query string matches beginning of matches;
831 | # otherwise look for matches anywhere in paths
832 |
833 | # zpm-zsh/colors has a global $c, so we'll avoid math expressions here
834 | if [[ ! -z ${(tP)opts[-c]} ]]; then
835 | _zshz_find_matches "$fnd*" $method $output_format
836 | else
837 | _zshz_find_matches "*$fnd*" $method $output_format
838 | fi
839 |
840 | local ret2=$?
841 |
842 | local cd
843 | cd=$REPLY
844 |
845 | # New experimental "uncommon" behavior
846 | #
847 | # If the best choice at this point is something like /foo/bar/foo/bar, and the # search pattern is `bar', go to /foo/bar/foo/bar; but if the search pattern
848 | # is `foo', go to /foo/bar/foo
849 | if (( ZSHZ_UNCOMMON )) && [[ -n $cd ]]; then
850 | if [[ -n $cd ]]; then
851 |
852 | # In the search pattern, replace spaces with *
853 | local q=${fnd//[[:space:]]/\*}
854 | q=${q%/} # Trailing slash has to be removed
855 |
856 | # As long as the best match is not case-insensitive
857 | if (( ! ZSHZ[CASE_INSENSITIVE] )); then
858 | # Count the number of characters in $cd that $q matches
859 | local q_chars=$(( ${#cd} - ${#${cd//${~q}/}} ))
860 | # Try dropping directory elements from the right; stop when it affects
861 | # how many times the search pattern appears
862 | until (( ( ${#cd:h} - ${#${${cd:h}//${~q}/}} ) != q_chars )); do
863 | cd=${cd:h}
864 | done
865 |
866 | # If the best match is case-insensitive
867 | else
868 | local q_chars=$(( ${#cd} - ${#${${cd:l}//${~${q:l}}/}} ))
869 | until (( ( ${#cd:h} - ${#${${${cd:h}:l}//${~${q:l}}/}} ) != q_chars )); do
870 | cd=${cd:h}
871 | done
872 | fi
873 |
874 | ZSHZ[CASE_INSENSITIVE]=0
875 | fi
876 | fi
877 |
878 | if (( ret2 == 0 )) && [[ -n $cd ]]; then
879 | if (( $+opts[-e] )); then # echo
880 | (( ZSHZ_TILDE )) && cd=${cd/#${HOME}/\~}
881 | print -- "$cd"
882 | else
883 | # cd if possible; echo the new path if $ZSHZ_ECHO == 1
884 | [[ -d $cd ]] && zshz_cd "$cd" && _zshz_echo
885 | fi
886 | else
887 | # if $req is a valid path, cd to it; echo the new path if $ZSHZ_ECHO == 1
888 | if ! (( $+opts[-e] || $+opts[-l] )) && [[ -d $req ]]; then
889 | zshz_cd "$req" && _zshz_echo
890 | else
891 | return $ret2
892 | fi
893 | fi
894 | }
895 |
896 | alias ${ZSHZ_CMD:-${_Z_CMD:-z}}='zshz 2>&1'
897 |
898 | ############################################################
899 | # precmd - add path to datafile unless `z -x' has just been
900 | # run
901 | #
902 | # Globals:
903 | # ZSHZ
904 | ############################################################
905 | _zshz_precmd() {
906 | # Protect against `setopt NO_UNSET'
907 | setopt LOCAL_OPTIONS UNSET
908 |
909 | # Do not add PWD to datafile when in HOME directory, or
910 | # if `z -x' has just been run
911 | [[ $PWD == "$HOME" ]] || (( ZSHZ[DIRECTORY_REMOVED] )) && return
912 |
913 | # Don't track directory trees excluded in ZSHZ_EXCLUDE_DIRS
914 | local exclude
915 | for exclude in ${(@)ZSHZ_EXCLUDE_DIRS:-${(@)_Z_EXCLUDE_DIRS}}; do
916 | case $PWD in
917 | ${exclude}|${exclude}/*) return ;;
918 | esac
919 | done
920 |
921 | # It appears that forking a subshell is so slow in Windows that it is better
922 | # just to add the PWD to the datafile in the foreground
923 | if [[ $OSTYPE == (cygwin|msys) ]]; then
924 | zshz --add "$PWD"
925 | else
926 | (zshz --add "$PWD" &)
927 | fi
928 |
929 | # See https://github.com/rupa/z/pull/247/commits/081406117ea42ccb8d159f7630cfc7658db054b6
930 | : $RANDOM
931 | }
932 |
933 | ############################################################
934 | # chpwd
935 | #
936 | # When the $PWD is removed from the datafile with `z -x',
937 | # Zsh-z refrains from adding it again until the user has
938 | # left the directory.
939 | #
940 | # Globals:
941 | # ZSHZ
942 | ############################################################
943 | _zshz_chpwd() {
944 | ZSHZ[DIRECTORY_REMOVED]=0
945 | }
946 |
947 | autoload -Uz add-zsh-hook
948 |
949 | add-zsh-hook precmd _zshz_precmd
950 | add-zsh-hook chpwd _zshz_chpwd
951 |
952 | ############################################################
953 | # Completion
954 | ############################################################
955 |
956 | # Standardized $0 handling
957 | # https://zdharma-continuum.github.io/Zsh-100-Commits-Club/Zsh-Plugin-Standard.html
958 | 0="${${ZERO:-${0:#$ZSH_ARGZERO}}:-${(%):-%N}}"
959 | 0="${${(M)0:#/*}:-$PWD/$0}"
960 |
961 | (( ${fpath[(ie)${0:A:h}]} <= ${#fpath} )) || fpath=( "${0:A:h}" "${fpath[@]}" )
962 |
963 | ############################################################
964 | # zsh-z functions
965 | ############################################################
966 | ZSHZ[FUNCTIONS]='_zshz_usage
967 | _zshz_add_or_remove_path
968 | _zshz_update_datafile
969 | _zshz_legacy_complete
970 | _zshz_printv
971 | _zshz_find_common_root
972 | _zshz_output
973 | _zshz_find_matches
974 | zshz
975 | _zshz_precmd
976 | _zshz_chpwd
977 | _zshz'
978 |
979 | ############################################################
980 | # Enable WARN_NESTED_VAR for functions listed in
981 | # ZSHZ[FUNCTIONS]
982 | ############################################################
983 | (( ${+ZSHZ_DEBUG} )) && () {
984 | if is-at-least 5.4.0; then
985 | local x
986 | for x in ${=ZSHZ[FUNCTIONS]}; do
987 | functions -W $x
988 | done
989 | fi
990 | }
991 |
992 | ############################################################
993 | # Unload function
994 | #
995 | # See https://github.com/agkozak/Zsh-100-Commits-Club/blob/master/Zsh-Plugin-Standard.adoc#unload-fun
996 | #
997 | # Globals:
998 | # ZSHZ
999 | # ZSHZ_CMD
1000 | ############################################################
1001 | zsh-z_plugin_unload() {
1002 | emulate -L zsh
1003 |
1004 | add-zsh-hook -D precmd _zshz_precmd
1005 | add-zsh-hook -d chpwd _zshz_chpwd
1006 |
1007 | local x
1008 | for x in ${=ZSHZ[FUNCTIONS]}; do
1009 | (( ${+functions[$x]} )) && unfunction $x
1010 | done
1011 |
1012 | unset ZSHZ
1013 |
1014 | fpath=( "${(@)fpath:#${0:A:h}}" )
1015 |
1016 | (( ${+aliases[${ZSHZ_CMD:-${_Z_CMD:-z}}]} )) &&
1017 | unalias ${ZSHZ_CMD:-${_Z_CMD:-z}}
1018 |
1019 | unfunction $0
1020 | }
1021 |
1022 | # vim: fdm=indent:ts=2:et:sts=2:sw=2:
1023 |
--------------------------------------------------------------------------------