From e8aae9738b012c3033c2d6df6940c7cb90547392 Mon Sep 17 00:00:00 2001 From: laily Date: Thu, 25 Jun 2026 10:46:07 +0800 Subject: [PATCH] feat: add nosudo --- cursor/config/settings.json | 104 ++++++++++++++++++------------------ fish/default.fish | 2 +- fish/functions/pwdc.fish | 7 +++ script/clip-copy.sh | 31 +++++++++++ script/dns-clean.sh | 76 ++++++++++++++++++++++++++ script/docker-group.sh | 51 ++++++++++++++++++ script/nosudo.sh | 97 +++++++++++++++++++++++++++++++++ script/scan-ip.sh | 32 +++++++++-- zsh/.zshrc | 13 +++++ 9 files changed, 358 insertions(+), 55 deletions(-) create mode 100644 fish/functions/pwdc.fish create mode 100755 script/clip-copy.sh create mode 100755 script/dns-clean.sh create mode 100755 script/docker-group.sh create mode 100755 script/nosudo.sh diff --git a/cursor/config/settings.json b/cursor/config/settings.json index 4b8ca9b..b2a44c2 100644 --- a/cursor/config/settings.json +++ b/cursor/config/settings.json @@ -20,7 +20,7 @@ "vim.normalModeKeyBindingsNonRecursive": [ { "before": [""], - "commands": ["vspacecode.space"], + "commands": ["vspacecode.space"] }, { "before": [","], @@ -28,15 +28,15 @@ "vspacecode.space", { "command": "whichkey.triggerKey", - "args": "m", - }, - ], - }, + "args": "m" + } + ] + } ], "vim.visualModeKeyBindingsNonRecursive": [ { "before": [""], - "commands": ["vspacecode.space"], + "commands": ["vspacecode.space"] }, { "before": [","], @@ -44,18 +44,18 @@ "vspacecode.space", { "command": "whichkey.triggerKey", - "args": "m", - }, - ], + "args": "m" + } + ] }, { "before": [">"], - "commands": ["editor.action.indentLines"], + "commands": ["editor.action.indentLines"] }, { "before": ["<"], - "commands": ["editor.action.outdentLines"], - }, + "commands": ["editor.action.outdentLines"] + } ], "workbench.panel.location": "right", "gitHistory.hideCommitViewExplorer": true, @@ -67,16 +67,16 @@ "sync.gist": "3c4e02a02a0d110622006b8c8b058fa5", "go.languageServerFlags": ["-rpc.trace"], "[json]": { - "editor.defaultFormatter": "vscode.json-language-features", + "editor.defaultFormatter": "vscode.json-language-features" }, "files.associations": { "*.html": "html", "*.json": "jsonc", "NOTES.txt": "helm", - "Dockerfile": "dockerfile", + "Dockerfile": "dockerfile" }, "[helm]": { - "editor.defaultFormatter": "redhat.vscode-yaml", + "editor.defaultFormatter": "redhat.vscode-yaml" // "editor.formatOnSave": false // alternatively, force it to leave you alone }, "gitlens.codeLens.recentChange.enabled": false, @@ -87,19 +87,19 @@ // "terminal.integrated.shell.osx": "/bin/zsh", "editor.fontFamily": "Fira Code, Sarasa Fixed SC Nerd Font, Menlo, Monaco, 'Courier New', monospace", "[javascript]": { - "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.defaultFormatter": "esbenp.prettier-vscode" }, "search.followSymlinks": false, "go.autocompleteUnimportedPackages": true, "[jsonc]": { - "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.defaultFormatter": "esbenp.prettier-vscode" }, "vscode-neovim.mouseSelectionStartVisualMode": true, "go.lintTool": "golangci-lint", "go.lintFlags": ["--config=~/.dotfile/golang/golangci.yml"], "go.liveErrors": { "enabled": false, - "delay": 500, + "delay": 500 }, "python.languageServer": "Pylance", "go.overwriteGoplsMiddleware": {}, @@ -107,20 +107,20 @@ "[go]": { "editor.formatOnSave": true, "editor.codeActionsOnSave": { - "source.organizeImports": "explicit", + "source.organizeImports": "explicit" }, "editor.snippetSuggestions": "none", - "editor.defaultFormatter": "golang.go", + "editor.defaultFormatter": "golang.go" }, "[go.mod]": { "editor.formatOnSave": true, "editor.codeActionsOnSave": { - "source.organizeImports": "explicit", - }, + "source.organizeImports": "explicit" + } }, "gopls": { "usePlaceholders": true, - "staticcheck": false, + "staticcheck": false }, "go.testFlags": ["-v"], "vscode-neovim.neovimExecutablePaths.darwin": "/usr/local/bin/nvim", @@ -136,22 +136,22 @@ "security.workspace.trust.untrustedFiles": "open", "workbench.tree.indent": 4, "[html]": { - "editor.defaultFormatter": "vscode.html-language-features", + "editor.defaultFormatter": "vscode.html-language-features" }, "python.defaultInterpreterPath": "/usr/local/bin/python3", "[python]": { "editor.formatOnType": true, - "editor.defaultFormatter": "ms-python.python", + "editor.defaultFormatter": "ms-python.python" }, "[go][go.mod]": { "editor.codeActionsOnSave": { - "source.organizeImports": "explicit", - }, + "source.organizeImports": "explicit" + } }, "update.showReleaseNotes": false, "terminal.integrated.defaultProfile.osx": "fish", "extensions.experimental.affinity": { - "asvetliakov.vscode-neovim": 1, + "asvetliakov.vscode-neovim": 1 }, "makefile.configureOnOpen": false, "python.terminal.activateEnvInCurrentTerminal": true, @@ -159,10 +159,12 @@ "remote.SSH.defaultForwardedPorts": [], "remote.SSH.useLocalServer": false, "remote.SSH.connectTimeout": 30, + "remote.SSH.localServerDownload": "always", "remote.SSH.remotePlatform": { "box1": "linux", "hx80": "linux", "hx20": "linux", + "pc30": "linux" }, "editor.formatOnSave": true, "pylint.enabled": false, @@ -170,50 +172,50 @@ "python.analysis.packageIndexDepths": [ { "name": "sklearn", - "depth": 2, + "depth": 2 }, { "name": "matplotlib", - "depth": 2, + "depth": 2 }, { "name": "scipy", - "depth": 2, + "depth": 2 }, { "name": "django", - "depth": 2, + "depth": 2 }, { "name": "flask", - "depth": 2, + "depth": 2 }, { "name": "fastapi", - "depth": 2, - }, + "depth": 2 + } ], "[shellscript]": { - "editor.defaultFormatter": "foxundermoon.shell-format", + "editor.defaultFormatter": "foxundermoon.shell-format" }, // "[dockerfile]": { // "editor.defaultFormatter": "foxundermoon.shell-format" // }, "[ignore]": { - "editor.defaultFormatter": "foxundermoon.shell-format", + "editor.defaultFormatter": "foxundermoon.shell-format" }, "[properties]": { - "editor.defaultFormatter": "foxundermoon.shell-format", + "editor.defaultFormatter": "foxundermoon.shell-format" }, "editor.codeActionsOnSave": { // "source.organizeImports": "explicit", - "source.fixAll.eslint": "explicit", + "source.fixAll.eslint": "explicit" }, "[proto3]": { - "editor.defaultFormatter": "zxh404.vscode-proto3", + "editor.defaultFormatter": "zxh404.vscode-proto3" }, "[dotenv]": { - "editor.defaultFormatter": "foxundermoon.shell-format", + "editor.defaultFormatter": "foxundermoon.shell-format" }, "typescript.preferences.quoteStyle": "single", "javascript.preferences.quoteStyle": "single", @@ -221,7 +223,7 @@ "pretty-formatter.quoteConvert": "single", "html.completion.attributeDefaultValue": "singlequotes", "[typescriptreact]": { - "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.defaultFormatter": "esbenp.prettier-vscode" }, "prettier.jsxSingleQuote": true, "go.buildFlags": ["-tags=wireinject"], @@ -231,7 +233,7 @@ "editor.tabSize": 2, "editor.autoIndent": "advanced", "diffEditor.ignoreTrimWhitespace": false, - "editor.defaultColorDecorators": "never", + "editor.defaultColorDecorators": "never" }, "html.format.unformatted": "", "editor.defaultFormatter": "esbenp.prettier-vscode", @@ -243,34 +245,33 @@ "name": "Space (Global Leader)", "type": "bind", "command": "vspacecode.space", - "when": "editorTextFocus || explorerViewletVisible || terminalFocus", + "when": "editorTextFocus || explorerViewletVisible || terminalFocus" }, { "keys": "w.t", "name": "Toggle Terminal", "type": "command", - "command": "workbench.action.terminal.toggleTerminal", + "command": "workbench.action.terminal.toggleTerminal" }, { "keys": "w.w", "name": "Toggle AI Sidebar", "type": "command", - "command": "workbench.action.toggleAuxiliaryBar", + "command": "workbench.action.toggleAuxiliaryBar" }, { "keys": "w.e", "name": "Focus First Editor Group", "type": "command", - "command": "workbench.action.focusFirstEditorGroup", - }, + "command": "workbench.action.focusFirstEditorGroup" + } ], "[java]": { - "editor.suggest.snippetsPreventQuickSuggestions": false, + "editor.suggest.snippetsPreventQuickSuggestions": false }, "git.confirmSync": false, "editor.dropIntoEditor.preferences": [], "whichkey.bindingOverrides": [], - "remote.SSH.localServerDownload": "always", "workbench.colorCustomizations": {}, "javascript.format.enable": false, "typescript.format.enable": false, @@ -282,8 +283,9 @@ "ANTHROPIC_DEFAULT_SONNET_MODEL": "MiniMax-M2.1", "ANTHROPIC_MODEL": "MiniMax-M2.1", "API_TIMEOUT_MS": "3000000", - "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": 1, + "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": 1 }, "claudeCode.preferredLocation": "panel", "window.autoDetectColorScheme": false, + "vim.disableExtension": true } diff --git a/fish/default.fish b/fish/default.fish index fc141b4..ce3830b 100644 --- a/fish/default.fish +++ b/fish/default.fish @@ -103,7 +103,7 @@ end ################################################################# # git # alias -alias gitb="git branch --show-current | tr -d '\n' | pbcopy" +alias gitb="git branch --show-current | tr -d '\n' | clip-copy.sh" alias gci="golangci-lint run -c=~/.dotfile/golang/golangci.yml ./..." alias gmt="go mod tidy" alias gtc="go test -gcflags=all=-l -cover ./..." diff --git a/fish/functions/pwdc.fish b/fish/functions/pwdc.fish new file mode 100644 index 0000000..237ddec --- /dev/null +++ b/fish/functions/pwdc.fish @@ -0,0 +1,7 @@ +function pwdc --description 'Copy current directory path to clipboard' + set -l dir (pwd -P) + if not printf '%s' $dir | clip-copy.sh + return 1 + end + echo "Copied: "(string replace $HOME '~' $dir) +end diff --git a/script/clip-copy.sh b/script/clip-copy.sh new file mode 100755 index 0000000..0f2e265 --- /dev/null +++ b/script/clip-copy.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# Copy stdin to the system clipboard (macOS / Linux X11 / Linux Wayland). + +set -euo pipefail + +if command -v pbcopy >/dev/null 2>&1; then + pbcopy +elif [ "${XDG_SESSION_TYPE:-}" = wayland ] || [ -n "${WAYLAND_DISPLAY:-}" ]; then + if command -v wl-copy >/dev/null 2>&1; then + wl-copy --no-newline + else + echo "clip-copy: wl-copy not found (Wayland session)" >&2 + exit 1 + fi +elif [ -n "${DISPLAY:-}" ]; then + if command -v xclip >/dev/null 2>&1; then + xclip -selection clipboard + elif command -v xsel >/dev/null 2>&1; then + xsel --clipboard --input + else + echo "clip-copy: xclip or xsel not found (DISPLAY=$DISPLAY)" >&2 + exit 1 + fi +elif command -v xclip >/dev/null 2>&1; then + xclip -selection clipboard +elif command -v xsel >/dev/null 2>&1; then + xsel --clipboard --input +else + echo "clip-copy: no clipboard tool (pbcopy / wl-copy / xclip / xsel)" >&2 + exit 1 +fi diff --git a/script/dns-clean.sh b/script/dns-clean.sh new file mode 100755 index 0000000..1e6a100 --- /dev/null +++ b/script/dns-clean.sh @@ -0,0 +1,76 @@ +#!/bin/bash + +# 用法: +# ./dns-clean.sh # 清理 macOS / Linux 系统 DNS 缓存 + +set -e + +SUDO="" +[ "$(id -u)" -ne 0 ] && SUDO="sudo" + +flush_macos() { + echo "macOS: 清理 DNS 缓存..." + $SUDO dscacheutil -flushcache + $SUDO killall -HUP mDNSResponder 2>/dev/null || true + $SUDO killall mDNSResponderHelper 2>/dev/null || true + echo "macOS DNS 缓存已清理" +} + +flush_linux() { + local flushed=0 + + echo "Linux: 清理 DNS 缓存..." + + if command -v resolvectl &>/dev/null; then + if $SUDO resolvectl flush-caches 2>/dev/null; then + echo " - systemd-resolved (resolvectl) 缓存已清理" + flushed=1 + fi + elif command -v systemd-resolve &>/dev/null; then + if $SUDO systemd-resolve --flush-caches 2>/dev/null; then + echo " - systemd-resolved (systemd-resolve) 缓存已清理" + flushed=1 + fi + fi + + if command -v systemctl &>/dev/null; then + if systemctl is-active --quiet nscd 2>/dev/null; then + $SUDO systemctl restart nscd + echo " - nscd 已重启" + flushed=1 + fi + + if systemctl is-active --quiet dnsmasq 2>/dev/null; then + $SUDO systemctl restart dnsmasq + echo " - dnsmasq 已重启" + flushed=1 + fi + + if systemctl is-active --quiet unbound 2>/dev/null; then + if command -v rndc &>/dev/null; then + $SUDO rndc flush 2>/dev/null && echo " - unbound 缓存已清理" && flushed=1 + else + $SUDO systemctl restart unbound + echo " - unbound 已重启" + flushed=1 + fi + fi + fi + + if [ "$flushed" -eq 0 ]; then + echo "未检测到常见 DNS 缓存服务 (systemd-resolved / nscd / dnsmasq / unbound)" + echo "若仍有问题,可尝试重启 NetworkManager 或检查本地 DNS 配置" + exit 1 + fi + + echo "Linux DNS 缓存已清理" +} + +case "$(uname -s)" in + Darwin) flush_macos ;; + Linux) flush_linux ;; + *) + echo "错误: 不支持的操作系统: $(uname -s)" + exit 1 + ;; +esac diff --git a/script/docker-group.sh b/script/docker-group.sh new file mode 100755 index 0000000..efa3134 --- /dev/null +++ b/script/docker-group.sh @@ -0,0 +1,51 @@ +#!/bin/bash +# 将用户加入 docker 组(免 sudo 使用 docker) +# 用法: sudo docker-group +# 示例: sudo docker-group xuser + +set -euo pipefail + +usage() { + cat <<'EOF' +用法: sudo docker-group + +将 加入 docker 组,之后可直接运行 docker(无需 sudo)。 + +示例: + sudo docker-group xuser + +说明: + - 用户需重新登录或 newgrp docker 后生效 + - 安装 Docker 时 docker-install.sh 只会把当时的 $USER 加入组 +EOF + exit 1 +} + +[[ "${1:-}" == "-h" || "${1:-}" == "--help" ]] && usage +[[ $# -eq 1 ]] || usage + +if [[ $EUID -ne 0 ]]; then + echo "错误: 请使用 sudo 运行,例如: sudo $0 $1" + exit 1 +fi + +user="$1" + +if ! id "$user" &>/dev/null; then + echo "错误: 用户不存在: $user" + exit 1 +fi + +if ! getent group docker &>/dev/null; then + echo "错误: docker 组不存在,请先安装 Docker(例如运行 docker-install.sh)" + exit 1 +fi + +if id -nG "$user" | tr ' ' '\n' | grep -qx docker; then + echo "用户 $user 已在 docker 组中" + exit 0 +fi + +usermod -aG docker "$user" +echo "已将 $user 加入 docker 组" +echo "请让用户重新登录,或执行: su - $user -c 'newgrp docker'" diff --git a/script/nosudo.sh b/script/nosudo.sh new file mode 100755 index 0000000..a46b6c3 --- /dev/null +++ b/script/nosudo.sh @@ -0,0 +1,97 @@ +#!/bin/bash +# 为指定用户配置某命令的免密 sudo(写入 /etc/sudoers.d/) +# 用法: sudo nosudo +# 示例: sudo nosudo xuser docker + +set -euo pipefail + +usage() { + cat <<'EOF' +用法: sudo nosudo + +为 配置对 的免密 sudo(NOPASSWD)。 + +示例: + sudo nosudo xuser docker + sudo nosudo xuser /usr/bin/systemctl + +说明: + - 每个命令单独一个文件(nosudo--<命令名>),可多次执行、互不覆盖 + 例如 nosudo xuser kubectl 与 nosudo xuser helm 会各写一条规则,同时生效 + - 仅允许免密执行解析到的那个可执行文件路径(含子命令参数仍受 sudo 规则约束) + - 对 docker,更推荐: sudo docker-group (无需 sudo 即可用 docker) + - 删除规则: sudo rm /etc/sudoers.d/nosudo-- +EOF + exit 1 +} + +[[ "${1:-}" == "-h" || "${1:-}" == "--help" ]] && usage +[[ $# -eq 2 ]] || usage + +if [[ $EUID -ne 0 ]]; then + echo "错误: 请使用 sudo 运行,例如: sudo $0 $*" + exit 1 +fi + +if [[ ! -d /etc/sudoers.d ]]; then + echo "错误: 未找到 /etc/sudoers.d(当前系统可能不支持此方式)" + exit 1 +fi + +user="$1" +cmd="$2" + +if ! id "$user" &>/dev/null; then + echo "错误: 用户不存在: $user" + exit 1 +fi + +if [[ "$cmd" == /* ]]; then + cmd_path="$cmd" +else + cmd_path="$(command -v "$cmd" 2>/dev/null || true)" +fi + +if [[ -z "$cmd_path" || ! -x "$cmd_path" ]]; then + echo "错误: 找不到可执行文件: $cmd" + echo "提示: 可先安装该命令,或传入绝对路径,例如 /usr/bin/docker" + exit 1 +fi + +name="$(basename "$cmd_path")" +file="/etc/sudoers.d/nosudo-${user}-${name}" + +if [[ -f "$file" ]]; then + if grep -qF "NOPASSWD: ${cmd_path}" "$file" 2>/dev/null; then + echo "已存在相同规则: $file" + exit 0 + fi +fi + +tmp="$(mktemp)" +trap 'rm -f "$tmp"' EXIT + +cat >"$tmp" </dev/null; then + echo "错误: sudoers 语法校验失败" + visudo -cf "$tmp" || true + exit 1 +fi + +install -m 0440 -o root -g root "$tmp" "$file" + +echo "已写入: $file" +echo "内容: ${user} ALL=(ALL) NOPASSWD: ${cmd_path}" +echo "" +echo "验证(切换到该用户后):" +echo " sudo -u ${user} sudo -n ${cmd_path} version 2>/dev/null || sudo -u ${user} sudo -n ${cmd_path} --help | head -1" +if [[ "$name" == "docker" ]]; then + echo "" + echo "提示: 若希望直接运行 docker(不用 sudo),请执行:" + echo " sudo docker-group ${user}" +fi diff --git a/script/scan-ip.sh b/script/scan-ip.sh index 900078b..dd4c559 100755 --- a/script/scan-ip.sh +++ b/script/scan-ip.sh @@ -2,14 +2,40 @@ # 用法: # ./scan-ip.sh # ICMP 扫描本网段在线 IP +# ./scan-ip.sh 10.0.0 # ICMP 扫描指定网段(10.0.0.1-254) # ./scan-ip.sh 22 # 扫描本网段中 22 端口开放的 IP # ./scan-ip.sh 80,443,8080 # 扫描本网段中任一指定端口开放的 IP -# ./scan-ip.sh 22 10.0.0 # 指定网段前缀(不写则使用本机所在网段) +# ./scan-ip.sh 22 10.0.0 # 指定网段前缀 + 端口(顺序不限) +# ./scan-ip.sh 10.0.0 22 # 同上 -PORT_SPEC="$1" -PREFIX_ARG="$2" TIMEOUT=1 +is_port_spec() { + [[ "$1" =~ ^[0-9]+$ || "$1" =~ ^[0-9]+-[0-9]+$ || "$1" =~ ^[0-9,]+$ ]] +} + +is_ip_prefix() { + [[ "$1" =~ ^[0-9]{1,3}(\.[0-9]{1,3}){1,3}$ ]] +} + +PORT_SPEC="" +PREFIX_ARG="" + +if [ -n "$1" ] && is_ip_prefix "$1"; then + PREFIX_ARG="$1" + [ -n "$2" ] && is_port_spec "$2" && PORT_SPEC="$2" +elif [ -n "$1" ]; then + is_port_spec "$1" && PORT_SPEC="$1" + [ -n "$2" ] && is_ip_prefix "$2" && PREFIX_ARG="$2" +fi + +if [ -n "$1" ] && [ -z "$PORT_SPEC" ] && [ -z "$PREFIX_ARG" ]; then + echo "错误: 无法识别参数: $1" + echo "端口支持: 单端口(22) / 列表(80,443) / 范围(8000-8100)" + echo "网段支持: 前缀(10.0.0 或 192.168.1.0)" + exit 1 +fi + if command -v ip &>/dev/null; then LOCAL_IP=$(ip addr show | grep 'inet ' | grep -v '127.0.0.1' | awk '{print $2}' | cut -d/ -f1 | head -n 1) elif command -v ifconfig &>/dev/null; then diff --git a/zsh/.zshrc b/zsh/.zshrc index e4da1b1..4237513 100644 --- a/zsh/.zshrc +++ b/zsh/.zshrc @@ -14,5 +14,18 @@ else echo "[zsh] Oh My Zsh 未安装,请运行: bash ~/.dotfile/script/zsh-init.sh" >&2 fi +# 复制当前 git 分支名到剪贴板(macOS / Linux) +gitb() { + git branch --show-current | tr -d '\n' | clip-copy.sh +} + +# 复制当前目录到剪贴板(macOS / Linux) +pwdc() { + local dir + dir="$(pwd -P)" + printf '%s' "$dir" | clip-copy.sh || return 1 + echo "Copied: ${dir/#$HOME/~}" +} + # 本机私有配置(不纳入 dotfile) [[ -f "$HOME/.zshrc.local" ]] && source "$HOME/.zshrc.local"