feat(z): replace rupa/z with agkozak/zsh-z (#11236)

This commit is contained in:
Carlo 2022-10-18 19:10:55 +02:00 committed by GitHub
parent 818f3de1fa
commit 7e3231b846
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 1417 additions and 600 deletions

21
plugins/z/LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018-2022 Alexandros Kozak
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

343
plugins/z/MANUAL.md Normal file
View file

@ -0,0 +1,343 @@
# Zsh-z
Zsh-z is a command line tool that allows you to jump quickly to directories that you have visited frequently in the past, 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. It is then in the position to guess where you want to go when you type a partial string, e.g., `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 much time 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 if you were using `cd`.
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. It was quite possibly 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`.
There is a noteworthy stability increase as well. Race conditions have always been a problem with `rupa/z`, and users of that utility will occasionally lose their `.z` databases. By having Zsh-z only use Zsh (`rupa/z` uses a hybrid shell code 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, even through extreme testing.
There are other, smaller improvements which I try to document in [Improvements and Fixes](#improvements-and-fixes). These include the new default behavior of sorting your tab completions by frecency rather than just letting Zsh sort the raw results alphabetically (a behavior which can be restored if you like it -- [see below](#settings)).
Zsh-z is a drop-in replacement for `rupa/z` and will, by default, use the same database (`~/.z`), so you can go on using `rupa/z` when you launch `bash`.
## Table of Contents
- [News](#news)
- [Installation](#installation)
- [Command Line Options](#command-line-options)
- [Settings](#settings)
- [Case Sensitivity](#case-sensitivity)
- [`ZSHZ_UNCOMMON`](#zshz_uncommon)
- [Making `--add` work for you](#making---add-work-for-you)
- [Other Improvements and Fixes](#other-improvements-and-fixes)
- [Migrating from Other Tools](#migrating-from-other-tools)
- [`COMPLETE_ALIASES`](#complete_aliases)
- [Known Bugs](#known-bugs)
## News
<details>
<summary>Here are the latest features and updates.</summary>
- June 29, 2022
+ Zsh-z is less likely to leave temporary files sitting around (props @mafredri).
- June 27, 2022
+ A bug was fixed which was preventing paths with spaces in them from being updated ([#61](https://github.com/agkozak/zsh-z/issues/61)).
+ If writing to the temporary database file fails, the database will not be clobbered (props @mafredri).
- December 19, 2021
+ ZSH-z will now display tildes for `HOME` during completion when `ZSHZ_TILDE=1` has been set.
- November 11, 2021
+ A bug was fixed which was preventing ranks from being incremented.
+ `--add` has been made to work with relative paths and has been documented for the user.
- October 14, 2021
+ Completions were being sorted alphabetically, rather than by rank; this error has been fixed.
- September 25, 2021
+ Orthographical change: "Zsh," not "ZSH."
- September 23, 2021
+ `z -xR` will now remove a directory *and its subdirectories* from the database.
+ `z -x` and `z -xR` can now take an argument; without one, `PWD` is assumed.
- September 7, 2021
+ Fixed the unload function so that it removes the `$ZSHZ_CMD` alias (default: `z`).
- August 27, 2021
+ Using `print -v ... -f` instead of `print -v` to work around longstanding bug in Zsh involving `print -v` and multibyte strings.
- August 13, 2021
+ Fixed the explanation string printed during completion so that it may be formatted with `zstyle`.
+ Zsh-z now declares `ZSHZ_EXCLUDE_DIRS` as an array with unique elements so that you do not have to.
- July 29, 2021
+ Temporarily disabling use of `print -v`, which seems to be mangling CJK multibyte strings.
- July 27, 2021
+ Internal escaping of path names now works with older versions of ZSH.
+ Zsh-z now detects and discards any incomplete or incorrectly formattted database entries.
- July 10, 2021
+ 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`.
- June 25, 2021
+ Setting `ZSHZ_TILDE=1` displays the `HOME` directory as `~`.
- May 7, 2021
+ Setting `ZSHZ_ECHO=1` will cause Zsh-z to display the new path when you change directories.
+ Better escaping of path names to deal paths containing the characters ``\`()[]``.
- February 15, 2021
+ 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.
- January 31, 2021
+ 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.
+ `_zshz_precmd` simply returns if `PWD` is `HOME` or in `ZSH_EXCLUDE_DIRS`, rather than waiting for `zshz` to do that.
- January 17, 2021
+ Made sure that the `PUSHD_IGNORE_DUPS` option is respected.
- January 14, 2021
+ The `z -h` help text now breaks at spaces.
+ `z -l` was not working for Zsh version < 5.
- January 11, 2021
+ Major refactoring of the code.
+ `z -lr` and `z -lt` work as expected.
+ `EXTENDED_GLOB` has been disabled within the plugin to accomodate old-fashioned Windows directories with names such as `Progra~1`.
+ Removed `zshelldoc` documentation.
- January 6, 2021
+ 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.
- January 5, 2021
+ 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.
- December 22, 2020
+ `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).
+ `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).
+ Symlinked datafiles were having their symlinks overwritten; this bug has been fixed.
</details>
## Installation
### General observations
This script can be installed simply by downloading it and sourcing it from your `.zshrc`:
source /path/to/zsh-z.plugin.zsh
For tab completion to work, you will want to have loaded `compinit`. The frameworks handle this themselves. If you are not using a framework, put
autoload -U compinit && compinit
in your .zshrc somewhere below where you source `zsh-z.plugin.zsh`.
If you add
zstyle ':completion:*' menu select
to your `.zshrc`, your completion menus will look very nice. This `zstyle` invocation should work with any of the frameworks below as well.
### For [antigen](https://github.com/zsh-users/antigen) users
Add the line
antigen bundle agkozak/zsh-z
to your `.zshrc`, somewhere above the line that says `antigen apply`.
### For [oh-my-zsh](http://ohmyz.sh/) users
Execute the following command:
git clone https://github.com/agkozak/zsh-z ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-z
and add `zsh-z` to the line of your `.zshrc` that specifies `plugins=()`, e.g., `plugins=( git zsh-z )`.
### For [prezto](https://github.com/sorin-ionescu/prezto) users
Execute the following command:
git clone https://github.com/agkozak/zsh-z.git ~/.zprezto-contrib/zsh-z
Then edit your `~/.zpreztorc` file. Make sure the line that says
zstyle ':prezto:load' pmodule-dirs $HOME/.zprezto-contrib
is uncommented. Then find the section that specifies which modules are to be loaded; it should look something like this:
zstyle ':prezto:load' pmodule \
'environment' \
'terminal' \
'editor' \
'history' \
'directory' \
'spectrum' \
'utility' \
'completion' \
'prompt'
Add a backslash to the end of the last line add `'zsh-z'` to the list, e.g.,
zstyle ':prezto:load' pmodule \
'environment' \
'terminal' \
'editor' \
'history' \
'directory' \
'spectrum' \
'utility' \
'completion' \
'prompt' \
'zsh-z'
Then relaunch `zsh`.
### For [zcomet](https://github.com/agkozak/zcomet) users
Simply add
zcomet load agkozak/zsh-z
to your `.zshrc` (below where you source `zcomet.zsh` and above where you run `zcomet compinit`).
### For [zgen](https://github.com/tarjoilija/zgen) users
Add the line
zgen load agkozak/zsh-z
somewhere above the line that says `zgen save`. Then run
zgen reset
zsh
to refresh your init script.
### For [Zim](https://github.com/zimfw/zimfw)
Add the following line to your `.zimrc`:
zmodule https://github.com/agkozak/zsh-z
Then run
zimfw install
and restart your shell.
### For [Zinit](https://github.com/zdharma-continuum/zinit) users
Add the line
zinit load agkozak/zsh-z
to your `.zshrc`.
`zsh-z` supports `zinit`'s `unload` feature; just run `zinit unload agkozak/zshz` to restore the shell to its state before `zsh-z` was loaded.
### For [Znap](https://github.com/marlonrichert/zsh-snap) users
Add the line
znap source agkozak/zsh-z
somewhere below the line where you `source` Znap itself.
### For [zplug](https://github.com/zplug/zplug) users
Add the line
zplug "agkozak/zsh-z"
somewhere above the line that says `zplug load`. Then run
zplug install
zplug load
to install `zsh-z`.
## Command Line Options
- `--add` Add a directory to the database
- `-c` Only match subdirectories of the current directory
- `-e` Echo the best match without going to it
- `-h` Display help
- `-l` List all matches without going to them
- `-r` Match by rank (i.e. how much time you spend in directories)
- `-t` Time -- match by how recently you have been to directories
- `-x` Remove a directory (by default, the current directory) from the database
- `-xR` Remove a directory (by default, the current directory) and its subdirectories from the database
# Settings
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` (they begin with `_Z_`).
* `ZSHZ_CMD` changes the command name (default: `z`)
* `ZSHZ_COMPLETION` can be `'frecent'` (default) or `'legacy'`, depending on whether you want your completion results sorted according to frecency or simply sorted alphabetically
* `ZSHZ_DATA` changes the database file (default: `~/.z`)
* `ZSHZ_ECHO` displays the new path name when changing directories (default: `0`)
* `ZSHZ_EXCLUDE_DIRS` is an array of directories to keep out of the database (default: empty)
* `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)
* `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)
* `ZSHZ_NO_RESOLVE_SYMLINKS` prevents symlink resolution (default: `0`)
* `ZSHZ_OWNER` allows usage when in `sudo -s` mode (default: empty)
* `ZSHZ_TILDE` displays the name of the `HOME` directory as a `~` (default: `0`)
* `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`)
* `ZSHZ_UNCOMMON` changes the logic used to calculate the directory jumped to; [see below](#zshz_uncommon`) (default: `0`)
## Case sensitivity
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.
Some users prefer simple case-insensitivity; this behavior can be enabled by setting
ZSHZ_CASE=ignore
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
ZSHZ_CASE=smart
## `ZSHZ_UNCOMMON`
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
/home/me/code/foo
/home/me/code/bar
/home/me/code/bat
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
/home/me/.vscode/foo
/home/me/code/foo
/home/me/code/bar
/home/me/code/bat
then there is no common prefix. In this case, `z code` will simply send you to the highest-ranking match, `/home/me/code/bat`.
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.
## Making `--add` Work for You
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.
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
for i in $(find $PWD -maxdepth 3 -name .git -type d); do
z --add ${i:h}
done
(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.)
## Other Improvements and Fixes
* `z -x` works, with the help of `chpwd_functions`.
* Zsh-z works on Solaris.
* Zsh-z uses the "new" `zshcompsys` completion system instead of the old `compctl` one.
* There is no error message when the database file has not yet been created.
* There is support for special characters (e.g., `[`) in directory names.
* If `z -l` only returns one match, a common root is not printed.
* Exit status codes increasingly make sense.
* Completions work with options `-c`, `-r`, and `-t`.
* If `~/foo` and `~/foob` are matches, `~/foo` is *not* the common root. Only a common parent directory can be a common root.
* `z -x` and the new, recursive `z -xR` can take an argument so that you can remove directories other than `PWD` from the database.
## Migrating from Other Tools
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.
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
awk -F "\t" '{printf("%s|%0.f|%s\n", $2, $1, '"$(date +%s)"')}' < /path/to/autojump.txt > ~/.z
## `COMPLETE_ALIASES`
`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
setopt COMPLETE_ALIASES
the line
compdef _zshz ${ZSHZ_CMD:-${_Z_CMD:-z}}
That will re-bind `z` or the command of your choice to the underlying Zsh-z function.
## Known Bugs
It is possible to run a completion on a string with spaces in it, e.g., `z us bi<TAB>` might take you to `/usr/local/bin`. This works, but as things stand, after the completion the command line reads
z us /usr/local/bin.
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.

View file

@ -1,4 +0,0 @@
readme:
@groff -man -Tascii z.1 | col -bx
.PHONY: readme

View file

@ -1,148 +0,0 @@
Z(1) User Commands Z(1)
NAME
z - jump around
SYNOPSIS
z [-chlrtx] [regex1 regex2 ... regexn]
AVAILABILITY
bash, zsh
DESCRIPTION
Tracks your most used directories, based on 'frecency'.
After a short learning phase, z will take you to the most 'frecent'
directory that matches ALL of the regexes given on the command line, in
order.
For example, z foo bar would match /foo/bar but not /bar/foo.
OPTIONS
-c restrict matches to subdirectories of the current directory
-e echo the best match, don't cd
-h show a brief help message
-l list only
-r match by rank only
-t match by recent access only
-x remove the current directory from the datafile
EXAMPLES
z foo cd to most frecent dir matching foo
z foo bar cd to most frecent dir matching foo, then bar
z -r foo cd to highest ranked dir matching foo
z -t foo cd to most recently accessed dir matching foo
z -l foo list all dirs matching foo (by frecency)
NOTES
Installation:
Put something like this in your $HOME/.bashrc or $HOME/.zshrc:
. /path/to/z.sh
cd around for a while to build up the db.
PROFIT!!
Optionally:
Set $_Z_CMD to change the command name (default z).
Set $_Z_DATA to change the datafile (default $HOME/.z).
Set $_Z_MAX_SCORE lower to age entries out faster (default
9000).
Set $_Z_NO_RESOLVE_SYMLINKS to prevent symlink resolution.
Set $_Z_NO_PROMPT_COMMAND to handle PROMPT_COMMAND/precmd your-
self.
Set $_Z_EXCLUDE_DIRS to an array of directory trees to exclude.
Set $_Z_OWNER to allow usage when in 'sudo -s' mode.
(These settings should go in .bashrc/.zshrc before the line
added above.)
Install the provided man page z.1 somewhere in your MANPATH,
like /usr/local/man/man1.
Aging:
The rank of directories maintained by z undergoes aging based on a sim-
ple formula. The rank of each entry is incremented every time it is
accessed. When the sum of ranks is over 9000, all ranks are multiplied
by 0.99. Entries with a rank lower than 1 are forgotten.
Frecency:
Frecency is a portmanteau of 'recent' and 'frequency'. It is a weighted
rank that depends on how often and how recently something occurred. As
far as I know, Mozilla came up with the term.
To z, a directory that has low ranking but has been accessed recently
will quickly have higher rank than a directory accessed frequently a
long time ago.
Frecency is determined at runtime.
Common:
When multiple directories match all queries, and they all have a common
prefix, z will cd to the shortest matching directory, without regard to
priority. This has been in effect, if undocumented, for quite some
time, but should probably be configurable or reconsidered.
Tab Completion:
z supports tab completion. After any number of arguments, press TAB to
complete on directories that match each argument. Due to limitations of
the completion implementations, only the last argument will be com-
pleted in the shell.
Internally, z decides you've requested a completion if the last argu-
ment passed is an absolute path to an existing directory. This may
cause unexpected behavior if the last argument to z begins with /.
ENVIRONMENT
A function _z() is defined.
The contents of the variable $_Z_CMD is aliased to _z 2>&1. If not set,
$_Z_CMD defaults to z.
The environment variable $_Z_DATA can be used to control the datafile
location. If it is not defined, the location defaults to $HOME/.z.
The environment variable $_Z_NO_RESOLVE_SYMLINKS can be set to prevent
resolving of symlinks. If it is not set, symbolic links will be
resolved when added to the datafile.
In bash, z appends a command to the PROMPT_COMMAND environment variable
to maintain its database. In zsh, z appends a function _z_precmd to the
precmd_functions array.
The environment variable $_Z_NO_PROMPT_COMMAND can be set if you want
to handle PROMPT_COMMAND or precmd yourself.
The environment variable $_Z_EXCLUDE_DIRS can be set to an array of
directory trees to exclude from tracking. $HOME is always excluded.
Directories must be full paths without trailing slashes.
The environment variable $_Z_OWNER can be set to your username, to
allow usage of z when your sudo environment keeps $HOME set.
FILES
Data is stored in $HOME/.z. This can be overridden by setting the
$_Z_DATA environment variable. When initialized, z will raise an error
if this path is a directory, and not function correctly.
A man page (z.1) is provided.
SEE ALSO
regex(7), pushd, popd, autojump, cdargs
Please file bugs at https://github.com/rupa/z/
z January 2013 Z(1)

View file

@ -1,8 +1,9 @@
# z - jump around # z - jump around
This plugin defines the [z command](https://github.com/rupa/z) that tracks your most visited directories and allows you to access them with very few keystrokes. This plugin defines the [z command](https://github.com/agkozak/zsh-z) that tracks your most visited directories and allows you to access them with very few keystrokes.
### Example ### Example
Assume that you have previously visited directory `~/.oh-my-zsh/plugins`. From any folder in your command line, you can quickly access it by using a regex match to this folder: Assume that you have previously visited directory `~/.oh-my-zsh/plugins`. From any folder in your command line, you can quickly access it by using a regex match to this folder:
```bash ```bash
@ -11,6 +12,7 @@ Assume that you have previously visited directory `~/.oh-my-zsh/plugins`. From a
``` ```
### Setup ### Setup
To enable z, add `z` to your `plugins` array in your zshrc file: To enable z, add `z` to your `plugins` array in your zshrc file:
```zsh ```zsh
@ -19,5 +21,4 @@ plugins=(... z)
### Further reading ### Further reading
For advanced usage and details of z, see [README](./README) (in man page format, copied from [rupa/z](https://github.com/rupa/z)). For advanced usage and details of z, see [MANUAL](./MANUAL.md) (copied from [agkozak/zsh-z](https://github.com/agkozak/zsh-z)).

82
plugins/z/_z Normal file
View file

@ -0,0 +1,82 @@
#compdef zshz ${ZSHZ_CMD:-${_Z_CMD:-z}}
#
# Zsh-z - jump around with Zsh - A native Zsh version of z without awk, sort,
# date, or sed
#
# https://github.com/agkozak/zsh-z
#
# Copyright (c) 2018-2022 Alexandros Kozak
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# z (https://github.com/rupa/z) is copyright (c) 2009 rupa deadwyler and
# licensed under the WTFPL license, Version 2.a
#
# shellcheck shell=ksh
############################################################
# Zsh-z COMPLETIONS
############################################################
emulate -L zsh
(( ZSHZ_DEBUG )) &&
setopt LOCAL_OPTIONS WARN_CREATE_GLOBAL NO_WARN_NESTED_VAR 2> /dev/null
# TODO: This routine currently reproduces z's feature of allowing spaces to be
# used as wildcards in completions, so that
#
# z us lo bi
#
# can expand to
#
# z /usr/local/bin
#
# but it also reproduces z's buggy display on the commandline, viz.
#
# z us lo /usr/local/bin
#
# Address.
local completions expl completion
local -a completion_list
completions=$(zshz --complete ${(@)words:1})
[[ -z $completions ]] && return 1
for completion in ${(f)completions[@]}; do
if (( ZSHZ_TILDE )) && [[ $completion == ${HOME}* ]]; then
completion="~${(q)${completion#${HOME}}}"
else
completion="${(q)completion}"
fi
completion_list+=( $completion )
done
_description -V completion_list expl 'directories'
if [[ $ZSHZ_COMPLETION == 'legacy' ]]; then
compadd "${expl[@]}" -QU -- "${completion_list[@]}"
else
compadd "${expl[@]}" -QU -V zsh-z -- "${completion_list[@]}"
fi
compstate[insert]=menu
return 0
# vim: ft=zsh:fdm=indent:ts=2:et:sts=2:sw=2:

View file

@ -1,173 +0,0 @@
.TH "Z" "1" "January 2013" "z" "User Commands"
.SH
NAME
z \- jump around
.SH
SYNOPSIS
z [\-chlrtx] [regex1 regex2 ... regexn]
.SH
AVAILABILITY
bash, zsh
.SH
DESCRIPTION
Tracks your most used directories, based on 'frecency'.
.P
After a short learning phase, \fBz\fR will take you to the most 'frecent'
directory that matches ALL of the regexes given on the command line, in order.
For example, \fBz foo bar\fR would match \fB/foo/bar\fR but not \fB/bar/foo\fR.
.SH
OPTIONS
.TP
\fB\-c\fR
restrict matches to subdirectories of the current directory
.TP
\fB\-e\fR
echo the best match, don't cd
.TP
\fB\-h\fR
show a brief help message
.TP
\fB\-l\fR
list only
.TP
\fB\-r\fR
match by rank only
.TP
\fB\-t\fR
match by recent access only
.TP
\fB\-x\fR
remove the current directory from the datafile
.SH EXAMPLES
.TP 14
\fBz foo\fR
cd to most frecent dir matching foo
.TP 14
\fBz foo bar\fR
cd to most frecent dir matching foo, then bar
.TP 14
\fBz -r foo\fR
cd to highest ranked dir matching foo
.TP 14
\fBz -t foo\fR
cd to most recently accessed dir matching foo
.TP 14
\fBz -l foo\fR
list all dirs matching foo (by frecency)
.SH
NOTES
.SS
Installation:
.P
Put something like this in your \fB$HOME/.bashrc\fR or \fB$HOME/.zshrc\fR:
.RS
.P
\fB. /path/to/z.sh\fR
.RE
.P
\fBcd\fR around for a while to build up the db.
.P
PROFIT!!
.P
Optionally:
.RS
Set \fB$_Z_CMD\fR to change the command name (default \fBz\fR).
.RE
.RS
Set \fB$_Z_DATA\fR to change the datafile (default \fB$HOME/.z\fR).
.RE
.RS
Set \fB$_Z_MAX_SCORE\fR lower to age entries out faster (default \fB9000\fR).
.RE
.RS
Set \fB$_Z_NO_RESOLVE_SYMLINKS\fR to prevent symlink resolution.
.RE
.RS
Set \fB$_Z_NO_PROMPT_COMMAND\fR to handle \fBPROMPT_COMMAND/precmd\fR yourself.
.RE
.RS
Set \fB$_Z_EXCLUDE_DIRS\fR to an array of directory trees to exclude.
.RE
.RS
Set \fB$_Z_OWNER\fR to allow usage when in 'sudo -s' mode.
.RE
.RS
(These settings should go in .bashrc/.zshrc before the line added above.)
.RE
.RS
Install the provided man page \fBz.1\fR somewhere in your \f$MANPATH, like
\fB/usr/local/man/man1\fR.
.RE
.SS
Aging:
The rank of directories maintained by \fBz\fR undergoes aging based on a simple
formula. The rank of each entry is incremented every time it is accessed. When
the sum of ranks is over 9000, all ranks are multiplied by 0.99. Entries with a
rank lower than 1 are forgotten.
.SS
Frecency:
Frecency is a portmanteau of 'recent' and 'frequency'. It is a weighted rank
that depends on how often and how recently something occurred. As far as I
know, Mozilla came up with the term.
.P
To \fBz\fR, a directory that has low ranking but has been accessed recently
will quickly have higher rank than a directory accessed frequently a long time
ago.
.P
Frecency is determined at runtime.
.SS
Common:
When multiple directories match all queries, and they all have a common prefix,
\fBz\fR will cd to the shortest matching directory, without regard to priority.
This has been in effect, if undocumented, for quite some time, but should
probably be configurable or reconsidered.
.SS
Tab Completion:
\fBz\fR supports tab completion. After any number of arguments, press TAB to
complete on directories that match each argument. Due to limitations of the
completion implementations, only the last argument will be completed in the
shell.
.P
Internally, \fBz\fR decides you've requested a completion if the last argument
passed is an absolute path to an existing directory. This may cause unexpected
behavior if the last argument to \fBz\fR begins with \fB/\fR.
.SH
ENVIRONMENT
A function \fB_z()\fR is defined.
.P
The contents of the variable \fB$_Z_CMD\fR is aliased to \fB_z 2>&1\fR. If not
set, \fB$_Z_CMD\fR defaults to \fBz\fR.
.P
The environment variable \fB$_Z_DATA\fR can be used to control the datafile
location. If it is not defined, the location defaults to \fB$HOME/.z\fR.
.P
The environment variable \fB$_Z_NO_RESOLVE_SYMLINKS\fR can be set to prevent
resolving of symlinks. If it is not set, symbolic links will be resolved when
added to the datafile.
.P
In bash, \fBz\fR appends a command to the \fBPROMPT_COMMAND\fR environment
variable to maintain its database. In zsh, \fBz\fR appends a function
\fB_z_precmd\fR to the \fBprecmd_functions\fR array.
.P
The environment variable \fB$_Z_NO_PROMPT_COMMAND\fR can be set if you want to
handle \fBPROMPT_COMMAND\fR or \fBprecmd\fR yourself.
.P
The environment variable \fB$_Z_EXCLUDE_DIRS\fR can be set to an array of
directory trees to exclude from tracking. \fB$HOME\fR is always excluded.
Directories must be full paths without trailing slashes.
.P
The environment variable \fB$_Z_OWNER\fR can be set to your username, to
allow usage of \fBz\fR when your sudo environment keeps \fB$HOME\fR set.
.SH
FILES
Data is stored in \fB$HOME/.z\fR. This can be overridden by setting the
\fB$_Z_DATA\fR environment variable. When initialized, \fBz\fR will raise an
error if this path is a directory, and not function correctly.
.P
A man page (\fBz.1\fR) is provided.
.SH
SEE ALSO
regex(7), pushd, popd, autojump, cdargs
.P
Please file bugs at https://github.com/rupa/z/

View file

@ -1,6 +1,968 @@
# Handle $0 according to the standard: ################################################################################
# https://zdharma-continuum.github.io/Zsh-100-Commits-Club/Zsh-Plugin-Standard.html # Zsh-z - jump around with Zsh - A native Zsh version of z without awk, sort,
0="${${ZERO:-${0:#$ZSH_ARGZERO}}:-${(%):-%N}}" # date, or sed
0="${${(M)0:#/*}:-$PWD/$0}" #
# https://github.com/agkozak/zsh-z
#
# Copyright (c) 2018-2022 Alexandros Kozak
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# z (https://github.com/rupa/z) is copyright (c) 2009 rupa deadwyler and
# licensed under the WTFPL license, Version 2.
#
# Zsh-z maintains a jump-list of the directories you actually use.
#
# INSTALL:
# * put something like this in your .zshrc:
# source /path/to/zsh-z.plugin.zsh
# * cd around for a while to build up the database
#
# USAGE:
# * z foo cd to the most frecent directory matching foo
# * z foo bar cd to the most frecent directory matching both foo and bar
# (e.g. /foo/bat/bar/quux)
# * z -r foo cd to the highest ranked directory matching foo
# * z -t foo cd to most recently accessed directory matching foo
# * z -l foo List matches instead of changing directories
# * z -e foo Echo the best match without changing directories
# * z -c foo Restrict matches to subdirectories of PWD
# * z -x Remove a directory (default: PWD) from the database
# * z -xR Remove a directory (default: PWD) and its subdirectories from
# the database
#
# ENVIRONMENT VARIABLES:
#
# ZSHZ_CASE -> if `ignore', pattern matching is case-insensitive; if `smart',
# pattern matching is case-insensitive only when the pattern is all
# lowercase
# ZSHZ_CMD -> name of command (default: z)
# ZSHZ_COMPLETION -> completion method (default: 'frecent'; 'legacy' for
# alphabetic sorting)
# ZSHZ_DATA -> name of datafile (default: ~/.z)
# ZSHZ_EXCLUDE_DIRS -> array of directories to exclude from your database
# (default: empty)
# ZSHZ_KEEP_DIRS -> array of directories that should not be removed from the
# database, even if they are not currently available (default: empty)
# ZSHZ_MAX_SCORE -> maximum combined score the database entries can have
# before beginning to age (default: 9000)
# ZSHZ_NO_RESOLVE_SYMLINKS -> '1' prevents symlink resolution
# ZSHZ_OWNER -> your username (if you want use Zsh-z while using sudo -s)
# ZSHZ_UNCOMMON -> if 1, do not jump to "common directories," but rather drop
# subdirectories based on what the search string was (default: 0)
################################################################################
source "${0:h}/z.sh" autoload -U is-at-least
if ! is-at-least 4.3.11; then
print "Zsh-z requires Zsh v4.3.11 or higher." >&2 && exit
fi
############################################################
# The help message
#
# Globals:
# ZSHZ_CMD
############################################################
_zshz_usage() {
print "Usage: ${ZSHZ_CMD:-${_Z_CMD:-z}} [OPTION]... [ARGUMENT]
Jump to a directory that you have visited frequently or recently, or a bit of both, based on the partial string ARGUMENT.
With no ARGUMENT, list the directory history in ascending rank.
--add Add a directory to the database
-c Only match subdirectories of the current directory
-e Echo the best match without going to it
-h Display this help and exit
-l List all matches without going to them
-r Match by rank
-t Match by recent access
-x Remove a directory from the database (by default, the current directory)
-xR Remove a directory and its subdirectories from the database (by default, the current directory)" |
fold -s -w $COLUMNS >&2
}
# Load zsh/datetime module, if necessary
(( $+EPOCHSECONDS )) || zmodload zsh/datetime
# Load zsh/files, if necessary
[[ ${builtins[zf_chown]} == 'defined' &&
${builtins[zf_mv]} == 'defined' &&
${builtins[zf_rm]} == 'defined' ]] ||
zmodload -F zsh/files b:zf_chown b:zf_mv b:zf_rm
# Load zsh/system, if necessary
[[ ${modules[zsh/system]} == 'loaded' ]] || zmodload zsh/system &> /dev/null
# Global associative array for internal use
typeset -gA ZSHZ
# Make sure ZSHZ_EXCLUDE_DIRS has been declared so that other scripts can
# simply append to it
(( ${+ZSHZ_EXCLUDE_DIRS} )) || typeset -gUa ZSHZ_EXCLUDE_DIRS
# Determine if zsystem flock is available
zsystem supports flock &> /dev/null && ZSHZ[USE_FLOCK]=1
# Determine if `print -v' is supported
is-at-least 5.3.0 && ZSHZ[PRINTV]=1
############################################################
# The Zsh-z Command
#
# Globals:
# ZSHZ
# ZSHZ_CASE
# ZSHZ_COMPLETION
# ZSHZ_DATA
# ZSHZ_DEBUG
# ZSHZ_EXCLUDE_DIRS
# ZSHZ_KEEP_DIRS
# ZSHZ_MAX_SCORE
# ZSHZ_OWNER
#
# Arguments:
# $* Command options and arguments
############################################################
zshz() {
# Don't use `emulate -L zsh' - it breaks PUSHD_IGNORE_DUPS
setopt LOCAL_OPTIONS NO_KSH_ARRAYS NO_SH_WORD_SPLIT EXTENDED_GLOB
(( ZSHZ_DEBUG )) && setopt LOCAL_OPTIONS WARN_CREATE_GLOBAL
local REPLY
local -a lines
# Allow the user to specify the datafile name in $ZSHZ_DATA (default: ~/.z)
# If the datafile is a symlink, it gets dereferenced
local datafile=${${ZSHZ_DATA:-${_Z_DATA:-${HOME}/.z}}:A}
# If the datafile is a directory, print a warning and exit
if [[ -d $datafile ]]; then
print "ERROR: Zsh-z's datafile (${datafile}) is a directory." >&2
exit
fi
# Make sure that the datafile exists before attempting to read it or lock it
# for writing
[[ -f $datafile ]] || touch "$datafile"
# Bail if we don't own the datafile and $ZSHZ_OWNER is not set
[[ -z ${ZSHZ_OWNER:-${_Z_OWNER}} && -f $datafile && ! -O $datafile ]] &&
return
# Load the datafile into an array and parse it
lines=( ${(f)"$(< $datafile)"} )
# Discard entries that are incomplete or incorrectly formatted
lines=( ${(M)lines:#/*\|[[:digit:]]##[.,]#[[:digit:]]#\|[[:digit:]]##} )
############################################################
# Add a path to or remove one from the datafile
#
# Globals:
# ZSHZ
# ZSHZ_EXCLUDE_DIRS
# ZSHZ_OWNER
#
# Arguments:
# $1 Which action to perform (--add/--remove)
# $2 The path to add
############################################################
_zshz_add_or_remove_path() {
local action=${1}
shift
if [[ $action == '--add' ]]; then
# TODO: The following tasks are now handled by _agkozak_precmd. Dead code?
# Don't add $HOME
[[ $* == $HOME ]] && return
# Don't track directory trees excluded in ZSHZ_EXCLUDE_DIRS
local exclude
for exclude in ${(@)ZSHZ_EXCLUDE_DIRS:-${(@)_Z_EXCLUDE_DIRS}}; do
case $* in
${exclude}|${exclude}/*) return ;;
esac
done
fi
# A temporary file that gets copied over the datafile if all goes well
local tempfile="${datafile}.${RANDOM}"
# See https://github.com/rupa/z/pull/199/commits/ed6eeed9b70d27c1582e3dd050e72ebfe246341c
if (( ZSHZ[USE_FLOCK] )); then
local lockfd
# Grab exclusive lock (released when function exits)
zsystem flock -f lockfd "$datafile" 2> /dev/null || return
fi
integer tmpfd
case $action in
--add)
exec {tmpfd}>|"$tempfile" # Open up tempfile for writing
_zshz_update_datafile $tmpfd "$*"
local ret=$?
;;
--remove)
local xdir # Directory to be removed
if (( ${ZSHZ_NO_RESOLVE_SYMLINKS:-${_Z_NO_RESOLVE_SYMLINKS}} )); then
[[ -d ${${*:-${PWD}}:a} ]] && xdir=${${*:-${PWD}}:a}
else
[[ -d ${${*:-${PWD}}:A} ]] && xdir=${${*:-${PWD}}:a}
fi
local -a lines_to_keep
if (( ${+opts[-R]} )); then
# Prompt user before deleting entire database
if [[ $xdir == '/' ]] && ! read -q "?Delete entire Zsh-z database? "; then
print && return 1
fi
# All of the lines that don't match the directory to be deleted
lines_to_keep=( ${lines:#${xdir}\|*} )
# Or its subdirectories
lines_to_keep=( ${lines_to_keep:#${xdir%/}/**} )
else
# All of the lines that don't match the directory to be deleted
lines_to_keep=( ${lines:#${xdir}\|*} )
fi
if [[ $lines != "$lines_to_keep" ]]; then
lines=( $lines_to_keep )
else
return 1 # The $PWD isn't in the datafile
fi
exec {tmpfd}>|"$tempfile" # Open up tempfile for writing
print -u $tmpfd -l -- $lines
local ret=$?
;;
esac
if (( tmpfd != 0 )); then
# Close tempfile
exec {tmpfd}>&-
fi
if (( ret != 0 )); then
# Avoid clobbering the datafile if the write to tempfile failed
zf_rm -f "$tempfile"
return $ret
fi
local owner
owner=${ZSHZ_OWNER:-${_Z_OWNER}}
if (( ZSHZ[USE_FLOCK] )); then
zf_mv "$tempfile" "$datafile" 2> /dev/null || zf_rm -f "$tempfile"
if [[ -n $owner ]]; then
zf_chown ${owner}:"$(id -ng ${owner})" "$datafile"
fi
else
if [[ -n $owner ]]; then
zf_chown "${owner}":"$(id -ng "${owner}")" "$tempfile"
fi
zf_mv -f "$tempfile" "$datafile" 2> /dev/null || zf_rm -f "$tempfile"
fi
# In order to make z -x work, we have to disable zsh-z's adding
# to the database until the user changes directory and the
# chpwd_functions are run
if [[ $action == '--remove' ]]; then
ZSHZ[DIRECTORY_REMOVED]=1
fi
}
############################################################
# Read the curent datafile contents, update them, "age" them
# when the total rank gets high enough, and print the new
# contents to STDOUT.
#
# Globals:
# ZSHZ_KEEP_DIRS
# ZSHZ_MAX_SCORE
#
# Arguments:
# $1 File descriptor linked to tempfile
# $2 Path to be added to datafile
############################################################
_zshz_update_datafile() {
integer fd=$1
local -A rank time
# Characters special to the shell (such as '[]') are quoted with backslashes
# See https://github.com/rupa/z/issues/246
local add_path=${(q)2}
local -a existing_paths
local now=$EPOCHSECONDS line dir
local path_field rank_field time_field count x
rank[$add_path]=1
time[$add_path]=$now
# Remove paths from database if they no longer exist
for line in $lines; do
if [[ ! -d ${line%%\|*} ]]; then
for dir in ${(@)ZSHZ_KEEP_DIRS}; do
if [[ ${line%%\|*} == ${dir}/* ||
${line%%\|*} == $dir ||
$dir == '/' ]]; then
existing_paths+=( $line )
fi
done
else
existing_paths+=( $line )
fi
done
lines=( $existing_paths )
for line in $lines; do
path_field=${(q)line%%\|*}
rank_field=${${line%\|*}#*\|}
time_field=${line##*\|}
# When a rank drops below 1, drop the path from the database
(( rank_field < 1 )) && continue
if [[ $path_field == $add_path ]]; then
rank[$path_field]=$rank_field
(( rank[$path_field]++ ))
time[$path_field]=$now
else
rank[$path_field]=$rank_field
time[$path_field]=$time_field
fi
(( count += rank_field ))
done
if (( count > ${ZSHZ_MAX_SCORE:-${_Z_MAX_SCORE:-9000}} )); then
# Aging
for x in ${(k)rank}; do
print -u $fd -- "$x|$(( 0.99 * rank[$x] ))|${time[$x]}" || return 1
done
else
for x in ${(k)rank}; do
print -u $fd -- "$x|${rank[$x]}|${time[$x]}" || return 1
done
fi
}
############################################################
# The original tab completion method
#
# String processing is smartcase -- case-insensitive if the
# search string is lowercase, case-sensitive if there are
# any uppercase letters. Spaces in the search string are
# treated as *'s in globbing. Read the contents of the
# datafile and print matches to STDOUT.
#
# Arguments:
# $1 The string to be completed
############################################################
_zshz_legacy_complete() {
local line path_field path_field_normalized
# Replace spaces in the search string with asterisks for globbing
1=${1//[[:space:]]/*}
for line in $lines; do
path_field=${line%%\|*}
path_field_normalized=$path_field
if (( ZSHZ_TRAILING_SLASH )); then
path_field_normalized=${path_field%/}/
fi
# If the search string is all lowercase, the search will be case-insensitive
if [[ $1 == "${1:l}" && ${path_field_normalized:l} == *${~1}* ]]; then
print -- $path_field
# Otherwise, case-sensitive
elif [[ $path_field_normalized == *${~1}* ]]; then
print -- $path_field
fi
done
# TODO: Search strings with spaces in them are currently treated case-
# insensitively.
}
############################################################
# `print' or `printf' to REPLY
#
# Variable assignment through command substitution, of the
# form
#
# foo=$( bar )
#
# requires forking a subshell; on Cygwin/MSYS2/WSL1 that can
# be surprisingly slow. Zsh-z avoids doing that by printing
# values to the variable REPLY. Since Zsh v5.3.0 that has
# been possible with `print -v'; for earlier versions of the
# shell, the values are placed on the editing buffer stack
# and then `read' into REPLY.
#
# Globals:
# ZSHZ
#
# Arguments:
# Options and parameters for `print'
############################################################
_zshz_printv() {
# NOTE: For a long time, ZSH's `print -v' had a tendency
# to mangle multibyte strings:
#
# https://www.zsh.org/mla/workers/2020/msg00307.html
#
# The bug was fixed in late 2020:
#
# https://github.com/zsh-users/zsh/commit/b6ba74cd4eaec2b6cb515748cf1b74a19133d4a4#diff-32bbef18e126b837c87b06f11bfc61fafdaa0ed99fcb009ec53f4767e246b129
#
# In order to support shells with the bug, we must use a form of `printf`,
# which does not exhibit the undesired behavior. See
#
# https://www.zsh.org/mla/workers/2020/msg00308.html
if (( ZSHZ[PRINTV] )); then
builtin print -v REPLY -f %s $@
else
builtin print -z $@
builtin read -rz REPLY
fi
}
############################################################
# If matches share a common root, find it, and put it in
# REPLY for _zshz_output to use.
#
# Arguments:
# $1 Name of associative array of matches and ranks
############################################################
_zshz_find_common_root() {
local -a common_matches
local x short
common_matches=( ${(@Pk)1} )
for x in ${(@)common_matches}; do
if [[ -z $short ]] || (( $#x < $#short )) || [[ $x != ${short}/* ]]; then
short=$x
fi
done
[[ $short == '/' ]] && return
for x in ${(@)common_matches}; do
[[ $x != $short* ]] && return
done
_zshz_printv -- $short
}
############################################################
# Calculate a common root, if there is one. Then do one of
# the following:
#
# 1) Print a list of completions in frecent order;
# 2) List them (z -l) to STDOUT; or
# 3) Put a common root or best match into REPLY
#
# Globals:
# ZSHZ_UNCOMMON
#
# Arguments:
# $1 Name of an associative array of matches and ranks
# $2 The best match or best case-insensitive match
# $3 Whether to produce a completion, a list, or a root or
# match
############################################################
_zshz_output() {
local match_array=$1 match=$2 format=$3
local common k x
local -a descending_list output
local -A output_matches
output_matches=( ${(Pkv)match_array} )
_zshz_find_common_root $match_array
common=$REPLY
case $format in
completion)
for k in ${(@k)output_matches}; do
_zshz_printv -f "%.2f|%s" ${output_matches[$k]} $k
descending_list+=( ${(f)REPLY} )
REPLY=''
done
descending_list=( ${${(@On)descending_list}#*\|} )
print -l $descending_list
;;
list)
local path_to_display
for x in ${(k)output_matches}; do
if (( ${output_matches[$x]} )); then
path_to_display=$x
(( ZSHZ_TILDE )) &&
path_to_display=${path_to_display/#${HOME}/\~}
_zshz_printv -f "%-10d %s\n" ${output_matches[$x]} $path_to_display
output+=( ${(f)REPLY} )
REPLY=''
fi
done
if [[ -n $common ]]; then
(( ZSHZ_TILDE )) && common=${common/#${HOME}/\~}
(( $#output > 1 )) && printf "%-10s %s\n" 'common:' $common
fi
# -lt
if (( $+opts[-t] )); then
for x in ${(@On)output}; do
print -- $x
done
# -lr
elif (( $+opts[-r] )); then
for x in ${(@on)output}; do
print -- $x
done
# -l
else
for x in ${(@on)output}; do
print $x
done
fi
;;
*)
if (( ! ZSHZ_UNCOMMON )) && [[ -n $common ]]; then
_zshz_printv -- $common
else
_zshz_printv -- ${(P)match}
fi
;;
esac
}
############################################################
# Match a pattern by rank, time, or a combination of the
# two, and output the results as completions, a list, or a
# best match.
#
# Globals:
# ZSHZ
# ZSHZ_CASE
# ZSHZ_KEEP_DIRS
# ZSHZ_OWNER
#
# Arguments:
# #1 Pattern to match
# $2 Matching method (rank, time, or [default] frecency)
# $3 Output format (completion, list, or [default] store
# in REPLY
############################################################
_zshz_find_matches() {
setopt LOCAL_OPTIONS NO_EXTENDED_GLOB
local fnd=$1 method=$2 format=$3
local -a existing_paths
local line dir path_field rank_field time_field rank dx escaped_path_field
local -A matches imatches
local best_match ibest_match hi_rank=-9999999999 ihi_rank=-9999999999
# Remove paths from database if they no longer exist
for line in $lines; do
if [[ ! -d ${line%%\|*} ]]; then
for dir in ${(@)ZSHZ_KEEP_DIRS}; do
if [[ ${line%%\|*} == ${dir}/* ||
${line%%\|*} == $dir ||
$dir == '/' ]]; then
existing_paths+=( $line )
fi
done
else
existing_paths+=( $line )
fi
done
lines=( $existing_paths )
for line in $lines; do
path_field=${line%%\|*}
rank_field=${${line%\|*}#*\|}
time_field=${line##*\|}
case $method in
rank) rank=$rank_field ;;
time) (( rank = time_field - EPOCHSECONDS )) ;;
*)
# Frecency routine
(( dx = EPOCHSECONDS - time_field ))
rank=$(( 10000 * rank_field * (3.75/((0.0001 * dx + 1) + 0.25)) ))
;;
esac
# Use spaces as wildcards
local q=${fnd//[[:space:]]/\*}
# If $ZSHZ_TRAILING_SLASH is set, use path_field with a trailing slash for matching.
local path_field_normalized=$path_field
if (( ZSHZ_TRAILING_SLASH )); then
path_field_normalized=${path_field%/}/
fi
# If $ZSHZ_CASE is 'ignore', be case-insensitive.
#
# If it's 'smart', be case-insensitive unless the string to be matched
# includes capital letters.
#
# Otherwise, the default behavior of Zsh-z is to match case-sensitively if
# possible, then to fall back on a case-insensitive match if possible.
if [[ $ZSHZ_CASE == 'smart' && ${1:l} == $1 &&
${path_field_normalized:l} == ${~q:l} ]]; then
imatches[$path_field]=$rank
elif [[ $ZSHZ_CASE != 'ignore' && $path_field_normalized == ${~q} ]]; then
matches[$path_field]=$rank
elif [[ $ZSHZ_CASE != 'smart' && ${path_field_normalized:l} == ${~q:l} ]]; then
imatches[$path_field]=$rank
fi
# Escape characters that would cause "invalid subscript" errors
# when accessing the associative array.
escaped_path_field=${path_field//'\'/'\\'}
escaped_path_field=${escaped_path_field//'`'/'\`'}
escaped_path_field=${escaped_path_field//'('/'\('}
escaped_path_field=${escaped_path_field//')'/'\)'}
escaped_path_field=${escaped_path_field//'['/'\['}
escaped_path_field=${escaped_path_field//']'/'\]'}
if (( matches[$escaped_path_field] )) &&
(( matches[$escaped_path_field] > hi_rank )); then
best_match=$path_field
hi_rank=${matches[$escaped_path_field]}
elif (( imatches[$escaped_path_field] )) &&
(( imatches[$escaped_path_field] > ihi_rank )); then
ibest_match=$path_field
ihi_rank=${imatches[$escaped_path_field]}
ZSHZ[CASE_INSENSITIVE]=1
fi
done
# Return 1 when there are no matches
[[ -z $best_match && -z $ibest_match ]] && return 1
if [[ -n $best_match ]]; then
_zshz_output matches best_match $format
elif [[ -n $ibest_match ]]; then
_zshz_output imatches ibest_match $format
fi
}
# THE MAIN ROUTINE
local -A opts
zparseopts -E -D -A opts -- \
-add \
-complete \
c \
e \
h \
-help \
l \
r \
R \
t \
x
if [[ $1 == '--' ]]; then
shift
elif [[ -n ${(M)@:#-*} && -z $compstate ]]; then
print "Improper option(s) given."
_zshz_usage
return 1
fi
local opt output_format method='frecency' fnd prefix req
for opt in ${(k)opts}; do
case $opt in
--add)
[[ ! -d $* ]] && return 1
local dir
# Cygwin and MSYS2 have a hard time with relative paths expressed from /
if [[ $OSTYPE == (cygwin|msys) && $PWD == '/' && $* != /* ]]; then
set -- "/$*"
fi
if (( ${ZSHZ_NO_RESOLVE_SYMLINKS:-${_Z_NO_RESOLVE_SYMLINKS}} )); then
dir=${*:a}
else
dir=${*:A}
fi
_zshz_add_or_remove_path --add "$dir"
return
;;
--complete)
if [[ -s $datafile && ${ZSHZ_COMPLETION:-frecent} == 'legacy' ]]; then
_zshz_legacy_complete "$1"
return
fi
output_format='completion'
;;
-c) [[ $* == ${PWD}/* || $PWD == '/' ]] || prefix="$PWD " ;;
-h|--help)
_zshz_usage
return
;;
-l) output_format='list' ;;
-r) method='rank' ;;
-t) method='time' ;;
-x)
# Cygwin and MSYS2 have a hard time with relative paths expressed from /
if [[ $OSTYPE == (cygwin|msys) && $PWD == '/' && $* != /* ]]; then
set -- "/$*"
fi
_zshz_add_or_remove_path --remove $*
return
;;
esac
done
req="$*"
fnd="$prefix$*"
[[ -n $fnd && $fnd != "$PWD " ]] || {
[[ $output_format != 'completion' ]] && output_format='list'
}
#########################################################
# If $ZSHZ_ECHO == 1, display paths as you jump to them.
# If it is also the case that $ZSHZ_TILDE == 1, display
# the home directory as a tilde.
#########################################################
_zshz_echo() {
if (( ZSHZ_ECHO )); then
if (( ZSHZ_TILDE )); then
print ${PWD/#${HOME}/\~}
else
print $PWD
fi
fi
}
if [[ ${@: -1} == /* ]] && (( ! $+opts[-e] && ! $+opts[-l] )); then
# cd if possible; echo the new path if $ZSHZ_ECHO == 1
[[ -d ${@: -1} ]] && builtin cd ${@: -1} && _zshz_echo && return
fi
# With option -c, make sure query string matches beginning of matches;
# otherwise look for matches anywhere in paths
# zpm-zsh/colors has a global $c, so we'll avoid math expressions here
if [[ ! -z ${(tP)opts[-c]} ]]; then
_zshz_find_matches "$fnd*" $method $output_format
else
_zshz_find_matches "*$fnd*" $method $output_format
fi
local ret2=$?
local cd
cd=$REPLY
# New experimental "uncommon" behavior
#
# 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
# is `foo', go to /foo/bar/foo
if (( ZSHZ_UNCOMMON )) && [[ -n $cd ]]; then
if [[ -n $cd ]]; then
# In the search pattern, replace spaces with *
local q=${fnd//[[:space:]]/\*}
q=${q%/} # Trailing slash has to be removed
# As long as the best match is not case-insensitive
if (( ! ZSHZ[CASE_INSENSITIVE] )); then
# Count the number of characters in $cd that $q matches
local q_chars=$(( ${#cd} - ${#${cd//${~q}/}} ))
# Try dropping directory elements from the right; stop when it affects
# how many times the search pattern appears
until (( ( ${#cd:h} - ${#${${cd:h}//${~q}/}} ) != q_chars )); do
cd=${cd:h}
done
# If the best match is case-insensitive
else
local q_chars=$(( ${#cd} - ${#${${cd:l}//${~${q:l}}/}} ))
until (( ( ${#cd:h} - ${#${${${cd:h}:l}//${~${q:l}}/}} ) != q_chars )); do
cd=${cd:h}
done
fi
ZSHZ[CASE_INSENSITIVE]=0
fi
fi
if (( ret2 == 0 )) && [[ -n $cd ]]; then
if (( $+opts[-e] )); then # echo
(( ZSHZ_TILDE )) && cd=${cd/#${HOME}/\~}
print -- "$cd"
else
# cd if possible; echo the new path if $ZSHZ_ECHO == 1
[[ -d $cd ]] && builtin cd "$cd" && _zshz_echo
fi
else
# if $req is a valid path, cd to it; echo the new path if $ZSHZ_ECHO == 1
if ! (( $+opts[-e] || $+opts[-l] )) && [[ -d $req ]]; then
builtin cd "$req" && _zshz_echo
else
return $ret2
fi
fi
}
alias ${ZSHZ_CMD:-${_Z_CMD:-z}}='zshz 2>&1'
############################################################
# precmd - add path to datafile unless `z -x' has just been
# run
#
# Globals:
# ZSHZ
############################################################
_zshz_precmd() {
# Do not add PWD to datafile when in HOME directory, or
# if `z -x' has just been run
[[ $PWD == "$HOME" ]] || (( ZSHZ[DIRECTORY_REMOVED] )) && return
# Don't track directory trees excluded in ZSHZ_EXCLUDE_DIRS
local exclude
for exclude in ${(@)ZSHZ_EXCLUDE_DIRS:-${(@)_Z_EXCLUDE_DIRS}}; do
case $PWD in
${exclude}|${exclude}/*) return ;;
esac
done
# It appears that forking a subshell is so slow in Windows that it is better
# just to add the PWD to the datafile in the foreground
if [[ $OSTYPE == (cygwin|msys) ]]; then
zshz --add "$PWD"
else
(zshz --add "$PWD" &)
fi
# See https://github.com/rupa/z/pull/247/commits/081406117ea42ccb8d159f7630cfc7658db054b6
: $RANDOM
}
############################################################
# chpwd
#
# When the $PWD is removed from the datafile with `z -x',
# Zsh-z refrains from adding it again until the user has
# left the directory.
#
# Globals:
# ZSHZ
############################################################
_zshz_chpwd() {
ZSHZ[DIRECTORY_REMOVED]=0
}
autoload -Uz add-zsh-hook
add-zsh-hook precmd _zshz_precmd
add-zsh-hook chpwd _zshz_chpwd
############################################################
# Completion
############################################################
# Standarized $0 handling
# (See https://github.com/agkozak/Zsh-100-Commits-Club/blob/master/Zsh-Plugin-Standard.adoc)
0=${${ZERO:-${0:#$ZSH_ARGZERO}}:-${(%):-%N}}
0=${${(M)0:#/*}:-$PWD/$0}
(( ${fpath[(ie)${0:A:h}]} <= ${#fpath} )) || fpath=( "${0:A:h}" "${fpath[@]}" )
############################################################
# zsh-z functions
############################################################
ZSHZ[FUNCTIONS]='_zshz_usage
_zshz_add_or_remove_path
_zshz_update_datafile
_zshz_legacy_complete
_zshz_printv
_zshz_find_common_root
_zshz_output
_zshz_find_matches
zshz
_zshz_precmd
_zshz_chpwd
_zshz'
############################################################
# Enable WARN_NESTED_VAR for functions listed in
# ZSHZ[FUNCTIONS]
############################################################
(( ZSHZ_DEBUG )) && () {
if is-at-least 5.4.0; then
local x
for x in ${=ZSHZ[FUNCTIONS]}; do
functions -W $x
done
fi
}
############################################################
# Unload function
#
# See https://github.com/agkozak/Zsh-100-Commits-Club/blob/master/Zsh-Plugin-Standard.adoc#unload-fun
#
# Globals:
# ZSHZ
# ZSHZ_CMD
############################################################
zsh-z_plugin_unload() {
emulate -L zsh
add-zsh-hook -D precmd _zshz_precmd
add-zsh-hook -d chpwd _zshz_chpwd
local x
for x in ${=ZSHZ[FUNCTIONS]}; do
(( ${+functions[$x]} )) && unfunction $x
done
unset ZSHZ
fpath=( "${(@)fpath:#${0:A:h}}" )
(( ${+aliases[${ZSHZ_CMD:-${_Z_CMD:-z}}]} )) &&
unalias ${ZSHZ_CMD:-${_Z_CMD:-z}}
unfunction $0
}
# vim: fdm=indent:ts=2:et:sts=2:sw=2:

View file

@ -1,267 +0,0 @@
# Copyright (c) 2009 rupa deadwyler. Licensed under the WTFPL license, Version 2
# maintains a jump-list of the directories you actually use
#
# INSTALL:
# * put something like this in your .bashrc/.zshrc:
# . /path/to/z.sh
# * cd around for a while to build up the db
# * PROFIT!!
# * optionally:
# set $_Z_CMD in .bashrc/.zshrc to change the command (default z).
# set $_Z_DATA in .bashrc/.zshrc to change the datafile (default ~/.z).
# set $_Z_MAX_SCORE lower to age entries out faster (default 9000).
# set $_Z_NO_RESOLVE_SYMLINKS to prevent symlink resolution.
# set $_Z_NO_PROMPT_COMMAND if you're handling PROMPT_COMMAND yourself.
# set $_Z_EXCLUDE_DIRS to an array of directories to exclude.
# set $_Z_OWNER to your username if you want use z while sudo with $HOME kept
#
# USE:
# * z foo # cd to most frecent dir matching foo
# * z foo bar # cd to most frecent dir matching foo and bar
# * z -r foo # cd to highest ranked dir matching foo
# * z -t foo # cd to most recently accessed dir matching foo
# * z -l foo # list matches instead of cd
# * z -e foo # echo the best match, don't cd
# * z -c foo # restrict matches to subdirs of $PWD
# * z -x # remove the current directory from the datafile
# * z -h # show a brief help message
[ -d "${_Z_DATA:-$HOME/.z}" ] && {
echo "ERROR: z.sh's datafile (${_Z_DATA:-$HOME/.z}) is a directory."
}
_z() {
local datafile="${_Z_DATA:-$HOME/.z}"
# if symlink, dereference
[ -h "$datafile" ] && datafile=$(readlink "$datafile")
# bail if we don't own ~/.z and $_Z_OWNER not set
[ -z "$_Z_OWNER" -a -f "$datafile" -a ! -O "$datafile" ] && return
_z_dirs () {
[ -f "$datafile" ] || return
local line
while read line; do
# only count directories
[ -d "${line%%\|*}" ] && echo "$line"
done < "$datafile"
return 0
}
# add entries
if [ "$1" = "--add" ]; then
shift
# $HOME and / aren't worth matching
[ "$*" = "$HOME" -o "$*" = '/' ] && return
# don't track excluded directory trees
if [ ${#_Z_EXCLUDE_DIRS[@]} -gt 0 ]; then
local exclude
for exclude in "${_Z_EXCLUDE_DIRS[@]}"; do
case "$*" in "$exclude"*) return;; esac
done
fi
# maintain the data file
local tempfile="$datafile.$RANDOM"
local score=${_Z_MAX_SCORE:-9000}
_z_dirs | awk -v path="$*" -v now="$(date +%s)" -v score=$score -F"|" '
BEGIN {
rank[path] = 1
time[path] = now
}
$2 >= 1 {
# drop ranks below 1
if( $1 == path ) {
rank[$1] = $2 + 1
time[$1] = now
} else {
rank[$1] = $2
time[$1] = $3
}
count += $2
}
END {
if( count > score ) {
# aging
for( x in rank ) print x "|" 0.99*rank[x] "|" time[x]
} else for( x in rank ) print x "|" rank[x] "|" time[x]
}
' 2>/dev/null >| "$tempfile"
# do our best to avoid clobbering the datafile in a race condition.
if [ $? -ne 0 -a -f "$datafile" ]; then
env rm -f "$tempfile"
else
[ "$_Z_OWNER" ] && chown $_Z_OWNER:"$(id -ng $_Z_OWNER)" "$tempfile"
env mv -f "$tempfile" "$datafile" || env rm -f "$tempfile"
fi
# tab completion
elif [ "$1" = "--complete" -a -s "$datafile" ]; then
_z_dirs | awk -v q="$2" -F"|" '
BEGIN {
q = substr(q, 3)
if( q == tolower(q) ) imatch = 1
gsub(/ /, ".*", q)
}
{
if( imatch ) {
if( tolower($1) ~ q ) print $1
} else if( $1 ~ q ) print $1
}
' 2>/dev/null
else
# list/go
local echo fnd last list opt typ
while [ "$1" ]; do case "$1" in
--) while [ "$1" ]; do shift; fnd="$fnd${fnd:+ }$1";done;;
-*) opt=${1:1}; while [ "$opt" ]; do case ${opt:0:1} in
c) fnd="^$PWD $fnd";;
e) echo=1;;
h) echo "${_Z_CMD:-z} [-cehlrtx] args" >&2; return;;
l) list=1;;
r) typ="rank";;
t) typ="recent";;
x) sed -i -e "\:^${PWD}|.*:d" "$datafile";;
esac; opt=${opt:1}; done;;
*) fnd="$fnd${fnd:+ }$1";;
esac; last=$1; [ "$#" -gt 0 ] && shift; done
[ "$fnd" -a "$fnd" != "^$PWD " ] || list=1
# if we hit enter on a completion just go there
case "$last" in
# completions will always start with /
/*) [ -z "$list" -a -d "$last" ] && builtin cd "$last" && return;;
esac
# no file yet
[ -f "$datafile" ] || return
local cd
cd="$( < <( _z_dirs ) awk -v t="$(date +%s)" -v list="$list" -v typ="$typ" -v q="$fnd" -F"|" '
function frecent(rank, time) {
# relate frequency and time
dx = t - time
return int(10000 * rank * (3.75/((0.0001 * dx + 1) + 0.25)))
}
function output(matches, best_match, common) {
# list or return the desired directory
if( list ) {
if( common ) {
printf "%-10s %s\n", "common:", common > "/dev/stderr"
}
cmd = "sort -n >&2"
for( x in matches ) {
if( matches[x] ) {
printf "%-10s %s\n", matches[x], x | cmd
}
}
} else {
if( common && !typ ) best_match = common
print best_match
}
}
function common(matches) {
# find the common root of a list of matches, if it exists
for( x in matches ) {
if( matches[x] && (!short || length(x) < length(short)) ) {
short = x
}
}
if( short == "/" ) return
for( x in matches ) if( matches[x] && index(x, short) != 1 ) {
return
}
return short
}
BEGIN {
gsub(" ", ".*", q)
hi_rank = ihi_rank = -9999999999
}
{
if( typ == "rank" ) {
rank = $2
} else if( typ == "recent" ) {
rank = $3 - t
} else rank = frecent($2, $3)
if( $1 ~ q ) {
matches[$1] = rank
} else if( tolower($1) ~ tolower(q) ) imatches[$1] = rank
if( matches[$1] && matches[$1] > hi_rank ) {
best_match = $1
hi_rank = matches[$1]
} else if( imatches[$1] && imatches[$1] > ihi_rank ) {
ibest_match = $1
ihi_rank = imatches[$1]
}
}
END {
# prefer case sensitive
if( best_match ) {
output(matches, best_match, common(matches))
exit
} else if( ibest_match ) {
output(imatches, ibest_match, common(imatches))
exit
}
exit(1)
}
')"
if [ "$?" -eq 0 ]; then
if [ "$cd" ]; then
if [ "$echo" ]; then echo "$cd"; else builtin cd "$cd"; fi
fi
else
return $?
fi
fi
}
alias ${_Z_CMD:-z}='_z 2>&1'
[ "$_Z_NO_RESOLVE_SYMLINKS" ] || _Z_RESOLVE_SYMLINKS="-P"
if type compctl >/dev/null 2>&1; then
# zsh
[ "$_Z_NO_PROMPT_COMMAND" ] || {
# populate directory list, avoid clobbering any other precmds.
if [ "$_Z_NO_RESOLVE_SYMLINKS" ]; then
_z_precmd() {
(_z --add "${PWD:a}" &)
: $RANDOM
}
else
_z_precmd() {
(_z --add "${PWD:A}" &)
: $RANDOM
}
fi
[[ -n "${precmd_functions[(r)_z_precmd]}" ]] || {
precmd_functions[$(($#precmd_functions+1))]=_z_precmd
}
}
_z_zsh_tab_completion() {
# tab completion
local compl
read -l compl
reply=(${(f)"$(_z --complete "$compl")"})
}
compctl -U -K _z_zsh_tab_completion _z
elif type complete >/dev/null 2>&1; then
# bash
# tab completion
complete -o filenames -C '_z --complete "$COMP_LINE"' ${_Z_CMD:-z}
[ "$_Z_NO_PROMPT_COMMAND" ] || {
# populate directory list. avoid clobbering other PROMPT_COMMANDs.
grep "_z --add" <<< "$PROMPT_COMMAND" >/dev/null || {
PROMPT_COMMAND="$PROMPT_COMMAND"$'\n''(_z --add "$(command pwd '$_Z_RESOLVE_SYMLINKS' 2>/dev/null)" 2>/dev/null &);'
}
}
fi