dotcli: Initial config
dotcli is my utility script for everything dotfiles. A bootstrap command is included in the README. A test suite and the necessary scaffolding for extending dotcli is also included.
This commit is contained in:
65
.claude/skills/dotfiles/DOT-CLI.md
Normal file
65
.claude/skills/dotfiles/DOT-CLI.md
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# The dot CLI
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
`dot` is defined in one file: `~/.config/fish/functions/dot.fish`. It holds
|
||||||
|
two functions:
|
||||||
|
|
||||||
|
- `dot` (`--wraps=git`) — dispatches `init` and any file found under
|
||||||
|
`~/.config/dot/commands/`, otherwise forwards everything to
|
||||||
|
`git --git-dir=~/.dotfiles --work-tree=$HOME $argv` (full passthrough).
|
||||||
|
- `__dot_init` — the bootstrap logic, inlined in the same file rather than
|
||||||
|
autoloaded separately, because it's the one subcommand that must work
|
||||||
|
before the dotfiles repo has ever been cloned onto a machine.
|
||||||
|
|
||||||
|
`dot init`:
|
||||||
|
|
||||||
|
- refuses to run if `~/.dotfiles` already exists (no re-init support)
|
||||||
|
- clones the bare repo from `--url` (default: the hardcoded Gitea remote) —
|
||||||
|
if the clone fails, it errors out; it never falls back to `git init`
|
||||||
|
- backs up any pre-existing file that checkout would clobber into
|
||||||
|
`~/.dotfiles-backup/<timestamp>/`, then retries the checkout
|
||||||
|
- explicitly sets `status.showUntrackedFiles=no` after cloning — this is a
|
||||||
|
local-only git setting, so a fresh `git clone` never carries it over
|
||||||
|
|
||||||
|
## Adding a subcommand
|
||||||
|
|
||||||
|
Beyond `init`, `dot` looks for `~/.config/dot/commands/<name>.fish`, sources
|
||||||
|
it, and calls `_dot_<name>`. These files are deliberately kept out of
|
||||||
|
`~/.config/fish/functions/` (fish's autoload path) so they never become
|
||||||
|
independently invokable top-level commands or clutter tab-completion outside
|
||||||
|
of `dot` itself.
|
||||||
|
|
||||||
|
1. Create `~/.config/dot/commands/<name>.fish` defining a `_dot_<name>`
|
||||||
|
function.
|
||||||
|
2. Confirm `dot <name>` dispatches to it. No other wiring is needed —
|
||||||
|
`~/.config/fish/completions/dot.fish` discovers new command files by
|
||||||
|
globbing that directory, and `--wraps=git` still covers raw git
|
||||||
|
subcommands.
|
||||||
|
3. Add a case to `~/.config/dot/tests/dot.fish` covering it and run
|
||||||
|
`fishtape ~/.config/dot/tests/dot.fish` until it passes.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Tests live at `~/.config/dot/tests/dot.fish`, run with
|
||||||
|
`fishtape ~/.config/dot/tests/dot.fish`. Fishtape is installed via Fisher
|
||||||
|
(`fisher install jorgebucaran/fishtape`) and tracked in
|
||||||
|
`~/.config/fish/fish_plugins` — a real, restorable dependency for developing
|
||||||
|
`dot`, but never required just to use it.
|
||||||
|
|
||||||
|
- Each scenario overrides `$HOME` (`set -gx HOME (mktemp -d)`) before calling
|
||||||
|
`dot`, so tests never touch the real `~/.dotfiles`.
|
||||||
|
- Build a throwaway bare "remote" fixture with `git init --bare` plus a
|
||||||
|
seeded commit, and explicitly set its `HEAD`
|
||||||
|
(`git --git-dir=$remote symbolic-ref HEAD refs/heads/main`). Pushing with
|
||||||
|
`git push origin HEAD:main` does **not** update the bare repo's `HEAD`
|
||||||
|
symref — skip this and a clone of the fixture can end up "on a branch yet
|
||||||
|
to be born."
|
||||||
|
- Don't use `.gitconfig` as a fake pre-existing "conflict" file in a
|
||||||
|
fixture — git parses `$HOME/.gitconfig` as its own global config on every
|
||||||
|
invocation, and garbage content there spams "key does not contain a
|
||||||
|
section" errors that drown out the real assertion. Use a harmless file
|
||||||
|
like `.bashrc` instead.
|
||||||
|
- `@test "description" <expr> <op> <expected>` mirrors fish's `test` builtin
|
||||||
|
(`-eq`, `-ne`, `=`, `-e`, `-f`, `-d`, `-n`, `-z`); `-a`/`-o` combinators
|
||||||
|
aren't supported.
|
||||||
26
.claude/skills/dotfiles/SKILL.md
Normal file
26
.claude/skills/dotfiles/SKILL.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
name: dotfiles
|
||||||
|
description: Conventions for this machine's dotfiles bare-repo setup. Use when editing any file under $HOME managed by this repo, or when extending the dot CLI (subcommands, completions, bootstrap, tests).
|
||||||
|
---
|
||||||
|
|
||||||
|
# Dotfiles
|
||||||
|
|
||||||
|
This machine's dotfiles are a bare git repo at `~/.dotfiles`, checked out with
|
||||||
|
`$HOME` as its work-tree. The `dot` fish function wraps that invocation
|
||||||
|
(`git --git-dir=~/.dotfiles --work-tree=$HOME $argv`, declared with
|
||||||
|
`--wraps=git`), so every git subcommand works through it: `dot status`,
|
||||||
|
`dot add`, `dot commit`, `dot push`, etc.
|
||||||
|
|
||||||
|
## Always add by explicit path
|
||||||
|
|
||||||
|
`status.showUntrackedFiles=no` is set locally, and `.gitignore` only excludes
|
||||||
|
`.dotfiles` itself plus OS/editor cruft — it is **not** a whitelist. That
|
||||||
|
means virtually everything under `$HOME` reads as untracked, and `git status`
|
||||||
|
deliberately hides all of it.
|
||||||
|
|
||||||
|
**Always run `dot add <specific-path>`.** Never `dot add -A`, `dot add .`, or
|
||||||
|
any wildcard add — that would try to stage the entire home directory (caches,
|
||||||
|
secrets, everything).
|
||||||
|
|
||||||
|
See [DOT-CLI.md](DOT-CLI.md) for the `dot` command's own architecture,
|
||||||
|
bootstrap logic, subcommand dispatch, and test suite.
|
||||||
0
.config/dot/commands/.gitkeep
Normal file
0
.config/dot/commands/.gitkeep
Normal file
72
.config/dot/tests/dot.fish
Normal file
72
.config/dot/tests/dot.fish
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
# Fixture: a fake bare "remote" repo with tracked dotfiles, shared read-only
|
||||||
|
# across every case below. dot init only ever clones from it, never mutates it.
|
||||||
|
set -l remote (mktemp -d)/dotfiles.git
|
||||||
|
git init -q --bare $remote
|
||||||
|
|
||||||
|
set -l seed (mktemp -d)
|
||||||
|
pushd $seed
|
||||||
|
git init -q -b main
|
||||||
|
git config user.email test@dot.fish
|
||||||
|
git config user.name dot-tests
|
||||||
|
mkdir -p .config/fish/functions
|
||||||
|
echo 'echo tracked-bashrc' >.bashrc
|
||||||
|
echo 'echo hi' >.config/fish/functions/greet.fish
|
||||||
|
git add -A
|
||||||
|
git commit -qm seed >/dev/null
|
||||||
|
git remote add origin $remote
|
||||||
|
git push -q origin HEAD:main >/dev/null 2>&1
|
||||||
|
popd
|
||||||
|
git --git-dir=$remote symbolic-ref HEAD refs/heads/main
|
||||||
|
|
||||||
|
# --- fresh bootstrap, no conflicts ---
|
||||||
|
set -gx HOME (mktemp -d)
|
||||||
|
dot init --url $remote >/dev/null 2>&1
|
||||||
|
set -l fresh_status $status
|
||||||
|
|
||||||
|
@test "dot init succeeds against a clean HOME" $fresh_status -eq 0
|
||||||
|
@test "clones the bare repo to ~/.dotfiles" -e $HOME/.dotfiles
|
||||||
|
@test "checks out tracked files onto HOME" -e $HOME/.bashrc
|
||||||
|
@test "checked-out file has the repo's content" (cat $HOME/.bashrc) = "echo tracked-bashrc"
|
||||||
|
@test "disables status.showUntrackedFiles" (git --git-dir=$HOME/.dotfiles config --local status.showuntrackedfiles) = no
|
||||||
|
|
||||||
|
dot init --url $remote >/dev/null 2>&1
|
||||||
|
set -l repeat_status $status
|
||||||
|
@test "re-running dot init refuses when already initialized" $repeat_status -eq 1
|
||||||
|
|
||||||
|
set -l passthrough_status (dot status >/dev/null 2>&1; echo $status)
|
||||||
|
@test "git passthrough still works (dot status)" $passthrough_status -eq 0
|
||||||
|
|
||||||
|
# --- a pre-existing conflicting file gets backed up, not clobbered ---
|
||||||
|
set -gx HOME (mktemp -d)
|
||||||
|
echo 'pre-existing-content' >$HOME/.bashrc
|
||||||
|
dot init --url $remote >/dev/null 2>&1
|
||||||
|
set -l conflict_status $status
|
||||||
|
|
||||||
|
@test "dot init still succeeds with a conflicting file present" $conflict_status -eq 0
|
||||||
|
@test "conflicting file ends up with the tracked content" (cat $HOME/.bashrc) = "echo tracked-bashrc"
|
||||||
|
@test "a backup directory was created" -d $HOME/.dotfiles-backup
|
||||||
|
@test "the pre-existing content was preserved in the backup" (cat $HOME/.dotfiles-backup/*/.bashrc) = "pre-existing-content"
|
||||||
|
|
||||||
|
# --- an unreachable URL never falls back to creating an empty repo ---
|
||||||
|
set -gx HOME (mktemp -d)
|
||||||
|
dot init --url /nonexistent/path.git >/dev/null 2>&1
|
||||||
|
set -l bad_url_status $status
|
||||||
|
set -l dotfiles_exists (test -e $HOME/.dotfiles; and echo yes; or echo no)
|
||||||
|
|
||||||
|
@test "dot init fails on an unreachable URL" $bad_url_status -eq 1
|
||||||
|
@test "no .dotfiles directory is left behind on failure" $dotfiles_exists = no
|
||||||
|
|
||||||
|
# --- dispatches to files under ~/.config/dot/commands/ without polluting
|
||||||
|
# the fish function namespace: the file only defines _dot_<name>, which
|
||||||
|
# only becomes known to fish once dot sources it on demand.
|
||||||
|
set -gx HOME (mktemp -d)
|
||||||
|
dot init --url $remote >/dev/null 2>&1
|
||||||
|
|
||||||
|
mkdir -p $HOME/.config/dot/commands
|
||||||
|
set -l marker (mktemp)
|
||||||
|
echo "function _dot_mark
|
||||||
|
echo marked >$marker
|
||||||
|
end" >$HOME/.config/dot/commands/mark.fish
|
||||||
|
|
||||||
|
dot mark >/dev/null 2>&1
|
||||||
|
@test "dispatches to a command file under ~/.config/dot/commands/" (cat $marker) = marked
|
||||||
6
.config/fish/completions/dot.fish
Normal file
6
.config/fish/completions/dot.fish
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
function __dot_custom_subcommands
|
||||||
|
echo init
|
||||||
|
path basename $HOME/.config/dot/commands/*.fish 2>/dev/null | path change-extension ''
|
||||||
|
end
|
||||||
|
|
||||||
|
complete -c dot -n __fish_use_subcommand -a "(__dot_custom_subcommands)"
|
||||||
2
.config/fish/completions/fishtape.fish
Normal file
2
.config/fish/completions/fishtape.fish
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
complete --command fishtape --short v --long version --description "Print version"
|
||||||
|
complete --command fishtape --short h --long help --description "Print help"
|
||||||
1
.config/fish/fish_plugins
Normal file
1
.config/fish/fish_plugins
Normal file
@@ -0,0 +1 @@
|
|||||||
|
jorgebucaran/fishtape
|
||||||
@@ -1,4 +1,89 @@
|
|||||||
function dot --wraps=git --description 'Manage dotfiles via a bare repo checked out over $HOME'
|
function dot --wraps=git --description 'Manage dotfiles via a bare repo checked out over $HOME'
|
||||||
git --git-dir=$HOME/.dotfiles --work-tree=$HOME $argv
|
set -l dotfiles_dir $HOME/.dotfiles
|
||||||
|
|
||||||
|
if test "$argv[1]" = init
|
||||||
|
set -e argv[1]
|
||||||
|
__dot_init $dotfiles_dir $argv
|
||||||
|
return $status
|
||||||
|
end
|
||||||
|
|
||||||
|
set -l commands_dir $HOME/.config/dot/commands
|
||||||
|
set -l command_file $commands_dir/$argv[1].fish
|
||||||
|
|
||||||
|
if test -n "$argv[1]" -a -f "$command_file"
|
||||||
|
source $command_file
|
||||||
|
_dot_$argv[1] $argv[2..-1]
|
||||||
|
return $status
|
||||||
|
end
|
||||||
|
|
||||||
|
git --git-dir=$dotfiles_dir --work-tree=$HOME $argv
|
||||||
|
end
|
||||||
|
|
||||||
|
# Kept inline (not a separate autoloaded function file) because this is the
|
||||||
|
# only subcommand that must work before the dotfiles repo has been cloned.
|
||||||
|
function __dot_init
|
||||||
|
set -l dotfiles_dir $argv[1]
|
||||||
|
set -e argv[1]
|
||||||
|
|
||||||
|
argparse 'url=' -- $argv
|
||||||
|
or return 1
|
||||||
|
|
||||||
|
set -l url $_flag_url
|
||||||
|
test -n "$url"; or set url ssh://gitea@git.alexion.dev:2022/alexion/dotfiles.git
|
||||||
|
|
||||||
|
if test -e $dotfiles_dir
|
||||||
|
echo "dot init: $dotfiles_dir already exists, refusing to re-initialize" >&2
|
||||||
|
return 1
|
||||||
|
end
|
||||||
|
|
||||||
|
git clone --bare $url $dotfiles_dir
|
||||||
|
or begin
|
||||||
|
echo "dot init: failed to clone $url" >&2
|
||||||
|
return 1
|
||||||
|
end
|
||||||
|
|
||||||
|
git --git-dir=$dotfiles_dir config status.showUntrackedFiles no
|
||||||
|
|
||||||
|
set -l checkout_output (git --git-dir=$dotfiles_dir --work-tree=$HOME checkout 2>&1)
|
||||||
|
set -l checkout_status $status
|
||||||
|
|
||||||
|
if test $checkout_status -ne 0
|
||||||
|
set -l conflicts
|
||||||
|
set -l in_block 0
|
||||||
|
|
||||||
|
for line in $checkout_output
|
||||||
|
if test $in_block -eq 1
|
||||||
|
if string match -rq '^\s' -- $line
|
||||||
|
set -a conflicts (string trim -- $line)
|
||||||
|
continue
|
||||||
|
else
|
||||||
|
set in_block 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
string match -q '*would be overwritten by checkout:*' -- $line
|
||||||
|
and set in_block 1
|
||||||
|
end
|
||||||
|
|
||||||
|
if test (count $conflicts) -eq 0
|
||||||
|
echo "dot init: checkout failed and no recoverable conflicts were found:" >&2
|
||||||
|
printf '%s\n' $checkout_output >&2
|
||||||
|
return 1
|
||||||
|
end
|
||||||
|
|
||||||
|
set -l backup_dir $HOME/.dotfiles-backup/(date +%Y%m%dT%H%M%S)
|
||||||
|
for f in $conflicts
|
||||||
|
mkdir -p (path dirname $backup_dir/$f)
|
||||||
|
mv $HOME/$f $backup_dir/$f
|
||||||
|
echo "dot init: backed up ~/$f to $backup_dir/$f"
|
||||||
|
end
|
||||||
|
|
||||||
|
git --git-dir=$dotfiles_dir --work-tree=$HOME checkout
|
||||||
|
or begin
|
||||||
|
echo "dot init: checkout still failing after backing up conflicts, aborting" >&2
|
||||||
|
return 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
echo "dot init: bootstrapped $dotfiles_dir from $url"
|
||||||
end
|
end
|
||||||
complete -c dot -w git
|
|
||||||
|
|||||||
116
.config/fish/functions/fishtape.fish
Normal file
116
.config/fish/functions/fishtape.fish
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
function fishtape --description "Test scripts, functions, and plugins in Fish"
|
||||||
|
switch "$argv"
|
||||||
|
case -v --version
|
||||||
|
echo "fishtape, version 3.0.1"
|
||||||
|
case "" -h --help
|
||||||
|
echo "Usage: fishtape <files ...> Run test files"
|
||||||
|
echo "Options:"
|
||||||
|
echo " -v or --version Print version"
|
||||||
|
echo " -h or --help Print this help message"
|
||||||
|
case \*
|
||||||
|
set --local files (realpath $argv)
|
||||||
|
|
||||||
|
for file in $files
|
||||||
|
if test ! -f $file
|
||||||
|
echo "fishtape: Invalid file or file not found: \"$file\"" >&2
|
||||||
|
return 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
set --local operators -{n,z,b,c,d,e,f,g,G,k,L,O,p,r,s,S,t,u,w,x}
|
||||||
|
set --local expectations \
|
||||||
|
"a non-zero length string" \
|
||||||
|
"a zero length string" \
|
||||||
|
"a block device" \
|
||||||
|
"a character device" \
|
||||||
|
"a directory" \
|
||||||
|
"an existing file" \
|
||||||
|
"a regular file" \
|
||||||
|
"a file with the set-group-ID bit set" \
|
||||||
|
"a file with same group ID as the current user" \
|
||||||
|
"a file with the sticky bit set" \
|
||||||
|
"a symbolic link" \
|
||||||
|
"a file owned by the current user" \
|
||||||
|
"a named pipe" \
|
||||||
|
"a file marked as readable" \
|
||||||
|
"a file of size greater than zero" \
|
||||||
|
"a socket" \
|
||||||
|
"a terminal tty file descriptor" \
|
||||||
|
"a file with the set-user-ID bit set" \
|
||||||
|
"a file marked as writable" \
|
||||||
|
"a file marked as executable"
|
||||||
|
|
||||||
|
set --universal _fishtape_test_number 0
|
||||||
|
set --universal _fishtape_test_passed 0
|
||||||
|
set --universal _fishtape_test_failed 0
|
||||||
|
|
||||||
|
function @echo
|
||||||
|
echo "# $argv"
|
||||||
|
end
|
||||||
|
|
||||||
|
function @test --argument-names name --inherit-variable operators --inherit-variable expectations
|
||||||
|
set --erase argv[1]
|
||||||
|
set --query argv[2] || set --append argv ""
|
||||||
|
|
||||||
|
set _fishtape_test_number (math $_fishtape_test_number + 1)
|
||||||
|
|
||||||
|
if test $argv
|
||||||
|
set _fishtape_test_passed (math $_fishtape_test_passed + 1)
|
||||||
|
|
||||||
|
echo "ok $_fishtape_test_number $name"
|
||||||
|
else
|
||||||
|
if test $argv[1] = "!"
|
||||||
|
set operator "! "
|
||||||
|
set expected "not "
|
||||||
|
set --erase argv[1]
|
||||||
|
end
|
||||||
|
|
||||||
|
if set --query argv[3]
|
||||||
|
set operator "$operator"$argv[2]
|
||||||
|
set expected (string escape -- $argv[3])
|
||||||
|
set actual (string escape -- $argv[1])
|
||||||
|
else
|
||||||
|
set operator "$operator"$argv[1]
|
||||||
|
set expected "$expected"$expectations[(contains --index -- $argv[1] $operators)]
|
||||||
|
set actual (string escape -- $argv[2])
|
||||||
|
end
|
||||||
|
|
||||||
|
set _fishtape_test_failed (math $_fishtape_test_failed + 1)
|
||||||
|
|
||||||
|
status print-stack-trace |
|
||||||
|
string replace --filter --regex -- "\s+called on line (\d+) of file (.+)" '$2:$1' |
|
||||||
|
read --local at
|
||||||
|
|
||||||
|
echo "not ok $_fishtape_test_number $name"
|
||||||
|
echo " ---"
|
||||||
|
echo " operator: $operator"
|
||||||
|
echo " expected: $expected"
|
||||||
|
echo " actual: $actual"
|
||||||
|
echo " at: $at"
|
||||||
|
echo " ..."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
echo TAP version 13
|
||||||
|
|
||||||
|
for file in $files
|
||||||
|
fish --init-command=(functions @echo | string collect) --init-command=(functions @test | string collect) $file
|
||||||
|
end
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "1..$_fishtape_test_number"
|
||||||
|
echo "# pass $_fishtape_test_passed"
|
||||||
|
test $_fishtape_test_failed -eq 0 &&
|
||||||
|
echo "# ok" ||
|
||||||
|
echo "# fail $_fishtape_test_failed"
|
||||||
|
|
||||||
|
functions --erase @echo @test
|
||||||
|
|
||||||
|
set --local failed $_fishtape_test_failed
|
||||||
|
set --erase _fishtape_test_number
|
||||||
|
set --erase _fishtape_test_passed
|
||||||
|
set --erase _fishtape_test_failed
|
||||||
|
|
||||||
|
test $failed -eq 0
|
||||||
|
end
|
||||||
|
end
|
||||||
53
.gitea/README.md
Normal file
53
.gitea/README.md
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# dotfiles
|
||||||
|
|
||||||
|
Managed as a bare git repo checked out over `$HOME`. Not cloned in the usual
|
||||||
|
sense — `git --git-dir=~/.dotfiles --work-tree=$HOME` treats `$HOME` itself as
|
||||||
|
the working tree.
|
||||||
|
|
||||||
|
## Everyday use
|
||||||
|
|
||||||
|
A fish function named `dot` wraps that invocation:
|
||||||
|
|
||||||
|
```fish
|
||||||
|
dot status
|
||||||
|
dot add .bashrc
|
||||||
|
dot commit -m 'update bashrc'
|
||||||
|
dot push
|
||||||
|
```
|
||||||
|
|
||||||
|
Any git subcommand works — `dot` forwards whatever you type straight to git.
|
||||||
|
|
||||||
|
## Bootstrapping a new machine
|
||||||
|
|
||||||
|
Before `dot` exists there's nothing to autoload it from, so the very first
|
||||||
|
step is fetching that one file by hand:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
mkdir -p ~/.config/fish/functions
|
||||||
|
curl -fsSL https://git.alexion.dev/alexion/dotfiles/raw/branch/main/.config/fish/functions/dot.fish \
|
||||||
|
-o ~/.config/fish/functions/dot.fish
|
||||||
|
fish -c 'dot init'
|
||||||
|
```
|
||||||
|
|
||||||
|
`dot init`:
|
||||||
|
|
||||||
|
- clones the bare repo to `~/.dotfiles`
|
||||||
|
- backs up any pre-existing files that would be overwritten by checkout into
|
||||||
|
`~/.dotfiles-backup/<timestamp>/`
|
||||||
|
- checks out the tracked files onto `$HOME`
|
||||||
|
- sets `status.showUntrackedFiles=no` so `dot status` doesn't list all of
|
||||||
|
`$HOME`
|
||||||
|
|
||||||
|
It refuses to run if `~/.dotfiles` already exists, and it never falls back to
|
||||||
|
creating a fresh empty repo if the clone fails.
|
||||||
|
|
||||||
|
## Adding new subcommands
|
||||||
|
|
||||||
|
Beyond `init`, `dot` looks for `~/.config/dot/commands/<name>.fish`. Each file
|
||||||
|
should define a `_dot_<name>` function; `dot <name> args...` sources the file
|
||||||
|
and calls it. These files are deliberately *not* under
|
||||||
|
`~/.config/fish/functions/`, so they never become independently invokable
|
||||||
|
commands or clutter tab-completion outside of `dot` itself.
|
||||||
|
|
||||||
|
`~/.config/fish/completions/dot.fish` discovers them automatically by
|
||||||
|
scanning that directory, so a new command file gets tab-completion for free.
|
||||||
Reference in New Issue
Block a user