17 Commits

Author SHA1 Message Date
a7074fc940 feat(tmux): F12 to passthru 2026-04-20 23:04:50 +08:00
4e3ce4f9d9 feat(tmux): prevent tmux from exiting copy mode after selection with mouse
see:
- https://www.reddit.com/r/tmux/comments/v73005/how_to_prevent_tmux_from_exiting_copy_mode_after/
- https://www.reddit.com/r/tmux/comments/1ltms6a/how_to_select_text_in_tmux_without_having_it_jump/
2026-04-12 01:18:13 +08:00
ed98a56e05 fix(ci): riot 2026-04-03 17:10:29 +08:00
e79b092054 feat(riot): ControlPersist=60s 2026-04-03 15:54:17 +08:00
f45f7e415c fix(riot): tmux error with window number and spaces
The fix to spaces is just a workaround. Issue may still exist when
command contains spaces.
2026-01-19 16:27:56 +08:00
9f1bc8f0fb fix(frigg): ddns get ip 2025-12-20 12:40:19 +08:00
ee44717891 feat: zsh completion for beam and beaml 2025-12-19 21:56:09 +08:00
d283671f25 feat(riot): conditionally RequestTTY 2025-12-19 21:28:16 +08:00
bdef3b52d1 feat(riot): -o ServerAliveInterval=60 2025-12-19 21:23:52 +08:00
c86cc85e24 feat(riot): sshr for ssh -R 2025-12-19 21:21:22 +08:00
426e97c10e feat(riot): sshl supports specifying local port and unix sock 2025-12-19 17:34:37 +08:00
e1f0ec0ebc fix(tmux): press ^a twice to send ^a 2025-11-20 20:22:17 +08:00
8dbb310e02 fix(zshrc): bnd exit when build error 2025-11-17 01:45:53 +08:00
8865dee1e9 feat: add bnd and nor and completions 2025-11-15 14:48:19 +08:00
65d690f47c build(riot-config): nasp.fit -> jump.nasp.fit 2025-10-28 17:18:56 +08:00
ecf652ad7b feat(riot): command git 2025-07-24 16:28:37 +08:00
86670306af feat(riot): support literal ipv6 addresses (with or without ports given)
feat(riot): -v, -4, and -6

fix(riot): do not call external binaries if possible
2025-07-16 21:09:04 +08:00
21 changed files with 146 additions and 1539 deletions

View File

@@ -28,34 +28,3 @@ SNt5JZCLBT4nLt0uQp9O/xctdHElZw+/W8OfnP5vxnPdccIeVOxpGIyzErwjD0+E
ZJc6v5e+iqbE1ECZLco5LjWqqfvFfYCrkqeOhCRsRkVPsXnGPo2QDDYdTm4EGCmg
dVZ6R452SZsrE4V+3LR011BxzEg=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIFQDCCAyigAwIBAgIRAJmQ852eXApHBvrQujWaFAEwDQYJKoZIhvcNAQELBQAw
ODESMBAGA1UEChMJRGljdCBUZWNoMSIwIAYDVQQDExlEaWN0IFRlY2ggSXNzdWlu
ZyBDQSAyMDI2MB4XDTI2MDgxMjE1NTQyM1oXDTI5MDgyMTExMDAwMFowKzESMBAG
A1UEChMJRGljdCBUZWNoMRUwEwYDVQQDEwxzazEuZHhuZy5uZXQwggEiMA0GCSqG
SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAkkF0Qc1YhhgmgZcujjfyn3liCxAAJSGl
Eme0eD6s908zm95mGd64fQJSoYVaFt5y9wrQLdWX16c+gvRyNaQdPfs5jxaP+G8V
r6PkkE79hwauEOQOTbr+q3bQJmfLe56cMV7oHrKVFffkJQTvp1mcK4+CYytsxJP5
v0m5TDyv4op6VcZ2m0gdrFEBV3DMMgsqF6/tSPkqbTTj9fMMSOFzXPq7qEQWgYmx
T+M0iQdgEiG7Z+INkofM56tBmQ7jSeO65/FTytB6ZMiI23NGmTmYvHPyxSDWXL0X
7tX42S0/A7QhxZHN0GUD0NGaSExLfgTawRLhARVHbUXC9iu5aCArAgMBAAGjggFQ
MIIBTDAOBgNVHQ8BAf8EBAMCBaAwKwYDVR0lBCQwIgYIKwYBBQUHAwIGCisGAQQB
gjcUAgIGCisGAQQBgjdDAQEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUOyThWF2A
BkoQv9xljyo3fFOclq0wHwYDVR0jBBgwFoAU9qXPjCjcil59akpPVISS8Yu/Jc0w
SgYIKwYBBQUHAQEEPjA8MDoGCCsGAQUFBzAChi5odHRwOi8vdG9vbHMuZHhuZy5j
bi9jZXJ0cy9pc3N1aW5nLWNhLTIwMjYuY2VyMCoGA1UdHwQjMCEwH6AdoBuGGWh0
dHA6Ly9jYS5pYmQuaW5rLzEuMC9jcmwwRwYMKwYBBAGCpGTGKEABBDcwNQIBAQQD
cGl2BCtaMnFWY05lMzlUaHZyc3RIbFVFWE5laXg1QXd2aFU3MjBlWEFFZWRlUG4w
MA0GCSqGSIb3DQEBCwUAA4ICAQCKSLjnMOlQiZ381IaBEl4iT7pCvY0QQxsVEMDD
RWYhIImx3JJDt7kiZXcJwlH70VG0opSz9eKrT3cw7OY6DnNm7Bm0JT2fJgyCSnWo
nG7J1soE74Rl2MiNnP1JTqhFlZKLrmIb6eP4U0eM+ZaJ7luyxizqiJAeRyJB+YCe
DM9uZ7rC33DYZeZ46V4f7DfJ23TjVJvaX/NFu0Ab/7ylF6X7frT8xf11XLlE2IyR
MmnLtQiREXXETaw9l5suMuH8fmVg01QrqnOHmz4r9Nuzh7vHxogEsJtgBLhv2Min
X6IfaKABhFXk1cVZGNgaC0Pao6cYbaWJwF2zcVHGq5EDKXhkKmCLeDo9em0rOJ2y
fU23yP3ByMGJ3Xi7nI8E5dfSw6Q39b3t1/kxj1y8T3PGdTN/3vFLVqf5A7XXdQL9
/uy2lYh3Fud/aZuthZyoL2rkC4O8N4seqfNIOBEHAmfbk5j6LoFFwSTrRGqzCAf4
4r6ckHjL3KBnR8EeMwwit324iRYd3ppUL3TINkeiHRNFVKU0flUvcG6T0v+hC2sX
yupYy0wylkonub6L0gUIINqx38nsDCR07eAfsDXJshIwTxUBFrVpKjPK7jVQOZ6+
9/yPu72BhyGMZs4bgYMEPwqKTbzUNitO2WfBIgXhzGe8YzMA1U2BMTRT0nIQ6oa9
8YbnQw==
-----END CERTIFICATE-----

View File

@@ -1,7 +1,6 @@
[user]
email = me@dxng.cn
email = me@beardic.cn
name = Dict Xiong
signingkey = 3E241558655D7FE06C6711A5A5D6250D1806CAA8
[core]
editor = vim
# test this by `git update-index --test-untracked-cache`

View File

@@ -44,6 +44,47 @@ jobs:
shell: /bin/zsh -ileo PIPE_FAIL {0}
run: source tools/test.zsh
test-macos:
name: test on macos
runs-on: macos-latest
steps:
- name: checkout repo
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: install dfs
run: |
rev=`git rev-parse HEAD`
pwd
set -x
DFS_NO_WALL=0 ./install.sh -adl
test `git rev-parse HEAD` = "$rev"
- name: antigen build
shell: /bin/zsh -ileo PIPE_FAIL {0}
run: |
echo $SHELL
antigen list
- name: run tests and reset
shell: /bin/zsh -ileo PIPE_FAIL {0}
run: |
source tools/test.zsh
antigen reset
rm -rf $ANTIGEN $HOME/.antigen $HOME/.config/dotfiles
./install.sh -dl
- name: antigen build with DFS_NO_WALL
shell: /bin/zsh -ileo PIPE_FAIL {0}
run: |
echo $SHELL
antigen list
- name: run tests
shell: /bin/zsh -ileo PIPE_FAIL {0}
run: source tools/test.zsh
test-autodep:
name: test of auto-install dependencies
runs-on: ubuntu-latest

View File

@@ -2,16 +2,12 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGakUcSuNhuH7tk+Nj9gDfRtxYfGPL9yb8toQ/EpSJAM openpgp:0xD9271B01
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEJ4FeeBwPcUe25JsZQ7xDmGtFVeO1TQ9332IPjFY0Xm openpgp:0x8774BF70
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCUN7IXF4nlFcVfgHesgik3LIAiXlVMYJPm3yD13EVarQx5jqdBgk8Dwgkgf4rPO6MFpvIpinOyEO8zOS6HHQrCLZUv5yTFaDkUuB7eQ0EmpicGbmk9bHqj1HkOZxaobkpEfQUmFKYvkp4EexVw66sO0qfXvjHZ4H6yCAJLK5aUnKfgrE8tODzP82sU/mpJjW+Pq3uanNq754gaHwhxCIXG143/zp8qzBAeKe38xVqqDq9fTkG4hvzFvkRdS88i6l1z++0P3n0HGdOjtSg7P7fO7+7ZyPYr0gO5vB720Om/zxqPrGd9cicWi4P+aVKa+0ujWH/pqufWG6uCjKWHnBs7 sk0/piv/9a
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDAkkF0Qc1YhhgmgZcujjfyn3liCxAAJSGlEme0eD6s908zm95mGd64fQJSoYVaFt5y9wrQLdWX16c+gvRyNaQdPfs5jxaP+G8Vr6PkkE79hwauEOQOTbr+q3bQJmfLe56cMV7oHrKVFffkJQTvp1mcK4+CYytsxJP5v0m5TDyv4op6VcZ2m0gdrFEBV3DMMgsqF6/tSPkqbTTj9fMMSOFzXPq7qEQWgYmxT+M0iQdgEiG7Z+INkofM56tBmQ7jSeO65/FTytB6ZMiI23NGmTmYvHPyxSDWXL0X7tX42S0/A7QhxZHN0GUD0NGaSExLfgTawRLhARVHbUXC9iu5aCAr sk1/piv/9a
sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIA71ukY9nzzeAGp9WYstPYiIwexzIANlPwMQ68QNQwqxAAAABHNzaDo= sk1/fido2
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMyZILj+GxTUhdCgz2w1TxQ+aTcggnOJIb84qA4u271S ltp0
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBN24pvN1RMN+iSZyHPdyExA1Rvt8pdr3e6ih6iX+KrQ ltp2
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGZFTyR+R86fUpZBDkDR9yYzJpmeNsIv2CMHTh6EuEeq pc0
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHLYgVj+NPino6sOmahULN7SbAMaVAgzqPfDjz2S8zDv pc1
@@ -20,4 +16,5 @@ ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIF52mPmbLEriZ+DfmrMHFtvg2kiO2JpRpitvs7PbJowh
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNvHya5i3GNniGww9Yx5ikos4Z5jevu8zz8hCQi05++SgejFPnbw9UjbHFjODK2M+ZHuhRF5mdf996r4/BxWyP8= pad0/sep
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMXu7UND6N1EKcbeSeAj2CwzGA3sM5/bRtLwoY0euWdR4vzB0nwcwd6nct5JCEyozjujJfMt8d7oCzhSt1w/1cU= ip17/sep
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBC5gu+U4picxVvCNYjLlmxGLpJl7svj3LklO7jSpwqdrNxlE+/xkx07PJx76AbA77dDM1Dxmm/VOvVQCKTRLTgk= ip14/sep

View File

@@ -31,14 +31,11 @@ bind r source-file ~/.tmux.conf \; display-message "tmux.conf reloaded"
bind m run 'old=$(tmux show -gv mouse); new="off"; if [ "$old" = "off" ]; then new="on"; fi; tmux set -g mouse $new && tmux display-message toggle\ mouse\ mode\ $new'
## prefix C-x: pane sync
bind C-x set-window-option synchronize-panes\; display-message "synchronize-panes is now #{?pane_synchronized,on,off}"
## tmux 3.6+: copy selection without jumping to the end
bind -T copy-mode-vi MouseDragEnd1Pane send-keys -X copy-selection
bind -T copy-mode MouseDragEnd1Pane send-keys -X copy-selection
## prevent jumping to end after mouse selection
bind -T copy-mode-vi MouseDragEnd1Pane send-keys -X copy-selection -x
## F12: passthru
bind -T root F12 set prefix None \; set key-table off \; set status-bg red \; refresh-client -S
bind -T off F12 set -u prefix \; set -u key-table \; set -u status-bg \; refresh-client -S
## ESC: exit copy mode
bind -T copy-mode-vi Escape send-keys -X cancel
# styles
# status bar

15
.zshrc2
View File

@@ -132,11 +132,7 @@ alias "jcfu"='jc -fu'
alias "sc"='systemctl'
alias "t"='tmux'
gbes() { git for-each-ref --sort=-committerdate refs/heads refs/remotes --format="%(authordate:format:%y-%m-%d.%a %H:%M %z)|%(color:red)%(objectname:short)|%(color:yellow)%(refname:short)%(color:reset)|%(color:reset)%(authorname): %(color:green)%(subject)" --color=always | column -ts"|" | less -FX ; }
sagt() {
local output
output="$("$DOTFILES/tools/sagent.sh" "$@")" || return $?
eval "$output"
}
sagt() { eval "$($DOTFILES/tools/sagent.sh $@)" ; }
## nixos
use() { nix --experimental-features nix-command --extra-experimental-features flakes shell "${(*)@/#%(#b)([^#]#)/nixpkgs#$match}" ; }
bnd() {
@@ -149,14 +145,7 @@ bnd() {
}
nor() {
test -n "$1" || { echo fatal: no verb given; return 1; }
local verb="$1"
local flake=".${2:+#$2}"
if command -v nh >/dev/null 2>&1 && [[ "$verb" != dry-build && "$verb" != dry-activate ]]; then
nh os "$verb" --bypass-root-check "$flake"
else
nixos-rebuild "$verb" --flake "$flake"
fi
nixos-rebuild "$1" --flake .${2:+#}$2
}
alias 'nors'='nor switch'
alias 'norbt'='nor boot'

View File

@@ -1,58 +1 @@
# dotfiles
My personal shell environment and command-line tools, mainly for Linux and
NixOS systems.
It includes configurations for zsh, tmux, Vim, Git, and SSH, together with a
few utility scripts used across my machines.
## Highlights
- `riot`: an SSH wrapper with host presets, jump hosts, port forwarding, tmux
sessions, and optional GPG agent forwarding.
- `sagt`: an SSH/GPG agent manager with support for PIV, 1Password, and
OpenPGP smart cards.
- zsh aliases and completions for the bundled tools.
- tmux and Vim configurations with plugin setup.
- automatic updates with selectable `main`, `dev`, and `latest` channels.
## Installation
Quick install:
```sh
curl -fsSL https://dotfiles.cn | bash
```
Alternatively, clone the repository so the installer can be reviewed first:
```sh
git clone https://github.com/DictXiong/dotfiles.git ~/dotfiles
~/dotfiles/install.sh -a
```
The installer adds small source/include entries to the existing zsh, tmux,
Vim, and Git configuration files instead of replacing them entirely.
## Useful commands
```sh
riot --help # show the SSH wrapper manual
sagt # start or reuse an ssh-agent
sagt gpg # use gpg-agent as SSH agent
sagt gpg-pin # cache the smart-card signature PIN
dfs update # update dotfiles from the selected channel
dfs config # edit machine-specific settings
```
## Configuration
Machine-specific settings can be placed in:
```text
~/.config/dotfiles/env
~/.config/riot-config.sh
```
This is a personal dotfiles repository. Review the configuration, SSH keys,
and installation script before using it on another machine.

View File

@@ -1,105 +0,0 @@
#compdef riot
_riot() {
local context state state_descr line
typeset -A opt_args
_arguments -C -s \
'(-4 -6)-4[Force ssh to use IPv4 addresses only]' \
'(-4 -6)-6[Force ssh to use IPv6 addresses only]' \
'(-D --dry-run)'{-D,--dry-run}'[Print commands without executing them]' \
'(-d --dev)'{-d,--dev}'[Enable shell execution tracing]' \
'(-g --gpg)'{-g,--gpg}'[Forward the local GPG agent for an interactive login]' \
'(-h --help)'{-h,--help}'[Display help and exit]' \
'(-l --lite)'{-l,--lite}'[Enable dotfiles lite mode]' \
'*-o[Pass an option to ssh]:SSH option:' \
'(-p --password)'{-p,--password}'[Use password authentication]' \
'(-q --quiet)'{-q,--quiet}'[Enable dotfiles quiet mode]' \
'(-t --trust)'{-t,--trust}'[Trust the remote and enable forwarding]' \
'*-v[Enable verbose ssh output]' \
'--color[Force colored output]' \
'--tmux[Open SSH sessions in tmux windows]' \
'1:remote:_riot_remotes' \
'2:command:_riot_commands' \
'*::command argument:->command-args'
case $state in
command-args)
case $line[2] in
ssh|tmux|zssh)
_riot_complete_with _ssh ssh "$line[1]" "${line[@]:2}"
;;
git)
_riot_complete_with _git git "${line[@]:2}"
;;
sftp)
_riot_complete_with _sftp sftp "$line[1]" "${line[@]:2}"
;;
scp)
_riot_complete_with _scp scp "${line[@]:2}"
;;
sshl|sshr)
_message 'forwarding specification'
;;
sshd)
_guard '[0-9]#' 'local port'
;;
rm|ping|ping4|ping6)
_message 'no more arguments'
;;
*)
_default
;;
esac
;;
esac
}
_riot_commands() {
local -a commands=(
'ssh:Open an SSH session (default)'
'tmux:Open SSH sessions in tmux windows'
'git:Run git on the remote host'
'sshl:Create local port forwarding with ssh -L'
'sshr:Create remote port forwarding with ssh -R'
'sshd:Create dynamic port forwarding with ssh -D'
'zssh:Open a zssh session'
'sftp:Open an SFTP session'
'scp:Copy files to or from the remote host'
'rm:Remove the remote host key from known_hosts'
'ping:Ping the remote host'
'ping4:Ping the remote host over IPv4'
'ping6:Ping the remote host over IPv6'
)
_describe -t commands 'riot command' commands
}
_riot_presets() {
local -a config_files presets
local file
[[ -z $DOTFILES ]] || config_files+=("$DOTFILES/riot-config.sh")
config_files+=("$HOME/.config/riot-config.sh" "$PWD/riot-config.sh")
for file in "${config_files[@]}"; do
[[ -r $file ]] || continue
presets+=("${(@f)$(sed -nE 's/^([[:alnum:]_.-]+)\.(remote|batch)\(\).*/\1/p' "$file" 2>/dev/null)}")
done
presets=(${(u)presets})
_describe -t presets 'riot preset' presets
}
_riot_remotes() {
_alternative \
'presets:riot preset:_riot_presets' \
'hosts:host:_hosts'
}
_riot_complete_with() {
local completer=$1
shift
local -a words=("$@")
local CURRENT=$#words
"$completer"
}
_riot "$@"

View File

@@ -1,3 +1,3 @@
#compdef sagt
compadd -- gpg gpg-pin kill ls op piv reset
compadd -- kill ls op piv reset

View File

@@ -1,27 +0,0 @@
#compdef scf
_scf() {
_arguments -s \
'(-r)-r[Recursively inspect referenced files and directories]' \
'(-n)-n[Open candidate number directly]:candidate number:' \
'--[End of options]' \
'1:systemd service:_scf_services'
}
_scf_services() {
local -a services
if (( $+commands[systemctl] )); then
services=("${(@f)$(systemctl list-unit-files --type=service --no-legend --no-pager 2>/dev/null \
| awk '$1 ~ /\.service$/ {print $1}' \
| sort -u)}")
fi
if (( ${#services} > 0 )); then
_describe -t services 'systemd service' services
else
_message 'systemd service'
fi
}
_scf "$@"

View File

@@ -1,13 +1,10 @@
#!/usr/bin/env bash
# connect to iot services
THIS_DIR=$( cd "$( dirname "${BASH_SOURCE[0]:-${(%):-%x}}" )" && pwd )
DFS_SKIP_ARG_PARSE=1
source "$THIS_DIR/../tools/common.sh"
unset DFS_SKIP_ARG_PARSE
RIOT_TRUST_CLIENT=${RIOT_TRUST_CLIENT:-${DFS_TRUST:-0}}
RIOT_TRUST_SERVER=${RIOT_TRUST_SERVER:-0}
EXTRA_SSH_OPTIONS=()
GPG_FORWARD=0
# config
RIOT_CONFIG_FILES=(
@@ -15,19 +12,16 @@ RIOT_CONFIG_FILES=(
"$HOME/.config/riot-config.sh"
"riot-config.sh"
)
load_riot_config() {
local file
for file in "${RIOT_CONFIG_FILES[@]}"; do
for file in "${RIOT_CONFIG_FILES[@]}"; do
if [[ -f "$file" ]]; then
source "$file"
fi
done
}
done
# check if port number valid
check_port() {
[[ "$1" =~ ^[1-9][0-9]{0,4}$ ]] || return 1
[[ $1 -lt 65536 && $1 -gt 0 ]] || return 1
[[ $1 < 65536 && $1 > 0 ]] || return 1
return 0
}
@@ -59,8 +53,8 @@ get_server_meta() {
"$remote" =~ ^[^:]+:[1-9][0-9]*$ # contains only one colon
|| "$remote" =~ ^\[.+\]:[1-9][0-9]*$ # in the form of [host]:port
|| "$remote" =~ :::[1-9][0-9]*$ # in the form of :::port
|| "$remote" =~ ^([0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}:[1-9][0-9]*$ # full ipv6 address with port
|| "$remote" =~ ^[0-9A-Fa-f:]+%.+:[1-9][0-9]*$ # ipv6 address with scope and port
|| "$remote" =~ ^([0-9a-f]{1,4}:){7}[0-9a-f]{1,4}:[1-9][0-9]*$ # full ipv6 address with port
|| "$remote" =~ ^[0-9a-f:]+%.+:[1-9][0-9]*$ # ipv6 address with scope and port
]]; then
RET_PORT=${remote##*:}
remote=${remote%:*}
@@ -107,7 +101,7 @@ parse_remote() {
SSH_OPTIONS+=("-o" "PermitLocalCommand=yes")
if [[ "$(get_os_type)" != "msys" ]]; then
test "$DFS_DRY_RUN" = "1" || mkdir -p ~/.ssh/master-socket
SSH_OPTIONS+=("-o" "ControlMaster=auto" "-o" "ControlPersist=5s" "-o" "ControlPath=~/.ssh/master-socket/%C")
SSH_OPTIONS+=("-o" "ControlMaster=auto" "-o" "ControlPersist=60s" "-o" "ControlPath=~/.ssh/master-socket/%C")
fi
fi
# handle input
@@ -141,129 +135,6 @@ parse_remote() {
fi
}
check_local_gpg_agent() {
LOCAL_GPG_EXTRA_SOCKET=""
GPG_FORWARD_ERROR=""
if ! command -v gpgconf > /dev/null 2>&1 || ! command -v gpg-connect-agent > /dev/null 2>&1; then
GPG_FORWARD_ERROR="gpgconf or gpg-connect-agent is not available"
return 1
fi
local agent_info
agent_info=$(gpg-connect-agent --no-autostart 'GETINFO pid' /bye 2>/dev/null || true)
if ! grep -qE '^D [1-9][0-9]*$' <<< "$agent_info"; then
GPG_FORWARD_ERROR="local gpg-agent is not running"
return 1
fi
LOCAL_GPG_EXTRA_SOCKET=$(gpgconf --list-dirs agent-extra-socket 2>/dev/null || true)
if [[ -z "$LOCAL_GPG_EXTRA_SOCKET" || "$LOCAL_GPG_EXTRA_SOCKET" != /* \
|| "$LOCAL_GPG_EXTRA_SOCKET" == *:* || "$LOCAL_GPG_EXTRA_SOCKET" == *$'\r'* \
|| "$LOCAL_GPG_EXTRA_SOCKET" == *$'\n'* || ! -S "$LOCAL_GPG_EXTRA_SOCKET" ]]; then
GPG_FORWARD_ERROR="local gpg-agent extra socket is unavailable"
return 1
fi
local extra_info
extra_info=$(gpg-connect-agent --raw-socket "$LOCAL_GPG_EXTRA_SOCKET" 'GETINFO version' /bye 2>/dev/null || true)
if ! grep -qE '^D [^[:space:]]+' <<< "$extra_info"; then
GPG_FORWARD_ERROR="local gpg-agent extra socket exists but is not accepting connections"
return 1
fi
}
probe_remote_gpg_socket() {
REMOTE_GPG_SOCKET=""
# The probe must be an independent connection: reusing or creating a
# multiplex master here races with the immediately following login.
local query_cmd=(ssh "-S" "none" "-o" "ClearAllForwardings=yes")
if [[ -n "$PORT" ]]; then
query_cmd+=("-p" "$PORT")
fi
query_cmd+=(
"${SSH_OPTIONS[@]}"
"${EXTRA_SSH_OPTIONS[@]}"
"-T"
"$USERNAME${USERNAME:+@}$SERVER"
'socket=$(gpgconf --list-dirs agent-socket 2>/dev/null) || exit 10
case "$socket" in /*/S.gpg-agent) ;; *) exit 11;; esac
case "$socket" in *:*) exit 11;; esac
case "$socket" in *"
"*) exit 11;; esac
carriage_return=$(printf "\r")
case "$socket" in *"$carriage_return"*) exit 11;; esac
if [ -e "$socket" ] && [ ! -S "$socket" ]; then exit 12; fi
if [ -S "$socket" ] && command -v gpg-connect-agent >/dev/null 2>&1; then
agent_mode=$(gpg-connect-agent --raw-socket "$socket" "GETINFO restricted" /bye 2>/dev/null || true)
case "$agent_mode" in *"D 1"*) exit 13;; esac
fi
systemd_socket=0
if command -v systemctl >/dev/null 2>&1 && systemctl --user is-active --quiet gpg-agent.socket >/dev/null 2>&1; then
systemd_socket=1
fi
gpgconf --kill gpg-agent >/dev/null 2>&1 || exit 14
rm -f "$socket" || exit 15
printf "%s\n%s\n" "$socket" "$systemd_socket"'
)
local output status
if output=$("${query_cmd[@]}"); then
status=0
else
status=$?
fi
case "$status" in
0) ;;
10) GPG_FORWARD_ERROR="gpgconf is unavailable on the remote host" ;;
11) GPG_FORWARD_ERROR="remote gpgconf returned an invalid agent socket" ;;
12) GPG_FORWARD_ERROR="refusing to remove the non-socket remote gpg-agent path" ;;
13) GPG_FORWARD_ERROR="another forwarded gpg-agent is already using the remote socket; close that session first" ;;
14) GPG_FORWARD_ERROR="failed to stop the remote gpg-agent (the socket may belong to another forwarding session)" ;;
15) GPG_FORWARD_ERROR="failed to remove the stale remote gpg-agent socket" ;;
*) GPG_FORWARD_ERROR="failed to query or clean the remote gpg-agent socket (ssh status $status)" ;;
esac
if [[ "$status" != "0" ]]; then
return 1
fi
local remote_systemd_socket
REMOTE_GPG_SOCKET=${output%%$'\n'*}
remote_systemd_socket=${output#*$'\n'}
if [[ -z "$REMOTE_GPG_SOCKET" || "$REMOTE_GPG_SOCKET" == *$'\r'* || "$REMOTE_GPG_SOCKET" == *$'\n'* \
|| "$REMOTE_GPG_SOCKET" != /*/S.gpg-agent || "$REMOTE_GPG_SOCKET" == *:* \
|| "$remote_systemd_socket" != "0" && "$remote_systemd_socket" != "1" ]]; then
GPG_FORWARD_ERROR="remote gpgconf returned an invalid agent socket: $output"
return 1
fi
if [[ "$remote_systemd_socket" == "1" ]]; then
fmt_warning "remote gpg-agent.socket is active and may race with GPG forwarding; consider disabling its socket activation"
fi
}
prepare_gpg_forwarding() {
if [[ "$DFS_DRY_RUN" == "1" ]]; then
REMOTE_GPG_SOCKET="<remote-gpg-agent-socket>"
LOCAL_GPG_EXTRA_SOCKET="<local-gpg-agent-extra-socket>"
else
if ! check_local_gpg_agent; then
fmt_fatal "cannot forward gpg-agent: $GPG_FORWARD_ERROR"
fi
if ! probe_remote_gpg_socket; then
fmt_fatal "cannot forward gpg-agent: $GPG_FORWARD_ERROR"
fi
fi
SSH_OPTIONS+=(
# The probe already removed the old socket. Do not let the main SSH
# connection unlink a path recreated during the gap between them.
"-o" "StreamLocalBindUnlink=no"
"-o" "ExitOnForwardFailure=yes"
"-R" "$REMOTE_GPG_SOCKET:$LOCAL_GPG_EXTRA_SOCKET"
)
}
print_cmd() {
local output=""
for s in "${CMD[@]}"; do
@@ -289,10 +160,7 @@ eval_or_echo() {
tmux_win=$((tmux_win+1))
$DO tmux new-window -t $TMUX_SESS:$tmux_win -d bash -l
fi
local command
printf -v command '%q ' "${CMD[@]}"
$DO tmux send-keys -l -t "$TMUX_SESS:$tmux_win" "${command% }"
$DO tmux send-keys -t "$TMUX_SESS:$tmux_win" Enter
$DO tmux send-keys -t $TMUX_SESS:$tmux_win $(printf '%s Space ' "${CMD[@]}") Enter
else
$DO "${CMD[@]}"
fi
@@ -435,147 +303,26 @@ remove_hostkey() {
# main
print_help()
{
local pager=(cat)
if [[ -t 1 ]] && command -v less > /dev/null 2>&1; then
pager=(less -R)
fi
cat <<EOF | "${pager[@]}"
NAME
riot - connect to remote hosts using SSH presets
SYNOPSIS
${0##*/} [OPTION]... REMOTE [OPTION]... [COMMAND [COMMAND-ARG]...]
DESCRIPTION
Connect to REMOTE using the matching configuration from riot-config.sh.
COMMAND defaults to ssh. Separate multiple remotes with commas and jump
hosts with slashes. OPTIONs may appear before COMMAND. Once COMMAND is
found, all remaining arguments are passed to it without further parsing.
OPTIONS
-4
Force ssh to use IPv4 addresses only.
-6
Force ssh to use IPv6 addresses only.
-D, --dry-run
Print commands without executing them.
-d, --dev
Enable shell execution tracing.
-g, --gpg
Forward the local GPG agent during an interactive SSH login. Use only
with trusted remote hosts.
-h, --help
Display this help and exit.
-l, --lite
Enable dotfiles lite mode for loaded configuration.
-o SSH-OPTION
Pass an option to ssh. This option may be specified multiple times.
-p, --password
Use password authentication instead of public-key authentication.
-q, --quiet
Enable dotfiles quiet mode for loaded configuration.
-t, --trust
Trust the remote and enable X11 and SSH agent forwarding.
-v
Enable verbose ssh output.
--color
Force colored output.
--tmux
Open SSH sessions in tmux windows.
--
End option parsing.
COMMANDS
ssh [SSH-ARG]...
Open an SSH session. This is the default command.
tmux [SSH-ARG]...
Open SSH sessions in multiple tmux windows.
git [GIT-ARG]...
Run git on the remote host.
sshl [LOCAL-PORT:REMOTE-HOST:]REMOTE-PORT
Create local port forwarding with ssh -L.
sshr [REMOTE-HOST:]REMOTE-PORT
Create remote port forwarding with ssh -R.
sshd [LOCAL-PORT]
Create dynamic port forwarding with ssh -D.
zssh [SSH-ARG]...
Open a zssh session.
sftp [SFTP-ARG]...
Open an SFTP session.
scp SOURCE DESTINATION
Copy files to or from the remote host.
rm
Remove the remote host key from known_hosts.
ping, ping4, ping6
Ping the remote host.
fmt_info "usage: $0 [-Ddhlqt] [--dry-run] [--dev] [--help] [--lite] [--quite] [--trust] [--tmux] [--password] [[-o ssh-option]...] remote [command] [--] [command-args]"
cat <<EOF
available commands:
- ssh [ssh-command-args] (default)
- tmux [ssh-command-args] (run ssh in multiple tmux windows)
- git [git-command-args] (run git on remote machine)
- sshl [local-port:remote-host:]remote-port (ssh -L)
- sshd [local-port] (ssh -D)
- zssh [ssh-command-args]
- sftp
- scp source destination
- rm (remove host keys)
- ping/ping4/ping6 (ping the remote servers)
EOF
}
router() {
local positional=()
local arg=""
local option=""
local remaining=""
local option_value=""
while [[ $# -gt 0 || -n "$arg" ]]; do
if [[ -z "$arg" ]]; then
arg=$1
shift
fi
# Normalize a long option or one item from a short-option group.
remaining=""
case "$arg" in
-- )
positional+=("$@")
break
;;
--* )
option=$arg
arg=""
;;
-?* )
option=${arg:0:2}
remaining=${arg:2}
arg=${remaining:+-$remaining}
;;
* )
positional+=("$arg")
arg=""
if [[ "${#positional[@]}" -ge 2 ]]; then
positional+=("$@")
break
fi
continue
;;
esac
case "$option" in
while [[ $# > 0 ]]; do
case "$1" in
-h|--help )
print_help
exit 0
@@ -583,68 +330,45 @@ router() {
-t|--trust )
RIOT_TRUST_SERVER=1
;;
-g|--gpg )
GPG_FORWARD=1
;;
--tmux )
USE_TMUX=1
;;
-p|--password )
--password )
EXTRA_SSH_OPTIONS+=("-o" "PasswordAuthentication=yes" "-o" "PubkeyAuthentication=no")
;;
-D|--dry-run )
export DFS_DRY_RUN=1
;;
-d|--dev )
export DFS_DEV=1
set -x
;;
-l|--lite )
export DFS_LITE=1
;;
-q|--quiet )
export DFS_QUIET=1
;;
--color )
export DFS_COLOR=1
setup_color
;;
-4|-6|-v )
EXTRA_SSH_OPTIONS+=("$option")
;;
-o )
if [[ -n "$remaining" ]]; then
option_value=$remaining
arg=""
else
[[ $# -gt 0 ]] || fmt_fatal "option '-o' requires an argument"
option_value=$1
EXTRA_SSH_OPTIONS+=("-o" "$2")
shift
fi
EXTRA_SSH_OPTIONS+=("-o" "$option_value")
;;
-4 )
EXTRA_SSH_OPTIONS+=("-4")
;;
-6 )
EXTRA_SSH_OPTIONS+=("-6")
;;
-v )
EXTRA_SSH_OPTIONS+=("-v")
;;
-- )
shift
positional+=("$@")
break
;;
-* )
fmt_fatal "unknown option: '$1'. if this option is for the remote command, add '--' before."
;;
* )
fmt_fatal "unknown option: '$option'"
positional+=("$1")
;;
esac
shift
done
if [[ "${positional[2]}" == "--" ]]; then
positional=("${positional[@]:0:2}" "${positional[@]:3}")
fi
IFS=',' read -ra remotes <<< "${positional[0]}"
for i in ${!remotes[@]}; do if [[ -z "${remotes[i]}" ]]; then unset remotes[i]; fi; done
if [[ "${#positional[@]}" == "0" || "${#remotes[@]}" == "0" ]]; then
print_help
exit 1
fi
load_riot_config
if [[ "$GPG_FORWARD" == "1" && ( \
( "${positional[1]}" != "" && "${positional[1]}" != "ssh" && "${positional[1]}" != "tmux" ) \
|| "${#positional[@]}" -gt 2 ) ]]; then
fmt_fatal "gpg-agent forwarding is only supported for interactive SSH login"
fi
for i in ${!remotes[@]}; do
remote="${remotes[i]}"
local batch_func="${remote}.batch"
@@ -656,9 +380,6 @@ router() {
case "${positional[1]}" in
ssh|tmux|"" )
[[ "${positional[1]}" == tmux ]] && USE_TMUX=1
if [[ "$GPG_FORWARD" == "1" ]]; then
prepare_gpg_forwarding
fi
run_ssh ssh "${positional[@]:2}"
;;
git )
@@ -719,4 +440,4 @@ router() {
fi
}
router "$@"
router "${GOT_OPTS[@]}"

View File

@@ -1,378 +0,0 @@
#!/usr/bin/env bash
# Author: OpenAI Codex
# AI-assisted implementation, generated and reviewed with OpenAI Codex.
set -euo pipefail
# Keep glob expansion and sorting stable across locales, so -n remains
# repeatable when the service and filesystem contents have not changed.
scf_lc_all_was_set=false
scf_original_lc_all=""
if [[ -n ${LC_ALL+x} ]]; then
scf_lc_all_was_set=true
scf_original_lc_all=$LC_ALL
fi
export LC_ALL=C
restore_locale() {
if [[ $scf_lc_all_was_set == true ]]; then
export LC_ALL=$scf_original_lc_all
else
unset LC_ALL
fi
}
usage() {
cat >&2 <<'EOF'
Usage: scf [-r] [-n NUM] SERVICE
Open the configuration file referenced by a systemd service's ExecStart.
SERVICE may be given with or without the .service suffix.
-r recursively inspect referenced files and directories (maximum reference depth: 3)
-n open candidate NUM directly, without displaying the selection list
Environment:
SCF_MAX_CANDIDATES maximum number of candidates (default: 20)
SCF_MAX_DEPTH maximum reference depth (default: 3)
EOF
exit 2
}
recursive=false
selection_number=""
service=""
while (( $# > 0 )); do
argument=$1
shift
if [[ $argument == -- ]]; then
[[ -z $service && $# -eq 1 ]] || usage
service=$1
shift
break
fi
if [[ $argument == -* ]]; then
options=${argument#-}
[[ -n $options ]] || usage
while [[ -n $options ]]; do
option=${options:0:1}
options=${options:1}
case $option in
r) recursive=true ;;
n)
if [[ -n $options ]]; then
selection_number=$options
options=""
else
(( $# > 0 )) || usage
selection_number=$1
shift
fi
;;
*) usage ;;
esac
done
else
[[ -z $service ]] || usage
service=$argument
fi
done
if [[ -z $service ]]; then
usage
fi
interactive=false
if [[ -t 0 && -t 1 && -t 2 ]]; then
interactive=true
fi
# Prompts and candidate details are written to stderr. Keep pipelines and
# redirected output free of escape sequences.
if [[ $interactive == true ]]; then
color_item=$'\033[1;36m'
color_detail=$'\033[2;37m'
color_prompt=$'\033[1;33m'
color_reset=$'\033[0m'
else
color_item=""
color_detail=""
color_prompt=""
color_reset=""
fi
if [[ -n $selection_number && ! $selection_number =~ ^[0-9]+$ ]]; then
echo "scf: invalid candidate number: $selection_number" >&2
exit 2
fi
if [[ -n $selection_number ]]; then
# Bash arithmetic treats numbers with a leading zero as octal.
selection_number=$((10#$selection_number))
if (( selection_number < 1 )); then
echo "scf: invalid candidate number: 0" >&2
exit 2
fi
fi
service_argument=$service
if [[ $service != *.service ]]; then
service+=.service
fi
# Keep the argument a unit name, rather than allowing it to become a
# systemctl option. This also makes accidental shell syntax harmless.
if [[ ! $service =~ ^[A-Za-z0-9_.@:%+-]+\.service$ ]]; then
echo "scf: invalid service name: $service_argument" >&2
exit 2
fi
if ! exec_start=$(systemctl show --no-pager --property=ExecStart --value "$service"); then
echo "scf: cannot inspect $service" >&2
exit 1
fi
if [[ -z $exec_start || $exec_start == "{}" ]]; then
echo "scf: $service has no ExecStart" >&2
exit 1
fi
# Keep recursion bounded even when configuration files refer to each other.
max_candidates=${SCF_MAX_CANDIDATES:-20}
max_reference_depth=${SCF_MAX_DEPTH:-3}
if [[ ! $max_candidates =~ ^[0-9]+$ ]]; then
echo "scf: SCF_MAX_CANDIDATES must be a positive integer" >&2
exit 2
fi
if [[ ! $max_reference_depth =~ ^[0-9]+$ ]]; then
echo "scf: SCF_MAX_DEPTH must be a non-negative integer" >&2
exit 2
fi
# Normalize user-provided decimal values before using Bash arithmetic.
max_candidates=$((10#$max_candidates))
max_reference_depth=$((10#$max_reference_depth))
if (( max_candidates < 1 )); then
echo "scf: SCF_MAX_CANDIDATES must be a positive integer" >&2
exit 2
fi
declare -A candidate_seen=()
declare -A candidate_depth=()
declare -A candidate_source=()
declare -A candidate_source_detail=()
config_paths=()
other_paths=()
recursive_paths=()
recursive_depths=()
declare -A recursive_seen=()
path_pattern="/[^[:space:];,'\"})]+"
extract_paths() {
grep -a -oE "$path_pattern" || true
}
has_config_extension() {
local path=$1
[[ $path =~ \.(conf|cfg|cnf|ini|json|toml|ya?ml|xml|properties)$ ]]
}
is_config_path() {
local path=$1
has_config_extension "$path" \
|| [[ $path == *config* || $path == *configuration* ]]
}
add_candidate() {
local path=$1
local depth=$2
local source=$3
local source_detail=${4:-}
[[ -f $path ]] || return 1
[[ -n ${candidate_seen[$path]+seen} ]] && return 0
(( ${#candidate_seen[@]} < max_candidates )) || return 1
candidate_seen[$path]=1
candidate_depth[$path]=$depth
candidate_source[$path]=$source
candidate_source_detail[$path]=$source_detail
if is_config_path "$path"; then
config_paths+=("$path")
else
other_paths+=("$path")
fi
}
enqueue_recursive() {
local path=$1
local depth=$2
[[ $recursive == true ]] || return 0
(( depth <= max_reference_depth )) || return 0
[[ -f $path ]] || return 0
[[ -n ${recursive_seen[$path]+seen} ]] && return 0
recursive_seen[$path]=1
recursive_paths+=("$path")
recursive_depths+=("$depth")
}
expand_directory() {
local directory=$1
local depth=$2
local source=$3
local path
local entries
[[ $recursive == true && -d $directory ]] || return 0
# dotglob also catches hidden configuration files. nullglob prevents the
# literal pattern from being treated as a candidate for an empty directory.
shopt -s dotglob nullglob
entries=("$directory"/*)
shopt -u dotglob nullglob
for path in "${entries[@]}"; do
# A service's configuration often contains runtime directories such as
# Python's bin directory. Only configuration-looking files from a
# directory should consume the candidate budget.
has_config_extension "$path" || continue
add_candidate "$path" "$depth" "$source" "directory $directory" || continue
enqueue_recursive "$path" "$depth"
done
}
consider_initial_path() {
local path=$1
if [[ -f $path ]]; then
add_candidate "$path" 0 "ExecStart" || true
elif [[ -d $path ]]; then
expand_directory "$path" 0 "ExecStart"
fi
}
# systemctl's ExecStart value contains both `path=` and `argv[]=`. Pull out
# absolute path tokens, then drop the first one (the executable itself).
# NixOS-generated configuration files are normally immutable regular files in
# /nix/store, but the same also works for /etc and /run paths.
mapfile -t initial_paths < <(
printf '%s\n' "$exec_start" \
| extract_paths \
| awk '!seen[$0]++'
)
if (( ${#initial_paths[@]} > 0 )); then
initial_paths=("${initial_paths[@]:1}")
fi
for path in "${initial_paths[@]}"; do
consider_initial_path "$path"
done
paths=("${config_paths[@]}")
if (( ${#paths[@]} == 0 )); then
paths=("${other_paths[@]}")
fi
if [[ $recursive == true ]]; then
for path in "${paths[@]}"; do
enqueue_recursive "$path" 0
done
recursive_index=0
while (( recursive_index < ${#recursive_paths[@]} )); do
path=${recursive_paths[recursive_index]}
depth=${recursive_depths[recursive_index]}
recursive_index=$((recursive_index + 1))
(( depth < max_reference_depth )) || continue
next_depth=$((depth + 1))
mapfile -t referenced_paths < <(extract_paths < "$path")
for referenced_path in "${referenced_paths[@]}"; do
if [[ -f $referenced_path ]]; then
add_candidate "$referenced_path" "$next_depth" "$path" || continue
enqueue_recursive "$referenced_path" "$next_depth"
elif [[ -d $referenced_path ]]; then
expand_directory "$referenced_path" "$next_depth" "$path"
fi
done
done
paths=("${config_paths[@]}")
if (( ${#paths[@]} == 0 )); then
paths=("${other_paths[@]}")
fi
fi
if (( ${#paths[@]} == 0 )); then
echo "scf: no existing configuration path found in ExecStart of $service" >&2
echo "ExecStart: $exec_start" >&2
exit 1
fi
if [[ $interactive == false && -z $selection_number ]]; then
selection_number=1
fi
if [[ -n $selection_number ]]; then
if (( selection_number > ${#paths[@]} )); then
echo "scf: candidate number $selection_number is out of range (1-${#paths[@]})" >&2
exit 2
fi
config_path=${paths[selection_number - 1]}
elif (( ${#paths[@]} > 1 )); then
declare -A displayed_indices=()
for i in "${!paths[@]}"; do
displayed_indices["${paths[i]}"]=$((i + 1))
done
printf '%sscf: multiple possible configuration files for %s:%s\n' \
"$color_prompt" "$service" "$color_reset" >&2
for i in "${!paths[@]}"; do
path=${paths[i]}
source=${candidate_source[$path]}
if [[ $source == ExecStart ]]; then
source_label=ExecStart
elif [[ -n ${displayed_indices[$source]+seen} ]]; then
source_label="#${displayed_indices[$source]}"
else
source_label=$source
fi
source_detail=${candidate_source_detail[$path]}
if [[ -n $source_detail ]]; then
source_label+=" ($source_detail)"
fi
printf '%s %d) %s%s\n' \
"$color_item" "$((i + 1))" "$path" "$color_reset" >&2
printf '%s depth: %d; derived from: %s%s\n' \
"$color_detail" "${candidate_depth[$path]}" "$source_label" "$color_reset" >&2
done
read -r -p "${color_prompt}Select a file [1]: ${color_reset}" selection
selection=${selection:-1}
if [[ ! $selection =~ ^[0-9]+$ ]]; then
echo "scf: invalid selection" >&2
exit 2
fi
selection=$((10#$selection))
if (( selection < 1 || selection > ${#paths[@]} )); then
echo "scf: invalid selection" >&2
exit 2
fi
config_path=${paths[selection - 1]}
else
config_path=${paths[0]}
fi
if [[ $interactive == false ]]; then
restore_locale
exec cat -- "$config_path"
fi
editor=${VISUAL:-${EDITOR:-vim}}
read -r -a editor_command <<< "$editor"
if (( ${#editor_command[@]} == 0 )); then
editor_command=(vim)
fi
restore_locale
exec "${editor_command[@]}" "$config_path"

View File

@@ -1,31 +0,0 @@
#!/usr/bin/env bash
if [ "$#" -eq 0 ]; then
echo "usage: sne <filename or regex pattern>"
echo "example: sne config.yaml"
exit 1
fi
SEARCH_PATTERN="$1"
files=()
while IFS= read -r -d $'\0' file; do
files+=("$file")
done < <(fd "$SEARCH_PATTERN" --type f --print0)
file_count=${#files[@]}
if [ "$file_count" -eq 0 ]; then
echo "no file match '$SEARCH_PATTERN'"
exit 0
fi
echo "🔎 found $file_count files:"
for file in "${files[@]}"; do
echo " 📄 $file"
done
echo "----------------------------------------"
read -r -p "edit? [Y/n] " response
response=${response:-Y}
if [[ "$response" =~ ^[Yy]$ ]]; then
vim "${files[@]}"
else
echo "❌ aborted"
fi

View File

@@ -6,19 +6,19 @@ if [[ -f ~/.config/dotfiles/env ]]; then set -a; source ~/.config/dotfiles/env;
if [[ "$DFS_DEV" == "1" ]]; then set -x; fi
DFS_CURL_OPTIONS="--retry 2 --max-time 20"
# Parse args and set env when sourced, unless the caller handles its own
# option boundary.
if [[ "${BASH_SOURCE[0]}" != "${0}" && "$DFS_SKIP_ARG_PARSE" != "1" ]]; then
# parse args and set env, when it is sourced
# todo: make this skipable
if [[ "${BASH_SOURCE[0]}" != "${0}" ]]; then
ORIGIN_ARGS=("$@")
ARG=""
GOT_OPTS=()
while [[ $# -gt 0 || -n "$ARG" ]]; do
while [[ $# > 0 || -n "$ARG" ]]; do
if [[ -z "$ARG" ]]; then
if [[ "$1" == "--" ]]; then GOT_OPTS+=("$@"); break; fi
ARG="$1"; shift;
fi
case $ARG in
-q*|--quiet ) export DFS_QUIET=1 ;;
-q*|--quite ) export DFS_QUIET=1 ;;
-l*|--lite ) export DFS_LITE=1 ;;
-d*|--dev ) export DFS_DEV=1; set -x ;;
-D*|--dry-run ) export DFS_DRY_RUN=1 ;;
@@ -41,9 +41,15 @@ fi
# Color settings
# Source: https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh
is_tty() {
[[ -t 1 || "$DFS_COLOR" == "1" ]]
}
if [[ -t 1 || "$DFS_COLOR" == "1" ]]; then
is_tty() {
true
}
else
is_tty() {
false
}
fi
supports_truecolor() {
case "$COLORTERM" in

View File

@@ -2,8 +2,8 @@
set -e
op=$(command -v op || command -v op.exe || true)
if [[ -z "$op" || ! -x "$op" ]]; then
echo "1Password CLI not found" >&2
exit 1
if [[ ! -x $op ]]; then
echo "1password cli not found" > /dev/stderr
exit -1
fi
exec "$op" read "op://Personal/id25519-passphrase/$(hostname)"
"$op" read "op://Personal/id25519-passphrase/$(hostname)"

View File

@@ -20,7 +20,6 @@ find_so_file()
return
fi
done
return 1
}
create_agent()
@@ -31,50 +30,27 @@ create_agent()
kill_agent()
{
local status
if pgrep -u "$EUID" -x ssh-agent > /dev/null; then
fmt_note "stopping existing ssh-agent"
if pkill -TERM -u "$EUID" -x ssh-agent; then
:
else
status=$?
[[ $status -eq 1 ]] || return "$status"
if pgrep -x ssh-agent > /dev/null; then
fmt_note "killing existing agent"
pkill -9 -x ssh-agent
fi
fi
if command -v gpgconf > /dev/null 2>&1; then
fmt_note "stopping gpg-agent if running"
gpgconf --kill gpg-agent
fi
unset SSH_AUTH_SOCK SSH_AGENT_PID
echo unset SSH_AUTH_SOCK SSH_AGENT_PID
}
add_piv()
{
local SO_FILE
if ! SO_FILE=$(find_so_file); then
local SO_FILE=$(find_so_file)
if [[ -n "$SO_FILE" ]]; then
echo ssh-add -s \"$SO_FILE\"
else
fmt_error "opensc-pkcs11.so not found"
return 1
fi
printf 'ssh-add -s %q\n' "$SO_FILE"
list
}
add_id25519_with_op()
{
local status
if SSH_ASKPASS_REQUIRE=force SSH_ASKPASS="$THIS_DIR/sagent-op.sh" timeout 60s ssh-add "$HOME/.ssh/id_ed25519"; then
SSH_ASKPASS_REQUIRE=force SSH_ASKPASS="$THIS_DIR/sagent-op.sh" timeout 60s ssh-add ~/.ssh/id_ed25519 || fmt_fatal "timed out when adding the key. probably the passphrase is wrong or 1password-cli is not working"
list
return
else
status=$?
fi
if [[ $status -eq 124 ]]; then
fmt_fatal "timed out when adding the key"
else
fmt_fatal "failed to add the key (ssh-add exit $status); check the key, agent, and 1Password CLI"
fi
}
list()
@@ -83,220 +59,30 @@ list()
echo ssh-add -l
}
configured_pinentry()
{
local agent_conf
local agent_confs=()
for agent_conf in "$@"; do
[[ -f "$agent_conf" ]] && agent_confs+=("$agent_conf")
done
[[ ${#agent_confs[@]} -gt 0 ]] || return 1
awk '
/^[[:space:]]*#/ { next }
{
line = $0
sub(/^[[:space:]]*/, "", line)
if (line ~ /^pinentry-program([[:space:]]|=)/) {
sub(/^pinentry-program[[:space:]=]*/, "", line)
sub(/[[:space:]]*$/, "", line)
pinentry = line
}
}
END {
if (pinentry == "") exit 1
print pinentry
}
' "${agent_confs[@]}"
}
check_pinentry()
{
local gnupg_home
local gpg_sysconfdir
local agent_conf
local system_agent_conf
local pinentry
local gpg_bindir
local candidate
gnupg_home=$(gpgconf --list-dirs homedir)
gpg_sysconfdir=$(gpgconf --list-dirs sysconfdir)
agent_conf="$gnupg_home/gpg-agent.conf"
system_agent_conf="$gpg_sysconfdir/gpg-agent.conf"
if pinentry=$(configured_pinentry "$system_agent_conf" "$agent_conf"); then
if [[ "$pinentry" == "~/"* ]]; then
pinentry="$HOME/${pinentry#\~/}"
fi
if [[ -x "$pinentry" ]]; then
return
fi
fmt_warning "configured pinentry is not executable: $pinentry"
else
gpg_bindir=$(gpgconf --list-dirs bindir)
if [[ -x "$gpg_bindir/pinentry" || -x "$gpg_bindir/pinentry-basic" ]]; then
return
fi
fi
for candidate in \
"$(command -v pinentry-curses 2>/dev/null || true)" \
"$(command -v pinentry 2>/dev/null || true)" \
"$(command -v pinentry-tty 2>/dev/null || true)"; do
[[ -n "$candidate" && -x "$candidate" ]] && break
candidate=""
done
if [[ -n "$candidate" ]]; then
fmt_warning "gpg-agent has no usable pinentry; add 'pinentry-program $candidate' to $agent_conf"
else
fmt_warning "gpg-agent has no usable pinentry; install one and configure pinentry-program in $agent_conf"
fi
}
prepare_gpg_agent()
{
command -v gpgconf > /dev/null 2>&1 || fmt_fatal "gpgconf not found"
command -v gpg-connect-agent > /dev/null 2>&1 || fmt_fatal "gpg-connect-agent not found"
check_pinentry
local current_tty
current_tty=$(tty) || fmt_fatal "unable to determine the current TTY"
export GPG_TTY="$current_tty"
gpgconf --launch gpg-agent
gpg-connect-agent updatestartuptty /bye > /dev/null
}
use_gpg_agent()
{
prepare_gpg_agent
local agent_socket
agent_socket=$(gpgconf --list-dirs agent-ssh-socket)
if [[ -z "$agent_socket" || ! -S "$agent_socket" ]]; then
fmt_fatal "gpg-agent SSH socket not found; add 'enable-ssh-support' to ~/.gnupg/gpg-agent.conf and restart gpg-agent"
fi
fmt_note "using gpg-agent: $agent_socket"
echo unset SSH_AGENT_PID
printf 'export GPG_TTY=%q\n' "$GPG_TTY"
printf 'export SSH_AUTH_SOCK=%q\n' "$agent_socket"
}
cache_gpg_pin()
{
[[ $# -le 1 ]] || fmt_fatal "usage: sagt gpg-pin [KEY]"
command -v gpg > /dev/null 2>&1 || fmt_fatal "gpg not found"
prepare_gpg_agent
local signing_key="${1:-}"
local temp_dir
local signature_file
local status=0
local gpg_args=(--detach-sign)
temp_dir=$(mktemp -d "${TMPDIR:-/tmp}/sagent-gpg-pin.XXXXXXXXXX") || \
fmt_fatal "failed to create a temporary directory"
signature_file="$temp_dir/signature.gpg"
gpg_args+=(--output "$signature_file")
[[ -z "$signing_key" ]] || gpg_args+=(--local-user "$signing_key")
fmt_note "performing a test signature; enter the GPG PIN and touch the token if prompted"
printf 'sagt gpg-pin\n' | gpg "${gpg_args[@]}" || status=$?
rm -f -- "$signature_file"
rmdir -- "$temp_dir"
[[ $status -eq 0 ]] || fmt_fatal "test signature failed (gpg exit $status)"
fmt_note "test signature completed; the GPG PIN should remain cached until the card or agent session is reset"
}
read_agent_file()
{
local agent_file="$1"
local line
local agent_socket=""
local agent_pid=""
while IFS= read -r line; do
case "$line" in
SSH_AUTH_SOCK=*)
agent_socket=${line#SSH_AUTH_SOCK=}
agent_socket=${agent_socket%%;*}
;;
SSH_AGENT_PID=*)
agent_pid=${line#SSH_AGENT_PID=}
agent_pid=${agent_pid%%;*}
;;
esac
done < "$agent_file"
[[ -n "$agent_socket" && "$agent_pid" =~ ^[1-9][0-9]*$ ]] || return 1
export SSH_AUTH_SOCK="$agent_socket"
export SSH_AGENT_PID="$agent_pid"
}
agent_is_usable()
{
[[ -S "$SSH_AUTH_SOCK" ]] || return 1
ps -p "$SSH_AGENT_PID" -o uid= -o comm= 2>/dev/null |
awk -v uid="$EUID" '$1 == uid && $2 ~ /(^|\/)ssh-agent$/ { found=1 } END { exit !found }' || return 1
local status
if ssh-add -l > /dev/null 2>&1; then
status=0
else
status=$?
fi
[[ $status -eq 0 || $status -eq 1 ]]
}
print_agent_env()
{
printf 'export SSH_AUTH_SOCK=%q\n' "$SSH_AUTH_SOCK"
printf 'export SSH_AGENT_PID=%q\n' "$SSH_AGENT_PID"
}
reset()
{
kill_agent
all already-killed
all
}
all()
{
local mode="${1:-}"
mkdir -p "$HOME/.ssh"
local agent_file="$HOME/.ssh/agent-$(whoami)"
[[ ! -L "$agent_file" ]] || fmt_fatal "refusing to use symlink as agent file: $agent_file"
unset SSH_AUTH_SOCK SSH_AGENT_PID
if [[ "$mode" != "already-killed" && -f "$agent_file" ]]; then
chmod 600 "$agent_file"
read_agent_file "$agent_file" || true
test -d ~/.ssh || mkdir ~/.ssh
local agent_file=~/.ssh/agent-$(whoami)
if [[ -f $agent_file ]]; then
source $agent_file > /dev/null
else
touch "$agent_file"
chmod 600 "$agent_file"
touch $agent_file
chmod 600 $agent_file
fi
if ! agent_is_usable; then
if [[ "$mode" != "already-killed" ]]; then
if ! ps -p "$SSH_AGENT_PID" 1>/dev/null 2>&1; then
kill_agent
fi
fmt_note "launching a new agent"
local agent_output
if ! agent_output=$(create_agent); then
fmt_fatal "failed to launch ssh-agent"
fi
printf '%s\n' "$agent_output" > "$agent_file"
chmod 600 "$agent_file"
read_agent_file "$agent_file" || fmt_fatal "ssh-agent returned invalid environment data"
agent_is_usable || fmt_fatal "new ssh-agent is not usable"
create_agent | tee $agent_file
else
fmt_note "using existing agent: $SSH_AGENT_PID"
cat $agent_file
fi
print_agent_env
}
route()
@@ -319,12 +105,6 @@ route()
op)
add_id25519_with_op
;;
gpg)
use_gpg_agent
;;
gpg-pin)
cache_gpg_pin "${@:2}"
;;
reset)
reset
;;
@@ -333,7 +113,6 @@ route()
;;
*)
fmt_error "unknown command: $1"
return 1
;;
esac
}

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env bash
set -ex
OPTS='-a -bcl --color --quiet --arg1=1 --arg2 2 " 1 2" yes'
OPTS='-a -bcl --color --arg1=1 --arg2 2 " 1 2" yes'
TARGET_OPTS='-a -b -c --arg1 1 --arg2 2 1 2 yes'
eval set -- $OPTS
@@ -8,11 +8,11 @@ THIS_DIR=$( cd "$( dirname "${BASH_SOURCE[0]:-${(%):-%x}}" )" && pwd )
source "$THIS_DIR/common.sh"
test "${GOT_OPTS[*]}" = "$TARGET_OPTS"
test $# -eq 9
test $# -eq 8
test "$*" = "${OPTS//\"/}"
test "$DFS_LITE" = "1"
is_tty
test "$DFS_QUIET" = "1"
test -z "$DFS_QUIET"
set +x
echo "test passed, args:"

View File

@@ -1,132 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
THIS_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
RIOT="$THIS_DIR/../scripts/riot"
TEST_DIR=$(mktemp -d /tmp/riot-gpg.XXXXXX)
trap 'rm -rf "$TEST_DIR"' EXIT
MOCK_BIN="$TEST_DIR/bin"
MOCK_HOME="$TEST_DIR/home"
MOCK_LOCAL_SOCKET="$TEST_DIR/local/S.gpg-agent.extra"
MOCK_REMOTE_SOCKET="$TEST_DIR/remote/S.gpg-agent"
MOCK_GPG_LOG="$TEST_DIR/gpg.log"
MOCK_SSH_LOG="$TEST_DIR/ssh.log"
mkdir -p "$MOCK_BIN" "$MOCK_HOME" "${MOCK_LOCAL_SOCKET%/*}" "${MOCK_REMOTE_SOCKET%/*}"
export MOCK_LOCAL_SOCKET MOCK_REMOTE_SOCKET MOCK_GPG_LOG MOCK_SSH_LOG
make_stale_socket() {
rm -f "$1"
python3 - "$1" <<'PY'
import socket
import sys
sock = socket.socket(socket.AF_UNIX)
sock.bind(sys.argv[1])
sock.close()
PY
}
cat > "$MOCK_BIN/gpgconf" <<'EOF'
#!/usr/bin/env bash
printf 'gpgconf %s\n' "$*" >> "$MOCK_GPG_LOG"
case "$*" in
'--list-dirs agent-extra-socket') printf '%s\n' "$MOCK_LOCAL_SOCKET" ;;
'--list-dirs agent-socket') printf '%s\n' "$MOCK_REMOTE_SOCKET" ;;
'--kill gpg-agent') exit "${MOCK_KILL_STATUS:-0}" ;;
*) exit 1 ;;
esac
EOF
cat > "$MOCK_BIN/gpg-connect-agent" <<'EOF'
#!/usr/bin/env bash
printf 'gpg-connect-agent %s\n' "$*" >> "$MOCK_GPG_LOG"
case "$*" in
*'GETINFO pid'*) printf 'D 123\nOK\n' ;;
*'GETINFO version'*)
[[ "${MOCK_LOCAL_LIVE:-1}" == 1 ]] || exit 1
printf 'D 2.4.0\nOK\n'
;;
*'GETINFO restricted'*) printf 'D %s\nOK\n' "${MOCK_REMOTE_RESTRICTED:-0}" ;;
*) exit 1 ;;
esac
EOF
cat > "$MOCK_BIN/systemctl" <<'EOF'
#!/usr/bin/env bash
[[ "${MOCK_SYSTEMD_ACTIVE:-0}" == 1 ]]
EOF
cat > "$MOCK_BIN/ssh" <<'EOF'
#!/usr/bin/env bash
{
printf 'CALL\n'
printf 'ARG=%s\n' "$@"
} >> "$MOCK_SSH_LOG"
is_probe=0
last_arg=''
for arg in "$@"; do
[[ "$arg" == '-T' ]] && is_probe=1
last_arg=$arg
done
if [[ "$is_probe" == 1 ]]; then
sh -c "$last_arg"
fi
EOF
chmod +x "$MOCK_BIN/gpgconf" "$MOCK_BIN/gpg-connect-agent" "$MOCK_BIN/systemctl" "$MOCK_BIN/ssh"
run_riot() {
HOME="$MOCK_HOME" RIOT_TRUST_CLIENT=0 PATH="$MOCK_BIN:$PATH" "$RIOT" "$@"
}
expect_failure() {
local expected=$1
shift
if run_riot "$@" > "$TEST_DIR/out" 2> "$TEST_DIR/err"; then
echo "expected riot to fail: $*" >&2
exit 1
fi
grep -Fq "$expected" "$TEST_DIR/err"
}
# Dry-run must not inspect or mutate either host.
: > "$MOCK_GPG_LOG"
: > "$MOCK_SSH_LOG"
DFS_DRY_RUN=1 run_riot -g example.test > "$TEST_DIR/out" 2> "$TEST_DIR/err"
grep -Fq '<remote-gpg-agent-socket>:<local-gpg-agent-extra-socket>' "$TEST_DIR/out"
[[ ! -s "$MOCK_GPG_LOG" && ! -s "$MOCK_SSH_LOG" ]]
# A socket inode without a listener must be rejected locally.
make_stale_socket "$MOCK_LOCAL_SOCKET"
MOCK_LOCAL_LIVE=0 expect_failure 'extra socket exists but is not accepting connections' -g example.test
# -g is valid only for an interactive SSH login.
expect_failure 'only supported for interactive SSH login' -g example.test scp ./a ./b
expect_failure 'only supported for interactive SSH login' -g example.test ssh -- true
# Never remove a regular file merely because it has the expected basename.
MOCK_LOCAL_LIVE=1
printf 'keep me\n' > "$MOCK_REMOTE_SOCKET"
expect_failure 'refusing to remove the non-socket remote gpg-agent path' -g example.test
grep -Fqx 'keep me' "$MOCK_REMOTE_SOCKET"
# A restricted agent at the remote socket represents another forwarding session.
make_stale_socket "$MOCK_REMOTE_SOCKET"
MOCK_REMOTE_RESTRICTED=1 expect_failure 'another forwarded gpg-agent is already using the remote socket' -g example.test
# A normal remote agent can be cleaned up; systemd activation is reported.
make_stale_socket "$MOCK_REMOTE_SOCKET"
: > "$MOCK_SSH_LOG"
MOCK_REMOTE_RESTRICTED=0 MOCK_SYSTEMD_ACTIVE=1 run_riot -g example.test > "$TEST_DIR/out" 2> "$TEST_DIR/err"
grep -Fq 'remote gpg-agent.socket is active' "$TEST_DIR/err"
grep -Fq 'ARG=none' "$MOCK_SSH_LOG"
grep -Fq 'ARG=ClearAllForwardings=yes' "$MOCK_SSH_LOG"
grep -Fq 'ARG=-T' "$MOCK_SSH_LOG"
! grep -Fq 'ARG=RequestTTY=' "$MOCK_SSH_LOG"
grep -Fq 'ARG=StreamLocalBindUnlink=no' "$MOCK_SSH_LOG"
grep -Fq "ARG=$MOCK_REMOTE_SOCKET:$MOCK_LOCAL_SOCKET" "$MOCK_SSH_LOG"
[[ ! -e "$MOCK_REMOTE_SOCKET" ]]
echo 'riot gpg forwarding tests passed'

View File

@@ -1,75 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
THIS_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
SAGENT="$THIS_DIR/sagent.sh"
TEST_DIR=$(mktemp -d /tmp/sagent-gpg-pin.XXXXXX)
trap 'rm -rf "$TEST_DIR"' EXIT
MOCK_BIN="$TEST_DIR/bin"
MOCK_HOME="$TEST_DIR/home"
MOCK_TMP="$TEST_DIR/tmp"
MOCK_GPG_ARGS="$TEST_DIR/gpg-args"
MOCK_GPG_INPUT="$TEST_DIR/gpg-input"
MOCK_AGENT_LOG="$TEST_DIR/agent-log"
mkdir -p "$MOCK_BIN" "$MOCK_HOME/gnupg" "$MOCK_HOME/sysconf" "$MOCK_TMP"
export MOCK_BIN MOCK_HOME MOCK_GPG_ARGS MOCK_GPG_INPUT MOCK_AGENT_LOG
cat > "$MOCK_BIN/gpgconf" <<'EOF'
#!/usr/bin/env bash
printf 'gpgconf %s\n' "$*" >> "$MOCK_AGENT_LOG"
case "$*" in
'--list-dirs homedir') printf '%s\n' "$MOCK_HOME/gnupg" ;;
'--list-dirs sysconfdir') printf '%s\n' "$MOCK_HOME/sysconf" ;;
'--list-dirs bindir') printf '%s\n' "$MOCK_BIN" ;;
'--launch gpg-agent') ;;
*) exit 1 ;;
esac
EOF
cat > "$MOCK_BIN/gpg-connect-agent" <<'EOF'
#!/usr/bin/env bash
printf 'gpg-connect-agent %s\n' "$*" >> "$MOCK_AGENT_LOG"
EOF
cat > "$MOCK_BIN/tty" <<'EOF'
#!/usr/bin/env bash
printf '/dev/pts/mock\n'
EOF
cat > "$MOCK_BIN/gpg" <<'EOF'
#!/usr/bin/env bash
printf '%s\n' "$@" > "$MOCK_GPG_ARGS"
output=''
while [[ $# -gt 0 ]]; do
if [[ "$1" == '--output' ]]; then
output=$2
shift 2
else
shift
fi
done
cat > "$MOCK_GPG_INPUT"
printf 'mock signature\n' > "$output"
EOF
printf '#!/usr/bin/env bash\n' > "$MOCK_BIN/pinentry"
chmod +x "$MOCK_BIN/gpgconf" "$MOCK_BIN/gpg-connect-agent" "$MOCK_BIN/tty" \
"$MOCK_BIN/gpg" "$MOCK_BIN/pinentry"
output=$(HOME="$MOCK_HOME" TMPDIR="$MOCK_TMP" PATH="$MOCK_BIN:$PATH" "$SAGENT" gpg-pin TEST-KEY 2> "$TEST_DIR/stderr")
[[ -z "$output" ]]
grep -Fxq -- '--detach-sign' "$MOCK_GPG_ARGS"
grep -Fxq -- '--local-user' "$MOCK_GPG_ARGS"
grep -Fxq -- 'TEST-KEY' "$MOCK_GPG_ARGS"
grep -Fxq -- 'sagt gpg-pin' "$MOCK_GPG_INPUT"
grep -Fq -- 'gpgconf --launch gpg-agent' "$MOCK_AGENT_LOG"
grep -Fq -- 'gpg-connect-agent updatestartuptty /bye' "$MOCK_AGENT_LOG"
grep -Fq -- 'test signature completed' "$TEST_DIR/stderr"
output=$(HOME="$MOCK_HOME" TMPDIR="$MOCK_TMP" PATH="$MOCK_BIN:$PATH" "$SAGENT" gpg-pin 2> "$TEST_DIR/stderr")
[[ -z "$output" ]]
! grep -Fxq -- '--local-user' "$MOCK_GPG_ARGS"
[[ -z $(find "$MOCK_TMP" -mindepth 1 -print -quit) ]]
echo "sagent gpg-pin tests passed"

View File

@@ -1,83 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
THIS_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
SCF="$THIS_DIR/../scripts/scf"
TEST_DIR=$(mktemp -d /tmp/scf.XXXXXX)
trap 'rm -rf "$TEST_DIR"' EXIT
MOCK_BIN="$TEST_DIR/bin"
DATA_DIR="$TEST_DIR/data"
MOCK_SYSTEMCTL_LOG="$TEST_DIR/systemctl.log"
MOCK_MAIN="$DATA_DIR/main.yaml"
MOCK_CHILD="$DATA_DIR/child.conf"
MOCK_NOTE="$DATA_DIR/notes.txt"
mkdir -p "$MOCK_BIN" "$DATA_DIR"
export MOCK_SYSTEMCTL_LOG MOCK_MAIN MOCK_CHILD MOCK_NOTE
printf 'include %s\n' "$MOCK_CHILD" > "$MOCK_MAIN"
printf 'child configuration\n' > "$MOCK_CHILD"
printf 'not a configuration candidate\n' > "$MOCK_NOTE"
cat > "$MOCK_BIN/systemctl" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
printf '%s\n' "$*" >> "$MOCK_SYSTEMCTL_LOG"
service=${@: -1}
case "$service" in
demo.service)
printf 'path=/usr/bin/demo ; argv[0]=demo ; argv[1]=%s ; argv[2]=%s ;\n' \
"$MOCK_MAIN" "$MOCK_NOTE"
;;
empty.service)
printf 'path=/usr/bin/empty ; argv[0]=empty ;\n'
;;
*)
exit 1
;;
esac
EOF
chmod +x "$MOCK_BIN/systemctl"
run_scf() {
PATH="$MOCK_BIN:$PATH" "$SCF" "$@"
}
# A service name without .service is accepted, and non-interactive mode prints
# the only configuration candidate instead of opening an editor.
output=$(run_scf demo 2> "$TEST_DIR/stderr")
grep -Fxq 'include '"$MOCK_CHILD" <<< "$output"
grep -Fq -- '--property=ExecStart --value demo.service' "$MOCK_SYSTEMCTL_LOG"
# Recursive mode discovers configuration files referenced by the first one.
output=$(run_scf -r -n 2 demo)
grep -Fxq 'child configuration' <<< "$output"
# Decimal values with a leading zero must not be treated as invalid octal.
output=$(SCF_MAX_DEPTH=08 SCF_MAX_CANDIDATES=08 run_scf demo)
grep -Fxq 'include '"$MOCK_CHILD" <<< "$output"
if run_scf -n 08 demo > "$TEST_DIR/out" 2> "$TEST_DIR/stderr"; then
echo 'expected scf -n 08 to fail because only one candidate exists' >&2
exit 1
fi
grep -Fq 'candidate number 8 is out of range' "$TEST_DIR/stderr"
# Invalid service names are rejected before systemctl is invoked.
before=$(wc -l < "$MOCK_SYSTEMCTL_LOG")
if run_scf 'demo; touch /tmp/unexpected' > "$TEST_DIR/out" 2> "$TEST_DIR/stderr"; then
echo 'expected an invalid service name to fail' >&2
exit 1
fi
grep -Fq 'invalid service name' "$TEST_DIR/stderr"
after=$(wc -l < "$MOCK_SYSTEMCTL_LOG")
test "$before" -eq "$after"
# A service without a referenced file reports a useful failure.
if run_scf empty > "$TEST_DIR/out" 2> "$TEST_DIR/stderr"; then
echo 'expected a service without a config path to fail' >&2
exit 1
fi
grep -Fq 'no existing configuration path found' "$TEST_DIR/stderr"
echo 'scf tests passed'

View File

@@ -34,18 +34,15 @@ dogo
doll
dfs cd
tools/test-getopts.sh
tools/test-scf.sh
tools/test-riot-gpg.sh
tools/test-sagent-gpg-pin.sh
tools/common.sh get_os_name
test $(echo y | tools/common.sh ask_for_yN "test") = "1"
test $(echo n | tools/common.sh ask_for_yN "test") = "0"
test $(echo | tools/common.sh ask_for_yN "test") = "0"
test $(echo | tools/common.sh ask_for_Yn "test") = "1"
test $(DFS_QUIET=1 tools/common.sh ask_for_Yn "test") = "1"
test "$(DFS_TRUST=1 riot time@is.impt:2222/yes@you-r.right/you@are.really.recht./ibd./try@it,another@host -D scp /tmp/ ./tmp 2>/dev/null)" = 'scp -P 12022 -o ServerAliveInterval=60 -o PermitLocalCommand=yes -o ControlMaster=auto -o ControlPersist=5s -o ControlPath=~/.ssh/master-socket/%C -o ProxyJump=time@is.impt:2222,yes@you-r.right:12022,you@are.really.recht:12022,root@ibd:12022 -r try@it.dxng.net:/tmp/ ./tmp
scp -P 12022 -o ServerAliveInterval=60 -o PermitLocalCommand=yes -o ControlMaster=auto -o ControlPersist=5s -o ControlPath=~/.ssh/master-socket/%C -o ForwardX11=yes -o ForwardAgent=yes -r another@host.dxng.net:/tmp/ ./tmp'
test "$(riot you@example.com:55 -tD --password ssh ping -c 1 2>/dev/null)" = 'ssh -p 55 -o ServerAliveInterval=60 -o ForwardX11=yes -o ForwardAgent=yes -o PasswordAuthentication=yes -o PubkeyAuthentication=no you@example.com ping -c 1'
test "$(DFS_TRUST=1 riot time@is.impt:2222/yes@you-r.right/you@are.really.recht./ibd./try@it,another@host scp /tmp/ ./tmp -D 2>/dev/null)" = 'scp -P 12022 -o ServerAliveInterval=60 -o PermitLocalCommand=yes -o ControlMaster=auto -o ControlPersist=60s -o ControlPath=~/.ssh/master-socket/%C -o ProxyJump=time@is.impt:2222,yes@you-r.right:12022,you@are.really.recht:12022,root@ibd:12022 -r try@it.dxng.net:/tmp/ ./tmp
scp -P 12022 -o ServerAliveInterval=60 -o PermitLocalCommand=yes -o ControlMaster=auto -o ControlPersist=60s -o ControlPath=~/.ssh/master-socket/%C -o ForwardX11=yes -o ForwardAgent=yes -r another@host.dxng.net:/tmp/ ./tmp'
test "$(riot you@example.com:55 -tD ssh --password -- ping -c 1 2>/dev/null)" = 'ssh -p 55 -o ServerAliveInterval=60 -o ForwardX11=yes -o ForwardAgent=yes -o PasswordAuthentication=yes -o PubkeyAuthentication=no you@example.com ping -c 1'
# check alias
alias p114