├── .gitignore ├── LICENSE ├── README.md ├── completions ├── abbr-add.fish ├── abbr-erase.fish ├── apt-description.fish ├── echo-variable.fish ├── tunnel.fish └── unsymlink.fish ├── functions ├── ......fish ├── .....fish ├── ....fish ├── _clone-cd.fish ├── abbr-add.fish ├── abbr-erase.fish ├── add-sourcehut-remote.fish ├── album.fish ├── apt-description.fish ├── backup.fish ├── capitalize.fish ├── char-count.fish ├── clean-unzip.fish ├── clip-pwd.fish ├── clip.fish ├── clone-cd.fish ├── clone-shallow-cd.fish ├── coln.fish ├── confirm.fish ├── copy.fish ├── create-file.fish ├── curdir.fish ├── default.fish ├── describe.fish ├── domain.fish ├── dtoh.fish ├── eat.fish ├── echo-variable.fish ├── edit-git.fish ├── edit-gitconfig.fish ├── edit-readme.fish ├── edit.fish ├── ensure-trailing-newline.fish ├── ensuredb.fish ├── equals.fish ├── expand-home-tilde.fish ├── fast-sync-repos.fish ├── file-exists.fish ├── fingerprint.fish ├── fish_greeting.fish ├── fish_prompt.fish ├── funced-last.fish ├── funcsave-last.fish ├── git-add.fish ├── git-changing-files.fish ├── git-commit.fish ├── git-protocol-https-to-git.fish ├── git-restage.fish ├── git-restore.fish ├── gitignore.fish ├── goto-definition.fish ├── guess-git-url.fish ├── hub.fish ├── in-git-repo.fish ├── init-sourcehut.fish ├── ip-addr.fish ├── is-clean-zip.fish ├── is-dir.fish ├── is-symlink.fish ├── isodate.fish ├── isodatetime.fish ├── krui.fish ├── last-col.fish ├── last-download.fish ├── last-function-name.fish ├── last-image-built.fish ├── lima-ssh.fish ├── lima-vnc.fish ├── line-count.fish ├── link-rc.fish ├── loop.fish ├── mkdir-cd.fish ├── move-last-download.fish ├── move.fish ├── music.fish ├── new.fish ├── pager.fish ├── paste_avoiding_double_git_clone.fish ├── play-random.fish ├── play.fish ├── port-ps.fish ├── project-replace.fish ├── pw.fish ├── query.fish ├── random-file.fish ├── readpass.fish ├── remove.fish ├── rename-pwd.fish ├── renamedb.fish ├── repeat.fish ├── repo-from-url.fish ├── reset-date.fish ├── restore.fish ├── retry.fish ├── rm-pycache.fish ├── rmdir-..fish ├── row.fish ├── runjava.fish ├── search.fish ├── shutdown.fish ├── skip-lines.fish ├── song.fish ├── songs.fish ├── ssh-segfault.fish ├── startswith.fish ├── string-empty.fish ├── symlink.fish ├── symlinks.fish ├── sync-repo.fish ├── sync-repos.fish ├── take.fish ├── tar-cd.fish ├── tgz.fish ├── title-case.fish ├── tree.fish ├── trim-left.fish ├── trim-right.fish ├── trim-scheme.fish ├── trim-trailing-slash.fish ├── tunnel.fish ├── uid.fish ├── unexpand-home-tilde.fish ├── unsymlink.fish ├── until-char.fish ├── unzip-cd.fish ├── url-size.fish ├── ver.fish ├── vim-call.fish ├── vim-client.fish ├── vim-plugin.fish ├── viw.fish ├── watchdns.fish ├── wifi-network-name.fish ├── wifi-password.fish ├── wifi-reset.fish ├── wikinews.fish ├── wip.fish └── word-count.fish └── test ├── test_readme.py └── test_symlink.fish /.gitignore: -------------------------------------------------------------------------------- 1 | fish_history 2 | fish_read_history 3 | functions/__* 4 | functions/private/* 5 | fish_variables 6 | /conf.d 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Public domain dedication 2 | 3 | I hereby release this software into the public domain. 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Functions for the [Fish Shell](https://fishshell.com), making common tasks more convenient. 2 | 3 | ## Installation 4 | 5 | Backup any existing `~/.config/fish`, then: 6 | 7 | ```sh 8 | git clone https://git.sr.ht/~razzi/fish-functions ~/.config/fish 9 | ``` 10 | 11 | In previous versions, other fish config including abbrs were included as well. 12 | That changed much more frequently than the functions, so I split them out. 13 | 14 | Now they are at https://git.sr.ht/~razzi/.dotfiles (see that repository's README for installation instructions). 15 | 16 | ## Contents 17 | 18 | - [`fish` Interactive Utilities](#fish-interactive-utilities) 19 | * [`abbr-add`](#abbr-add-name-expansion-args-source) 20 | * [`abbr-erase`](#abbr-erase-name-source) 21 | * [`clip`](#clip-args-source) 22 | * [`funcsave-last`](#funcsave-last-source) 23 | * [`mkdir-cd`](#mkdir-cd-directory-source) 24 | - [File Manipulation](#file-manipulation) 25 | * [`backup`](#backup-file-source) 26 | * [`copy`](#copy-source-destination-source) 27 | * [`create-file`](#create-file-target-source) 28 | * [`eat`](#eat-target-source) 29 | * [`move`](#move-source-destination-source) 30 | * [`move-last-download`](#move-last-download-dest-source) 31 | * [`remove`](#remove-target-source) 32 | * [`restore`](#restore-backup-source) 33 | - [Zipfile Utilities](#zipfile-utilities) 34 | * [`clean-unzip`](#clean-unzip-zipfile-source) 35 | * [`unzip-cd`](#unzip-cd-zipfile-source) 36 | - [Text Utilities](#text-utilities) 37 | * [`coln`](#coln-number-source) 38 | * [`row`](#row-number-source) 39 | * [`skip-lines`](#skip-lines-number-source) 40 | * [`take`](#take-lines-number-source) 41 | * [`word-count`](#word-count-source) 42 | * [`line-count`](#line-count-source) 43 | * [`char-count`](#char-count-source) 44 | - [`fish` Scripting Utilities](#fish-utilities) 45 | * [`string-empty`](#string-empty-empty-value-source) 46 | * [`file-exists`](#file-exists-file-source) 47 | * [`is-dir`](#is-dir-file-source) 48 | * [`is-symlink`](#is-symlink-file-source) 49 | * [`confirm`](#confirm-source) 50 | - [Environment Utilities](#environment-utilities) 51 | * [`curdir`](#curdir-source) 52 | * [`echo-variable`](#echo-variable-variable-source) 53 | * [`readpass`](#readpass-name-source) 54 | - [Symlink Utilities](#symlink-utilities) 55 | * [`symlink`](#symlink-from-to-source) 56 | * [`unsymlink`](#unsymlink-file-source) 57 | * [`symlinks`](#symlinks-dir-source) 58 | * [`link-rc`](#link-rc-file-source) 59 | - [`git` Utilities](#git-utilities) 60 | * [`clone-cd`](#clone-cd-url-destination-source) 61 | * [`clone-shallow-cd`](#clone-shallow-cd-url-destination-source) 62 | * [`wip`](#wip-message-source) 63 | * [`git-add`](#git-add-paths-source) 64 | * [`git-commit`](#git-commit-message-source) 65 | * [`gitignore`](#gitignore-pattern-source) 66 | - [`lima` Utilities](#lima-utilities) 67 | * [`lima-ssh`](#lima-ssh-source) 68 | * [`lima-vnc`](#lima-vnc-source) 69 | - [`vim` Utilities](#vim-utilities) 70 | * [`vim-plugin`](#vim-plugin-url-source) 71 | - [Postgres Utilities](#postgres-utilities) 72 | * [`ensuredb`](#ensuredb-name-source) 73 | * [`renamedb`](#renamedb-from-to-source) 74 | - [Date Utilities](#date-utilities) 75 | * [`isodate`](#isodate-source) 76 | * [`isodatetime`](#isodatetime-source) 77 | - [MacOS Utilities](#macos-utilities) 78 | * [`wifi-network-name`](#wifi-network-name-source) 79 | * [`wifi-password`](#wifi-password-source) 80 | * [`wifi-reset`](#wifi-reset-source) 81 | 82 | ## `fish` Interactive Utilities 83 | 84 | Fish functions designed to be typed and run in the shell. 85 | 86 | ### `abbr-add []` [(source)](functions/abbr-add.fish) 87 | 88 | Adds an abbr and syncs your abbrs to `~/.config/fish/conf.d/abbrs.fish`. 89 | 90 | This way the abbr will be loaded the next time you open your shell. 91 | 92 | Without `abbr-add`, you can use `abbr -a` to make your own abbrs, 93 | and add `abbr -a` calls to your fish config manually, 94 | but I recommend using `abbr-add` and tracking 95 | `~/.config/fish/conf.d/abbrs.fish` in version control. 96 | 97 | All `abbr` options work with this command, so for example you can run: 98 | 99 | ```fish 100 | $ abbr-add --position anywhere isntall install 101 | ``` 102 | 103 | Recommended abbreviation: `abbr-add ab abbr-add` 104 | 105 | ### `abbr-erase ` [(source)](functions/abbr-erase.fish) 106 | 107 | Erases an abbr and removes it from `~/.config/fish/conf.d/abbrs.fish`. 108 | 109 | Recommended abbreviation: `abbr-add ae abbr-erase` 110 | 111 | [Completion](completions/abbr-erase.fish): completes abbr names. 112 | 113 | ### `clip [args]` [(source)](functions/clip.fish) 114 | 115 | Copies the arguments that follow `clip` to the clipboard. 116 | 117 | ```fish 118 | $ clip cat ~/.ssh/id_ed25519.pub 119 | # now "cat ~/.ssh/id_ed25519.pub" is on the clipboard 120 | $ echo (fish_clipboard_paste) 121 | cat ~/.ssh/id_ed25519.pub 122 | ``` 123 | 124 | This is useful when you want to copy a command to your clipboard 125 | (possibly to paste the command into documentation). 126 | 127 | You can press the up arrow or `control+p` to get to the previous command, 128 | then hit `control-a` to move your cursor to the start, prepend `clip `, 129 | and you can quickly copy a command. 130 | 131 | If you want to copy the _output_ of a command, pipe it to `fish_clipboard_copy`: 132 | 133 | ```fish 134 | $ echo 1 | fish_clipboard_copy 135 | $ echo (fish_clipboard_paste) 136 | 1 137 | ``` 138 | 139 | ### `funcsave-last` [(source)](functions/funcsave-last.fish) 140 | 141 | Save the last-edited `fish` function. 142 | 143 | ```fish 144 | $ function hi 145 | echo hi 146 | end 147 | $ funcsave-last 148 | Saved hi 149 | ``` 150 | 151 | Recommended abbreviation: `abbr-add fs funcsave-last` 152 | 153 | ### `mkdir-cd ` [(source)](functions/mkdir-cd.fish) 154 | 155 | Make a directory and cd into it. 156 | 157 | ```fish 158 | $ mkdir-cd folder 159 | folder $ 160 | ``` 161 | 162 | Recommended abbreviation: `abbr-add mc mkdir-cd` 163 | 164 | ## File Manipulation 165 | 166 | ### `backup ` [(source)](functions/backup.fish) 167 | 168 | Creates a copy of `file` as `file.bak`. 169 | 170 | ```fish 171 | $ ls 172 | README.md 173 | $ backup README.md 174 | $ ls 175 | README.md README.md.bak 176 | ``` 177 | 178 | ### `copy ... []` [(source)](functions/copy.fish) 179 | 180 | `cp` with some extra behaviors. 181 | 182 | Automatic recursive copy for directories. Rather than only copying the files from a directory, copies the directory itself. 183 | 184 | Also uses -i flag by default, which will warn you if a copy would overwrite a destination file. 185 | 186 | Example: 187 | 188 | ```fish 189 | $ mkdir testdir 190 | $ touch testdir/file.txt 191 | $ mkdir destdir 192 | 193 | # Standard cp needs -r flag 194 | $ cp testdir/ destdir/ 195 | cp: testdir/ is a directory (not copied). 196 | 197 | # And does not preserve the source folder 198 | $ cp -r testdir/ destdir/ 199 | $ ls destdir/ 200 | file.txt 201 | 202 | # Cleaning up... 203 | $ rm destdir/file.txt 204 | 205 | # In contrast, using `copy` function: 206 | $ copy testdir/ destdir/ 207 | $ ls destdir/ 208 | testdir 209 | ``` 210 | 211 | Recommended abbreviation: `abbr-add cp copy` 212 | 213 | If you do this abbreviation, use `command cp` for the low-level `cp`. 214 | 215 | ### `create-file ` [(source)](functions/create-file.fish) 216 | 217 | Creates a file, including parent directories as necessary. 218 | 219 | ```fish 220 | $ create-file a/b/c 221 | $ tree 222 | . 223 | └── a 224 | └── b 225 | └── c 226 | ``` 227 | 228 | ### `eat ` [(source)](functions/eat.fish) 229 | 230 | Moves a directory's contents to the current directory and removes the empty directory. 231 | 232 | ```fish 233 | $ tree 234 | . 235 | └── a 236 | └── b 237 | └── c 238 | $ eat a 239 | $ tree 240 | . 241 | └── b 242 | └── c 243 | ``` 244 | 245 | If a file in the current directory would be overwritten by `eat`, it will error with exit status 1. 246 | 247 | An illustration of this: 248 | 249 | ```fish 250 | $ tree 251 | . 252 | ├── dir-a 253 | │ └── dir-b 254 | │ └── some_file 255 | └── dir-b 256 | └── would_be_overwritten 257 | 258 | 3 directories, 3 files 259 | $ eat dir-a 260 | eat: file would be overwritten: ./dir-b 261 | ``` 262 | 263 | ### `move ... ` [(source)](functions/move.fish) 264 | 265 | Like `mv` but uses `-i` flag by default, 266 | which will warn you if `mv` would overwrite a destination file. 267 | 268 | Also warns you if you are trying to move a directory symlink which is ending in slash: 269 | 270 | ```fish 271 | $ mkdir mydir 272 | $ ln -s mydir mylink 273 | $ mv mylink/ renamed 274 | mv: cannot move 'mylink/' to 'renamed': Not a directory 275 | ``` 276 | 277 | `move` gives a more descriptive error: 278 | 279 | ```fish 280 | $ move mylink/ renamed 281 | move: `from` argument "mylink/" is a symlink with a trailing slash. 282 | move: to rename a symlink, remove the trailing slash from the argument. 283 | ``` 284 | 285 | This arises because tab completion adds the slash. Completion for `move` could avoid the slash, but then again you might want to move a file within the symlinked directory. 286 | 287 | ### `move-last-download []` [(source)](functions/move-last-download.fish) 288 | 289 | Move the latest download to destination directory, which is the current directory if none is specified. 290 | 291 | Recommended abbreviation: `abbr-add mvl move-last-download` 292 | 293 | ### `remove ` [(source)](functions/remove.fish) 294 | 295 | `rm` with an extra behavior. 296 | 297 | If removing a directory with write-protected `.git`, confirm once to ensure the git directory is desired to be removed. 298 | 299 | ```fish 300 | $ ls -a dodo 301 | . .. .git x 302 | $ remove dodo 303 | Remove .git directory dodo/.git?> y 304 | ``` 305 | 306 | Using plain `rm`: 307 | 308 | ```fish 309 | $ rm -r dodo 310 | override r--r--r-- razzi/staff for dodo/.git/objects/58/05b676e247eb9a8046ad0c4d249cd2fb2513df? y 311 | override r--r--r-- razzi/staff for dodo/.git/objects/f3/7f81fa1f16e78ac451e2d9ce42eab8933bd99f? y 312 | override r--r--r-- razzi/staff for dodo/.git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391? ^C 313 | $ rm -rf dodo 314 | ``` 315 | 316 | Recommended abbreviation: `abbr-add rm remove` 317 | 318 | If you do this abbreviation, use `command rm` for the low-level `rm`. 319 | 320 | ### `restore ` [(source)](functions/restore.fish) 321 | 322 | Rename a backup such as `file.bak` to remove the `.bak` extension. 323 | 324 | ```fish 325 | $ ls 326 | README.md README.md.bak 327 | $ restore README.md.bak 328 | $ ls 329 | README.md 330 | ``` 331 | 332 | ## Zipfile Utilities 333 | 334 | ### `clean-unzip ` [(source)](functions/clean-unzip.fish) 335 | 336 | Unzips a `.zip` archive without polluting the current directory, by creating a 337 | directory even if the zipfile does not have a folder level. 338 | 339 | ### `unzip-cd ` [(source)](functions/unzip-cd.fish) 340 | 341 | Unzip a zip directory and cd into it. Uses `clean-unzip` to create a folder if 342 | the zipfile doesn't have one. 343 | 344 | ```fish 345 | $ unzip-cd files.zip 346 | Archive: files.zip 347 | extracting: out/a.txt 348 | extracting: out/b.txt 349 | files $ ls 350 | a.txt b.txt 351 | ``` 352 | 353 | ## Text Utilities 354 | 355 | ### `coln ` [(source)](functions/coln.fish) 356 | 357 | Splits input on whitespace and prints the column indicated. 358 | 359 | ```fish 360 | $ echo 1 2 | coln 2 361 | 2 362 | ``` 363 | 364 | ### `row ` [(source)](functions/row.fish) 365 | 366 | Prints the row of input indicated. 367 | 368 | ```fish 369 | $ seq 3 | row 2 370 | 2 371 | ``` 372 | 373 | ### `skip-lines ` [(source)](functions/skip-lines.fish) 374 | 375 | Skips the first n lines of stdin. 376 | 377 | ```fish 378 | $ seq 5 | skip-lines 2 379 | 3 380 | 4 381 | 5 382 | ``` 383 | 384 | ### `take ` [(source)](functions/take.fish) 385 | 386 | Take the first `n` lines of standard input. 387 | ```fish 388 | $ seq 5 | take 3 389 | 1 390 | 2 391 | 3 392 | ``` 393 | 394 | ### `word-count` [(source)](functions/word-count.fish) 395 | 396 | Count the words from standard input. Like `wc -w` but does not put spaces around the number. 397 | 398 | ```fish 399 | $ echo a b | word-count 400 | 2 401 | # Compare to: 402 | $ echo a b | wc -w 403 | 2 404 | ``` 405 | 406 | ### `line-count` [(source)](functions/line-count.fish) 407 | 408 | Count the lines from standard input. Like `wc -l` but does not put spaces around the number. 409 | 410 | ```fish 411 | $ seq 3 | line-count 412 | 3 413 | # Compare to: 414 | $ seq 3 | wc -l 415 | 3 416 | ``` 417 | 418 | ### `char-count` [(source)](functions/char-count.fish) 419 | 420 | Count the characters from standard input. Like `wc -c` but does not put spaces around the number. 421 | 422 | ```fish 423 | $ echo -n a b | char-count 424 | 3 425 | # Compare to: 426 | $ echo -n a b | wc -c 427 | 3 428 | ``` 429 | 430 | ## `fish` Scripting utilities 431 | 432 | ### `string-empty ` [(source)](functions/string-empty.fish) 433 | 434 | Test if the value is the empty string. 435 | 436 | ```fish 437 | $ string-empty '' 438 | $ echo $status 439 | 0 440 | ``` 441 | 442 | Can be used to test for arguments: 443 | 444 | ```fish 445 | $ function something 446 | if string-empty $argv 447 | echo No arguments passed 448 | else 449 | echo Arguments were passed 450 | end 451 | end 452 | $ something 453 | No arguments passed 454 | $ something 1 455 | Arguments were passed 456 | ``` 457 | 458 | If you use this on a variable, be sure to get the variable's *value* using `$`: 459 | 460 | ```fish 461 | $ if string-empty $VIRTUAL_ENV 462 | echo in venv 463 | end 464 | ``` 465 | 466 | since `string-empty VIRTUAL_ENV` will always return `false`. 467 | 468 | ### `file-exists ` [(source)](functions/file-exists.fish) 469 | 470 | Test if `$file` exists. 471 | 472 | ### `is-dir ` [(source)](functions/is-dir.fish) 473 | 474 | Check if `$path` is a directory. 475 | 476 | ### `is-symlink ` [(source)](functions/is-symlink.fish) 477 | 478 | Check if `$path` is a symlink. 479 | 480 | ### `confirm` [(source)](functions/confirm.fish) 481 | 482 | Prompts the user for confirmation. Exit with status according to whether they answered `y`, `Y`, `yes`, or `YES`. 483 | 484 | ## Environment Utilities 485 | 486 | ### `curdir` [(source)](functions/curdir.fish) 487 | 488 | Just the current directory name, please. 489 | 490 | ```fish 491 | mydir $ curdir 492 | mydir 493 | ``` 494 | 495 | You probably won't need this interactively since the current directory is usually part of your `fish_prompt`, 496 | but this is useful for scripting. 497 | 498 | ### `echo-variable ` [(source)](functions/echo-variable.fish) 499 | 500 | Like `echo`, but without the `$` or capitalization. 501 | 502 | ```fish 503 | $ echo-variable user 504 | razzi 505 | $ echo $USER 506 | razzi 507 | ``` 508 | 509 | Recommended abbreviation: `abbr-add ev echo-variable` 510 | 511 | [Completion](completions/echo-variable.fish): completes environment variable names. 512 | 513 | ### `readpass ` [(source)](functions/readpass.fish) 514 | 515 | Prompt for a password. Does not echo entered characters. 516 | 517 | ```fish 518 | $ readpass email 519 | ●●●●●●●●●●●●●●●●● 520 | $ echo $email 521 | razzi@abuissa.net 522 | ``` 523 | 524 | ## symlink utilities 525 | 526 | ### `symlink ` [(source)](functions/symlink.fish) 527 | 528 | Create a symbolic link, using absolute paths. 529 | 530 | ```fish 531 | ~/dotfiles $ symlink .prettierrc ~ 532 | ~/dotfiles $ cat ~/.prettierrc 533 | singleQuote: true 534 | semi: false 535 | ``` 536 | 537 | Without using absolute paths: 538 | 539 | ```fish 540 | ~/dotfiles $ ln -s .prettierrc ~ 541 | ~/dotfiles $ cat ~/.prettierrc 542 | cat: /Users/razzi/.prettierrc: Too many levels of symbolic links 543 | ``` 544 | 545 | ### `unsymlink ` [(source)](functions/unsymlink.fish) 546 | 547 | Remove a symlink. Errors if the file is not a symlink. 548 | 549 | Recommended abbreviation: `abbr-add us unsymlink` 550 | 551 | ### `symlinks []` [(source)](functions/symlinks.fish) 552 | 553 | List symlinks in the given directory, or the current directory if none is passed. 554 | 555 | ### `link-rc []` [(source)](functions/link-rc.fish) 556 | 557 | Create a symlink from `$file` to the home directory (`~`). 558 | 559 | Sample usage: 560 | 561 | ```fish 562 | .dotfiles $ link-rc .tmux.conf 563 | .dotfiles $ head -1 ~/.tmux.conf 564 | set -g prefix ^Space 565 | ``` 566 | 567 | Recommended abbreviation: `abbr-add lrc link-rc` 568 | 569 | ## git utilities 570 | 571 | ### `clone-cd url [destination]` [(source)](functions/clone-cd.fish) 572 | 573 | Clone a `git` repository into the current directory (or the optional `$destination`), and `cd` into it. 574 | 575 | If a folder by that name already exists, great, you probably already cloned it, just cd into the directory and pull. 576 | 577 | If it's trying to clone into a non-empty directory, make a new folder in that directory with the repository name and clone into that, instead of erroring. 578 | 579 | Recommended abbreviation: `abbr-add cc clone-cd` 580 | 581 | ### `clone-shallow-cd url [destination]` [(source)](functions/clone-shallow-cd.fish) 582 | 583 | Like `clone-cd` but clones with `--depth=1` for speed. 584 | 585 | ### `wip [message]` [(source)](functions/wip.fish) 586 | 587 | Adds untracked changes and commits them with a WIP message. Additional arguments are added to the WIP message. 588 | 589 | I use this instead of `git stash` so that changes are associated with the branch they're on, and the commit is tracked in the reflog. 590 | 591 | ```fish 592 | $ git stat 593 | ## master 594 | M tests.py 595 | $ git switch -c testing 596 | $ wip failing tests 597 | [testing 0078f7f] WIP failing tests 598 | $ git switch - 599 | ``` 600 | 601 | ### `git-add [paths]` [(source)](functions/git-add.fish) 602 | 603 | Like `git add`, but defaults to `.` if no arguments given, rather than erroring. 604 | 605 | Also understand `...` to mean `../..`. If you need more levels of `../..` I guess they could be added. 606 | 607 | Did I mention I have a function called `...` that `cd`s up 2 levels? 608 | 609 | Recommended abbreviation: `abbr-add ga git-add` 610 | 611 | ### `git-commit [message]` [(source)](functions/git-commit.fish) 612 | 613 | Like `git commit -m` without the need to quote the commit message. 614 | 615 | If no commit message is given and there's only 1 file changed, commit "(Add / Update / Delete) (that file)". 616 | 617 | ```fish 618 | $ git-commit 619 | [master c77868d] Update README.md 620 | 1 file changed, 57 insertions(+), 18 deletions(-) 621 | $ git reset @^ 622 | Unstaged changes after reset: 623 | M README.md 624 | $ git-add 625 | $ git-commit Fix typo in README.md 626 | [master 0078f7f] Fix typo in README.md 627 | 1 file changed, 57 insertions(+), 18 deletions(-) 628 | ``` 629 | 630 | Recommended abbreviation: `abbr-add gc git-commit` 631 | 632 | ### `gitignore ` [(source)](functions/gitignore.fish) 633 | 634 | Add a pattern to the `.gitignore`. 635 | 636 | Recommended abbreviation: `abbr-add giti gitignore` 637 | 638 | ## [`lima`](https://lima-vm.io/) Utilities 639 | 640 | ### `lima-ssh` [(source)](functions/lima-ssh.fish) 641 | 642 | Connect to a default lima virtual machine. 643 | 644 | Creates and starts the machine as necessary, so it works in 1 command. 645 | 646 | ### `lima-vnc` [(source)](functions/lima-vnc.fish) 647 | 648 | Connect to a Lima virtual machine over VNC. 649 | 650 | Creates and starts the machine as necessary, like `lima-ssh`. 651 | 652 | ## Vim Utilities 653 | 654 | ### `vim-plugin ` [(source)](functions/vim-plugin.fish) 655 | 656 | Install a vim plugin using the builtin vim plugin mechanism. 657 | 658 | ## Postgres Utilities 659 | 660 | ### `ensuredb ` [(source)](functions/ensuredb.fish) 661 | 662 | Ensure that a fresh database by the name given is created. 663 | Drops a database by that name if it exists, clearing database connections as necessary. 664 | 665 | ### `renamedb ` [(source)](functions/renamedb.fish) 666 | 667 | Renames a database. 668 | 669 | ## Date Utilities 670 | 671 | ### `isodate` [(source)](functions/isodate.fish) 672 | 673 | Prints the date in ISO format. 674 | 675 | ```fish 676 | $ isodate 677 | 2020-01-28 678 | ``` 679 | 680 | ### `isodatetime` [(source)](functions/isodatetime.fish) 681 | 682 | Prints the date and time in ISO format. 683 | 684 | ```fish 685 | $ isodatetime 686 | 2025-03-07T23:11:53 687 | ``` 688 | 689 | ## MacOS Utilities 690 | 691 | ### `wifi-network-name` [(source)](functions/wifi-network-name.fish) 692 | 693 | Prints the current wifi network name. 694 | 695 | ### `wifi-password` [(source)](functions/wifi-password.fish) 696 | 697 | Prints the current wifi network password. 698 | 699 | ### `wifi-reset` [(source)](functions/wifi-reset.fish) 700 | 701 | Turns the wifi off and on again. 702 | -------------------------------------------------------------------------------- /completions/abbr-add.fish: -------------------------------------------------------------------------------- 1 | complete abbr-add -a "(functions | string split ,)" 2 | -------------------------------------------------------------------------------- /completions/abbr-erase.fish: -------------------------------------------------------------------------------- 1 | complete abbr-erase -a "(abbr -l)" 2 | -------------------------------------------------------------------------------- /completions/apt-description.fish: -------------------------------------------------------------------------------- 1 | complete -x -c apt-description -a '(apt-mark showmanual) (apt-mark showauto)' 2 | -------------------------------------------------------------------------------- /completions/echo-variable.fish: -------------------------------------------------------------------------------- 1 | complete -x -c echo-variable -a '(set | coln 1)' 2 | -------------------------------------------------------------------------------- /completions/tunnel.fish: -------------------------------------------------------------------------------- 1 | complete -c tunnel --wraps ssh 2 | -------------------------------------------------------------------------------- /completions/unsymlink.fish: -------------------------------------------------------------------------------- 1 | complete -c unsymlink -f 2 | complete -c unsymlink -a '(__unsymlink_complete_args)' 3 | 4 | function __unsymlink_complete_args 5 | set -l previous_token (commandline -x)[-1] 6 | 7 | if equals $previous_token unsymlink 8 | set base_path '' 9 | else 10 | # Anything after the last slash shouldn't be passed to find, 11 | # so delete non-slashes at the end 12 | set base_path (echo $previous_token | string replace -r '[^/]+$' '') 13 | end 14 | 15 | if equals $base_path '' 16 | set find_path . 17 | else 18 | set find_path $base_path 19 | end 20 | 21 | # Make directories end with / to keep exploring them with further tab completions 22 | set result_path "$base_path%f/" 23 | 24 | # mindepth to skip current directory, maxdepth to only suggest one level of folder at a time 25 | find $find_path -mindepth 1 -maxdepth 1 -type d -printf "$result_path\tDirectory\n" 26 | 27 | # Symlinks end in just the symlink name 28 | find $find_path -mindepth 1 -maxdepth 1 -type l -printf "$base_path%f\tSymlink\n" 29 | end 30 | -------------------------------------------------------------------------------- /functions/......fish: -------------------------------------------------------------------------------- 1 | function ..... 2 | cd ../../../.. 3 | end 4 | -------------------------------------------------------------------------------- /functions/.....fish: -------------------------------------------------------------------------------- 1 | function .... 2 | cd ../../.. 3 | end 4 | -------------------------------------------------------------------------------- /functions/....fish: -------------------------------------------------------------------------------- 1 | function ... 2 | cd ../.. 3 | end 4 | -------------------------------------------------------------------------------- /functions/_clone-cd.fish: -------------------------------------------------------------------------------- 1 | function _clone-cd --argument url destination cloneargs 2 | if file-exists $destination 3 | echo 'Already cloned. Attempting pull...' 4 | cd $destination && git pull 5 | return 6 | end 7 | 8 | git clone $cloneargs $url $destination && cd $destination 9 | end 10 | -------------------------------------------------------------------------------- /functions/abbr-add.fish: -------------------------------------------------------------------------------- 1 | function abbr-add 2 | set cleaned_args (echo $argv | unexpand-home-tilde | trim-trailing-slash) 3 | abbr -a (echo $cleaned_args | string split ' ') 4 | 5 | abbr > "$HOME/.config/fish/conf.d/abbrs.fish" 6 | end 7 | -------------------------------------------------------------------------------- /functions/abbr-erase.fish: -------------------------------------------------------------------------------- 1 | function abbr-erase --argument abbr 2 | abbr --erase $abbr 3 | abbr > "$HOME/.config/fish/conf.d/abbrs.fish" 4 | end 5 | -------------------------------------------------------------------------------- /functions/add-sourcehut-remote.fish: -------------------------------------------------------------------------------- 1 | function add-sourcehut-remote 2 | git remote add origin git@git.sr.ht:~$USER/(curdir | string replace ' ' -) 3 | end 4 | -------------------------------------------------------------------------------- /functions/album.fish: -------------------------------------------------------------------------------- 1 | function album --argument the_album 2 | set script (echo "\ 3 | for f in \"$the_album\"*.{flac,mp3,m4a,wav} 4 | echo \"\$f\" 5 | afplay \"\$f\" 6 | end 7 | ") 8 | begin 9 | fish -c "$script" 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /functions/apt-description.fish: -------------------------------------------------------------------------------- 1 | function apt-description --argument package 2 | apt-cache show $package | sed '/^$/Q' | grep --color=never -E '^Description(-en)?:|^ ' 3 | end 4 | -------------------------------------------------------------------------------- /functions/backup.fish: -------------------------------------------------------------------------------- 1 | function backup --argument filename 2 | cp $filename $filename.bak 3 | end 4 | -------------------------------------------------------------------------------- /functions/capitalize.fish: -------------------------------------------------------------------------------- 1 | function capitalize 2 | read text 3 | set first (echo $text | cut -c 1 | string upper) 4 | set rest (echo $text | cut -c 2-) 5 | echo $first$rest 6 | end 7 | -------------------------------------------------------------------------------- /functions/char-count.fish: -------------------------------------------------------------------------------- 1 | function char-count 2 | wc -c | string trim 3 | end 4 | -------------------------------------------------------------------------------- /functions/clean-unzip.fish: -------------------------------------------------------------------------------- 1 | function clean-unzip --argument zipfile 2 | if not test (echo $zipfile | string sub --start=-4) = .zip 3 | echo (status function): argument must be a zipfile 4 | return 1 5 | end 6 | 7 | if is-clean-zip $zipfile 8 | unzip $zipfile 9 | else 10 | set folder_name (echo $zipfile | trim-right '.zip') 11 | set target (basename $folder_name) 12 | mkdir $target || return 1 13 | unzip $zipfile -d $target 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /functions/clip-pwd.fish: -------------------------------------------------------------------------------- 1 | function clip-pwd 2 | pwd | unexpand-home-tilde | fish_clipboard_copy 3 | end 4 | -------------------------------------------------------------------------------- /functions/clip.fish: -------------------------------------------------------------------------------- 1 | function clip 2 | echo -n $argv | unexpand-home-tilde | fish_clipboard_copy 3 | end 4 | -------------------------------------------------------------------------------- /functions/clone-cd.fish: -------------------------------------------------------------------------------- 1 | function clone-cd --argument url _destination 2 | set destination (default $_destination (repo-from-url $url)) 3 | 4 | _clone-cd $url $destination 5 | end 6 | -------------------------------------------------------------------------------- /functions/clone-shallow-cd.fish: -------------------------------------------------------------------------------- 1 | function clone-shallow-cd --argument url _destination 2 | set destination (default $_destination (repo-from-url $url)) 3 | 4 | _clone-cd $url $destination --depth=1 5 | end 6 | -------------------------------------------------------------------------------- /functions/coln.fish: -------------------------------------------------------------------------------- 1 | function coln 2 | awk '{print $'$argv[1]'}' 3 | end 4 | -------------------------------------------------------------------------------- /functions/confirm.fish: -------------------------------------------------------------------------------- 1 | function confirm 2 | read -P "$argv> " response 3 | contains $response y Y yes YES 4 | end 5 | -------------------------------------------------------------------------------- /functions/copy.fish: -------------------------------------------------------------------------------- 1 | function copy 2 | set count (count $argv | tr -d \n) 3 | if test "$count" = 2; and test -d "$argv[1]" 4 | set from (echo $argv[1] | trim-right /) 5 | set to (echo $argv[2]) 6 | mkdir -p (basename $to) 7 | command cp -i -r $from $to 8 | else 9 | command cp -i $argv 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /functions/create-file.fish: -------------------------------------------------------------------------------- 1 | function create-file --argument target 2 | mkdir -p (dirname $target) 3 | touch $target 4 | end 5 | -------------------------------------------------------------------------------- /functions/curdir.fish: -------------------------------------------------------------------------------- 1 | function curdir 2 | basename $PWD 3 | end 4 | -------------------------------------------------------------------------------- /functions/default.fish: -------------------------------------------------------------------------------- 1 | function default --argument val default 2 | if not string-empty $val 3 | echo $val 4 | else 5 | echo $default 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /functions/describe.fish: -------------------------------------------------------------------------------- 1 | function describe --argument command 2 | for flag in $argv[2..] 3 | man $command | sed -ne '/[[:space:]]*'$flag'/,/^$/p' 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /functions/domain.fish: -------------------------------------------------------------------------------- 1 | function domain 2 | read url 3 | echo $url | trim-scheme | cut -d / -f 1 4 | end 5 | -------------------------------------------------------------------------------- /functions/dtoh.fish: -------------------------------------------------------------------------------- 1 | function dtoh --argument number 2 | echo 'obase=16; '$number | bc 3 | end 4 | -------------------------------------------------------------------------------- /functions/eat.fish: -------------------------------------------------------------------------------- 1 | function eat --argument dir 2 | set files_to_move (find $dir -maxdepth 1 -not -path $dir) 3 | 4 | for f in $files_to_move 5 | set filename (echo $f | string replace $dir '' | trim-left /) 6 | if file-exists ./$filename 7 | echo "eat: file would be overwritten: ./$filename" 8 | return 1 9 | end 10 | end 11 | 12 | set target (dirname $dir) 13 | 14 | for f in $files_to_move 15 | mv $f $target 16 | end 17 | 18 | rmdir $dir 19 | end 20 | -------------------------------------------------------------------------------- /functions/echo-variable.fish: -------------------------------------------------------------------------------- 1 | function echo-variable --no-scope-shadowing 2 | if set -q -- $argv 3 | set varname $argv 4 | else 5 | set varname (echo $argv | string upper) 6 | end 7 | eval 'echo $'$varname 8 | end 9 | -------------------------------------------------------------------------------- /functions/edit-git.fish: -------------------------------------------------------------------------------- 1 | function edit-git 2 | $EDITOR (git-changing-files | head -1) 3 | end 4 | -------------------------------------------------------------------------------- /functions/edit-gitconfig.fish: -------------------------------------------------------------------------------- 1 | function edit-gitconfig 2 | $EDITOR (git root)/.git/config 3 | end 4 | -------------------------------------------------------------------------------- /functions/edit-readme.fish: -------------------------------------------------------------------------------- 1 | function edit-readme 2 | set root (git root) 3 | for option in $root/README.md $root/README 4 | if file-exists $option 5 | $EDITOR $option 6 | return 7 | end 8 | end 9 | echo No README.md / README found 10 | return 1 11 | end 12 | -------------------------------------------------------------------------------- /functions/edit.fish: -------------------------------------------------------------------------------- 1 | function edit 2 | if string-empty $argv 3 | eval "$editor_command" 4 | return 5 | end 6 | 7 | $EDITOR $argv 8 | end 9 | -------------------------------------------------------------------------------- /functions/ensure-trailing-newline.fish: -------------------------------------------------------------------------------- 1 | function ensure-trailing-newline --argument file 2 | if not file-exists $file 3 | return 4 | end 5 | 6 | if test -n (tail -c 1 $file) 7 | echo >> $file 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /functions/ensuredb.fish: -------------------------------------------------------------------------------- 1 | function ensuredb --argument db 2 | psql -d $db -c "select pg_terminate_backend(pid) from pg_stat_activity where pid <> pg_backend_pid() and datname = '$db'" >/dev/null 3 | dropdb $db >/dev/null 4 | createdb $db 5 | end 6 | -------------------------------------------------------------------------------- /functions/equals.fish: -------------------------------------------------------------------------------- 1 | function equals --argument a b 2 | test "$a" = "$b" 3 | end 4 | -------------------------------------------------------------------------------- /functions/expand-home-tilde.fish: -------------------------------------------------------------------------------- 1 | function expand-home-tilde 2 | cat - | string replace '~' $HOME 3 | end 4 | -------------------------------------------------------------------------------- /functions/fast-sync-repos.fish: -------------------------------------------------------------------------------- 1 | function fast-sync-repos 2 | cat ~/.repos.txt | xargs -I@ -P (nproc) fish -c 'sync-repo @' 3 | end 4 | -------------------------------------------------------------------------------- /functions/file-exists.fish: -------------------------------------------------------------------------------- 1 | function file-exists --argument file 2 | test -e $file 3 | end 4 | -------------------------------------------------------------------------------- /functions/fingerprint.fish: -------------------------------------------------------------------------------- 1 | function fingerprint --argument keyfile 2 | ssh-keygen -l -E md5 -f $keyfile 3 | end 4 | -------------------------------------------------------------------------------- /functions/fish_greeting.fish: -------------------------------------------------------------------------------- 1 | function fish_greeting 2 | echo '>*)))>< <*)))>< "how is your posture?"' 3 | end 4 | -------------------------------------------------------------------------------- /functions/fish_prompt.fish: -------------------------------------------------------------------------------- 1 | function fish_prompt --description 'Write out the prompt' 2 | set -l last_status $status 3 | 4 | # if set -q VIRTUAL_ENV 5 | # echo -n -s (set_color -b blue white) "(" (basename "$VIRTUAL_ENV") ")" (set_color normal) " " 6 | # end 7 | 8 | if not set -q __fish_prompt_normal 9 | set -g __fish_prompt_normal (set_color normal) 10 | end 11 | 12 | # PWD 13 | set_color $fish_color_cwd 14 | # echo -n (prompt_pwd) 15 | if not test $PWD = $HOME 16 | echo -n (basename $PWD)' ' 17 | end 18 | set_color normal 19 | 20 | # printf '%s ' (__fish_git_prompt) 21 | if in-git-repo 22 | set stashes (git stash list | line-count) 23 | if test $stashes -gt 0 24 | printf '%s ' $stashes 25 | end 26 | end 27 | 28 | if not test $last_status -eq 0 29 | set_color $fish_color_error 30 | end 31 | 32 | if jobs -q 33 | echo -s -n (set_color -b purple) '.' (set_color normal) 34 | end 35 | 36 | if set -q VIRTUAL_ENV 37 | echo -s -n (set_color -b blue) '$' (set_color normal) 38 | else 39 | echo -s -n '$' 40 | end 41 | echo -n ' ' 42 | set_color normal 43 | end 44 | -------------------------------------------------------------------------------- /functions/funced-last.fish: -------------------------------------------------------------------------------- 1 | function funced-last 2 | set function_name (last-function-name) 3 | funced -s $function_name 4 | end 5 | -------------------------------------------------------------------------------- /functions/funcsave-last.fish: -------------------------------------------------------------------------------- 1 | function funcsave-last 2 | set function_name (last-function-name) 3 | funcsave $function_name 4 | end 5 | -------------------------------------------------------------------------------- /functions/git-add.fish: -------------------------------------------------------------------------------- 1 | function git-add 2 | if not string-empty $argv 3 | # TODO `...` could be replaced with ../.. as abbr 4 | if test "$argv" = '...' 5 | git add ../.. 6 | else 7 | git add $argv 8 | end 9 | else 10 | git add . 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /functions/git-changing-files.fish: -------------------------------------------------------------------------------- 1 | function git-changing-files 2 | git diff --name-only 3 | git untracked-files 4 | end 5 | -------------------------------------------------------------------------------- /functions/git-commit.fish: -------------------------------------------------------------------------------- 1 | function git-commit 2 | function staged-files-matching-filter --argument filter 3 | git diff --cached --name-only --diff-filter=$filter | string split0 4 | end 5 | 6 | function count-files 7 | awk NF | wc -l | string trim 8 | end 9 | 10 | if string-empty $argv 11 | set new_files (staged-files-matching-filter A) 12 | set n_new_files (echo -n "$new_files" | count-files) 13 | 14 | set deleted_files (staged-files-matching-filter D) 15 | set n_deleted_files (echo -n "$deleted_files" | count-files) 16 | 17 | set updated_files (staged-files-matching-filter M) 18 | set n_updated_files (echo -n "$updated_files" | count-files) 19 | 20 | set renamed_files (staged-files-matching-filter R) 21 | set n_renamed_files (echo -n "$renamed_files" | count-files) 22 | 23 | set n_changed_files (math $n_new_files + $n_deleted_files + $n_updated_files + $n_renamed_files) 24 | 25 | switch $n_changed_files 26 | case 0 27 | echo "Nothing staged" 28 | case 1 29 | if [ $n_new_files = 1 ] 30 | git commit -m "Add $new_files" 31 | else if [ $n_deleted_files = 1 ] 32 | git commit -m "Delete $deleted_files" 33 | else if [ $n_updated_files = 1 ] 34 | git commit -m "Update $updated_files" 35 | else if [ $n_renamed_files = 1 ] 36 | git diff --cached --name-status | read _unused from to 37 | git commit -m "Rename $from to $to" 38 | end 39 | case '*' 40 | echo "Multiple files changed; add a commit message" 41 | end 42 | else 43 | git commit -m (echo $argv) 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /functions/git-protocol-https-to-git.fish: -------------------------------------------------------------------------------- 1 | function git-protocol-https-to-git 2 | sed -i 's|https://\([[:alnum:]\.]\+\)/|git@\1:|' (git rev-parse --show-toplevel)/.git/config 3 | end 4 | -------------------------------------------------------------------------------- /functions/git-restage.fish: -------------------------------------------------------------------------------- 1 | function git-restage 2 | git add (git diff --name-only --cached) 3 | end 4 | -------------------------------------------------------------------------------- /functions/git-restore.fish: -------------------------------------------------------------------------------- 1 | function git-restore 2 | if not string-empty $argv 3 | git restore $argv 4 | else 5 | git restore . 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /functions/gitignore.fish: -------------------------------------------------------------------------------- 1 | function gitignore 2 | set gitignore_file (git root)/.gitignore || return 1 3 | ensure-trailing-newline $gitignore_file 4 | for pattern in $argv 5 | echo $pattern >> $gitignore_file 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /functions/goto-definition.fish: -------------------------------------------------------------------------------- 1 | function goto-definition --argument symbol 2 | set def (rg --type-not h --line-number --no-heading --multiline '(int|def|class)\s+'$symbol | tail -1) 3 | set file (echo $def | cut -d : -f 1) 4 | set line (echo $def | cut -d : -f 2) 5 | $EDITOR $file +$line 6 | end 7 | -------------------------------------------------------------------------------- /functions/guess-git-url.fish: -------------------------------------------------------------------------------- 1 | function guess-git-url --argument value 2 | if string match -qe https:// $value || string match -qe git@ $value 3 | echo $value 4 | else if string match -qe / $value 5 | echo https://github.com/$value 6 | else 7 | # Maybe it's a https://github.com/fish-shell/fish-shell type of url 8 | echo https://github.com/$value/$value 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /functions/hub.fish: -------------------------------------------------------------------------------- 1 | function hub 2 | set url (git config --get remote.origin.url) 3 | set https_url (echo $url | string replace -r 'git@(.+):' 'https://$1/') 4 | $BROWSER $https_url 5 | end 6 | -------------------------------------------------------------------------------- /functions/in-git-repo.fish: -------------------------------------------------------------------------------- 1 | function in-git-repo 2 | equals (git rev-parse --git-dir 2> /dev/null) true 3 | end 4 | -------------------------------------------------------------------------------- /functions/init-sourcehut.fish: -------------------------------------------------------------------------------- 1 | function init-sourcehut --argument _visibility 2 | set visibility (default $_visibility private) 3 | add-sourcehut-remote 4 | and git push origin -o visibility=$visibility --set-upstream 5 | end 6 | -------------------------------------------------------------------------------- /functions/ip-addr.fish: -------------------------------------------------------------------------------- 1 | function ip-addr 2 | curl api.ipify.org 3 | end 4 | -------------------------------------------------------------------------------- /functions/is-clean-zip.fish: -------------------------------------------------------------------------------- 1 | function is-clean-zip --argument zipfile 2 | if string-empty $zipfile 3 | echo 'is-clean-zip: missing filename' >&2 4 | return 1 5 | end 6 | 7 | set summary (zip -sf $zipfile | string split0) 8 | set first_file (echo $summary | row 2 | string trim) 9 | set first_file_last_char (echo $first_file | string sub --start=-1) 10 | set n_files (echo $summary | awk NF | tail -1 | coln 2) 11 | test $n_files = 1 && test $first_file_last_char = / 12 | end 13 | -------------------------------------------------------------------------------- /functions/is-dir.fish: -------------------------------------------------------------------------------- 1 | function is-dir --argument dir 2 | test -d $dir 3 | end 4 | -------------------------------------------------------------------------------- /functions/is-symlink.fish: -------------------------------------------------------------------------------- 1 | function is-symlink --argument file --argument extra 2 | if not string-empty $extra 3 | echo 'is-symlink: too many arguments' >&2 4 | return 1 5 | end 6 | test -L (echo $file | trim-trailing-slash) 7 | end 8 | -------------------------------------------------------------------------------- /functions/isodate.fish: -------------------------------------------------------------------------------- 1 | function isodate 2 | date +%Y-%m-%d 3 | end 4 | -------------------------------------------------------------------------------- /functions/isodatetime.fish: -------------------------------------------------------------------------------- 1 | function isodatetime 2 | date +"%Y-%m-%dT%H:%M:%S" 3 | end 4 | -------------------------------------------------------------------------------- /functions/krui.fish: -------------------------------------------------------------------------------- 1 | function krui 2 | vlc -I rc http://krui.student-services.uiowa.edu:8000/listen 3 | end 4 | -------------------------------------------------------------------------------- /functions/last-col.fish: -------------------------------------------------------------------------------- 1 | function last-col 2 | awk '{print $NF}' 3 | end 4 | -------------------------------------------------------------------------------- /functions/last-download.fish: -------------------------------------------------------------------------------- 1 | function last-download 2 | echo ~/Downloads/(ls -t ~/Downloads/ | head -1) 3 | end 4 | -------------------------------------------------------------------------------- /functions/last-function-name.fish: -------------------------------------------------------------------------------- 1 | function last-function-name --argument index 2 | history --prefix func | grep -w -E '^(functions?|funced)' | string replace -- ' -s ' ' ' | awk 'NF>1' | row 1 | coln 2 3 | end 4 | -------------------------------------------------------------------------------- /functions/last-image-built.fish: -------------------------------------------------------------------------------- 1 | function last-image-built 2 | podman images --format "{{.ID}} {{.CreatedAt}}" | sort -rk 2 | awk 'NR==1{print $1}' 3 | end 4 | -------------------------------------------------------------------------------- /functions/lima-ssh.fish: -------------------------------------------------------------------------------- 1 | function lima-ssh 2 | set default_status (limactl list default -f '{{.Status}}' 2> /dev/null) 3 | 4 | if test $status != 0 || string-empty $default_status 5 | limactl start --tty=false 6 | end 7 | 8 | if equals $default_status Stopped 9 | limactl start 10 | end 11 | 12 | lima 13 | end 14 | -------------------------------------------------------------------------------- /functions/lima-vnc.fish: -------------------------------------------------------------------------------- 1 | function lima-vnc 2 | set vnc_status (limactl list vnc -f '{{.Status}}' 2> /dev/null) 3 | 4 | if test $status != 0 || string-empty $vnc_status 5 | limactl start /usr/local/share/lima/templates/experimental/vnc.yaml --tty=false 6 | end 7 | 8 | if equals $vnc_status Stopped 9 | limactl start vnc 10 | end 11 | 12 | open vnc://:(cat ~/.lima/vnc/vncpassword)@127.0.0.1:5900 13 | end 14 | -------------------------------------------------------------------------------- /functions/line-count.fish: -------------------------------------------------------------------------------- 1 | function line-count 2 | wc -l | string trim 3 | end 4 | -------------------------------------------------------------------------------- /functions/link-rc.fish: -------------------------------------------------------------------------------- 1 | function link-rc --argument file 2 | symlink $file ~ 3 | end 4 | -------------------------------------------------------------------------------- /functions/loop.fish: -------------------------------------------------------------------------------- 1 | function loop --description 'loop ' 2 | for i in (seq 1 $argv[1]) 3 | eval $argv[2..-1] 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /functions/mkdir-cd.fish: -------------------------------------------------------------------------------- 1 | function mkdir-cd --argument dir 2 | mkdir -p -- $dir 3 | and cd -- $dir 4 | end 5 | -------------------------------------------------------------------------------- /functions/move-last-download.fish: -------------------------------------------------------------------------------- 1 | function move-last-download 2 | set destination (default $argv[1] .) 3 | move ~/Downloads/(ls -t -A ~/Downloads/ | head -1) $destination 4 | end 5 | -------------------------------------------------------------------------------- /functions/move.fish: -------------------------------------------------------------------------------- 1 | function move 2 | set from $argv[1] 3 | if is-symlink $from; and string match --quiet --regex --entire '/$' $from 4 | echo move: `from` argument '"'$from'"' is a symlink with a trailing slash. 5 | echo move: to rename a symlink, remove the trailing slash from the argument. 6 | return 1 7 | end 8 | mv -i $argv 9 | end 10 | -------------------------------------------------------------------------------- /functions/music.fish: -------------------------------------------------------------------------------- 1 | function music --argument dir 2 | for f in $dir/*.{flac,mp3,m4a,wav} 3 | mplayer -vo null $f || break 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /functions/new.fish: -------------------------------------------------------------------------------- 1 | function new --argument project 2 | mkdir $project \ 3 | && cd $project \ 4 | && git init \ 5 | && echo "# $project" >README.md \ 6 | && git add README.md \ 7 | && git commit -m 'Initial commit' 8 | end 9 | -------------------------------------------------------------------------------- /functions/pager.fish: -------------------------------------------------------------------------------- 1 | function pager 2 | less --no-init --quit-if-one-screen --RAW-CONTROL-CHARS 3 | end 4 | -------------------------------------------------------------------------------- /functions/paste_avoiding_double_git_clone.fish: -------------------------------------------------------------------------------- 1 | function paste_avoiding_double_git_clone 2 | set command (commandline | string trim) 3 | set clipboard (fish_clipboard_paste | string collect -N) 4 | if string match -qr '(clone-cd|git clone)' "$command" 5 | set clipboard (echo $clipboard | string replace 'git clone ' '') 6 | end 7 | commandline -i -- $clipboard 8 | end 9 | -------------------------------------------------------------------------------- /functions/play-random.fish: -------------------------------------------------------------------------------- 1 | function play-random 2 | play -q (random-file) 3 | end 4 | -------------------------------------------------------------------------------- /functions/play.fish: -------------------------------------------------------------------------------- 1 | function play 2 | mplayer -vo null "$argv" 3 | end 4 | -------------------------------------------------------------------------------- /functions/port-ps.fish: -------------------------------------------------------------------------------- 1 | function port-ps --argument port 2 | sudo lsof -i :$port 3 | end 4 | -------------------------------------------------------------------------------- /functions/project-replace.fish: -------------------------------------------------------------------------------- 1 | function project-replace --argument a b extension 2 | for f in (fd -t file $extension) 3 | sed -i "s/$a/$b/g" $f 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /functions/pw.fish: -------------------------------------------------------------------------------- 1 | function pw --wraps=pass --argument url 2 | set domain_raw $url 3 | 4 | set domain (echo $domain_raw | domain | trim-left www.) 5 | 6 | if file-exists ~/.password-store/$domain.gpg 7 | pass -c $domain 8 | return 0 9 | end 10 | 11 | # TODO doesn't handle subdirectories 12 | set matches (find ~/.password-store -maxdepth 1 -type f | grep $domain) 13 | if test (echo $matches | word-count) -gt 1 14 | echo "pw: more than 1: $matches" 15 | return 1 16 | end 17 | 18 | set match (echo $matches | trim-right .gpg | trim-left ~/.password-store/) 19 | if not string-empty $match 20 | pass -c $match 21 | else 22 | pass generate -c $domain 23 | git -C ~/.password-store push 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /functions/query.fish: -------------------------------------------------------------------------------- 1 | function query 2 | open 'https://duckduckgo.com/?q='(echo $argv | string replace --all ' ' '%20') 3 | end 4 | -------------------------------------------------------------------------------- /functions/random-file.fish: -------------------------------------------------------------------------------- 1 | function random-file 2 | find . -type f | shuf -n1 3 | end 4 | -------------------------------------------------------------------------------- /functions/readpass.fish: -------------------------------------------------------------------------------- 1 | function readpass --argument var 2 | read --silent localvar 3 | export $var=$localvar 4 | end 5 | -------------------------------------------------------------------------------- /functions/remove.fish: -------------------------------------------------------------------------------- 1 | function remove 2 | set original_args $argv 3 | 4 | argparse r f -- $argv 5 | 6 | if not set -q _flag_r || set -q _flag_f 7 | rm $original_args 8 | return 9 | end 10 | 11 | function confirm-remove --argument dir 12 | set display_dir (echo $dir | unexpand-home-tilde) 13 | 14 | if confirm "Remove .git directory $display_dir?" 15 | rm -rf $dir 16 | return 17 | end 18 | 19 | return 1 20 | end 21 | 22 | for f in $argv 23 | if not file-exists $f 24 | echo "remove: $f: No such file or directory" 25 | return 1 26 | end 27 | 28 | set gitdirs (find $f -mindepth 1 -name .git) 29 | for gitdir in $gitdirs 30 | if not confirm-remove $gitdir 31 | echo 'remove: cancelling.' 32 | return 1 33 | end 34 | end 35 | end 36 | 37 | # could be cool to allow remove . 38 | rm $original_args 39 | end 40 | -------------------------------------------------------------------------------- /functions/rename-pwd.fish: -------------------------------------------------------------------------------- 1 | function rename-pwd --argument new_name 2 | set current (basename $PWD) 3 | cd .. 4 | mv -n $current $new_name 5 | cd $new_name 6 | end 7 | -------------------------------------------------------------------------------- /functions/renamedb.fish: -------------------------------------------------------------------------------- 1 | function renamedb --argument from to 2 | echo "alter database \"$from\" rename to \"$to\"" | psql -d template1 3 | end 4 | -------------------------------------------------------------------------------- /functions/repeat.fish: -------------------------------------------------------------------------------- 1 | function repeat 2 | while true 3 | $argv 4 | sleep 1 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /functions/repo-from-url.fish: -------------------------------------------------------------------------------- 1 | function repo-from-url --argument url 2 | basename $url | trim-right .git 3 | end 4 | -------------------------------------------------------------------------------- /functions/reset-date.fish: -------------------------------------------------------------------------------- 1 | function reset-date 2 | sudo sntp -sS time.apple.com 3 | end 4 | -------------------------------------------------------------------------------- /functions/restore.fish: -------------------------------------------------------------------------------- 1 | function restore --argument file 2 | mv $file (echo $file | sed s/.bak//) 3 | end 4 | -------------------------------------------------------------------------------- /functions/retry.fish: -------------------------------------------------------------------------------- 1 | function retry 2 | while true 3 | $argv && break 4 | sleep 1 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /functions/rm-pycache.fish: -------------------------------------------------------------------------------- 1 | function rm-pycache 2 | find . -type f -name '*.py[co]' -delete -o -type d -name __pycache__ -delete 3 | end 4 | -------------------------------------------------------------------------------- /functions/rmdir-..fish: -------------------------------------------------------------------------------- 1 | function rmdir-. 2 | set dir (pwd) 3 | rmdir $dir && cd .. 4 | end 5 | -------------------------------------------------------------------------------- /functions/row.fish: -------------------------------------------------------------------------------- 1 | function row --argument index 2 | sed -n "$index p" 3 | end 4 | -------------------------------------------------------------------------------- /functions/runjava.fish: -------------------------------------------------------------------------------- 1 | function runjava --argument-names file 2 | javac $file 3 | java (string replace .java '' $file) $argv[2..0] 4 | end 5 | -------------------------------------------------------------------------------- /functions/search.fish: -------------------------------------------------------------------------------- 1 | function search 2 | if not file-exists $RIPGREP_CONFIG_PATH 3 | echo search: warning: '$RIPGREP_CONFIG_PATH' not configured properly 4 | end 5 | rg "$argv" 6 | end 7 | -------------------------------------------------------------------------------- /functions/shutdown.fish: -------------------------------------------------------------------------------- 1 | function shutdown 2 | if not string-empty $argv 3 | command sudo shutdown $argv 4 | else 5 | command sudo shutdown -h now 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /functions/skip-lines.fish: -------------------------------------------------------------------------------- 1 | function skip-lines --argument n 2 | tail +(math 1 + $n) 3 | end 4 | -------------------------------------------------------------------------------- /functions/song.fish: -------------------------------------------------------------------------------- 1 | function song 2 | fd --follow --full-path '.*'(echo "$argv" | string replace --all ' ' '.*')".*.(m4a|mp3|flac|wav)" ~/Music/ | sort | read -z songs 3 | set n_songs (echo -n "$songs" | wc -l | coln 1) 4 | if test $n_songs -gt 1 5 | echo more than 1 6 | echo -n "$songs" 7 | echo -n "$songs" | while read -l song 8 | play "$song" 9 | end 10 | else 11 | if test -z "$songs" 12 | echo empty 13 | else 14 | play (echo $songs | head -1) 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /functions/songs.fish: -------------------------------------------------------------------------------- 1 | function songs 2 | for f in $argv/*.{mp3,flac,m4a} 3 | afplay $f 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /functions/ssh-segfault.fish: -------------------------------------------------------------------------------- 1 | function ssh-segfault 2 | expect -c ' 3 | log_user 0 4 | spawn ssh root@segfault.net 5 | expect "root@segfault.net\'s password: " 6 | send "segfault\n" 7 | interact' 8 | end 9 | -------------------------------------------------------------------------------- /functions/startswith.fish: -------------------------------------------------------------------------------- 1 | function startswith --argument prefix string 2 | string match -qr "^$prefix" $string 3 | end 4 | -------------------------------------------------------------------------------- /functions/string-empty.fish: -------------------------------------------------------------------------------- 1 | function string-empty --argument val 2 | test "$val" = '' 3 | end 4 | -------------------------------------------------------------------------------- /functions/symlink.fish: -------------------------------------------------------------------------------- 1 | function symlink --argument _from _to 2 | if string-empty $_from; or string-empty $_to 3 | echo "symlink: must provide from and to arguments" 4 | return 1 5 | end 6 | 7 | set abs_to (realpath "$_to") || return 1 8 | 9 | if test -d $_to && not test -d $_from 10 | set to "$abs_to/"(basename $_from) 11 | else 12 | set to "$abs_to" 13 | end 14 | 15 | set from (realpath "$_from") || return 1 16 | 17 | if not test -e $from 18 | echo "symlink: `from` argument '$from' does not exist" >&2 19 | return 1 20 | end 21 | 22 | ln -s $from $to 23 | end 24 | -------------------------------------------------------------------------------- /functions/symlinks.fish: -------------------------------------------------------------------------------- 1 | function symlinks --argument _path 2 | set path (default (echo $_path | trim-right /) .) 3 | find $path -type l -maxdepth 1 4 | end 5 | -------------------------------------------------------------------------------- /functions/sync-repo.fish: -------------------------------------------------------------------------------- 1 | function sync-repo --argument repo 2 | set repo_branch (git -C "$repo" rev-parse --abbrev-ref HEAD) || return 1 3 | 4 | git -C "$repo" status -sb 5 | git -C "$repo" pull origin $repo_branch 6 | 7 | set commits_behind (git -C "$repo" rev-list --count origin/$repo_branch..HEAD) 8 | 9 | and if test $commits_behind -gt 0 10 | git -C "$repo" push 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /functions/sync-repos.fish: -------------------------------------------------------------------------------- 1 | function sync-repos 2 | if not file-exists ~/.repos.txt 3 | echo Put the repositories to sync in ~/.repos.txt 4 | return 1 5 | end 6 | 7 | for path in (cat ~/.repos.txt | grep -v '^#') 8 | set repo (echo $path | expand-home-tilde) 9 | 10 | if not is-dir $repo 11 | echo $repo not present 12 | continue 13 | end 14 | 15 | echo $path 16 | if not sync-repo $repo 17 | echo 'sync-repos: canceling' 18 | return 1 19 | end 20 | echo 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /functions/take.fish: -------------------------------------------------------------------------------- 1 | function take --argument number 2 | head -$number 3 | end 4 | -------------------------------------------------------------------------------- /functions/tar-cd.fish: -------------------------------------------------------------------------------- 1 | function tar-cd --argument tarfile 2 | tar -xvzf $tarfile 3 | cd (echo $tarfile | trim-right '.tar.gz') 4 | end 5 | -------------------------------------------------------------------------------- /functions/tgz.fish: -------------------------------------------------------------------------------- 1 | function tgz --argument contents _destination 2 | set default_archive_name (echo $contents | trim-trailing-slash)".tar.gz" 3 | set destination (default $_destination $default_archive_name) 4 | tar --create --gzip --file=$destination $contents 5 | end 6 | -------------------------------------------------------------------------------- /functions/title-case.fish: -------------------------------------------------------------------------------- 1 | function title-case 2 | read text 3 | set words (string split " " -- $text) 4 | set output (echo $words[1] | capitalize) 5 | for word in $words[2..-1] 6 | set output "$output "(echo $word | capitalize) 7 | end 8 | echo $output 9 | end 10 | -------------------------------------------------------------------------------- /functions/tree.fish: -------------------------------------------------------------------------------- 1 | function tree 2 | command tree --noreport $argv 3 | end 4 | -------------------------------------------------------------------------------- /functions/trim-left.fish: -------------------------------------------------------------------------------- 1 | function trim-left --argument str 2 | sed "s|^$str||" 3 | end 4 | -------------------------------------------------------------------------------- /functions/trim-right.fish: -------------------------------------------------------------------------------- 1 | function trim-right --argument char 2 | sed "s|$char\$||" 3 | end 4 | -------------------------------------------------------------------------------- /functions/trim-scheme.fish: -------------------------------------------------------------------------------- 1 | function trim-scheme 2 | sed -r 's|.+://||' 3 | end 4 | -------------------------------------------------------------------------------- /functions/trim-trailing-slash.fish: -------------------------------------------------------------------------------- 1 | function trim-trailing-slash 2 | read str 3 | string replace -r '/$' '' -- $str 4 | end 5 | -------------------------------------------------------------------------------- /functions/tunnel.fish: -------------------------------------------------------------------------------- 1 | function tunnel --argument host port 2 | echo "> ssh -NL $port:$host:$port $host" 3 | 4 | ssh -NL $port:$host:$port $host & 5 | set ssh_pid $last_pid 6 | sleep .5 7 | 8 | # Check if ssh is still running; if not, exit 9 | kill -0 $ssh_pid 2> /dev/null || return 1 10 | 11 | retry sh -c "nc -z localhost $port 2> /dev/null" 12 | echo "Opening http://localhost:$port in browser..." 13 | open http://localhost:$port 14 | trap "kill $ssh_pid" SIGINT 15 | echo "Hit CONTROL-C to stop." 16 | wait $ssh_pid 17 | end 18 | -------------------------------------------------------------------------------- /functions/uid.fish: -------------------------------------------------------------------------------- 1 | function uid 2 | id -u (whoami) 3 | end 4 | -------------------------------------------------------------------------------- /functions/unexpand-home-tilde.fish: -------------------------------------------------------------------------------- 1 | function unexpand-home-tilde 2 | cat - | string replace $HOME '~' 3 | end 4 | -------------------------------------------------------------------------------- /functions/unsymlink.fish: -------------------------------------------------------------------------------- 1 | function unsymlink --argument _file 2 | set file (echo $_file | trim-trailing-slash) 3 | 4 | if not is-symlink $file 5 | echo "unsymlink: not a symlink: $file" 6 | return 1 7 | end 8 | 9 | # Use unlink because it can't do things like recursive deletes 10 | unlink $file 11 | end 12 | -------------------------------------------------------------------------------- /functions/until-char.fish: -------------------------------------------------------------------------------- 1 | function until-char --argument delimiter 2 | cut -d $delimiter -f 1 3 | end 4 | -------------------------------------------------------------------------------- /functions/unzip-cd.fish: -------------------------------------------------------------------------------- 1 | function unzip-cd --argument zipfile 2 | clean-unzip $zipfile && cd (echo $zipfile | trim-right .zip) 3 | end 4 | -------------------------------------------------------------------------------- /functions/url-size.fish: -------------------------------------------------------------------------------- 1 | function url-size --description 'Print the human readable size of a URL.' --argument url 2 | curl -s -I $url | grep -i content-length | tr -d '\r' | coln 2 | numfmt --to=iec 3 | end 4 | -------------------------------------------------------------------------------- /functions/ver.fish: -------------------------------------------------------------------------------- 1 | function ver --argument program 2 | if contains $program ssh tmux 3 | $program -V 4 | else if equals $program java 5 | $program -version 6 | else if equals $program go 7 | $program version 8 | else if contains $program vi vim 9 | # vim version info is way too verbose 10 | $program --version | take 1 11 | else 12 | $program --version 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /functions/vim-call.fish: -------------------------------------------------------------------------------- 1 | function vim-call --argument command 2 | if string-empty $VIM_TERMINAL 3 | echo not in vim terminal, exiting 4 | return 1 5 | end 6 | echo \x1b]51';["call", "'$command'", ["'$argv[2]'"]]'\x07 7 | end 8 | -------------------------------------------------------------------------------- /functions/vim-client.fish: -------------------------------------------------------------------------------- 1 | function vim-client --argument file 2 | if string-empty $VIM_TERMINAL 3 | echo not in vim terminal 4 | return 1 5 | end 6 | 7 | echo -e "\033]51;[\"drop\", \"$file\"]\007" 8 | end 9 | -------------------------------------------------------------------------------- /functions/vim-plugin.fish: -------------------------------------------------------------------------------- 1 | function vim-plugin --argument url 2 | set target (repo-from-url $url) 3 | git clone --depth=1 $url ~/.vim/pack/vendor/opt/$target 4 | and vim -c ":helptags ALL | q" 5 | end 6 | -------------------------------------------------------------------------------- /functions/viw.fish: -------------------------------------------------------------------------------- 1 | function viw --argument script 2 | vim (which $script) 3 | end 4 | -------------------------------------------------------------------------------- /functions/watchdns.fish: -------------------------------------------------------------------------------- 1 | function watchdns 2 | sudo tcpdump -vvi any port 53 3 | end 4 | -------------------------------------------------------------------------------- /functions/wifi-network-name.fish: -------------------------------------------------------------------------------- 1 | function wifi-network-name 2 | networksetup -getairportnetwork en0 | awk '{print $4}' 3 | end 4 | -------------------------------------------------------------------------------- /functions/wifi-password.fish: -------------------------------------------------------------------------------- 1 | function wifi-password 2 | security find-generic-password -wa (wifi-network-name) 3 | end 4 | -------------------------------------------------------------------------------- /functions/wifi-reset.fish: -------------------------------------------------------------------------------- 1 | function wifi-reset 2 | networksetup -setairportpower en0 off 3 | networksetup -setairportpower en0 on 4 | end 5 | -------------------------------------------------------------------------------- /functions/wikinews.fish: -------------------------------------------------------------------------------- 1 | function wikinews 2 | open https://en.wikipedia.org/wiki/Portal:Current_events/(date +'%Y_%B_%-d') 3 | end 4 | -------------------------------------------------------------------------------- /functions/wip.fish: -------------------------------------------------------------------------------- 1 | function wip 2 | if git diff --cached --quiet 3 | git add . 4 | end 5 | git commit --no-verify -m "wip $argv" 6 | end 7 | -------------------------------------------------------------------------------- /functions/word-count.fish: -------------------------------------------------------------------------------- 1 | function word-count 2 | wc -w | string trim 3 | end 4 | -------------------------------------------------------------------------------- /test/test_readme.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | with open('README.md') as f: 4 | readme = f.read() 5 | 6 | function_lines = [ 7 | line for line in readme.splitlines() 8 | if line.startswith('###') 9 | ] 10 | 11 | functions = [line.split()[1].strip('`') for line in function_lines] 12 | 13 | missing_functions = [ 14 | function 15 | for function in functions 16 | if not os.path.exists(f'functions/{function}.fish') 17 | ] 18 | 19 | if missing_functions: 20 | print(f'Missing functions: {missing_functions}') 21 | -------------------------------------------------------------------------------- /test/test_symlink.fish: -------------------------------------------------------------------------------- 1 | # symlink should not allow linking from a nonexistant file 2 | # If given a nonexistant file, it should exit with status 1 and print a message to stderr 3 | 4 | mkdir-cd test_symlink 5 | 6 | function __cleanup 7 | if string-empty $DEBUG 8 | rm error.txt 9 | rmdir-. 10 | end 11 | end 12 | 13 | symlink nonexistant anything 2> error.txt 14 | set result_status $status 15 | 16 | set expected_status 1 17 | 18 | if not equals $result_status $expected_status 19 | echo `symlink` status did not match expected. 20 | echo Expected status $expected_status but got $result_status 21 | __cleanup 22 | return 1 23 | end 24 | 25 | set desired_pattern "symlink: `from` argument '.*' does not exist" 26 | set output (cat error.txt) 27 | 28 | if not string match -qr $desired_pattern $output 29 | echo `symlink` output did not match expected. 30 | echo Expected: $desired_pattern 31 | echo Actual: $output 32 | __cleanup 33 | return 1 34 | end 35 | 36 | __cleanup 37 | --------------------------------------------------------------------------------