macOS + zsh 5.9 + Homebrew 的 mysql-client。Linux 上除了 UseKeychain(macOS 钥匙串专有)之外都能用。
敲 shop-test 就连上测试库,敲 aliyun-01 就登服务器。
两张表怎么分
判据一句话:要不要拼参数、要不要存密码?
| 用哪张表 | 例子 | |
|---|---|---|
| 要拼参数、要存密码 | db/connections.conf | MySQL、Redis |
| 命令现成,只是长 | shortcuts.conf | ssh、端口转发、kubectl context |
目录结构
~/.zshrc # 只是个 loader,按序 source rc.d/*.zsh ~/.config/zsh/ ├── rc.d/ │ ├── 00-path.zsh # PATH │ ├── 10-completion.zsh # 补全系统 │ ├── 20-plugins.zsh # autosuggestions / syntax-highlighting │ ├── 30-prompt.zsh # starship │ ├── 40-aliases.zsh # 通用别名 ll / la │ ├── 50-db.zsh # 读注册表 → 生成连接别名 + db 命令 │ ├── 55-shortcuts.zsh # 读快捷表 → 生成短名 + sc 命令 │ ├── 60-integrations.zsh # 终端集成 │ └── 99-local.zsh # 本机私有,gitignore ├── db/connections.conf # 数据库注册表(无密码,可入库) └── shortcuts.conf # 短名 → 命令 ~/.ssh/config + conf.d/*.conf # 主机细节:IP / 密钥 / 跳板机 ~/.mylogin.cnf # 密码,600,绝不入库
加一类配置就加一个分片文件,不往 .zshrc 里堆。~/.zshrc 全文就这几行:
export ZSH_CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/zsh"
for _rc in "$ZSH_CONFIG_DIR"/rc.d/*.zsh(N.on); do
source "$_rc"
done
unset _rc
命令速查
# 数据库 db ls # 列表,COMMAND 列就是能直接敲的命令 db show <name> # 明细(不含密码) db auth <name> # 录入密码,交互输入 db forget <name> # 删除本机密码 db q <name> "select 1" # 跑一条 SQL,表格输出,不进交互 <name> # 直接连,等价于 db connect <name> # 其他命令 sc ls # 列表 + 生效状态 sc show <短名> # 看展开成什么 sc edit # 编辑快捷表 sc reload # 重载 shell
db 和 sc 都带 Tab 补全。
db ls 的样子:
NAME COMMAND HOST PORT USER DATABASE ENV AUTH shop-test shop-test db-test.internal.example 3306 app_ro app_test test ✔
AUTH 是本机有没有存密码。每个连接生成两个别名,shop-test 和 mysql-shop-test,效果一样,后者方便 mysql- + Tab 找同类。
加一个数据库
三步。
1. 登记连接,db edit 打开注册表,加一行:
# name engine host port user database env shop-test mysql db-test.internal.example 3306 app_ro app_test test
字段之间空格分隔,database 不需要就填 -。env 填 prod 时连接前会要求输入 yes 二次确认,防手滑。
2. 录密码:
db auth shop-test
交互输入,存进 ~/.mylogin.cnf。这一步是 mysql_config_editor 干的,密码不会进注册表,所以注册表可以放心提交到 dotfiles。
3. 生效:
exec zsh
之后 shop-test 直接可用,额外参数会透传:
shop-test -e "show tables"
加一个服务器或其他命令
两步。sc edit 加一行,第一个字段是短名,余下整行都是要执行的命令:
# 服务器(主机细节写在 ~/.ssh/conf.d/ 里) aliyun-01 ssh aliyun-01 aliyun-02 ssh aliyun-02 # 端口转发:本地 13306 通到测试库 tun-shop ssh -N -L 13306:db-test.internal.example:3306 bastion # k8s k8s-test kubectl --context=test
然后 sc reload。只支持整行注释,不支持行尾注释。
短名撞上已有命令时会被跳过并告警,不会覆盖系统命令。试过写一个叫 ls 的短名,结果是它被跳过,whence -w ls 仍然是 command,sc ls 里那行标成「冲突/未生效」。数据库那边同理,裸短名撞了就退回只能用 mysql-<name>。
SSH 主机定义
~/.ssh/config 只留 Include 和全局默认,主机按用途拆到 conf.d/:
Include conf.d/*.conf
Host *
AddKeysToAgent yes
UseKeychain yes
ControlMaster auto
ControlPath ~/.ssh/cm/%C
ControlPersist 5m
ServerAliveInterval 60
IdentitiesOnly yes
conf.d/10-servers.conf:
Host aliyun-01
HostName 1.2.3.4
User work
IdentityFile ~/.ssh/id_ed25519
Host shop-test-*
User work
ProxyJump bastion
ssh 是「首次取到的值生效」。Include 放到 Host * 后面的话,分片里的设置会被全局默认盖掉。
ControlPath 那个目录要自己建:mkdir -p ~/.ssh/cm && chmod 700 ~/.ssh/cm。
顺带说下这几个全局默认的作用:ControlMaster 是连接复用,走跳板机时第二次连接秒开;IdentitiesOnly 让 ssh 只试配置里指定的密钥,密钥多了能避免 too many authentication failures。
zsh 5.9 的 _ssh_hosts 补全函数会展开 include,所以拆分之后 ssh + Tab 照样能列出所有 Host。
换机器怎么迁
能直接拷的:~/.config/zsh/ 整个目录、~/.ssh/conf.d/*.conf。
不能拷的:~/.mylogin.cnf(每台机器重新 db auth)、~/.ssh/id_* 私钥、含内网 IP 的分片(用 90- 前缀 + gitignore 隔离)。
做成 dotfiles:
mkdir -p ~/.dotfiles && cd ~/.dotfiles && git init mv ~/.config/zsh zsh && ln -s ~/.dotfiles/zsh ~/.config/zsh mv ~/.zshrc zshrc && ln -s ~/.dotfiles/zshrc ~/.zshrc
~/.ssh 不要整体入库,里面有私钥。
注意事项
- 生成的是 alias,只在交互式 shell 生效。写 shell 脚本时要用完整命令。
~/.mylogin.cnf是混淆存储不是加密,MySQL 官方文档明确说它只防无意窥视。能读到 home 目录的人就能解出密码。- 改完两张表都要
exec zsh(或sc reload)才生效,当前窗口不会自动重载。
附录:完整代码
rc.d/50-db.zsh
# ══════════════════════════════════════════════════════════════════════
# 数据库快捷连接
#
# db ls 列出所有已登记连接
# db show <name> 查看某连接明细(不含密码)
# db auth <name> 录入/更新密码到 ~/.mylogin.cnf(交互输入)
# db forget <name> 删除本机保存的密码
# db connect <name> [...] 连接(额外参数透传给客户端)
# db q <name> <sql> 执行一条 SQL 并返回结果
# db <name> = db connect <name>
#
# 每个连接会生成两个别名:<name> 和 <engine>-<name>
# 连接信息见 $DB_REGISTRY,密码存在 ~/.mylogin.cnf,永不写入本仓库。
# ══════════════════════════════════════════════════════════════════════
export DB_REGISTRY="${DB_REGISTRY:-$ZSH_CONFIG_DIR/db/connections.conf}"
# 内部:输出注册表里的有效数据行(去掉注释与空行)
_db_rows() {
[[ -r $DB_REGISTRY ]] || { print -ru2 "db: 注册表不存在或不可读:$DB_REGISTRY"; return 1 }
awk '!/^[[:space:]]*(#|$)/' "$DB_REGISTRY"
}
# 内部:按短名取一行;找不到返回 1
_db_row() {
local line
line=$(_db_rows | awk -v n="$1" '$1 == n { print; exit }') || return 1
[[ -n $line ]] || { print -ru2 "db: 未登记的连接:$1"; return 1 }
print -r -- "$line"
}
# 内部:所有短名,供补全使用
_db_names() { _db_rows 2>/dev/null | awk '{print $1}' }
# 内部:该连接对应的 login-path 名(加前缀避免与其他工具冲突)
_db_login_path() { print -r -- "db-$1" }
# 内部:本机是否已保存该连接的密码
_db_has_auth() {
[[ -n $(mysql_config_editor print --login-path="$(_db_login_path "$1")" 2>/dev/null) ]]
}
# 内部:短名是否会覆盖已有的命令/函数/内建
_db_conflict() {
(( $+commands[$1] || $+functions[$1] || $+builtins[$1] ))
}
# 内部:该连接实际可直接敲的命令(裸短名优先,其次 <engine>-<name>)
_db_cmd_of() {
if [[ -n ${aliases[$1]} ]]; then
print -r -- "$1"
elif [[ -n ${aliases[${2}-${1}]} ]]; then
print -r -- "${2}-${1}"
else
print -r -- "db connect $1"
fi
}
db() {
local cmd=$1
case $cmd in
ls|list|'')
{ print -r -- "NAME│COMMAND│HOST│PORT│USER│DATABASE│ENV│AUTH"
local -a f
_db_rows | while read -rA f; do
print -r -- "$f[1]│$(_db_cmd_of $f[1] $f[2])│$f[3]│$f[4]│$f[5]│$f[6]│$f[7]│$(_db_has_auth $f[1] && print '✔' || print '✘')"
done
} | column -t -s '│'
;;
show)
local -a f; f=(${=$(_db_row "$2")}) || return 1
print -r -- "name : $f[1]"
print -r -- "engine : $f[2]"
print -r -- "host:port : $f[3]:$f[4]"
print -r -- "user : $f[5]"
print -r -- "database : $f[6]"
print -r -- "env : $f[7]"
print -r -- "login-path: $(_db_login_path $f[1]) ($(_db_has_auth $f[1] && print '已保存密码' || print '未保存密码'))"
print -r -- "alias : $f[2]-$f[1]"
;;
auth)
local -a f; f=(${=$(_db_row "$2")}) || return 1
print -r -- "为 $f[1] ($f[5]@$f[3]:$f[4]) 录入密码,将保存到 ~/.mylogin.cnf"
mysql_config_editor set --login-path="$(_db_login_path $f[1])" --password \
&& print -r -- "已保存。现在可以直接执行:$f[1]"
;;
forget)
local -a f; f=(${=$(_db_row "$2")}) || return 1
mysql_config_editor remove --login-path="$(_db_login_path $f[1])" \
&& print -r -- "已删除 $f[1] 的本机凭据。"
;;
connect|q|query)
local -a f; f=(${=$(_db_row "$2")}) || return 1
shift 2
local name=$f[1] engine=$f[2] host=$f[3] port=$f[4] user=$f[5] database=$f[6] env=$f[7]
if ! _db_has_auth $name; then
print -ru2 "db: 尚未保存 $name 的密码,请先执行:db auth $name"
return 1
fi
# 生产环境二次确认,防手滑
if [[ $env == prod* ]]; then
print -n -- "⚠️ $name 是生产环境,确认连接?(输入 yes) "
local answer; read -r answer
[[ $answer == yes ]] || { print -r -- "已取消。"; return 1 }
fi
local -a args
case $engine in
mysql)
args=(--login-path="$(_db_login_path $name)"
--host="$host" --port="$port" --user="$user")
[[ $database != - ]] && args+=(--database="$database")
if [[ $cmd == q || $cmd == query ]]; then
command mysql "${args[@]}" --table --execute="$*"
else
command mysql "${args[@]}" "$@"
fi
;;
*)
print -ru2 "db: 暂不支持 engine=$engine(在这里扩展 case 分支)"
return 1
;;
esac
;;
edit) ${EDITOR:-vi} "$DB_REGISTRY" ;;
help|-h|--help)
print -r -- "用法: db {ls|show|auth|forget|connect|q|edit} [name] [args...]"
;;
*)
# db <name> 等价于 db connect <name>
db connect "$@"
;;
esac
}
# 为每个连接生成两个别名:裸短名 + <engine>-<name>
# 裸短名撞上已有命令时跳过并告警,绝不覆盖系统命令。
if [[ -r $DB_REGISTRY ]]; then
typeset -a _db_f _db_skipped
while read -rA _db_f; do
if _db_conflict "${_db_f[1]}"; then
_db_skipped+=("${_db_f[1]}")
else
alias "${_db_f[1]}"="db connect ${_db_f[1]}"
fi
_db_conflict "${_db_f[2]}-${_db_f[1]}" \
|| alias "${_db_f[2]}-${_db_f[1]}"="db connect ${_db_f[1]}"
done < <(_db_rows 2>/dev/null)
if (( $#_db_skipped )); then
print -ru2 "db: 以下短名与已有命令冲突,只能用 <engine>-<name> 形式:${_db_skipped[*]}"
fi
unset _db_f _db_skipped
fi
# 补全:db 的子命令与连接名
_db_complete() {
local -a subcmds
subcmds=(ls show auth forget connect q edit help)
if (( CURRENT == 2 )); then
compadd -X '子命令' -- $subcmds
compadd -X '连接' -- $(_db_names)
elif (( CURRENT == 3 )); then
compadd -- $(_db_names)
fi
}
compdef _db_complete db
rc.d/55-shortcuts.zsh
# ══════════════════════════════════════════════════════════════════════
# 命令快捷方式:把 shortcuts.conf 里的每一行变成一个可直接执行的短名
#
# sc ls 查看全部快捷方式
# sc show <n> 查看某个短名展开成什么
# sc edit 编辑快捷表
# sc reload 重载 shell 使改动生效
#
# 生成的是 alias,因此只在交互式 shell 生效;脚本里请写完整命令。
# ══════════════════════════════════════════════════════════════════════
export SHORTCUTS_FILE="${SHORTCUTS_FILE:-$ZSH_CONFIG_DIR/shortcuts.conf}"
# 内部:输出有效数据行(去掉注释与空行)
_sc_rows() {
[[ -r $SHORTCUTS_FILE ]] || { print -ru2 "sc: 快捷表不存在或不可读:$SHORTCUTS_FILE"; return 1 }
awk '!/^[[:space:]]*(#|$)/' "$SHORTCUTS_FILE"
}
# 内部:短名是否会覆盖已有的命令/函数/内建,或已被前序分片占用的 alias
# (定义完全相同时不算冲突,便于重复 source)
_sc_conflict() {
(( $+commands[$1] || $+functions[$1] || $+builtins[$1] )) && return 0
[[ -n ${aliases[$1]} && ${aliases[$1]} != $2 ]]
}
# 加载:逐行生成 alias,撞名则跳过并告警
if [[ -r $SHORTCUTS_FILE ]]; then
typeset -a _sc_skipped
while read -r _sc_name _sc_cmd; do
[[ -n $_sc_name && -n $_sc_cmd ]] || continue
if _sc_conflict $_sc_name "$_sc_cmd"; then
_sc_skipped+=("$_sc_name")
continue
fi
alias "$_sc_name"="$_sc_cmd"
done < <(_sc_rows 2>/dev/null)
if (( $#_sc_skipped )); then
print -ru2 "sc: 以下短名与已有命令冲突,已跳过:${_sc_skipped[*]}"
print -ru2 " 请在 $SHORTCUTS_FILE 中改名"
fi
unset _sc_name _sc_cmd _sc_skipped
fi
sc() {
case ${1:-ls} in
ls|list)
{ print -r -- "SHORTCUT│COMMAND│STATUS"
local n c
_sc_rows | while read -r n c; do
[[ -n $n && -n $c ]] || continue
print -r -- "$n │$c│$(if [[ ${aliases[$n]} == $c ]]; then print '生效'; else print '冲突/未生效'; fi)"
done
} | column -t -s '│'
;;
show)
local n=$2
[[ -n $n ]] || { print -ru2 "用法: sc show <短名>"; return 1 }
local c
c=$(_sc_rows | awk -v n="$n" '$1 == n { $1=""; sub(/^[ \t]+/,""); print; exit }')
[[ -n $c ]] || { print -ru2 "sc: 快捷表中没有 $n"; return 1 }
print -r -- "$n → $c"
[[ ${aliases[$n]} == $c ]] || print -ru2 "(当前未生效:可能与已有命令冲突,或尚未 sc reload)"
;;
edit) ${EDITOR:-vi} "$SHORTCUTS_FILE" ;;
reload) exec zsh ;;
help|-h|--help)
print -r -- "用法: sc {ls|show <短名>|edit|reload}"
;;
*)
print -ru2 "sc: 未知子命令 $1(可用:ls show edit reload help)"
return 1
;;
esac
}
# 补全:sc 的子命令与已登记短名
_sc_complete() {
if (( CURRENT == 2 )); then
compadd -X '子命令' -- ls show edit reload help
elif (( CURRENT == 3 )) && [[ $words[2] == show ]]; then
compadd -- $(_sc_rows 2>/dev/null | awk '{print $1}')
fi
}
compdef _sc_complete sc
评论加载中...