├── .github └── workflows │ └── rust.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE.MIT ├── README.md ├── TODO.md ├── src ├── main.rs └── store.rs └── z.sh /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Build 20 | run: cargo build --verbose 21 | - name: Run tests 22 | run: cargo test --verbose 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | *.swp 3 | .idea 4 | *.iml 5 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "anstream" 16 | version = "0.6.18" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 19 | dependencies = [ 20 | "anstyle", 21 | "anstyle-parse", 22 | "anstyle-query", 23 | "anstyle-wincon", 24 | "colorchoice", 25 | "is_terminal_polyfill", 26 | "utf8parse", 27 | ] 28 | 29 | [[package]] 30 | name = "anstyle" 31 | version = "1.0.10" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 34 | 35 | [[package]] 36 | name = "anstyle-parse" 37 | version = "0.2.6" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 40 | dependencies = [ 41 | "utf8parse", 42 | ] 43 | 44 | [[package]] 45 | name = "anstyle-query" 46 | version = "1.1.2" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 49 | dependencies = [ 50 | "windows-sys", 51 | ] 52 | 53 | [[package]] 54 | name = "anstyle-wincon" 55 | version = "3.0.7" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" 58 | dependencies = [ 59 | "anstyle", 60 | "once_cell", 61 | "windows-sys", 62 | ] 63 | 64 | [[package]] 65 | name = "anyhow" 66 | version = "1.0.98" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" 69 | 70 | [[package]] 71 | name = "bitflags" 72 | version = "2.9.0" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" 75 | 76 | [[package]] 77 | name = "cfg-if" 78 | version = "1.0.0" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 81 | 82 | [[package]] 83 | name = "cfg_aliases" 84 | version = "0.2.1" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 87 | 88 | [[package]] 89 | name = "clap" 90 | version = "4.5.37" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071" 93 | dependencies = [ 94 | "clap_builder", 95 | ] 96 | 97 | [[package]] 98 | name = "clap_builder" 99 | version = "4.5.37" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2" 102 | dependencies = [ 103 | "anstream", 104 | "anstyle", 105 | "clap_lex", 106 | "strsim", 107 | ] 108 | 109 | [[package]] 110 | name = "clap_lex" 111 | version = "0.7.4" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 114 | 115 | [[package]] 116 | name = "colorchoice" 117 | version = "1.0.3" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 120 | 121 | [[package]] 122 | name = "dirs" 123 | version = "6.0.0" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" 126 | dependencies = [ 127 | "dirs-sys", 128 | ] 129 | 130 | [[package]] 131 | name = "dirs-sys" 132 | version = "0.5.0" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" 135 | dependencies = [ 136 | "libc", 137 | "option-ext", 138 | "redox_users", 139 | "windows-sys", 140 | ] 141 | 142 | [[package]] 143 | name = "errno" 144 | version = "0.3.11" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" 147 | dependencies = [ 148 | "libc", 149 | "windows-sys", 150 | ] 151 | 152 | [[package]] 153 | name = "fastrand" 154 | version = "2.3.0" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 157 | 158 | [[package]] 159 | name = "getrandom" 160 | version = "0.2.15" 161 | source = "registry+https://github.com/rust-lang/crates.io-index" 162 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 163 | dependencies = [ 164 | "cfg-if", 165 | "libc", 166 | "wasi 0.11.0+wasi-snapshot-preview1", 167 | ] 168 | 169 | [[package]] 170 | name = "getrandom" 171 | version = "0.3.2" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" 174 | dependencies = [ 175 | "cfg-if", 176 | "libc", 177 | "r-efi", 178 | "wasi 0.14.2+wasi-0.2.4", 179 | ] 180 | 181 | [[package]] 182 | name = "is_terminal_polyfill" 183 | version = "1.70.1" 184 | source = "registry+https://github.com/rust-lang/crates.io-index" 185 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 186 | 187 | [[package]] 188 | name = "libc" 189 | version = "0.2.172" 190 | source = "registry+https://github.com/rust-lang/crates.io-index" 191 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" 192 | 193 | [[package]] 194 | name = "libredox" 195 | version = "0.1.3" 196 | source = "registry+https://github.com/rust-lang/crates.io-index" 197 | checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" 198 | dependencies = [ 199 | "bitflags", 200 | "libc", 201 | ] 202 | 203 | [[package]] 204 | name = "linux-raw-sys" 205 | version = "0.9.4" 206 | source = "registry+https://github.com/rust-lang/crates.io-index" 207 | checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" 208 | 209 | [[package]] 210 | name = "memchr" 211 | version = "2.7.4" 212 | source = "registry+https://github.com/rust-lang/crates.io-index" 213 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 214 | 215 | [[package]] 216 | name = "nix" 217 | version = "0.29.0" 218 | source = "registry+https://github.com/rust-lang/crates.io-index" 219 | checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" 220 | dependencies = [ 221 | "bitflags", 222 | "cfg-if", 223 | "cfg_aliases", 224 | "libc", 225 | ] 226 | 227 | [[package]] 228 | name = "once_cell" 229 | version = "1.21.3" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 232 | 233 | [[package]] 234 | name = "option-ext" 235 | version = "0.2.0" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" 238 | 239 | [[package]] 240 | name = "proc-macro2" 241 | version = "1.0.95" 242 | source = "registry+https://github.com/rust-lang/crates.io-index" 243 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 244 | dependencies = [ 245 | "unicode-ident", 246 | ] 247 | 248 | [[package]] 249 | name = "quote" 250 | version = "1.0.40" 251 | source = "registry+https://github.com/rust-lang/crates.io-index" 252 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 253 | dependencies = [ 254 | "proc-macro2", 255 | ] 256 | 257 | [[package]] 258 | name = "r-efi" 259 | version = "5.2.0" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" 262 | 263 | [[package]] 264 | name = "redox_users" 265 | version = "0.5.0" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" 268 | dependencies = [ 269 | "getrandom 0.2.15", 270 | "libredox", 271 | "thiserror", 272 | ] 273 | 274 | [[package]] 275 | name = "regex" 276 | version = "1.11.1" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 279 | dependencies = [ 280 | "aho-corasick", 281 | "memchr", 282 | "regex-automata", 283 | "regex-syntax", 284 | ] 285 | 286 | [[package]] 287 | name = "regex-automata" 288 | version = "0.4.9" 289 | source = "registry+https://github.com/rust-lang/crates.io-index" 290 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 291 | dependencies = [ 292 | "aho-corasick", 293 | "memchr", 294 | "regex-syntax", 295 | ] 296 | 297 | [[package]] 298 | name = "regex-syntax" 299 | version = "0.8.5" 300 | source = "registry+https://github.com/rust-lang/crates.io-index" 301 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 302 | 303 | [[package]] 304 | name = "rustix" 305 | version = "1.0.5" 306 | source = "registry+https://github.com/rust-lang/crates.io-index" 307 | checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" 308 | dependencies = [ 309 | "bitflags", 310 | "errno", 311 | "libc", 312 | "linux-raw-sys", 313 | "windows-sys", 314 | ] 315 | 316 | [[package]] 317 | name = "strsim" 318 | version = "0.11.1" 319 | source = "registry+https://github.com/rust-lang/crates.io-index" 320 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 321 | 322 | [[package]] 323 | name = "syn" 324 | version = "2.0.100" 325 | source = "registry+https://github.com/rust-lang/crates.io-index" 326 | checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" 327 | dependencies = [ 328 | "proc-macro2", 329 | "quote", 330 | "unicode-ident", 331 | ] 332 | 333 | [[package]] 334 | name = "tempfile" 335 | version = "3.19.1" 336 | source = "registry+https://github.com/rust-lang/crates.io-index" 337 | checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" 338 | dependencies = [ 339 | "fastrand", 340 | "getrandom 0.3.2", 341 | "once_cell", 342 | "rustix", 343 | "windows-sys", 344 | ] 345 | 346 | [[package]] 347 | name = "thiserror" 348 | version = "2.0.12" 349 | source = "registry+https://github.com/rust-lang/crates.io-index" 350 | checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" 351 | dependencies = [ 352 | "thiserror-impl", 353 | ] 354 | 355 | [[package]] 356 | name = "thiserror-impl" 357 | version = "2.0.12" 358 | source = "registry+https://github.com/rust-lang/crates.io-index" 359 | checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" 360 | dependencies = [ 361 | "proc-macro2", 362 | "quote", 363 | "syn", 364 | ] 365 | 366 | [[package]] 367 | name = "twoway" 368 | version = "0.2.2" 369 | source = "registry+https://github.com/rust-lang/crates.io-index" 370 | checksum = "c57ffb460d7c24cd6eda43694110189030a3d1dfe418416d9468fd1c1d290b47" 371 | dependencies = [ 372 | "memchr", 373 | "unchecked-index", 374 | ] 375 | 376 | [[package]] 377 | name = "unchecked-index" 378 | version = "0.2.2" 379 | source = "registry+https://github.com/rust-lang/crates.io-index" 380 | checksum = "eeba86d422ce181a719445e51872fa30f1f7413b62becb52e95ec91aa262d85c" 381 | 382 | [[package]] 383 | name = "unicode-ident" 384 | version = "1.0.18" 385 | source = "registry+https://github.com/rust-lang/crates.io-index" 386 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 387 | 388 | [[package]] 389 | name = "utf8parse" 390 | version = "0.2.2" 391 | source = "registry+https://github.com/rust-lang/crates.io-index" 392 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 393 | 394 | [[package]] 395 | name = "wasi" 396 | version = "0.11.0+wasi-snapshot-preview1" 397 | source = "registry+https://github.com/rust-lang/crates.io-index" 398 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 399 | 400 | [[package]] 401 | name = "wasi" 402 | version = "0.14.2+wasi-0.2.4" 403 | source = "registry+https://github.com/rust-lang/crates.io-index" 404 | checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" 405 | dependencies = [ 406 | "wit-bindgen-rt", 407 | ] 408 | 409 | [[package]] 410 | name = "windows-sys" 411 | version = "0.59.0" 412 | source = "registry+https://github.com/rust-lang/crates.io-index" 413 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 414 | dependencies = [ 415 | "windows-targets", 416 | ] 417 | 418 | [[package]] 419 | name = "windows-targets" 420 | version = "0.52.6" 421 | source = "registry+https://github.com/rust-lang/crates.io-index" 422 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 423 | dependencies = [ 424 | "windows_aarch64_gnullvm", 425 | "windows_aarch64_msvc", 426 | "windows_i686_gnu", 427 | "windows_i686_gnullvm", 428 | "windows_i686_msvc", 429 | "windows_x86_64_gnu", 430 | "windows_x86_64_gnullvm", 431 | "windows_x86_64_msvc", 432 | ] 433 | 434 | [[package]] 435 | name = "windows_aarch64_gnullvm" 436 | version = "0.52.6" 437 | source = "registry+https://github.com/rust-lang/crates.io-index" 438 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 439 | 440 | [[package]] 441 | name = "windows_aarch64_msvc" 442 | version = "0.52.6" 443 | source = "registry+https://github.com/rust-lang/crates.io-index" 444 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 445 | 446 | [[package]] 447 | name = "windows_i686_gnu" 448 | version = "0.52.6" 449 | source = "registry+https://github.com/rust-lang/crates.io-index" 450 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 451 | 452 | [[package]] 453 | name = "windows_i686_gnullvm" 454 | version = "0.52.6" 455 | source = "registry+https://github.com/rust-lang/crates.io-index" 456 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 457 | 458 | [[package]] 459 | name = "windows_i686_msvc" 460 | version = "0.52.6" 461 | source = "registry+https://github.com/rust-lang/crates.io-index" 462 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 463 | 464 | [[package]] 465 | name = "windows_x86_64_gnu" 466 | version = "0.52.6" 467 | source = "registry+https://github.com/rust-lang/crates.io-index" 468 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 469 | 470 | [[package]] 471 | name = "windows_x86_64_gnullvm" 472 | version = "0.52.6" 473 | source = "registry+https://github.com/rust-lang/crates.io-index" 474 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 475 | 476 | [[package]] 477 | name = "windows_x86_64_msvc" 478 | version = "0.52.6" 479 | source = "registry+https://github.com/rust-lang/crates.io-index" 480 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 481 | 482 | [[package]] 483 | name = "wit-bindgen-rt" 484 | version = "0.39.0" 485 | source = "registry+https://github.com/rust-lang/crates.io-index" 486 | checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" 487 | dependencies = [ 488 | "bitflags", 489 | ] 490 | 491 | [[package]] 492 | name = "zrs" 493 | version = "0.1.9" 494 | dependencies = [ 495 | "anyhow", 496 | "clap", 497 | "dirs", 498 | "nix", 499 | "regex", 500 | "tempfile", 501 | "twoway", 502 | ] 503 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Chris West (Faux) "] 3 | name = "zrs" 4 | version = "0.1.9" 5 | 6 | description = "Jump to recently used directories" 7 | repository = "https://github.com/FauxFaux/zrs" 8 | readme = "README.md" 9 | 10 | categories = ["command-line-utilities", "development-tools"] 11 | license = "MIT OR Apache-2.0" 12 | 13 | edition = "2024" 14 | 15 | [dependencies] 16 | anyhow = "1" 17 | clap = { version = "4", features = ["cargo"] } 18 | dirs = "6" 19 | regex = "1" 20 | tempfile = "3" 21 | twoway = "0.2" 22 | nix = { version = "0.29", features = ["fs", "process", "user"] } 23 | 24 | [profile.release] 25 | lto = true 26 | -------------------------------------------------------------------------------- /LICENSE.MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Chris West 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 | ## zrs 2 | 3 | [![](https://img.shields.io/crates/v/zrs.svg)](https://crates.io/crates/zrs) 4 | 5 | `zrs` is a directory switching helper, based on 6 | [rupa's z](https://github.com/rupa/z). 7 | 8 | It tracks which directories you frequently visit, and 9 | how recently you have been using them. It will try to take 10 | you to the best matching directory for some inputs. 11 | 12 | For example, `z bar` could take you to `/home/you/code/bar`, and 13 | `z foo bar` could take you to `/var/lib/dogfood/libs/bombard`. 14 | 15 | ## Installation 16 | 17 | `zrs` consists of two parts. 18 | 19 | * `zrs` is a Rust binary that needs to 20 | be in your path. `cargo install zrs` should work, if you have 21 | `~/.cargo/bin` in your path. 22 | 23 | * `z.sh` is a helper script that must be `source`d in your shell. 24 | 25 | `zrs` can add this for you: 26 | 27 | ``` 28 | $ zrs --add-to-profile 29 | written helper script to "/home/faux/.local/share/zrs/z.sh" 30 | 31 | couldn't append to "/home/faux/.bashrc": Os { code: 2, kind: NotFound, message: "No such file or directory" } 32 | 33 | appended '. .../z.sh' to "/home/faux/.zshrc" 34 | ``` 35 | 36 | ## Why? 37 | 38 | rupa's shell implementation of `z` has a number of performance and 39 | safety issues. `zrs` solves these by being written as a single binary, 40 | and by being much more careful about touching the filesystem, and 41 | `fork`ing (releasing the shell) before doing anything slow. 42 | 43 | 44 | ## Significant differences 45 | 46 | * some features missing 47 | * much faster and much less likely to lose your data file writes 48 | (try holding down return in a shell some time) 49 | * regex syntax is PCRE 50 | * missing directories will only be eliminated on explicit `--clean` 51 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | * echo (`-e` flag) 2 | 3 | * remove (`-x` flag) 4 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod store; 2 | 3 | use std::cmp; 4 | use std::env; 5 | use std::ffi::OsStr; 6 | use std::fs; 7 | use std::io::Write; 8 | use std::path::Path; 9 | use std::path::PathBuf; 10 | use std::process; 11 | use std::time; 12 | 13 | use anyhow::anyhow; 14 | use anyhow::ensure; 15 | use anyhow::Context; 16 | use anyhow::Result; 17 | use clap::ArgGroup; 18 | use clap::{Arg, ArgAction}; 19 | use nix::unistd; 20 | 21 | use crate::store::Row; 22 | 23 | const HELPER_SCRIPT: &[u8] = include_bytes!("../z.sh"); 24 | 25 | #[derive(Debug)] 26 | struct ScoredRow { 27 | path: PathBuf, 28 | score: f32, 29 | } 30 | 31 | #[derive(Copy, Clone)] 32 | enum Scorer { 33 | Rank, 34 | Recent(u64), 35 | Frecent(u64), 36 | } 37 | 38 | impl Scorer { 39 | fn scored(self, row: Row) -> Result { 40 | let score = match self { 41 | Scorer::Rank => row.rank, 42 | Scorer::Recent(now) => -(time_delta(now, row.time) as f32), 43 | Scorer::Frecent(now) => frecent(row.rank, time_delta(now, row.time)), 44 | }; 45 | 46 | ensure!( 47 | score.is_finite(), 48 | "computed non-finite score from {:?}", 49 | row 50 | ); 51 | 52 | Ok(ScoredRow { 53 | path: row.path, 54 | score, 55 | }) 56 | } 57 | } 58 | 59 | fn frecent(rank: f32, dx: u64) -> f32 { 60 | const HOUR: u64 = 3600; 61 | const DAY: u64 = HOUR * 24; 62 | const WEEK: u64 = DAY * 7; 63 | 64 | // relate frequency and time 65 | if dx < HOUR { 66 | rank * 4.0 67 | } else if dx < DAY { 68 | rank * 2.0 69 | } else if dx < WEEK { 70 | rank / 2.0 71 | } else { 72 | rank / 4.0 73 | } 74 | } 75 | 76 | fn search>(data_file: P, expr: &str, mode: Scorer) -> Result> { 77 | let table = 78 | store::parse(store::open_data_file(data_file)?).with_context(|| anyhow!("parsing"))?; 79 | 80 | let mut matches: Vec<_> = { 81 | let sensitive = regex::RegexBuilder::new(expr) 82 | .case_insensitive(false) 83 | .build() 84 | .with_context(|| anyhow!("parsing regex: {:?}", expr))?; 85 | 86 | table 87 | .iter() 88 | .filter(|row| sensitive.is_match(&row.path.to_string_lossy())) 89 | .cloned() 90 | .collect() 91 | }; 92 | 93 | if matches.is_empty() { 94 | let insensitive = regex::RegexBuilder::new(expr) 95 | .case_insensitive(true) 96 | .build()?; 97 | 98 | matches = table 99 | .into_iter() 100 | .filter(|row| insensitive.is_match(&row.path.to_string_lossy())) 101 | .collect(); 102 | } 103 | 104 | let mut scored = matches 105 | .into_iter() 106 | .map(|row| mode.scored(row)) 107 | .collect::>>()?; 108 | 109 | if let Some(prefix) = common_prefix(&scored) { 110 | if let Some(row) = scored.iter_mut().find(|row| prefix == row.path) { 111 | // if all of the matches have a common prefix, 112 | // and that common prefix is in the list, 113 | // then it is *much* more likely to be our guy. 114 | row.score *= 100.; 115 | } 116 | } 117 | 118 | scored.sort_by(compare_score); 119 | 120 | Ok(scored) 121 | } 122 | 123 | fn common_prefix(rows: &[ScoredRow]) -> Option { 124 | if rows.len() <= 1 { 125 | return None; 126 | } 127 | 128 | let mut rows = rows.iter(); 129 | let mut shortest = rows.next().expect("len > 1").path.to_path_buf(); 130 | 131 | for part in rows { 132 | let part = part.path.to_path_buf(); 133 | while !part.starts_with(&shortest) { 134 | if !shortest.pop() || shortest.parent().is_none() { 135 | return None; 136 | } 137 | } 138 | } 139 | 140 | Some(shortest) 141 | } 142 | 143 | fn total_rank(table: &[Row]) -> f32 { 144 | table.iter().map(|line| line.rank).sum() 145 | } 146 | 147 | fn do_add>(table: &mut Vec, what: Q) -> Result<()> { 148 | let what = what.as_ref(); 149 | 150 | let found = match table.iter_mut().find(|row| row.path == what) { 151 | Some(row) => { 152 | row.rank += 1.0; 153 | row.time = unix_time(); 154 | true 155 | } 156 | None => false, 157 | }; 158 | 159 | if !found { 160 | table.push(Row { 161 | path: what.to_path_buf(), 162 | rank: 1.0, 163 | time: unix_time(), 164 | }); 165 | } 166 | 167 | // aging 168 | if total_rank(table) > 9000.0 { 169 | for line in table { 170 | line.rank *= 0.99; 171 | } 172 | } 173 | 174 | Ok(()) 175 | } 176 | 177 | fn run() -> Result { 178 | let data_file = match env::var_os("_Z_DATA") { 179 | Some(x) => PathBuf::from(&x), 180 | None => home_dir()?.join(".z"), 181 | }; 182 | 183 | let matches = clap::command!() 184 | .group(ArgGroup::new("sort-mode").args(&["rank", "recent", "frecent"])) 185 | .arg( 186 | Arg::new("frecent") 187 | .short('f') 188 | .long("frecent") 189 | .action(ArgAction::SetTrue) 190 | .help("sort by a hybrid of the rank and age (default)"), 191 | ) 192 | .arg( 193 | Arg::new("rank") 194 | .short('r') 195 | .long("rank") 196 | .action(ArgAction::SetTrue) 197 | .help("sort by the match's rank directly (ignore the time component)"), 198 | ) 199 | .arg( 200 | Arg::new("recent") 201 | .short('t') 202 | .long("recent") 203 | .action(ArgAction::SetTrue) 204 | .help("sort by the match's age directly (ignore the rank component)"), 205 | ) 206 | .arg( 207 | Arg::new("current-dir") 208 | .short('c') 209 | .long("current-dir") 210 | .action(ArgAction::SetTrue) 211 | .help("only return matches in the current dir"), 212 | ) 213 | .arg( 214 | Arg::new("list") 215 | .short('l') 216 | .long("list") 217 | .action(ArgAction::SetTrue) 218 | .help("show all matching values"), 219 | ) 220 | .arg( 221 | Arg::new("expressions") 222 | .num_args(0..) 223 | .help("terms to filter by"), 224 | ) 225 | .arg( 226 | Arg::new("clean") 227 | .long("clean") 228 | .action(ArgAction::SetTrue) 229 | .help("remove entries which aren't dirs right now"), 230 | ) 231 | .arg( 232 | Arg::new("add-to-profile") 233 | .long("add-to-profile") 234 | .hide_short_help(true) 235 | .action(ArgAction::SetTrue) 236 | .help("adds the helper script to the profile"), 237 | ) 238 | .arg( 239 | Arg::new("add") 240 | .long("add") 241 | .hide_short_help(true) 242 | .value_name("PATH") 243 | .help("add a new entry to the database"), 244 | ) 245 | .arg( 246 | Arg::new("add-blocking") 247 | .long("add-blocking") 248 | .hide_short_help(true) 249 | .value_name("PATH") 250 | .help("add a new entry, without forking"), 251 | ) 252 | .arg( 253 | Arg::new("complete") 254 | .long("complete") 255 | .value_name("PREFIX") 256 | .hide_short_help(true) 257 | .help("the line we're trying to complete"), 258 | ) 259 | .get_matches(); 260 | 261 | { 262 | if let Some(mut blocking) = matches.get_raw("add-blocking") { 263 | return add_entry(&data_file, false, blocking.next().expect("required arg")); 264 | } 265 | if let Some(mut normal) = matches.get_raw("add") { 266 | return add_entry(&data_file, true, normal.next().expect("required argument")); 267 | } 268 | } 269 | 270 | if let Some(line) = matches.get_one::<&str>("complete") { 271 | return complete(&data_file, line); 272 | } 273 | 274 | if matches.get_flag("clean") { 275 | return clean(&data_file); 276 | } 277 | 278 | if matches.get_flag("add-to-profile") { 279 | return add_to_profile(); 280 | } 281 | 282 | let mode = if matches.get_flag("recent") { 283 | Scorer::Recent(unix_time()) 284 | } else if matches.get_flag("rank") { 285 | Scorer::Rank 286 | } else { 287 | Scorer::Frecent(unix_time()) 288 | }; 289 | 290 | let mut list = matches.get_flag("list"); 291 | let mut expr = String::new(); 292 | 293 | if matches.get_flag("current-dir") { 294 | expr.push_str(®ex::escape( 295 | env::current_dir() 296 | .with_context(|| anyhow!("finding current dir"))? 297 | .to_str() 298 | .ok_or_else(|| anyhow!("current directory isn't valid utf-8"))?, 299 | )); 300 | expr.push('/'); 301 | } 302 | 303 | if let Some(values) = matches.get_many::("expressions") { 304 | for val in values { 305 | if !expr.is_empty() { 306 | expr.push_str(".*"); 307 | } 308 | expr.push_str(val); 309 | } 310 | } else { 311 | // even if there wasn't an explicit request to list, we had no expressions, 312 | // so we'll just print the whole thing 313 | list = true; 314 | } 315 | 316 | let table = search(&data_file, expr.as_str(), mode).with_context(|| anyhow!("main search"))?; 317 | 318 | if table.is_empty() { 319 | // It's empty! 320 | return Ok(Return::NoOutput); 321 | } 322 | 323 | if list { 324 | for row in table { 325 | println!("{:>10.3} {:?}", row.score, row.path); 326 | } 327 | Ok(Return::Success) 328 | } else { 329 | for row in table.into_iter().rev() { 330 | if !row.path.is_dir() { 331 | eprintln!("not a dir (run --clean to expunge): {:?}", row.path); 332 | continue; 333 | } 334 | println!("{}", row.path.to_string_lossy()); 335 | 336 | return Ok(Return::DoCd); 337 | } 338 | 339 | Ok(Return::NoOutput) 340 | } 341 | } 342 | 343 | fn add_entry(data_file: &PathBuf, non_blocking_add: bool, path: &OsStr) -> Result { 344 | // this must not be called while there are threaded operations running 345 | if non_blocking_add && fork_is_parent().with_context(|| anyhow!("forking"))? { 346 | return Ok(Return::NoOutput); 347 | } 348 | 349 | store::update_file(data_file, |table| do_add(table, path)) 350 | .with_context(|| anyhow!("adding to file"))?; 351 | 352 | Ok(Return::NoOutput) 353 | } 354 | 355 | fn complete(data_file: &PathBuf, mut line: &str) -> Result { 356 | let cmd = env::var("_Z_CMD").unwrap_or_else(|_err| "z".to_string()); 357 | if line.starts_with(&cmd) { 358 | line = line[cmd.len()..].trim_start(); 359 | } 360 | 361 | let escaped = regex::escape(line); 362 | 363 | for row in search(data_file, &escaped, Scorer::Frecent(unix_time())) 364 | .with_context(|| anyhow!("searching for completion data"))? 365 | .into_iter() 366 | .rev() 367 | { 368 | println!("{}", row.path.to_string_lossy()); 369 | } 370 | 371 | Ok(Return::Success) 372 | } 373 | 374 | fn clean(data_file: &PathBuf) -> Result { 375 | let modified = store::update_file(data_file, |table| { 376 | let start = table.len(); 377 | table.retain(|row| row.path.is_dir()); 378 | Ok(start - table.len()) 379 | }) 380 | .with_context(|| anyhow!("cleaning data file"))?; 381 | 382 | println!( 383 | "Cleaned {} {}.", 384 | modified, 385 | if 1 == modified { "entry" } else { "entries" } 386 | ); 387 | 388 | Ok(Return::Success) 389 | } 390 | 391 | fn add_to_profile() -> Result { 392 | let mut data = 393 | dirs::data_local_dir().ok_or_else(|| anyhow!("couldn't find your .local/share dir"))?; 394 | 395 | data.push("zrs"); 396 | fs::create_dir_all(&data).with_context(|| anyhow!("creating {:?}", data))?; 397 | 398 | data.push("z.sh"); 399 | fs::OpenOptions::new() 400 | .create(true) 401 | .write(true) 402 | .truncate(true) 403 | .open(&data) 404 | .with_context(|| anyhow!("opening {:?}", data))? 405 | .write_all(HELPER_SCRIPT) 406 | .with_context(|| anyhow!("writing helper script"))?; 407 | 408 | println!("written helper script to {:?}", data); 409 | 410 | let data = data 411 | .to_str() 412 | .ok_or_else(|| anyhow!("lazily refusing to handle non-utf8 paths"))?; 413 | ensure!( 414 | !data.contains('\''), 415 | "cowardly refusing to handle paths with single quotes" 416 | ); 417 | 418 | let source_line = format!("\n\n. '{}'\n", data); 419 | 420 | let path = home_dir()?; 421 | 422 | for rc in &[".zshrc", ".bashrc"] { 423 | let mut path = path.to_path_buf(); 424 | path.push(rc); 425 | match fs::read(&path) { 426 | Ok(current) => { 427 | if twoway::find_bytes(¤t, data.as_bytes()).is_some() { 428 | println!("appears to already be present, not appending: {:?}", path); 429 | continue; 430 | } 431 | } 432 | Err(e) => { 433 | eprintln!("couldn't open {:?}: {:?}", path, e); 434 | continue; 435 | } 436 | } 437 | match fs::OpenOptions::new().append(true).open(&path) { 438 | Ok(mut zshrc) => { 439 | zshrc.write_all(source_line.as_bytes())?; 440 | drop(zshrc); 441 | println!("appended '. .../z.sh' to {:?}", path); 442 | } 443 | Err(e) => eprintln!("couldn't append to {:?}: {:?}", path, e), 444 | } 445 | } 446 | 447 | Ok(Return::Success) 448 | } 449 | 450 | fn compare_score(left: &ScoredRow, right: &ScoredRow) -> cmp::Ordering { 451 | left.score 452 | .partial_cmp(&right.score) 453 | .expect("no NaNs in scoring") 454 | } 455 | 456 | enum Return { 457 | DoCd, 458 | NoOutput, 459 | Success, 460 | } 461 | 462 | fn main() -> Result<()> { 463 | match run() { 464 | Ok(exit) => process::exit(match exit { 465 | Return::DoCd => 69, 466 | Return::NoOutput => 70, 467 | Return::Success => 0, 468 | }), 469 | Err(e) => Err(e), 470 | } 471 | } 472 | 473 | fn fork_is_parent() -> Result { 474 | // this is a cut-down version of unistd::daemon(), 475 | // except we return instead of exiting. Just being paranoid, 476 | // not actually expecting to be running long enough that this will matter. 477 | // Unsafe iff the parent's threads are doing other stuff. We don't have threads. 478 | match unsafe { unistd::fork()? } { 479 | unistd::ForkResult::Parent { .. } => Ok(true), 480 | unistd::ForkResult::Child => { 481 | env::set_current_dir("/")?; 482 | unistd::close(0)?; 483 | Ok(false) 484 | } 485 | } 486 | } 487 | 488 | fn unix_time() -> u64 { 489 | time::SystemTime::now() 490 | .duration_since(time::UNIX_EPOCH) 491 | .unwrap() 492 | .as_secs() 493 | } 494 | 495 | fn time_delta(now: u64, then: u64) -> u64 { 496 | now.saturating_sub(then) 497 | } 498 | 499 | fn home_dir() -> Result { 500 | dirs::home_dir().ok_or_else(|| anyhow!("home directory must be locatable")) 501 | } 502 | 503 | #[cfg(test)] 504 | mod tests { 505 | use std::path::Path; 506 | use std::path::PathBuf; 507 | 508 | use super::ScoredRow; 509 | 510 | #[test] 511 | fn pathbuf_pop() { 512 | let mut p = PathBuf::from("/home/faux"); 513 | assert!(p.pop()); 514 | assert_eq!(PathBuf::from("/home"), p); 515 | assert!(p.pop()); 516 | assert_eq!(PathBuf::from("/"), p); 517 | // a path for / has no parent, but `pop()` succeeded 518 | assert_eq!(None, p.parent()); 519 | assert!(!p.pop()); 520 | 521 | // further popping doesn't remove anything 522 | assert_eq!(PathBuf::from("/"), p); 523 | } 524 | 525 | #[test] 526 | fn common() { 527 | use super::common_prefix; 528 | assert_eq!(None, common_prefix(&[])); 529 | assert_eq!(None, common_prefix(&[s("/home")])); 530 | assert_eq!(None, common_prefix(&[s("/home"), s("/etc")])); 531 | assert_eq!( 532 | Some(PathBuf::from("/home")), 533 | common_prefix(&[s("/home/faux"), s("/home/john")]) 534 | ); 535 | 536 | assert_eq!( 537 | Some(PathBuf::from("/home")), 538 | common_prefix(&[ 539 | s("/home/faux"), 540 | s("/home/alex/public_html"), 541 | s("/home/john"), 542 | s("/home/alex") 543 | ]) 544 | ); 545 | } 546 | 547 | fn s>(path: P) -> ScoredRow { 548 | ScoredRow { 549 | path: path.as_ref().to_path_buf(), 550 | score: 0., 551 | } 552 | } 553 | } 554 | -------------------------------------------------------------------------------- /src/store.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::io; 3 | use std::io::BufRead; 4 | use std::io::Read; 5 | use std::io::Write; 6 | use std::mem; 7 | use std::ops::Deref; 8 | use std::path::Path; 9 | use std::path::PathBuf; 10 | 11 | use anyhow::anyhow; 12 | use anyhow::ensure; 13 | use anyhow::Context; 14 | use anyhow::Result; 15 | use nix::fcntl; 16 | use tempfile::NamedTempFile; 17 | 18 | #[derive(Debug, Clone)] 19 | pub struct Row { 20 | pub path: PathBuf, 21 | pub rank: f32, 22 | pub time: u64, 23 | } 24 | 25 | fn to_row(line: &str) -> Result { 26 | let mut parts = line.split('|'); 27 | 28 | let path = PathBuf::from(parts.next().ok_or_else(|| anyhow!("row needs a path"))?); 29 | 30 | let rank = parts 31 | .next() 32 | .ok_or_else(|| anyhow!("row needs a rank"))? 33 | .parse::()?; 34 | 35 | let time = parts 36 | .next() 37 | .ok_or_else(|| anyhow!("row needs a time"))? 38 | .parse()?; 39 | 40 | ensure!( 41 | rank.is_finite(), 42 | "file contained non-finite rank: {:?}", 43 | rank 44 | ); 45 | 46 | Ok(Row { path, rank, time }) 47 | } 48 | 49 | pub fn parse(data_file: R) -> Result> { 50 | let mut ret = Vec::with_capacity(500); 51 | for line in io::BufReader::new(data_file).lines() { 52 | let line = line.with_context(|| anyhow!("IO error during read"))?; 53 | match to_row(&line) { 54 | Ok(row) => ret.push(row), 55 | Err(e) => eprintln!("couldn't parse {:?}: {:?}", line, e), 56 | } 57 | } 58 | 59 | Ok(ret) 60 | } 61 | 62 | pub fn update_file, F, R>(data_file: P, apply: F) -> Result 63 | where 64 | F: FnOnce(&mut Vec) -> Result, 65 | { 66 | let lock = open_data_file(&data_file)?; 67 | let lock = fcntl::Flock::lock(lock, fcntl::FlockArg::LockExclusive) 68 | .map_err(|(_, e)| e) 69 | .with_context(|| anyhow!("locking"))?; 70 | 71 | // Mmm, if we pass this by value, it will be dropped immediately, which we don't want 72 | let mut table = parse(lock.deref()).with_context(|| anyhow!("parsing"))?; 73 | 74 | let result = apply(&mut table).with_context(|| anyhow!("processing"))?; 75 | 76 | let tmp = NamedTempFile::new_in( 77 | data_file 78 | .as_ref() 79 | .parent() 80 | .ok_or_else(|| anyhow!("data file cannot be at the root"))?, 81 | ) 82 | .with_context(|| anyhow!("couldn't make a temporary file near data file"))?; 83 | 84 | { 85 | let mut writer = io::BufWriter::new(&tmp); 86 | for line in table { 87 | if line.rank < 0.98 { 88 | continue; 89 | } 90 | 91 | let path = match line.path.to_str() { 92 | Some(path) if path.contains('|') || path.contains('\n') => continue, 93 | Some(path) => path, 94 | None => continue, 95 | }; 96 | writeln!(writer, "{}|{}|{}", path, line.rank, line.time) 97 | .with_context(|| anyhow!("writing temporary value"))?; 98 | } 99 | } 100 | 101 | // best effort attempt to maintain uid/gid 102 | // TODO: other attributes; mode is handled by umask.. maybe. 103 | if let Ok(stat) = nix::sys::stat::stat(data_file.as_ref()) { 104 | let _ = nix::unistd::chown( 105 | tmp.path(), 106 | Some(nix::unistd::Uid::from_raw(stat.st_uid)), 107 | Some(nix::unistd::Gid::from_raw(stat.st_gid)), 108 | ); 109 | } 110 | 111 | tmp.persist(data_file) 112 | .with_context(|| anyhow!("replacing"))?; 113 | 114 | // just being explicit about when we expect the lock to live to 115 | mem::drop(lock); 116 | 117 | Ok(result) 118 | } 119 | 120 | pub fn open_data_file>(data_file: P) -> Result { 121 | let data_file = data_file.as_ref(); 122 | fs::OpenOptions::new() 123 | .read(true) 124 | .write(true) 125 | .create(true) 126 | .open(data_file) 127 | .with_context(|| anyhow!("opening/creating data file at {:?}", data_file)) 128 | } 129 | -------------------------------------------------------------------------------- /z.sh: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018 Chris West. Licensed under MIT OR Apache-2.0 2 | # Parts Copyright (c) 2009 rupa deadwyler. Licensed under the WTFPL license, Version 2 3 | 4 | # maintains a jump-list of the directories you actually use 5 | # 6 | # INSTALL: 7 | # * put something like this in your .bashrc/.zshrc: 8 | # . /path/to/z.sh 9 | # * cd around for a while to build up the db 10 | # * PROFIT!! 11 | # * optionally: 12 | # set $_Z_CMD in .bashrc/.zshrc to change the command (default z). 13 | # set $_Z_DATA in .bashrc/.zshrc to change the datafile (default ~/.z). 14 | # TODO: set $_Z_NO_RESOLVE_SYMLINKS to prevent symlink resolution. 15 | # set $_Z_NO_PROMPT_COMMAND if you're handling PROMPT_COMMAND yourself. 16 | # TODO: set $_Z_EXCLUDE_DIRS to an array of directories to exclude. 17 | # TODO: set $_Z_OWNER to your username if you want use z while sudo with $HOME kept 18 | # 19 | # USE: 20 | # * z foo # cd to most frecent dir matching foo 21 | # * z foo bar # cd to most frecent dir matching foo and bar 22 | # * z -r foo # cd to highest ranked dir matching foo 23 | # * z -t foo # cd to most recently accessed dir matching foo 24 | # * z -l foo # list matches instead of cd 25 | # * z -e foo # echo the best match, don't cd 26 | # * z -c foo # restrict matches to subdirs of $PWD 27 | 28 | [ -d "${_Z_DATA:-$HOME/.z}" ] && { 29 | echo "ERROR: z.sh's datafile (${_Z_DATA:-$HOME/.z}) is a directory." 30 | } 31 | 32 | _z() { 33 | 34 | # if symlink, dereference 35 | #[ -h "$datafile" ] && datafile=$(readlink "$datafile") 36 | 37 | # bail if we don't own ~/.z and $_Z_OWNER not set 38 | #[ -z "$_Z_OWNER" -a -f "$datafile" -a ! -O "$datafile" ] && return 39 | 40 | local output ret 41 | output="$(zrs "$@")" 42 | ret=$? 43 | case ${ret} in 44 | 69) 45 | # 69: DoCd 46 | builtin cd "${output}" 47 | ;; 48 | 70) 49 | # 70: NoOutput 50 | ;; 51 | 0) 52 | # 0: Success 53 | echo "${output}" 54 | ;; 55 | *) 56 | echo "zrs failed: ${ret}" 57 | ;; 58 | esac 59 | } 60 | 61 | alias ${_Z_CMD:-z}='_z 2>&1' 62 | 63 | [ "$_Z_NO_RESOLVE_SYMLINKS" ] || _Z_RESOLVE_SYMLINKS="-P" 64 | 65 | if type compctl >/dev/null 2>&1; then 66 | # zsh 67 | [ "$_Z_NO_PROMPT_COMMAND" ] || { 68 | # populate directory list, avoid clobbering any other precmds. 69 | if [ "$_Z_NO_RESOLVE_SYMLINKS" ]; then 70 | _z_precmd() { 71 | (_z --add "${PWD:a}" &) 72 | } 73 | else 74 | _z_precmd() { 75 | (_z --add "${PWD:A}" &) 76 | } 77 | fi 78 | [[ -n "${precmd_functions[(r)_z_precmd]}" ]] || { 79 | precmd_functions[$(($#precmd_functions+1))]=_z_precmd 80 | } 81 | } 82 | _z_zsh_tab_completion() { 83 | # tab completion 84 | local compl 85 | read -l compl 86 | reply=(${(f)"$(zrs --complete "$compl")"}) 87 | } 88 | compctl -U -K _z_zsh_tab_completion _z 89 | elif type complete >/dev/null 2>&1; then 90 | # bash 91 | # tab completion 92 | complete -o filenames -C '_z --complete "$COMP_LINE"' ${_Z_CMD:-z} 93 | [ "$_Z_NO_PROMPT_COMMAND" ] || { 94 | # populate directory list. avoid clobbering other PROMPT_COMMANDs. 95 | grep "_z --add" <<< "$PROMPT_COMMAND" >/dev/null || { 96 | PROMPT_COMMAND="$PROMPT_COMMAND"$'\n''(_z --add "$(command pwd '$_Z_RESOLVE_SYMLINKS' 2>/dev/null)" 2>/dev/null &);' 97 | } 98 | } 99 | fi 100 | --------------------------------------------------------------------------------