├── .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 | [![MIT License](img/mit_license.svg)](https://opensource.org/licenses/MIT) 4 | ![Zsh version 4.3.11 and higher](img/zsh_4.3.11_plus.svg) 5 | [![GitHub stars](https://img.shields.io/github/stars/agkozak/zsh-z.svg)](https://github.com/agkozak/zsh-z/stargazers) 6 | 7 | ![Zsh-z demo](img/demo.gif) 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 | licenselicenseMITMIT -------------------------------------------------------------------------------- /img/zsh_4.3.11_plus.svg: -------------------------------------------------------------------------------- 1 | zshzsh4.3.11+4.3.11+ -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------