Compare commits

..

6 Commits

Author SHA1 Message Date
renovate[bot]
62c620efad fix(github-action): update ivuorinen/actions (25.6.25 → 25.6.30) (#144) 2025-07-03 09:45:24 +03:00
07fe18af75 fix(scripts): correct usage and help exit status (#143) 2025-06-30 08:51:06 +03:00
renovate[bot]
29d3676b38 fix(github-action): update ivuorinen/actions (25.6.23 → 25.6.25) (#142) 2025-06-30 08:04:34 +03:00
0b9e1803d4 docs(agents): update instructions for yarn (#141) 2025-06-30 07:59:58 +03:00
github-actions[bot]
c45ad9710d chore: update pre-commit hooks (#140) 2025-06-30 07:52:04 +03:00
cf7ca2109f feat: add bats tests, docs (#139)
* fix(test): ensure bats file list uses xargs

* docs(readme): use yarn for testing instructions

* fix(test): ensure pipelines fail properly

* docs(alias): fix table header

---------

Signed-off-by: Ismo Vuorinen <ismo@ivuorinen.net>
2025-06-30 04:30:06 +03:00
95 changed files with 1395 additions and 1622 deletions

View File

@@ -28,7 +28,7 @@ indent_size = 1
indent_size = 1
indent_style = tab
[{local/bin/*,local/dfm/*,**/*.sh,**/zshrc,config/*,scripts/*}]
[{local/bin/*,**/*.sh,**/zshrc,config/*,scripts/*}]
indent_size = 2
tab_width = 2
shell_variant = bash # --language-variant

5
.gitattributes vendored
View File

@@ -85,6 +85,7 @@ LICENSE text
NEWS text
readme text
*README* text
# Files literally named "TODO", not a todo list item
TODO text
# Templates
@@ -122,7 +123,8 @@ package.json text eol=lf
package-lock.json text eol=lf -diff
pnpm-lock.yaml text eol=lf -diff
.prettierrc text
yarn.lock text -diff
# Ensure yarn.lock shows textual diffs
yarn.lock text eol=lf
*.toml text
*.yaml text
*.yml text
@@ -251,3 +253,4 @@ install text eol=lf diff=shell
*.snippets text eol=lf
*.theme text eol=lf
*.yamlfmt text eol=lf
*.bats text eol=lf diff=shell

30
.github/AGENTS.md vendored Normal file
View File

@@ -0,0 +1,30 @@
# Guidelines for AI contributors
These instructions help language models work with this repository.
## Setup
1. Run `yarn install` to get linting tools and the Bats test framework.
## Formatting
- Format code and docs with Prettier and markdownlint:
```bash
yarn fix:prettier
yarn fix:markdown
```
- Shell scripts should pass `shellcheck`.
## Testing
- When code changes, run `yarn test` to execute Bats tests.
- If only comments or documentation change, tests may be skipped.
## Commits and PRs
- Use Semantic Commit messages: `type(scope): summary`.
- Keep PR titles in the same format.
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

View File

@@ -60,7 +60,7 @@ representative at an online or offline event.
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
ismo@ivuorinen.net.
<ismo@ivuorinen.net>.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
@@ -116,7 +116,7 @@ the community.
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
<https://www.contributor-covenant.org/version/2/0/code_of_conduct.html>.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
@@ -124,5 +124,5 @@ enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.
<https://www.contributor-covenant.org/faq>. Translations are available at
<https://www.contributor-covenant.org/translations>.

54
.github/README.md vendored
View File

@@ -16,11 +16,9 @@ see what interesting stuff you've done with it. Sharing is caring.
### First time setup
1. Clone this repository to `$HOME/.dotfiles`
2. Install [shellcheck](https://github.com/koalaman/shellcheck) and [pre-commit](https://pre-commit.com/)
3. Run `pre-commit install` to enable Git hooks
4. `./install`
5. ???
6. Profit
2. `./install`
3. ???
4. Profit
### Updates
@@ -45,6 +43,12 @@ see what interesting stuff you've done with it. Sharing is caring.
| `local/bin` | Helper scripts that I've collected or wrote. |
| `scripts` | Setup scripts. |
### Host specific configuration
Configurations under `hosts/<hostname>` are applied only when running on the
matching machine. Each host folder contains its own `install.conf.yaml` that
is processed by Dotbot during installation.
### dotfile folders
| Repo | Destination | Description |
@@ -60,6 +64,21 @@ see what interesting stuff you've done with it. Sharing is caring.
Running `dfm` gives you a list of available commands.
#### Documentation generation
`dfm docs` generates Markdown documentation under the `docs/` directory. The
subcommands are:
```bash
dfm docs alias # regenerate alias table
dfm docs folders # document interesting folders
dfm docs keybindings # update keybinding docs for tmux, nvim and others
dfm docs all # run every docs task
```
The `docs/` folder contains generated cheat sheets, keybindings and other
reference files. New documentation can be added without modifying this README.
## Configuration
The folder structure follows [XDG Base Directory Specification][xdg] where possible.
@@ -75,6 +94,31 @@ The folder structure follows [XDG Base Directory Specification][xdg] where possi
Please see [docs/folders.md][docs-folders] for more information.
## Managing submodules
This repository uses Git submodules for external dependencies. After cloning,
run:
```bash
git submodule update --init --recursive
```
To pull submodule updates later use:
```bash
git submodule update --remote --merge
```
The helper script `add-submodules.sh` documents how each submodule is added and
configured. Submodules are automatically updated by the
[update-submodules.yml](.github/workflows/update-submodules.yml) workflow.
## Testing
Shell scripts under `local/bin` are validated with [Bats](https://github.com/bats-core/bats-core).
Run `yarn test` to execute every test file. Bats is installed as a development
dependency, so run `yarn install` first if needed.
[dfm]: https://github.com/ivuorinen/dotfiles/blob/main/local/bin/dfm
[docs-folders]: https://github.com/ivuorinen/dotfiles/blob/main/docs/folders.md
[xdg]: https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html

View File

@@ -33,4 +33,4 @@ jobs:
- name: Run PR Lint
# https://github.com/ivuorinen/actions
uses: ivuorinen/actions/pr-lint@9480614ba2231013d99dd5b9c730d2b105b9e160 # 25.6.25
uses: ivuorinen/actions/pr-lint@c0f919957eb60743682a6e0cd6d454fd4b142ac9 # 25.6.30

View File

@@ -29,4 +29,4 @@ jobs:
issues: write
steps:
- uses: ivuorinen/actions/sync-labels@99f3911475dbb5b8d43d314b24c0882997433868 # 25.6.23
- uses: ivuorinen/actions/sync-labels@c0f919957eb60743682a6e0cd6d454fd4b142ac9 # 25.6.30

View File

@@ -49,7 +49,7 @@ repos:
- id: actionlint
- repo: https://github.com/renovatebot/pre-commit-hooks
rev: 41.11.1
rev: 41.17.2
hooks:
- id: renovate-config-validator

View File

@@ -1 +1,7 @@
[{"account_id":"S5Z2DMNFKJEZBPCWRHRWC4DCGI","vault_id":"injcin7obv3jdet3r2u3kfihfy","item_id":"f6vinbnc6l7ngdzvlw66ayewlq"}]
[
{
"account_id": "S5Z2DMNFKJEZBPCWRHRWC4DCGI",
"vault_id": "injcin7obv3jdet3r2u3kfihfy",
"item_id": "f6vinbnc6l7ngdzvlw66ayewlq"
}
]

11
hosts/README.md Normal file
View File

@@ -0,0 +1,11 @@
# Host specific directories
Host folders contain machine specific overrides and an `install.conf.yaml` file that Dotbot processes during setup.
Current hosts:
- **air** personal computer
- **lakka** remote server
- **s** work laptop
- **tunkki** local server
- **v** work desktop

29
local/bin/a.md Normal file
View File

@@ -0,0 +1,29 @@
# a
Encrypt or decrypt files and directories using `age` and your GitHub SSH keys.
## Usage
```bash
a encrypt <file|dir>
a decrypt <file.age|dir>
```
Options:
- `-v`, `--verbose` show log output
Environment variables:
- `AGE_KEYSFILE` location of the keys file
- `AGE_KEYSSOURCE` URL to fetch keys if missing
- `AGE_LOGFILE` log file path
## Example
```bash
a encrypt secret.txt
a decrypt secret.txt.age
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

19
local/bin/ad.md Normal file
View File

@@ -0,0 +1,19 @@
# ad
Decrypt a file encrypted with `age` using your GitHub SSH keys.
## Usage
```bash
ad <file.age>
```
Uses `AGE_KEYSFILE` and `AGE_KEYSSOURCE` if keys are missing.
## Example
```bash
ad secret.txt.age
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

19
local/bin/ae.md Normal file
View File

@@ -0,0 +1,19 @@
# ae
Encrypt a file with `age` using your GitHub SSH keys.
## Usage
```bash
ae <file>
```
Uses `AGE_KEYSFILE` and `AGE_KEYSSOURCE` if keys are missing.
## Example
```bash
ae secret.txt
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

26
local/bin/dfm.md Normal file
View File

@@ -0,0 +1,26 @@
# dfm
Dotfiles manager and installation helper. Provides wrappers for many
setup tasks defined in this repository.
## Usage
```bash
dfm <command> [options]
```
Common commands include:
- `install` install tools or run platform specific setup
- `brew` manage Homebrew packages
- `docs` regenerate markdown documentation
Set `VERBOSE=1` to see debug output.
### Example
```bash
dfm install all
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

21
local/bin/fzf-tmux.md Normal file
View File

@@ -0,0 +1,21 @@
# fzf-tmux
Wrapper around [`fzf`](https://github.com/junegunn/fzf) that opens the
interface inside a tmux pane or popup.
## Usage
```bash
fzf-tmux [layout options] [--] [fzf options]
```
Layout flags like `-p` or `-d` control popup and split behaviour. Use
`--` to pass arguments directly to `fzf`.
### Example
```bash
fzf-tmux -p 80%,60% -- --reverse
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

16
local/bin/fzf.md Normal file
View File

@@ -0,0 +1,16 @@
# fzf
Binary of the fuzzy finder [fzf](https://github.com/junegunn/fzf).
Use `fzf` as you would normally; this wrapper ships the prebuilt
binary in the dotfiles.
## Usage
```bash
fzf [options]
```
Refer to the upstream `fzf` documentation for all available
flags and features.
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

View File

@@ -0,0 +1,25 @@
# git-attributes
Checks that every tracked file has a matching pattern in `.gitattributes`.
Can optionally suggest or write missing rules.
## Usage
```bash
git-attributes [options]
```
Options include:
- `-v, --verbose` show progress information
- `-e, --exit` exit with non-zero status if missing rules
- `-p, --pattern <glob>` pattern to check (default: `text: auto`)
- `-w, --write` append suggestions to `.gitattributes`
### Example
```bash
git-attributes -v --write
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

View File

@@ -0,0 +1,15 @@
# iterm2_shell_integration.zsh
Official iTerm2 shell integration script for zsh. Source this file to
enable prompt tracking and command notifications in iTerm2.
## Usage
```bash
source iterm2_shell_integration.zsh
```
No parameters are required. The script modifies your prompt to work
with iTerm2 features such as badges and profile switching.
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

20
local/bin/msgr.md Normal file
View File

@@ -0,0 +1,20 @@
# msgr
Helper library for printing colorized log messages from shell scripts.
## Usage
```bash
msgr <type> "message" [extra]
```
Message types include `ok`, `warn`, `err`, `run` and many more. The
script is primarily sourced by other scripts.
### Example
```bash
msgr ok "Installation complete"
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

23
local/bin/php-switcher.md Normal file
View File

@@ -0,0 +1,23 @@
# php-switcher
Switch between Homebrew-installed PHP versions or list installed versions.
## Usage
```bash
php-switcher <version>|--auto [options]
```
Options:
- `--installed` list versions installed via Homebrew
- `--current` print currently active PHP version
- `--auto` read version from `.php-version` in current directory
### Example
```bash
php-switcher 8.3
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

View File

@@ -56,22 +56,31 @@ __pushover_send_message()
response="$(eval "${curl_cmd}")"
printf "%s\n" "$response"
# TODO: Parse response
r="${?}"
if [ "${r}" -ne 0 ]; then
printf "%s: Failed to send message\n" "${0}" >&2
# Parse response status. Expect JSON like: {"status":1,"request":"..."}
if echo "$response" | grep -q '"status"[[:space:]]*:[[:space:]]*1'; then
r=0
else
r=1
fi
return "${r}"
if [ "$r" -ne 0 ]; then
# Extract possible error message from JSON
err=$(echo "$response" | grep -o '"errors".*' | sed 's/"errors"[:,\[]//g' | tr -d '[]"')
[ -n "$err" ] && printf "%s: %s\n" "$0" "$err" >&2
printf "%s: Failed to send message\n" "$0" >&2
fi
return "$r"
}
CURL="$(which curl)"
CURL="$(command -v curl)"
PUSHOVER_URL="https://api.pushover.net/1/messages.json"
TOKEN=$PUSHOVER_TOKEN
USER=$PUSHOVER_USER
CURL_OPTS=""
devices="${devices} ${device}"
optstring="c:d:D:e:f:p:r:t:T:s:u:U:a:h"
devices=""
optstring="c:d:D:e:p:r:t:T:s:u:U:a:h"
OPTIND=1
while getopts ${optstring} c; do
@@ -97,7 +106,7 @@ while getopts ${optstring} c; do
t)
title="${OPTARG}"
;;
k)
T)
TOKEN="${OPTARG}"
;;
s)

25
local/bin/pushover.md Normal file
View File

@@ -0,0 +1,25 @@
# pushover
Send notifications via the Pushover API.
## Usage
```bash
pushover -T <token> -U <user> [-t title] [-p priority] message
```
Common options:
- `-c <callback>` callback URL
- `-d <device>` target device
- `-s <sound>` notification sound name
- `-T <token>` application token (or `PUSHOVER_TOKEN` env)
- `-U <user>` user key (or `PUSHOVER_USER` env)
## Example
```bash
pushover -T $TOKEN -U $USER -t "Build" "Finished successfully"
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

23
local/bin/t.md Normal file
View File

@@ -0,0 +1,23 @@
# t
Launch or switch to a tmux session based on a directory selected with
`fzf`. Inspired by scripts from ThePrimeagen and Jess Archer.
## Usage
```bash
t
```
Environment variables:
- `T_ROOT` base directory to search (default: `~/Code`)
- `T_MAX_DEPTH` recursion depth for directory search
### Example
```bash
T_ROOT=~/projects t
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

View File

@@ -0,0 +1,20 @@
# x-backup-folder
Create a compressed archive of a folder with a timestamped name.
## Usage
```bash
x-backup-folder <folder> [archive-name]
```
- `folder` directory to back up
- `archive-name` optional prefix for the generated tar.gz
## Example
```bash
x-backup-folder ~/Documents Notes
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

View File

@@ -0,0 +1,21 @@
# x-backup-mysql-with-prefix
Dump MySQL tables matching a prefix to a timestamped file.
## Usage
```bash
x-backup-mysql-with-prefix <prefix> <name> [database]
```
- `prefix` table prefix to match (e.g. `wp_`)
- `name` file name prefix
- `database` database name (default: `wordpress`)
## Example
```bash
x-backup-mysql-with-prefix wp_ blog
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

View File

@@ -0,0 +1,20 @@
# x-change-alacritty-theme
Adapted from <https://gist.github.com/xqm32/17777d035930d622d0ff7530bfab61fd>
## Usage
```bash
x-change-alacritty-theme <day|night>
```
Switches Alacritty's theme by copying a theme file under
`~/.config/alacritty/`.
### Example
```bash
x-change-alacritty-theme night
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

View File

@@ -0,0 +1,19 @@
# x-clean-vendordirs
Remove `vendor` and `node_modules` directories recursively.
## Usage
```bash
x-clean-vendordirs [directory]
```
- `directory` root directory to clean (default: current directory)
## Example
```bash
x-clean-vendordirs ~/projects
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

View File

@@ -0,0 +1,14 @@
# x-compare-versions.py
Compare version strings using Python's packaging library.
## Usage
```bash
echo "1.2.3 >= 1.0.0" | x-compare-versions.py
```
The script reads comparison expressions from standard input and exits
with status 0 if all comparisons are true.
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

19
local/bin/x-dc.md Normal file
View File

@@ -0,0 +1,19 @@
# x-dc
Create a directory if it does not exist.
## Usage
```bash
x-dc <directory>
```
Set `VERBOSE=1` to see log messages.
## Example
```bash
x-dc ~/tmp/mydir
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

View File

@@ -0,0 +1,14 @@
# x-dfm-docs-xterm-keybindings
Generate `docs/tmux-keybindings.md` using tmux's key list.
## Usage
```bash
x-dfm-docs-xterm-keybindings
```
No parameters are needed. The script writes the file under `docs/` and
overwrites any existing version.
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

21
local/bin/x-env-list.md Normal file
View File

@@ -0,0 +1,21 @@
# x-env-list
Lists environment variables grouped by their prefix. Sensitive values
are hidden by default.
## Usage
```bash
x-env-list [options]
```
Use `--json` for machine readable output or specify
`X_ENV_GROUPING` with a YAML file to override the default groups.
### Example
```bash
X_ENV_GROUPING=~/env-groups.yaml x-env-list --json
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

20
local/bin/x-foreach.md Normal file
View File

@@ -0,0 +1,20 @@
# x-foreach
Run a command in each directory produced by another command.
## Usage
```bash
x-foreach "<list-cmd>" "<cmd>"
```
- `list-cmd` command that outputs directories
- `cmd` command to run inside each directory
## Example
```bash
x-foreach "ls -d */" "git status"
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

View File

@@ -0,0 +1,19 @@
# x-gh-get-latest-release-targz
Fetch the tarball URL of the latest GitHub release or download it directly.
## Usage
```bash
x-gh-get-latest-release-targz <owner/repo> [--get]
```
- `--get` download and extract the tarball instead of printing the URL
## Example
```bash
x-gh-get-latest-release-targz ivuorinen/dotfiles --get
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

View File

@@ -97,7 +97,7 @@ Examples:
# Use GitHub Enterprise API
GITHUB_API_URL="https://github.example.com/api/v3/repos" $BIN ivuorinen/dotfiles
EOF
exit 1
exit 0
}
# Check that required dependencies are installed

View File

@@ -0,0 +1,21 @@
# x-git-largest-files.py
Lists the largest files in a git repository.
```bash
x-git-largest-files.py [options]
```
Options:
- `-c NUM` number of files to show (default: 10)
- `--files-exceeding N` list files larger than N KB
- `-p` sort by on-disk size instead of pack size
## Example
```bash
x-git-largest-files.py -c 5
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

18
local/bin/x-have.md Normal file
View File

@@ -0,0 +1,18 @@
# x-have
Check if a command exists on the system. Exits with status 0 if found
and 1 otherwise.
## Usage
```bash
x-have <command>
```
### Example
```bash
x-have git && echo "git installed"
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

19
local/bin/x-hr.md Normal file
View File

@@ -0,0 +1,19 @@
# x-hr
Print a horizontal rule. Useful for visually separating log output.
## Usage
```bash
x-hr [character]
```
If no character is given a red `-` is used.
### Example
```bash
x-hr "="
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

19
local/bin/x-ip.md Normal file
View File

@@ -0,0 +1,19 @@
# x-ip
Fetch your public IP address using `curl`.
## Usage
```bash
x-ip [curl-options]
```
Any arguments are passed directly to `curl`.
### Example
```bash
x-ip -4
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

View File

@@ -0,0 +1,21 @@
# x-load-configs
Source shell configuration files for aliases and exports. Intended to
be run after `dfm install` or when switching hosts.
## Usage
```bash
x-load-configs
```
Set `VERBOSE=1` to print each file as it is sourced. Use `DEBUG=1` to
enable tracing.
### Example
```bash
VERBOSE=1 x-load-configs
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

25
local/bin/x-localip.md Normal file
View File

@@ -0,0 +1,25 @@
# x-localip
Display local IPv4 and IPv6 addresses with optional interface filtering.
## Usage
```bash
x-localip [--ipv4] [--ipv6] [interface]
```
- `--ipv4` show only IPv4 addresses
- `--ipv6` show only IPv6 addresses
- `interface` limit output to the named interface
## Example
```bash
# Show all addresses
x-localip
# IPv4 for wlan0
x-localip --ipv4 wlan0
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env bash
#
# Create a directory and cd into it
# Usage: mkcd <dir>
# Usage: x-mkd <dir>
set -euo pipefail

19
local/bin/x-mkd.md Normal file
View File

@@ -0,0 +1,19 @@
# x-mkd
Create a directory and immediately `cd` into it.
## Usage
```bash
x-mkd <dir>
```
Set `VERBOSE=1` for status messages.
## Example
```bash
x-mkd project && git init
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

20
local/bin/x-multi-ping.md Normal file
View File

@@ -0,0 +1,20 @@
# x-multi-ping
Multi-protocol ping helper supporting IPv4 and IPv6.
## Usage
```bash
x-multi-ping [--loop] [--sleep=N] host1 host2...
```
- `--loop` ping continuously
- `--sleep` seconds to wait between iterations
## Example
```bash
x-multi-ping --loop --sleep=5 example.com
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

View File

@@ -0,0 +1,20 @@
# x-multi-ping.pl
Ping multiple hosts with IPv4/IPv6 support.
## Usage
```bash
x-multi-ping.pl [--loop|--forever] [--sleep N] host1 host2 ...
```
`--loop` keeps pinging each host until interrupted. `--sleep` controls
the delay between attempts.
### Example
```bash
x-multi-ping.pl --loop --sleep 2 example.com 1.1.1.1
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

19
local/bin/x-open-ports.md Normal file
View File

@@ -0,0 +1,19 @@
# x-open-ports
List listening ports using `lsof` or `ss`.
## Usage
```bash
x-open-ports [--json]
```
- `--json` output as JSON instead of Markdown
## Example
```bash
x-open-ports --json
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

View File

@@ -0,0 +1,19 @@
# x-path-append
Append directories to the `PATH` variable without duplicates.
## Usage
```bash
x-path-append <dir1> [dir2 ...]
```
Set `VERBOSE=1` for verbose logging.
## Example
```bash
x-path-append /usr/local/sbin "$HOME/bin"
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

View File

@@ -0,0 +1,19 @@
# x-path-prepend
Prepend directories to the `PATH` variable without duplicates.
## Usage
```bash
x-path-prepend <dir1> [dir2 ...]
```
Set `VERBOSE=1` for verbose logging.
## Example
```bash
x-path-prepend "$HOME/bin" /opt/tools
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

View File

@@ -0,0 +1,19 @@
# x-path-remove
Remove directories from the `PATH` variable.
## Usage
```bash
x-path-remove <dir1> [dir2 ...]
```
Set `VERBOSE=1` for verbose logging.
## Example
```bash
x-path-remove /usr/local/bin
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

30
local/bin/x-path.md Normal file
View File

@@ -0,0 +1,30 @@
# x-path
Manage entries in the `PATH` variable through subcommands.
## Usage
```bash
x-path <command> <directory1> [directory2 ...]
```
### Commands
- `append` / `a` Append directories to `PATH`
- `prepend` / `p` Prepend directories to `PATH`
- `remove` Remove directories from `PATH`
- `check` Validate directories (default: all in `PATH`)
Set `VERBOSE=1` for progress output.
## Examples
```bash
# Prepend /opt/bin to PATH
x-path prepend /opt/bin
# Remove /usr/local/bin from PATH
x-path remove /usr/local/bin
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

View File

@@ -0,0 +1,14 @@
# x-quota-usage.php
Display filesystem quota usage in a human readable table.
## Usage
```bash
x-quota-usage.php
```
Runs the `quota` command and formats the output. Requires PHP with the
`shell_exec` function enabled.
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

19
local/bin/x-record.md Normal file
View File

@@ -0,0 +1,19 @@
# x-record
Interactive screen recording wrapper around `giph`.
## Usage
```bash
x-record <gif|mkv|webm|mp4> <fullscreen|set>
```
The script asks for file type and area when omitted.
## Example
```bash
x-record gif fullscreen
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

View File

@@ -0,0 +1,14 @@
# x-set-php-aliases
---
## Usage
```bash
source x-set-php-aliases
```
Generates shell aliases (`php80`, `php81` ...) for each Homebrew PHP
installation. Caches the list under `$XDG_CACHE_HOME/x-set-php-aliases`.
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

View File

@@ -0,0 +1,22 @@
# x-sha256sum-matcher
Compare two files by calculating their SHA256 checksums.
## Usage
```bash
x-sha256sum-matcher [options] file1 file2
```
Options:
- `-v` verbose output
- `-h, --help` show help
### Example
```bash
x-sha256sum-matcher original.iso download.iso
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

View File

@@ -0,0 +1,22 @@
# x-ssl-expiry-date
Check the expiry date of an SSL certificate for one or more hosts.
## Usage
```bash
x-ssl-expiry-date [-d] [-p PORT] host1 host2 ...
```
Options:
- `-d` show days left instead of the full date
- `-p <port>` use custom port (default: 443)
### Example
```bash
x-ssl-expiry-date -d github.com
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

View File

@@ -0,0 +1,14 @@
# x-term-colors
Display a table of 24bit color codes for testing terminal color
support.
## Usage
```bash
x-term-colors
```
Pipe the output to `less -R` to view with color highlighting.
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

26
local/bin/x-thumbgen.md Normal file
View File

@@ -0,0 +1,26 @@
# x-thumbgen
Generate thumbnails using ImageMagick (magick) with MIME type filtering.
## Usage
```bash
x-thumbgen [options] source_directory
```
Options:
- `-o DIR` output directory (default: same as source)
- `-s STR` suffix for thumbnails
- `-h` show help
Environment variables like `THUMB_BACKGROUND` control the background
color.
### Example
```bash
THUMB_BACKGROUND=black x-thumbgen -o ~/thumbs ~/images
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

View File

@@ -0,0 +1,20 @@
# x-until-error
Repeatedly execute a command until it returns a non-zero exit status.
## Usage
```bash
x-until-error [--sleep SECONDS] command [args...]
```
Use `--sleep` to wait between runs. The command is executed at least
once.
### Example
```bash
x-until-error --sleep 2 ping -c1 example.com
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

View File

@@ -0,0 +1,20 @@
# x-until-success
Repeat a command until it succeeds (exit status 0). The command is
always executed at least once.
## Usage
```bash
x-until-success [--sleep SECONDS] command [args...]
```
Use `--sleep` to control the delay between attempts.
### Example
```bash
x-until-success --sleep 5 curl -I https://example.com
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

View File

@@ -0,0 +1,14 @@
# x-validate-sha256sum.sh
This script contains a helper for sha256 validating your downloads
## Usage
```bash
x-validate-sha256sum.sh file sha256sum
```
The script computes the SHA256 hash of `file` and compares it to the
expected value. It exits non-zero if the sums differ.
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

View File

@@ -0,0 +1,13 @@
# x-welcome-banner
Print a colorful MOTD with greeting, system info and today's weather.
## Usage
```bash
x-welcome-banner
```
Requires optional tools: `neofetch`, `figlet`, `lolcat` and `curl` for extra info.
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

17
local/bin/x-when-down.md Normal file
View File

@@ -0,0 +1,17 @@
# x-when-down
Wait until a host stops responding to ping, then run a command.
## Usage
```bash
x-when-down <host> <command...>
```
## Example
```bash
x-when-down 1.2.3.4 echo "server down"
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

19
local/bin/x-when-up.md Normal file
View File

@@ -0,0 +1,19 @@
# x-when-up
Wait for a host to respond to ping before running a command.
## Usage
```bash
x-when-up <host> <command...>
```
If the command is `ssh`, the host argument may be omitted.
## Example
```bash
x-when-up 1.2.3.4 ssh 1.2.3.4
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

View File

@@ -1,239 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# Source core dfm libraries
source "$(dirname "${BASH_SOURCE[0]}")/../lib/common.sh"
source "$(dirname "${BASH_SOURCE[0]}")/../lib/utils.sh"
# Default paths can be overridden via environment variables
: "${DOTFILES:=$HOME/.dotfiles}"
: "${BREWFILE:=$DOTFILES/config/homebrew/Brewfile}"
: "${TEMP_DIR:=$(mktemp -d)}"
: "${DFM_MAX_RETRIES:=3}"
# Remove temp folder on exit
trap 'rm -rf "$TEMP_DIR"' EXIT
# Installation functions for dfm, the dotfile manager
#
# @author Ismo Vuorinen <https://github.com/ivuorinen>
# @license MIT
# Installs all required packages in the correct order.
#
# Description:
# Orchestrates the installation process for the dotfile manager by sequentially invoking
# the installation routines for fonts, Homebrew, and Rust (cargo). It logs the start of the
# overall installation process before calling each respective function.
#
# Globals:
# lib::log - Function used to log installation progress messages.
#
# Arguments:
# None.
#
# Outputs:
# Logs an informational message indicating the start of the installation process.
#
# Returns:
# None.
#
# Example:
# all
#
# @description
# Parse command line options controlling installation steps.
parse_options()
{
NO_AUTOMATION=0
SKIP_FONTS=0
SKIP_BREW=0
SKIP_CARGO=0
while [[ $# -gt 0 ]]; do
case "$1" in
--no-automation)
NO_AUTOMATION=1
;;
--no-fonts)
SKIP_FONTS=1
;;
--no-brew)
SKIP_BREW=1
;;
--no-cargo)
SKIP_CARGO=1
;;
*)
lib::error "Unknown option: $1"
return 1
;;
esac
shift
done
}
# @description
# Install all configured components by calling each individual
# installation routine unless skipped via options.
install_all()
{
parse_options "$@"
lib::log "Installing all packages..."
if [[ $SKIP_FONTS -eq 0 ]]; then
install_fonts
fi
if [[ $SKIP_BREW -eq 0 ]]; then
install_brew
fi
if [[ $SKIP_CARGO -eq 0 ]]; then
install_cargo
fi
}
# Installs fonts required by the dotfile manager.
#
# Globals:
# None.
#
# Arguments:
# None.
#
# Outputs:
# Logs a message to STDOUT indicating that the font installation process has started.
#
# Returns:
# None.
#
# Example:
# install_fonts
#
# @description Install all configured fonts from helper script, prompting the user unless automation is disabled.
install_fonts()
{
: "${SKIP_FONTS:=0}"
: "${NO_AUTOMATION:=0}"
if [[ $SKIP_FONTS -eq 1 ]]; then
lib::log "Skipping fonts installation"
return 0
fi
if [[ $NO_AUTOMATION -eq 0 ]]; then
utils::interactive::confirm "Install fonts?" || return 0
fi
lib::log "Installing fonts..."
local script="${DOTFILES}/scripts/install-fonts.sh"
if [[ ! -x "$script" ]]; then
lib::error "Font installation script not found: $script"
return 1
fi
bash "$script"
}
# Install Homebrew and set it up.
#
# Installs the Homebrew package manager on macOS.
#
# Globals:
# lib::log - Logging utility used to report installation progress.
#
# Outputs:
# Logs a message indicating the start of the Homebrew installation process.
#
# Example:
# install_brew
#
# @description Install Homebrew and declared packages using the Brewfile.
install_brew()
{
: "${SKIP_BREW:=0}"
: "${NO_AUTOMATION:=0}"
if [[ $SKIP_BREW -eq 1 ]]; then
lib::log "Skipping Homebrew installation"
return 0
fi
if [[ $NO_AUTOMATION -eq 0 ]]; then
utils::interactive::confirm "Install Homebrew packages?" || return 0
fi
lib::log "Installing Homebrew..."
if ! utils::is_installed brew; then
lib::log "Homebrew not found, installing..."
local installer="$TEMP_DIR/homebrew-install.sh"
utils::retry "$DFM_MAX_RETRIES" \
curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh \
-o "$installer"
NONINTERACTIVE=1 bash "$installer"
fi
if utils::is_installed brew; then
brew bundle install --file="$BREWFILE" --force --quiet
else
lib::error "Homebrew installation failed"
return 1
fi
}
# Installs Rust and cargo packages.
#
# Description:
# Logs the start of the installation process for Rust and cargo packages.
# The installation logic is intended to be implemented where indicated.
#
# Globals:
# Uses lib::log for logging the installation process.
#
# Example:
# install_cargo
#
# @description Install Rust tooling and cargo packages using helper scripts.
install_cargo()
{
: "${SKIP_CARGO:=0}"
: "${NO_AUTOMATION:=0}"
if [[ $SKIP_CARGO -eq 1 ]]; then
lib::log "Skipping Rust and cargo installation"
return 0
fi
if [[ $NO_AUTOMATION -eq 0 ]]; then
utils::interactive::confirm "Install Rust and cargo packages?" || return 0
fi
lib::log "Installing Rust and cargo packages..."
if ! utils::is_installed cargo; then
lib::log "Rust not found, installing rustup..."
local installer="$TEMP_DIR/rustup-init.sh"
utils::retry "$DFM_MAX_RETRIES" \
curl https://sh.rustup.rs -sSf -o "$installer"
sh "$installer" -y
source "$HOME/.cargo/env"
fi
local script="${DOTFILES}/scripts/install-cargo-packages.sh"
if [[ -x "$script" ]]; then
bash "$script"
else
lib::error "Cargo packages script not found: $script"
return 1
fi
}

View File

@@ -1,104 +0,0 @@
#!/usr/bin/env bash
# dfm - dotfiles manager
set -euo pipefail
# allow overriding core directories
DFM_SCRIPT_DIR="${DFM_SCRIPT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
readonly DFM_SCRIPT_DIR
export DFM_SCRIPT_DIR
DFM_CMD_DIR="${DFM_CMD_DIR:-${DFM_SCRIPT_DIR}/cmd}"
readonly DFM_CMD_DIR
export DFM_CMD_DIR
DFM_LIB_DIR="${DFM_LIB_DIR:-${DFM_SCRIPT_DIR}/lib}"
readonly DFM_LIB_DIR
export DFM_LIB_DIR
DFM_DEFAULT_CONFIG_PATH="${DFM_DEFAULT_CONFIG_PATH:-$HOME/.config}"
readonly DFM_DEFAULT_CONFIG_PATH
export DFM_DEFAULT_CONFIG_PATH
DFM_MAX_RETRIES="${DFM_MAX_RETRIES:-3}"
readonly DFM_MAX_RETRIES
export DFM_MAX_RETRIES
export DFM_DEFAULT_INSTALL_DIR="${DFM_DEFAULT_INSTALL_DIR:-$HOME/.local}"
export DFM_DEFAULT_VERBOSE="${DFM_DEFAULT_VERBOSE:-0}"
TEMP_DIR="${TEMP_DIR:-$(mktemp -d)}"
export TEMP_DIR
# Load the common and utility functions from the lib directory.
[[ -f "${DFM_LIB_DIR}/common.sh" ]] || {
echo "Error: Required file ${DFM_LIB_DIR}/common.sh not found"
exit 1
}
[[ -f "${DFM_LIB_DIR}/utils.sh" ]] || {
echo "Error: Required file ${DFM_LIB_DIR}/utils.sh not found"
exit 1
}
source "${DFM_LIB_DIR}/common.sh"
source "${DFM_LIB_DIR}/utils.sh"
# Display help information
#
# @return None
main::show_help()
{
cat << EOF
Usage: dfm [command] [function] [arguments]
dotfiles manager utility for installing and configuring dotfiles.
If no arguments are provided, lists all available commands.
If only a command is provided, lists available functions for that command.
If a command and function are provided, executes the specified function.
Examples:
dfm # List all available commands
dfm install # List available functions for the install command
dfm install all # Execute the 'all' function from the install command
EOF
}
# Main function for the dfm script.
#
# The function checks if any arguments were provided. If no arguments are
# provided, it lists all available commands. If a command name is provided,
# it lists the available functions for that command. If a command and function
# name are provided, it executes the function with the provided arguments.
#
# @param args The command-line arguments.
# @return None
main()
{
if [[ $# -eq 0 ]]; then
main::list_available_commands
return 0
fi
local cmd="$1"
shift
if [[ "$cmd" == "-h" || "$cmd" == "--help" ]]; then
main::show_help
return 0
fi
if [[ $# -eq 0 ]]; then
# Show the available functions for the command
local cmd_file="${DFM_CMD_DIR}/${cmd}.sh"
if [[ -f "$cmd_file" ]]; then
list::print_group "Available functions for '$cmd'"
list::loop_functions "$cmd_file"
else
lib::error "Command '$cmd' not found"
return 1
fi
return 0
fi
local func="$1"
shift
main::execute_command "$cmd" "$func" "$@"
}
main "$@"

View File

@@ -1,368 +0,0 @@
#!/usr/bin/env bash
# dfm common functions for logging and error handling, etc.
# Source this file to use the functions in your scripts.
#
# @author Ismo Vuorinen <https://github.com/ivuorinen>
# @license MIT
set -euo pipefail
declare -A ERROR_CODES=(
[SUCCESS]=0
[INVALID_ARGUMENT]=1
[COMMAND_NOT_FOUND]=2
[FUNCTION_NOT_FOUND]=3
[EXECUTION_FAILED]=4
[FILE_NOT_FOUND]=5
)
declare -A LOG_LEVELS=(
[DEBUG]=0
[INFO]=1
[WARN]=2
[ERROR]=3
)
# Simple logging function
#
# @example
# lib::log "Hello, world!"
#
# @description Log a message to the console
# @param $* Message to log
# Logs a message with a timestamp.
#
# Description:
# Outputs the provided message(s) to standard output, prepended with the current date and
# time in the format [YYYY-MM-DD HH:MM:SS]. This timestamp helps in tracking log events.
#
# Arguments:
# One or more strings that form the log message.
#
# Outputs:
# Writes the timestamped log message to standard output.
#
# Example:
# lib::log "Server started" # Outputs: [2025-02-28 09:45:00] Server started
lib::log()
{
printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*"
}
# Simple error logging function
#
# @example
# lib::error "Something went wrong"
#
# @description Log an error message to the console
# @param $* Error message
# Logs an error message with a timestamp to standard error.
#
# This function formats the provided message(s) by prefixing it with the current date
# and time along with an "ERROR:" label, then outputs the result to STDERR.
#
# Arguments:
# $* - The error message or messages to be logged.
#
# Outputs:
# Writes the formatted error message to STDERR.
#
# Example:
# lib::error "Failed to read the configuration file."
lib::error()
{
printf '[%s] ERROR: %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" >&2
}
LOG_LEVEL="${LOG_LEVEL:-INFO}"
if [[ -z "${LOG_LEVELS[$LOG_LEVEL]+_}" ]]; then
lib::error "Invalid LOG_LEVEL: $LOG_LEVEL"
exit "${ERROR_CODES[INVALID_ARGUMENT]}"
fi
# Handle an error by logging an error message to the console
# and exiting with an error code based on the error type.
#
# @example
# lib::error::handle $LINENO $0
#
# @description Handle an error
# @param $1 Line number
# @param $2 Command
# Logs an error message based on the previous command's exit code and the provided context.
#
# This function captures the exit code from the last executed command and, using the provided
# line number and command string, determines the appropriate error message to log based on
# predefined error codes stored in the ERROR_CODES associative array.
#
# Globals:
# ERROR_CODES - An associative array mapping error code names to numeric values.
# lib::error - Logs error messages to STDERR.
#
# Arguments:
# line_no - The line number in the script where the error occurred.
# command - The command that was executed when the error occurred.
#
# Outputs:
# Writes a descriptive error message to STDERR.
#
# Returns:
# The exit code of the failed command.
#
# Example:
# # If a command fails with an exit code corresponding to an invalid argument:
# lib::error::handle 42 "some_command"
# # This logs: "Invalid argument at line 42 in command 'some_command'" (if the exit code matches ERROR_CODES[INVALID_ARGUMENT])
lib::error::handle()
{
local exit_code=$?
local line_no=$1
local command=$2
case $exit_code in
"${ERROR_CODES[INVALID_ARGUMENT]}")
lib::error "Invalid argument at line $line_no in command '$command'"
;;
"${ERROR_CODES[COMMAND_NOT_FOUND]}")
lib::error "Command not found at line $line_no"
;;
"${ERROR_CODES[FUNCTION_NOT_FOUND]}")
lib::error "Function not found at line $line_no in command '$command'"
;;
"${ERROR_CODES[EXECUTION_FAILED]}")
lib::error "Execution failed at line $line_no in command '$command'"
;;
*)
lib::error "Unknown error ($exit_code) at line $line_no in command '$command'"
;;
esac
return $exit_code
}
# Throw an error by logging an error message to the console and exiting
# with an error code based on the error type. The error code name is used
# to determine the error code from the ERROR_CODES associative array.
# The error message is passed as arguments to the function.
#
# @example
# lib::error::throw INVALID_ARGUMENT "Invalid argument"
# lib::error::throw COMMAND_NOT_FOUND "Command not found"
# lib::error::throw FUNCTION_NOT_FOUND "Function not found"
# lib::error::throw EXECUTION_FAILED "Execution failed"
#
# @description Throw an error
# @param $1 Error code name
# @param $* Error message
# Logs an error message and terminates the script by performing cleanup with a specified error code.
#
# Globals:
# ERROR_CODES - Associative array mapping error code names to numeric exit values.
#
# Arguments:
# code_name - The key to retrieve the error code from the ERROR_CODES array.
# message - The error message to log, constructed from all subsequent arguments.
#
# Outputs:
# Logs the error message to standard error.
#
# Returns:
# Exits the script via the cleanup function; does not return.
#
# Example:
# lib::error::throw "FILE_NOT_FOUND" "Required file not found: /path/to/file"
lib::error::throw()
{
local code_name=$1
shift
local message=$*
if [[ -z "${ERROR_CODES[$code_name]+_}" ]]; then
lib::error "Unknown error code: $code_name"
cleanup "${ERROR_CODES[INVALID_ARGUMENT]}"
fi
lib::error "$message"
cleanup "${ERROR_CODES[$code_name]}"
}
# Logs a message to the console if the current log level is set so that the
# message is displayed. The log level is compared to the log level of the
# message and if the message log level is greater than or equal to the current
# log level, the message is displayed.
# The log levels are defined in the LOG_LEVELS associative array.
#
# @example
# logger::log "INFO" "This is an info message"
# logger::log "DEBUG" "This is a debug message"
# logger::log "WARN" "This is a warning message"
# logger::log "ERROR" "This is an error message"
#
# @description Log a message to the console based on the log level setting.
# @param $1 Log level
# @param $2 Message
# Logs a message if its severity meets or exceeds the global log level.
#
# Globals:
# LOG_LEVELS - Associative array mapping log level names to severity values.
# LOG_LEVEL - The current log level threshold.
#
# Arguments:
# level: A string representing the log severity (e.g., DEBUG, INFO, WARN, ERROR).
# msg: The message to log.
#
# Outputs:
# Prints a formatted log message with a timestamp to STDERR when the specified level qualifies.
#
# Example:
# logger::log INFO "Initialization complete"
logger::log()
{
local level=$1
if [[ -z "${LOG_LEVELS[$level]:-}" ]]; then
lib::error "Invalid log level: $level"
return 1
fi
shift
local msg="$*"
if [[ ${LOG_LEVELS[$level]} -ge ${LOG_LEVELS[$LOG_LEVEL]} ]]; then
printf '[%s] [%s]: %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$level" "$msg" >&2
fi
}
# Logs a debug message to the console, if the current log level is set to DEBUG or greater.
# The message is passed as arguments to the function.
# The function is defined above.
#
# @example
# logger::debug "This is a debug message"
#
# @description Log a debug message to the console
# @param $* Message
# Logs a debug-level message.
#
# This function logs a message at the DEBUG level by delegating to logger::log.
# It accepts one or more arguments that form the debug message, which are passed along
# to the underlying logger::log function.
#
# Example:
# logger::debug "Debug info for variable x:" "$x"
logger::debug()
{
logger::log "DEBUG" "$@"
}
# Logs an info message to the console, if the current log level is set to INFO or greater.
# The message is passed as arguments to the function.
# The function is defined above.
#
# @example
# logger::info "This is an info message"
#
# @description Log an info message to the console
# @param $* Message
# Logs an informational message to the console.
#
# Description:
# This function wraps the logger::log function to log messages at the "INFO" level. All provided arguments are
# forwarded to logger::log, where the message is formatted and output based on the current logging configuration.
#
# Arguments:
# A message string followed by optional additional parameters used to format the message.
#
# Outputs:
# The formatted informational message is written to STDOUT if the INFO log level is enabled.
#
# Example:
# logger::info "Service started successfully on port" 8080
logger::info()
{
logger::log "INFO" "$@"
}
# Logs a warning message to the console, if the current log level is set to WARN or greater.
# The message is passed as arguments to the function.
# The function is defined above.
#
# @example
# logger::warn "This is a warning message"
#
# @description Log a warning message to the console
# @param $* Message
# Logs a warning message.
#
# This function acts as a wrapper around `logger::log` by setting the log level to "WARN"
# for all provided message arguments. It forwards the given messages to the logger for output.
#
# Arguments:
# A variable list of strings representing the warning message.
#
# Outputs:
# Writes a formatted warning message to the console.
#
# Example:
# logger::warn "Low disk space" "Free up some space to avoid issues"
logger::warn()
{
logger::log "WARN" "$@"
}
# Logs an error message to the console, if the current log level is set to ERROR or greater.
# The message is passed as arguments to the function.
# The function is defined above.
#
# @example
# logger::error "This is an error message"
#
# @description Log an error message to the console
# @param $* Message
# Logs an error message at the ERROR level.
#
# This function wraps the generic logging mechanism to record error messages by automatically
# specifying the ERROR severity level. It passes all provided arguments to the underlying logging function.
#
# Arguments:
# Error message(s) One or more strings that describe the error.
#
# Example:
# logger::error "Unable to open file" "/path/to/file"
logger::error()
{
logger::log "ERROR" "$@"
}
# Cleanup function to remove temporary files and directories
# when the script exits or is interrupted by a signal (e.g. Ctrl+C).
# The function is registered with the `EXIT` trap.
#
# @description Remove temporary files and directories
# Cleans up temporary resources before exiting.
#
# Globals:
# TEMP_DIR - Path to the temporary directory to be removed if it exists.
#
# Returns:
# Exits the script with the original exit code.
#
# Example:
# trap cleanup EXIT
cleanup() {
local exit_code=${1:-$?}
if [[ -n ${TEMP_DIR:-} && -d $TEMP_DIR ]]; then
rm -rf "$TEMP_DIR"
fi
exit "$exit_code"
}
# Register the cleanup function to run on EXIT signal.
# This ensures temporary files and directories are removed
# when the script exits or is interrupted.
trap cleanup EXIT
# Handle errors by logging an error message to the console.
# The `ERR` trap passes the line number and command to lib::error::handle.
#
# Example:
# lib::error::handle ${LINENO} "$BASH_COMMAND"
trap 'lib::error::handle ${LINENO} "$BASH_COMMAND"' ERR

View File

@@ -1,806 +0,0 @@
#!/usr/bin/env bash
# dfm utility functions for common tasks
# Source this file to use the functions in your scripts.
#
# @author Ismo Vuorinen <https://github.com/ivuorinen>
# @license MIT
set -euo pipefail
# ANSI escape codes
readonly RESET="\033[0m"
readonly BOLD="\033[1m"
readonly DIM="\033[2m"
readonly ITALIC="\033[3m"
readonly UNDERLINE="\033[4m"
# Colors
readonly BLACK="\033[30m"
readonly RED="\033[31m"
readonly GREEN="\033[32m"
readonly YELLOW="\033[33m"
readonly BLUE="\033[34m"
readonly MAGENTA="\033[35m"
readonly CYAN="\033[36m"
readonly WHITE="\033[37m"
# Prints the provided text in black color using ANSI escape codes.
#
# Globals:
# BLACK - ANSI escape code for black text.
# RESET - ANSI escape code to reset terminal formatting.
#
# Arguments:
# One or more strings that will be concatenated and printed.
#
# Outputs:
# Writes the colored text to STDOUT (without a trailing newline).
#
# Example:
# clr::black "This text will appear in black."
clr::black()
{
printf "${BLACK}%s${RESET}" "$*"
}
# Prints the provided text in red using ANSI escape codes.
#
# Globals:
# RED - ANSI escape code for red.
# RESET - ANSI escape code to reset terminal formatting.
#
# Arguments:
# One or more strings that will be printed in red.
#
# Outputs:
# Writes the red formatted text to STDOUT.
#
# Example:
# clr::red "Error: Invalid input"
clr::red()
{
printf "${RED}%s${RESET}" "$*"
}
# Prints the given text in green using ANSI escape codes.
#
# Arguments:
# * One or more strings to output in green. Multiple arguments are concatenated.
#
# Outputs:
# Writes the formatted green text to STDOUT without a trailing newline.
#
# Example:
# clr::green "Operation successful"
clr::green()
{
printf "${GREEN}%s${RESET}" "$*"
}
# Prints the provided text in yellow color.
#
# Globals:
# YELLOW - ANSI escape code for yellow text.
# RESET - ANSI escape code to reset text formatting.
#
# Arguments:
# Any text passed as parameters will be printed in yellow.
#
# Outputs:
# Colored text printed to STDOUT.
#
# Example:
# clr::yellow "Hello, World!"
clr::yellow()
{
printf "${YELLOW}%s${RESET}" "$*"
}
# Prints the provided text in blue using ANSI escape codes.
#
# Globals:
# BLUE ANSI escape sequence for blue text.
# RESET ANSI escape sequence to reset text formatting.
#
# Arguments:
# $@ One or more strings to be printed in blue.
#
# Outputs:
# Prints the input text in blue to STDOUT.
#
# Example:
# clr::blue "Hello, World!"
clr::blue()
{
printf "${BLUE}%s${RESET}" "$*"
}
# Prints the provided text in magenta color.
#
# This function outputs one or more strings wrapped in ANSI escape sequences
# to display them in magenta. It uses the global variables MAGENTA for the color
# and RESET to revert to the default formatting.
#
# Globals:
# MAGENTA - ANSI escape sequence for magenta.
# RESET - ANSI escape code to reset formatting.
#
# Arguments:
# One or more strings to print in magenta.
#
# Outputs:
# Writes the formatted string directly to STDOUT.
#
# Example:
# clr::magenta "Hello, World!"
clr::magenta()
{
printf "${MAGENTA}%s${RESET}" "$*"
}
# Prints the provided text in white color using ANSI escape codes.
#
# Globals:
# WHITE - ANSI escape code for white.
# RESET - ANSI escape code to reset text formatting.
#
# Arguments:
# Any text passed as arguments will be concatenated and printed.
#
# Outputs:
# Writes the formatted text to STDOUT.
#
# Example:
# clr::white "Hello, World!"
clr::white()
{
printf "${WHITE}%s${RESET}" "$*"
}
# Applies bold styling to the provided text and prints it to STDOUT.
#
# Globals:
# BOLD - ANSI escape code for enabling bold text.
# RESET - ANSI escape code for resetting text formatting.
#
# Arguments:
# One or more strings to be printed in bold.
#
# Outputs:
# Bold-formatted text is printed to STDOUT.
#
# Example:
# style::bold "This is bold text"
style::bold()
{
printf "${BOLD}%s${RESET}" "$*"
}
# Print the provided text in a dim style using ANSI escape codes.
#
# Globals:
# DIM - ANSI escape code for applying dim styling.
# RESET - ANSI escape code to reset text formatting.
#
# Arguments:
# $* - The text to be printed in dim style.
#
# Outputs:
# Writes the formatted dim text to STDOUT.
#
# Example:
# style::dim "This text will appear dimmed"
style::dim()
{
printf "${DIM}%s${RESET}" "$*"
}
# Prints the provided text in italic style using ANSI escape sequences.
#
# Globals:
# ITALIC - ANSI escape sequence for italic text styling.
# RESET - ANSI escape sequence to reset text styling.
#
# Arguments:
# All passed arguments are combined and printed in italic formatting.
#
# Outputs:
# The styled text is printed to STDOUT without an automatic newline.
#
# Example:
# style::italic "Hello, world!"
style::italic()
{
printf "${ITALIC}%s${RESET}" "$*"
}
# Underlines the provided text using ANSI escape codes.
#
# Globals:
# UNDERLINE - ANSI escape sequence to start underlining.
# RESET - ANSI escape sequence to reset text formatting.
#
# Arguments:
# $* - The text to be underlined.
#
# Outputs:
# Prints the underlined text to STDOUT.
#
# Example:
# style::underline "Underlined text"
style::underline()
{
printf "${UNDERLINE}%s${RESET}" "$*"
}
# Prints a formatted line to STDOUT using the provided format string and arguments.
#
# Globals:
# RESET - ANSI escape code to reset text formatting.
#
# Arguments:
# $1 - A format string that may include ANSI styling codes (do not include a conversion specifier for the text).
# $@ - The text to be formatted and printed.
#
# Outputs:
# Writes the formatted text to STDOUT with an appended newline, ensuring that styling is reset afterward.
#
# Example:
# list::print_formatted "${BOLD}" "Bold Text"
list::print_formatted()
{
local format=$1
shift
printf "${format}%s${RESET}\n" "$@"
}
# Prints a formatted header with a decorative underline.
#
# Globals:
# BOLD - ANSI escape code for bold text.
# BLUE - ANSI escape code for blue text.
# RESET - ANSI escape code to reset text formatting.
#
# Arguments:
# $1 - The header title to be displayed.
#
# Outputs:
# Writes a styled header to STDOUT, including the title in bold blue and a subsequent decorative line.
#
# Example:
# list::print_header "Available Commands"
list::print_header()
{
printf "\n ${BOLD}${BLUE}%s${RESET}\n" "$1"
printf "%s\n" " $(printf '%.s─' {1..60})"
}
# Prints a group header with bold yellow formatting.
#
# Globals:
# YELLOW - ANSI escape code for yellow color.
# BOLD - ANSI escape code for bold text.
# RESET - ANSI escape code to reset text formatting.
#
# Arguments:
# group - The title text to display as the group header.
#
# Outputs:
# Writes the formatted group header to STDOUT.
#
# Example:
# list::print_group "My Group"
list::print_group()
{
local group=$1
printf "\n ${YELLOW}${BOLD}%s${RESET}\n\n" "$group"
}
# Prints a formatted command with an optional description.
#
# Globals:
# BOLD - ANSI escape sequence for bold text.
# CYAN - ANSI escape sequence for cyan text.
# RESET - ANSI escape sequence to reset text formatting.
# DIM - ANSI escape sequence for dim text.
#
# Arguments:
# cmd - The command name to display.
# desc - Optional description of the command (defaults to an empty string).
#
# Outputs:
# Writes the formatted command and description to STDOUT.
#
# Example:
# list::print_command "ls" "List directory contents"
list::print_command()
{
local cmd=$1
local desc=${2:-""}
printf " ${BOLD}${CYAN}%-15s${RESET} ${DIM}%s${RESET}\n" "$cmd" "$desc"
}
# Prints a subcommand in a formatted style.
#
# This function displays a subcommand name in green with a fixed width for neat alignment,
# followed by an optional description text. The ANSI escape codes for green text and reset
# styling are used to highlight the subcommand.
#
# Globals:
# GREEN - ANSI escape code applied to the subcommand name.
# RESET - ANSI escape code used to reset text formatting.
#
# Arguments:
# cmd - The subcommand name to print.
# desc - (Optional) A description string for the subcommand. Defaults to empty if not provided.
#
# Outputs:
# Prints the formatted subcommand and optional description to STDOUT.
#
# Example:
# list::print_subcommand "deploy" "Deploy the application to the production server"
list::print_subcommand()
{
local cmd=$1
local desc=${2:-""}
printf " ${GREEN}%-13s${RESET} ${desc}\n" "$cmd"
}
# Iterates over functions defined in a command file and prints each as a formatted subcommand.
#
# This function reads function names from the specified command file, retrieves their descriptions
# (removing any '@description' prefix), and prints each function name as a bullet point with its
# associated description if available.
#
# Arguments:
# cmd_file - The path to the command file containing function definitions.
#
# Outputs:
# Prints formatted subcommand entries to STDOUT.
#
# Example:
# list::loop_functions "/path/to/command_file.sh"
list::loop_functions()
{
local cmd_file="$1"
while IFS= read -r func; do
# Get the function description from the function definition in the
# command file. If no description is found, print only the function name.
# The description is printed without the @description prefix.
# If the function is not found, print only the function name.
# The function name is printed with a bullet point.
local doc
doc=$(main::get_function_description "$cmd_file" "$func")
if [[ -n "$doc" ]]; then
list::print_subcommand "$func:" "${doc#*@description}"
else
list::print_subcommand "$func" ""
fi
done < <(main::get_command_functions "$cmd_file")
}
# Extracts and prints the documentation associated with a specific function from a command file.
#
# Globals:
# None
#
# Arguments:
# cmd_file - The file containing function definitions and their associated documentation.
# func - The name of the function whose documentation should be extracted.
#
# Outputs:
# Writes the extracted documentation tags and their content to STDOUT.
#
# Returns:
# None
#
# Example:
# list::get_function_docs "commands.sh" "build_project"
list::get_function_docs()
{
local cmd_file="$1"
local func="$2"
awk -v func="$func" '
# Start collecting documentation when a function is found and the line contains @
/^[[:space:]]*#[[:space:]]*@/ {
tag = $2
sub(/^[[:space:]]*#[[:space:]]*@[[:space:]]*[a-zA-Z]+[[:space:]]*/, "")
docs[tag] = $0
last_tag = tag
}
# Collect multi-line documentation
/^[[:space:]]*#/ && last_tag && !/^[[:space:]]*#[[:space:]]*@/ {
sub(/^[[:space:]]*#[[:space:]]*/, "")
docs[last_tag] = docs[last_tag] " " $0
}
# Empty line or comment line ends documentation
!/^[[:space:]]*#/ {
last_tag = ""
}
# When the function is found, print the documentation
$0 ~ "^[[:space:]]*(function[[:space:]]+)?" func "\\(\\)" {
for (tag in docs) {
printf "@%s %s\n", tag, docs[tag]
}
}
' "$cmd_file"
}
# Check if a command exists in the current environment and return 0 if it does.
# Otherwise, return 1.
#
# @example
# if utils::is_installed curl; then
# echo "curl is installed"
# else
# echo "curl is not installed"
# fi
#
# @description Check if a command exists
# @param $1 Command to check
# Checks if a specified command is available in the system.
#
# Arguments:
# $1 - Command name to check.
#
# Returns:
# 0 if the command is found in the system's PATH, 1 otherwise.
#
# Example:
# utils::is_installed "git" && echo "Git is installed" || echo "Git is not installed"
utils::is_installed()
{
command -v "$1" > /dev/null 2>&1
}
# Check if an executable exists in one of the directories listed in PATH and
# return 0 if it does. Otherwise, return 1.
#
# @example
# if utils::in_path ls; then
# echo "ls is available in PATH"
# else
# echo "ls is not available in PATH"
# fi
#
# @description Check if an executable is in PATH
# @param $1 Command to check
# Checks if a specified executable is available in one of the directories in the PATH.
#
# Globals:
# PATH - The system's PATH environment variable listing directories to search.
#
# Arguments:
# cmd: The name of the executable file to look for.
#
# Returns:
# 0 if the executable is found in one of the PATH directories, 1 otherwise.
#
# Example:
# utils::in_path ls && echo "ls is available in PATH"
utils::in_path()
{
local cmd=$1
local result=1
IFS=: read -ra path <<< "$PATH"
for p in "${path[@]}"; do
if [[ -x "$p/$cmd" ]]; then
result=0
break
fi
done
return $result
}
# Retry a command until it succeeds or the maximum number of retries is reached.
# Logs a warning message if the command fails and is retried after a short delay.
#
# @example
# if utils::retry 3 curl -sSL https://example.com; then
# echo "Success"
# else
# echo "Failed"
# fi
#
# @description Retry a command
# @param $1 Maximum number of retries
# @param $2.. Command to run
# @return 0 if the command succeeds, 1 otherwise
# Retries a command until it succeeds or the maximum number of attempts is reached.
#
# Arguments:
# tries - Maximum number of attempts to execute the command.
# command and its args - The command to run and any arguments to pass.
#
# Globals:
# logger::warn - Logs a warning message for each failed attempt.
#
# Outputs:
# Warning messages are printed to STDERR for each retry.
#
# Returns:
# 0 if the command eventually succeeds; 1 if all attempts fail.
#
# Example:
# utils::retry 3 my_command --option value
#
# Dependencies:
# logger::warn
utils::retry()
{
local tries=$1
shift
local count=1
until "$@"; do
[[ $count -gt $tries ]] && return 1
logger::warn "Failed, retry $count/$tries"
((count++))
sleep 1
done
return 0
}
# Ask for confirmation before proceeding. The default value is used if the user
# presses Enter without providing an answer.
#
# @example
# if utils::interactive::confirm "Are you sure?"; then
# echo "Confirmed"
# else
# echo "Not confirmed"
# fi
#
# @description Confirm an action
# @param $1 Prompt message
# @param $2 Default value
# Prompts the user for confirmation with a yes/no question.
#
# Arguments:
# prompt: The message displayed to the user when asking for confirmation.
# default: An optional default answer used if no input is provided (defaults to "Y").
#
# Outputs:
# Repeatedly prompts the user until a valid yes or no answer is received.
# An error message is displayed for any invalid response.
#
# Returns:
# 0 if the user confirms (answers yes), 1 if the user declines (answers no).
#
# Example:
# if utils::interactive::confirm "Do you want to proceed?"; then
# echo "Proceeding..."
# else
# echo "Operation cancelled."
# fi
utils::interactive::confirm()
{
local prompt=$1
local default=${2:-Y}
while true; do
read -rp "$prompt [Y/n]: " response
case ${response:-$default} in
[Yy]*) return 0 ;;
[Nn]*) return 1 ;;
*) echo "Please answer yes or no" ;;
esac
done
}
# Find all command files in the cmd directory and return them
# as a space-separated string of filenames (e.g. "cmd1.sh cmd2.sh").
#
# The function uses a while loop to read the output of the find command
# line by line. The -print0 option is used to separate the filenames with
# a null character (\0) instead of a newline. This is necessary to handle
# filenames with spaces correctly.
#
# The read command reads the null-separated filenames and appends them to
# the cmd_files array. Finally, the function prints the array elements
# separated by a space.
#
# Finds all command script files (*.sh) in the directory specified by DFM_CMD_DIR.
#
# Globals:
# DFM_CMD_DIR - The directory to search for command files.
#
# Outputs:
# Echoes a space-separated list of command file paths.
#
# Example:
# files=$(main::find_commands)
# echo "$files" # Displays the list of found command files.
main::find_commands()
{
local cmd_files=()
while IFS= read -r -d '' file; do
cmd_files+=("$file")
done < <(find "$DFM_CMD_DIR" -type f -name "*.sh" -print0)
echo "${cmd_files[@]}"
}
# Get the function names from a command file.
#
# The function uses grep to find function definitions (function xxx() or xxx())
# and sed to extract the function names. The function names are printed one per
# line.
#
# @param cmd_file The command file to extract function names from.
# Extracts the names of functions defined in the specified command file.
#
# This function parses the provided file for Bash function definitions using
# regex patterns matching both "function name() {" and "name() {" styles.
# It outputs the names of the functions, one per line.
#
# Globals:
# None.
#
# Arguments:
# cmd_file - Path to the file containing Bash function definitions.
#
# Outputs:
# Writes the list of function names to STDOUT.
#
# Returns:
# A list of function names extracted from the file.
#
# Example:
# main::get_command_functions "/path/to/command_file.sh"
main::get_command_functions()
{
local cmd_file="$1"
# Find function definitions (function xxx() or xxx())
grep -E '^[[:space:]]*(function[[:space:]]+)?[a-zA-Z0-9_]+\(\)[[:space:]]*{' "$cmd_file" \
| sed -E 's/^[[:space:]]*(function[[:space:]]+)?([a-zA-Z0-9_]+).*/\2/'
}
# Get the description of a function from a command file.
#
# The function uses grep to find the function definition and sed to extract
# the description. The description is printed without the @description prefix.
#
# @param cmd_file The command file to extract the function description from.
# @param func The function name.
# Retrieves the annotated description of a specified function from a command file.
#
# This function searches the provided command file for an "@description" comment
# preceding the definition of the designated function. It then extracts and prints
# the description text. If no description is found, nothing is printed.
#
# Arguments:
# cmd - Command name or path to the file containing the function definitions.
# func - Name of the function whose description is to be extracted.
#
# Outputs:
# The extracted description text is printed to STDOUT.
#
# Example:
# desc=$(main::get_function_description "install" "my_function")
main::get_function_description()
{
local cmd_file="$1"
local func="$2"
if [[ ! -f "$cmd_file" ]]; then
[[ -n ${DFM_CMD_DIR:-} ]] || return 1
cmd_file="${DFM_CMD_DIR}/${cmd_file}"
[[ "$cmd_file" == *.sh ]] || cmd_file="${cmd_file}.sh"
fi
[[ -f "$cmd_file" ]] || return 1
local escaped_func
escaped_func=$(printf '%s' "$func" | sed 's/[][\\.^$*+?(){}|]/\\&/g')
grep -B5 -E "^[[:space:]]*(function[[:space:]]*)?${escaped_func}[[:space:]]*\\(\\)[[:space:]]*(\\{)?[[:space:]]*$" "$cmd_file" \
| grep "@description" \
| sed -E 's/^[[:space:]]*#[[:space:]]*@description[[:space:]]*//'
}
# List all available commands and their functions.
#
# The function uses main::find_commands to get a list of command files.
# It then iterates over the files and prints the command name and
# its functions.
#
# Lists all available commands and their subcommands.
#
# Description:
# Uses main::find_commands to locate command files and prints a header followed by a group title.
# For each command file, extracts the command name (removing the '.sh' extension) and prints it,
# then calls list::loop_functions to display detailed subcommands.
#
# Globals:
# None.
#
# Arguments:
# None.
#
# Outputs:
# Writes the formatted list of commands and associated subcommands to STDOUT.
#
# Example:
# main::list_available_commands
main::list_available_commands()
{
local cmd_files
cmd_files=$(main::find_commands)
list::print_header "dfm - dotfiles manager"
list::print_group "Available commands"
for cmd_file in $cmd_files; do
local cmd_name
cmd_name=$(basename "$cmd_file" .sh)
list::print_command "$cmd_name"
list::loop_functions "$cmd_file"
done
}
# Execute a command function.
#
# The function loads the command file and checks if the function exists.
# If the function exists, it executes the function with the provided arguments.
#
# @param cmd The command name.
# @param func The function name.
# @param args The function arguments.
# Executes a specified function from a command file.
#
# This function validates and runs a function defined within a command file. It checks that both
# the command and function names contain only allowed characters (alphanumeric, underscores, or dashes),
# verifies that the command file (located in DFM_CMD_DIR) exists, is readable, and is free of syntax errors,
# and then sources the file. If the specified function exists in the file, it is executed with any additional
# arguments provided.
#
# Globals:
# DFM_CMD_DIR - Directory containing command files.
#
# Arguments:
# command: The command file name (without .sh extension) to execute. Must match ^[a-zA-Z0-9_-]+$.
# function: The function name to be executed from the command file. Must match ^[a-zA-Z0-9_-]+$.
# [additional arguments]: Extra parameters to pass to the executed function.
#
# Outputs:
# Any output generated by the executed function. Error messages are output via lib::error.
#
# Returns:
# 0 if the function executes successfully; 1 if an error occurs (e.g., invalid names, missing or unreadable
# command file, syntax errors in the command file, or if the specified function is not found).
#
# Example:
# main::execute_command "deploy" "run_deploy" "arg1" "arg2"
main::execute_command()
{
local cmd="$1"
shift
local func="$1"
shift
# Validate input
if [[ ! "$cmd" =~ ^[a-zA-Z0-9_-]+$ ]] || [[ ! "$func" =~ ^[a-zA-Z0-9_-]+$ ]]; then
lib::error "Invalid command or function name"
return 1
fi
local cmd_file="${DFM_CMD_DIR}/${cmd}.sh"
if [[ ! -f "$cmd_file" ]] || [[ ! -r "$cmd_file" ]]; then
lib::error "Command '$cmd' not found"
return 1
fi
# Validate command file
if ! bash -n "$cmd_file"; then
lib::error "Command file '$cmd' contains syntax errors"
return 1
fi
# Source the command file
# shellcheck source=/dev/null
source "$cmd_file"
# Check if the function exists
if ! declare -f "$func" > /dev/null; then
lib::error "Function '$func' not found in command '$cmd'"
return 1
fi
# Run the function with the provided arguments
"$func" "$@"
}

View File

@@ -10,7 +10,7 @@
"fix:markdown": "npx markdownlint -df .",
"lint:prettier": "npx prettier . --check",
"fix:prettier": "npx prettier . --write",
"test": "npx bats --recursive tests/"
"test": "bash test-all.sh"
},
"repository": {
"type": "git",
@@ -32,5 +32,5 @@
"bats": "^1.12.0",
"typescript": "^5.8.3"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
"packageManager": "yarn@1.22.22"
}

View File

@@ -0,0 +1,11 @@
# create-aerospace-keymaps
Generates `docs/aerospace-keybindings.md` using `aerospace config --json`.
## Usage
```bash
scripts/create-aerospace-keymaps.php
```
Requires the `aerospace` CLI tool to be installed.

View File

@@ -0,0 +1,11 @@
# create-nvim-keymaps
Outputs current Neovim key mappings to `docs/nvim-keybindings.md`.
## Usage
```bash
scripts/create-nvim-keymaps.sh
```
Requires Neovim to be installed.

View File

@@ -0,0 +1,11 @@
# create-wezterm-keymaps
Generates `docs/wezterm-keybindings.md` by invoking `wezterm show-keys`.
## Usage
```bash
scripts/create-wezterm-keymaps.sh
```
Requires wezterm to be installed.

View File

@@ -0,0 +1,12 @@
# install-cargo-packages
Install Rust packages listed in `config/asdf/cargo-packages`.
## Usage
```bash
scripts/install-cargo-packages.sh
```
The script installs each package with `cargo install` and runs
`cargo-install-update` when available to update existing packages.

View File

@@ -0,0 +1,12 @@
# install-cheat-purebashbible
Fetches the Pure Bash Bible repository and installs its cheatsheets for the
`cheat` utility.
## Usage
```bash
scripts/install-cheat-purebashbible.sh
```
Requires `git` and `cheat` to be available in PATH.

View File

@@ -0,0 +1,11 @@
# install-composer
Installs the PHP package manager [Composer](https://getcomposer.org/).
## Usage
```bash
scripts/install-composer.sh
```
The script downloads the latest Composer PHAR and places it in `$HOME/.local/bin`.

11
scripts/install-fonts.md Normal file
View File

@@ -0,0 +1,11 @@
# install-fonts
Installs Nerd Fonts used by various terminal and editor setups.
## Usage
```bash
scripts/install-fonts.sh
```
Fonts are downloaded to `$HOME/.local/share/fonts` and refreshed automatically.

View File

@@ -0,0 +1,12 @@
# install-gh-extensions
Installs a curated set of GitHub CLI extensions defined in
`config/gh/extensions`.
## Usage
```bash
scripts/install-gh-extensions.sh
```
The script installs each extension using the `gh extension install` command.

View File

@@ -0,0 +1,12 @@
# install-git-crypt
Installs `git-crypt` for transparent encryption of files in Git repositories.
## Usage
```bash
scripts/install-git-crypt.sh
```
After installation you can run `git-crypt init` inside a repository to begin
encrypting files.

View File

@@ -0,0 +1,12 @@
# install-go-packages
Installs Go binaries defined in `config/go/packages`.
## Usage
```bash
scripts/install-go-packages.sh
```
The script uses `go install` for each package path listed in the configuration
file.

View File

@@ -0,0 +1,11 @@
# install-macos-defaults
Applies a set of macOS defaults for a consistent developer environment.
## Usage
```bash
scripts/install-macos-defaults.sh
```
Requires macOS and the `defaults` command.

View File

@@ -0,0 +1,11 @@
# install-npm-packages
Installs global npm packages listed in `config/npm/packages`.
## Usage
```bash
scripts/install-npm-packages.sh
```
Uses `npm install -g` for each package in the configuration file.

11
scripts/install-ntfy.md Normal file
View File

@@ -0,0 +1,11 @@
# install-ntfy
Installs the lightweight notification tool [`ntfy`](https://ntfy.sh/).
## Usage
```bash
scripts/install-ntfy.sh
```
After running you can send notifications using `ntfy publish`.

View File

@@ -0,0 +1,11 @@
# install-pip-packages
Installs Python packages from `config/pip/packages` using `pip`.
## Usage
```bash
scripts/install-pip-packages.sh
```
The script uses `pip install --user` for each package entry.

View File

@@ -0,0 +1,11 @@
# install-xcode-cli-tools
Installs the Xcode Command Line Tools on macOS using `osascript` prompts.
## Usage
```bash
scripts/install-xcode-cli-tools.sh
```
Requires macOS with administrator privileges.

11
scripts/install-z.md Normal file
View File

@@ -0,0 +1,11 @@
# install-z
Installs [z](https://github.com/rupa/z), a directory jumping tool.
## Usage
```bash
scripts/install-z.sh
```
Once installed add `. ~/.z` to your shell startup file to enable the command.

13
scripts/shared.md Normal file
View File

@@ -0,0 +1,13 @@
# shared
Collection of helper functions shared across install scripts.
## Usage
Source the file in your script:
```bash
source "${DOTFILES}/scripts/shared.sh"
```
Provides messaging helpers and common environment checks.

View File

@@ -10,6 +10,7 @@
[ -z "$SHARED_SCRIPTS_SOURCED" ] && {
source "${DOTFILES}/config/shared.sh"
# Warn the user if the shared configuration hasn't been loaded yet
msgr warn "(!) shared.sh not sourced"
# Set variable that checks if the shared.sh script has been

View File

@@ -0,0 +1,11 @@
# update-readme-aliases
Regenerates the alias table located at `docs/alias.md`.
## Usage
```bash
scripts/update-readme-aliases.sh
```
This script parses `config/alias` and writes the markdown table to the docs.

15
test-all.sh Executable file
View File

@@ -0,0 +1,15 @@
#!/usr/bin/env bash
# Run all bats tests
set -euo pipefail
if [ -x "node_modules/bats/bin/bats" ]; then
git ls-files '*.bats' -z | xargs -0 node_modules/bats/bin/bats
elif command -v npx >/dev/null; then
git ls-files '*.bats' -z | xargs -0 npx --yes bats
elif command -v bats >/dev/null; then
git ls-files '*.bats' -z | xargs -0 bats
else
echo "bats not installed. Run 'yarn install' first." >&2
exit 1
fi

11
tests/dfm.bats Normal file
View File

@@ -0,0 +1,11 @@
#!/usr/bin/env bats
setup() {
export DOTFILES="$PWD"
}
@test "dfm help shows usage" {
run bash local/bin/dfm help
[ "$status" -eq 0 ]
[[ "$output" == *"Usage: dfm"* ]]
}

View File

@@ -1,16 +0,0 @@
run_with_dfm() {
local cmd="$*"
run env \
PROJECT_ROOT="$PROJECT_ROOT" \
DFM_CMD_DIR="$PROJECT_ROOT/local/dfm/cmd" \
DFM_LIB_DIR="$PROJECT_ROOT/local/dfm/lib" \
DOTFILES="${DOTFILES:-$PROJECT_ROOT}" \
NO_AUTOMATION="${NO_AUTOMATION:-1}" \
TEMP_DIR="$TEMP_DIR" \
bash -c 'set -e
cmd="$1"
source "$PROJECT_ROOT/local/dfm/lib/common.sh"
source "$PROJECT_ROOT/local/dfm/lib/utils.sh"
set +e
eval "$cmd"' bash "$cmd"
}

View File

@@ -1,56 +0,0 @@
load "$BATS_TEST_DIRNAME/helpers.bash"
setup() {
set -euo pipefail
PROJECT_ROOT="$BATS_TEST_DIRNAME/../.."
TEMP_DIR="$(mktemp -d)"
export TEMP_DIR
}
teardown() {
[[ -n "${TEMP_DIR:-}" ]] && rm -rf "$TEMP_DIR"
}
@test "list_available_commands shows commands" {
run_with_dfm main::list_available_commands
[ "$status" -eq 0 ]
echo "$output" | grep -q "Available commands"
echo "$output" | grep -q "install"
}
@test "interactive confirm returns 0 on yes" {
run_with_dfm 'utils::interactive::confirm "Proceed?" <<< "y"; echo $?'
[ "$status" -eq 0 ]
[ "${lines[-1]}" = "0" ]
}
@test "interactive confirm returns 1 on no" {
run_with_dfm 'utils::interactive::confirm "Proceed?" <<< "n"; echo $?'
[ "$status" -eq 0 ]
[ "${lines[-1]}" = "1" ]
}
@test "execute_command runs function" {
run_with_dfm "main::execute_command install fonts"
[ "$status" -eq 0 ]
echo "$output" | grep -q "Installing fonts"
}
@test "execute_command fails on missing function" {
run_with_dfm "main::execute_command install nofunc >/dev/null 2>&1"
[ "$status" -eq 1 ]
}
@test "install all respects skip options" {
run_with_dfm "main::execute_command install all --no-brew --no-cargo --no-automation"
[ "$status" -eq 0 ]
echo "$output" | grep -q "Installing fonts"
[[ "$output" != *"Installing Homebrew"* ]]
[[ "$output" != *"Rust and cargo packages"* ]]
}
@test "get_function_description returns description" {
run_with_dfm "main::get_function_description \"$PROJECT_ROOT/local/dfm/cmd/install.sh\" fonts"
[ "$status" -eq 0 ]
echo "$output" | grep -q "Install all configured fonts"
}

View File

@@ -0,0 +1,7 @@
#!/usr/bin/env bats
@test "x-gh-get-latest-version help" {
run bash local/bin/x-gh-get-latest-version --help
[ "$status" -eq 0 ]
[[ "$output" == "Usage: x-gh-get-latest-version"* ]]
}

13
tests/x-localip.bats Normal file
View File

@@ -0,0 +1,13 @@
#!/usr/bin/env bats
@test "x-localip prints version" {
run bash local/bin/x-localip --version
[ "$status" -eq 0 ]
[[ "$output" == "x-localip version"* ]]
}
@test "x-localip help" {
run bash local/bin/x-localip --help
[ "$status" -eq 0 ]
[[ "$output" == "Usage:"* ]]
}

14
tests/x-mkd.bats Normal file
View File

@@ -0,0 +1,14 @@
#!/usr/bin/env bats
@test "x-mkd creates directory" {
dir="$BATS_TMPDIR/mkd-test"
run env VERBOSE=1 bash local/bin/x-mkd "$dir"
[ "$status" -eq 0 ]
[ -d "$dir" ]
}
@test "x-mkd with no args shows usage" {
run bash local/bin/x-mkd
[ "$status" -eq 1 ]
[[ "$output" == "Usage:"* ]]
}

28
tests/x-path.bats Normal file
View File

@@ -0,0 +1,28 @@
#!/usr/bin/env bats
@test "x-path-append adds directory" {
mkdir -p "$BATS_TMPDIR/dir"
PATH="/usr/bin"
VERBOSE=1 source local/bin/x-path-append "$BATS_TMPDIR/dir"
[ "$PATH" = "/usr/bin:$BATS_TMPDIR/dir" ]
}
@test "x-path-prepend adds directory to start" {
mkdir -p "$BATS_TMPDIR/dir"
PATH="/usr/bin:/bin"
VERBOSE=1 source local/bin/x-path-prepend "$BATS_TMPDIR/dir"
[ "$PATH" = "$BATS_TMPDIR/dir:/usr/bin:/bin" ]
}
@test "x-path-remove removes directory" {
mkdir -p "$BATS_TMPDIR/dir"
PATH="$BATS_TMPDIR/dir:/usr/bin"
VERBOSE=1 source local/bin/x-path-remove "$BATS_TMPDIR/dir"
[ "$PATH" = "/usr/bin" ]
}
@test "x-path-append skips missing directory" {
PATH="/usr/bin"
VERBOSE=1 source local/bin/x-path-append "$BATS_TMPDIR/no-such"
[ "$PATH" = "/usr/bin" ]
}

View File

@@ -1252,11 +1252,6 @@ balanced-match@^3.0.0:
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-3.0.1.tgz#e854b098724b15076384266497392a271f4a26a0"
integrity sha512-vjtV3hiLqYDNRoiAv0zC4QaGAMPomEoq83PRmYIofPswwZurCeWR5LByXm7SyoL0Zh5+2z0+HC7jG8gSZJUh0w==
bats@^1.12.0:
version "1.12.0"
resolved "https://registry.yarnpkg.com/bats/-/bats-1.12.0.tgz#3ed99170325141e5d6dd53a84c3e5a702d5ab5be"
integrity sha512-1HTv2n+fjn3bmY9SNDgmzS6bjoKtVlSK2pIHON5aSA2xaqGkZFoCCWP46/G6jm9zZ7MCi84mD+3Byw4t3KGwBg==
before-after-hook@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-4.0.0.tgz#cf1447ab9160df6a40f3621da64d6ffc36050cb9"