Shell函数自动加载器开发指南:Bash、Zsh双终端支持+延迟加载实现

前言

获取~/.config/shell_functions目录下的所有 shell 文件中的函数,将函数注册到~/.bashrc~/.zshrc文件,方便终端使用

一、创建 shell 函数目录

1
mkdir -p ~/.config/shell_functions

二、添加功能函数

1. 添加函数加载器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
cat > ~/.config/shell_functions/function_loader.sh <<'EOF'
#!/bin/bash

# ====================== 函数自动加载器 ======================
# 文件位置: ~/.config/shell_functions/function_loader.sh

# 颜色定义
COLOR_RED='\033[0;31m'
COLOR_GREEN='\033[0;32m'
COLOR_YELLOW='\033[0;33m'
COLOR_BLUE='\033[0;34m'
COLOR_CYAN='\033[0;36m'
COLOR_RESET='\033[0m'

# 主加载函数
__load_shell_function() {
local func_name="$1"
local loaded_files=()

for func_file in ~/.config/shell_functions/*.sh; do
[[ "$func_file" == *"function_loader.sh" ]] && continue

if [[ -f "$func_file" ]]; then
if grep -q -E "^(function[[:space:]]+)?${func_name}[[:space:]]*[({]" "$func_file"; then
if [[ ! " ${loaded_files[*]} " =~ " ${func_file} " ]]; then
source "$func_file"
loaded_files+=("$func_file")
fi
return 0
fi
fi
done
if [ -z "$ZSH_VERSION" ]; then
# 如果没有找到函数实现,输出错误信息
echo -e "${COLOR_RED}错误: 未找到函数 ${func_name} 的实现${COLOR_RESET}" >&2
fi
return 1
}

# 安全提取函数名
__extract_function_name() {
local line="$1"
# 严格匹配函数定义行
if [ -n "$ZSH_VERSION" ]; then
if [[ "$line" =~ "^[[:space:]]*(function[[:space:]]+)?([a-zA-Z_][a-zA-Z0-9_]*)[[:space:]]*[\(\{]" ]]; then
echo "${match[2]}"
fi
else
if [[ "$line" =~ ^[[:space:]]*(function[[:space:]]+)?([a-zA-Z_][a-zA-Z0-9_]*)[[:space:]]*[\(\{] ]]; then
echo "${BASH_REMATCH[2]}"
fi
fi
}

# 动态发现所有可用函数
# 动态发现所有可用函数
__discover_shell_functions() {
# 使用普通数组代替关联数组
declare -a FUNC_CATEGORIES
declare -a SHELL_FUNCTIONS=()

for func_file in ~/.config/shell_functions/*.sh; do
[[ "$func_file" == *"function_loader.sh" ]] && continue

if [[ -f "$func_file" ]]; then
local category=$(basename "$func_file" .sh)
local functions_in_file=()

while IFS= read -r line; do
[[ "$line" =~ ^[[:space:]]*# ]] && continue
[[ -z "${line//[[:space:]]/}" ]] && continue

local func_name=$(__extract_function_name "$line")
[[ -n "$func_name" ]] && functions_in_file+=("$func_name")
done <"$func_file"

local public_functions=()
for func in $(printf "%s\n" "${functions_in_file[@]}" | sort -u); do
[[ "$func" != _* ]] && public_functions+=("$func")
done

if ((${#public_functions[@]} > 0)); then
FUNC_CATEGORIES+=("${category}:${public_functions[*]}")
SHELL_FUNCTIONS+=("${public_functions[@]}")
fi
fi
done

# 显示分类信息
if [ -z "$ZSH_VERSION" ]; then
echo -e "${COLOR_BLUE}\n可用命令分类:${COLOR_RESET}" >&2
for entry in "${FUNC_CATEGORIES[@]}"; do
local category="${entry%%:*}"
local functions="${entry#*:}"
echo -e "${COLOR_YELLOW}${category}:${COLOR_RESET}" >&2
echo "$functions" | tr ' ' '\n' | sed 's/^/ /' >&2
done
fi

printf "%s\n" "${SHELL_FUNCTIONS[@]}" | sort -u
}

# 安全创建函数桩
__create_function_stub() {
local func="$1"

# 验证函数名是否有效
if [[ "$func" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]] && [[ -n "$func" ]]; then
if ! declare -f "$func" >/dev/null 2>&1; then
eval "function ${func}() { __load_shell_function \"${func}\" && \"${func}\" \"\$@\"; }"
fi
else
if [ -z "$ZSH_VERSION" ]; then
echo -e "${COLOR_RED}警告: 跳过无效函数名 '${func}'${COLOR_RESET}" >&2
fi
fi
}

# 主初始化过程
function __init_function_loader() {
# 初始化数组
local all_functions=()

# 发现所有函数
while IFS= read -r func; do
all_functions+=("$func")
done < <(__discover_shell_functions)

# 为每个函数创建桩
for func in "${all_functions[@]}"; do
__create_function_stub "$func"
done

# 显示统计信息
if [ -z "$ZSH_VERSION" ]; then
echo -e "\n${COLOR_GREEN}已注册 ${#all_functions[@]} 个延迟加载函数${COLOR_RESET}" >&2
echo -e "${COLOR_CYAN}提示: 首次使用命令时会自动加载相应模块${COLOR_RESET}" >&2
fi
}

# 执行初始化
__init_function_loader

EOF

2. 添加功能函数

以添加 proxy_utils.sh 为例(该函数功能在于能够便捷地开关代理,其中的代理端口、测试地址等根据实际情况改写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
cat > ~/.config/shell_functions/proxy_utils.sh <<'EOF'
#!/bin/bash

# ====================== 代理控制模块 ======================
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color

# 配置区(修改这里适应你的环境)
PROXY_HOST="127.0.0.1"
HTTP_PORT="7897"
SOCKS_PORT="7897"
NO_PROXY="localhost,127.0.0.1,192.168.0.0/16,10.0.0.0/8,.internal,.local"
TEST_URL="https://www.google.com" # 用于测试代理的网站
DIRECT_TEST_URL="https://www.baidu.com" # 用于测试直连的网站
TIMEOUT=3 # 测试超时时间(秒)

# 网络测试函数
_test_connection() {
local url=$1
local proxy_type=$2
local proxy_addr=$3

echo -e "${CYAN}测试连接: $url ...${NC}"

local start_time=$(date +%s.%N)

if [ -z "$proxy_type" ]; then
# 直连测试
if curl -Is "$url" --connect-timeout $TIMEOUT &>/dev/null; then
local end_time=$(date +%s.%N)
local elapsed=$(printf "%.2f" $(echo "$end_time - $start_time" | bc))
echo -e "${GREEN}✓ 直连成功 (${elapsed}s)${NC}"
return 0
else
echo -e "${RED}✗ 直连失败${NC}"
return 1
fi
else
# 代理测试
case $proxy_type in
http)
if curl -Is "$url" --connect-timeout $TIMEOUT -x "$proxy_addr" &>/dev/null; then
local end_time=$(date +%s.%N)
local elapsed=$(printf "%.2f" $(echo "$end_time - $start_time" | bc))
echo -e "${GREEN}✓ HTTP代理成功 (${elapsed}s)${NC}"
return 0
else
echo -e "${RED}✗ HTTP代理失败${NC}"
return 1
fi
;;
socks5)
if curl -Is "$url" --connect-timeout $TIMEOUT --socks5-hostname "$proxy_addr" &>/dev/null; then
local end_time=$(date +%s.%N)
local elapsed=$(printf "%.2f" $(echo "$end_time - $start_time" | bc))
echo -e "${GREEN}✓ SOCKS5代理成功 (${elapsed}s)${NC}"
return 0
else
echo -e "${RED}✗ SOCKS5代理失败${NC}"
return 1
fi
;;
esac
fi
}

# 状态检测
_proxy_status_check() {
echo -e "${BLUE}====== 代理状态 ======${NC}"
[ -n "$http_proxy" ] && echo -e "HTTP 代理: ${GREEN}已启用${NC} ($http_proxy)" || echo -e "HTTP 代理: ${RED}未启用${NC}"
[ -n "$https_proxy" ] && echo -e "HTTPS 代理: ${GREEN}已启用${NC} ($https_proxy)" || echo -e "HTTPS 代理: ${RED}未启用${NC}"
[ -n "$all_proxy" ] && echo -e "SOCKS 代理: ${GREEN}已启用${NC} ($all_proxy)" || echo -e "SOCKS 代理: ${RED}未启用${NC}"
echo -e "${BLUE}=====================${NC}"
}

# 启用代理
proxy_on() {
export http_proxy="http://${PROXY_HOST}:${HTTP_PORT}"
export https_proxy="http://${PROXY_HOST}:${HTTP_PORT}"
export all_proxy="socks5://${PROXY_HOST}:${SOCKS_PORT}"
export no_proxy="$NO_PROXY"

echo -e "${YELLOW}正在启用代理并测试连接...${NC}"
echo -e "HTTP 代理: http://${PROXY_HOST}:${HTTP_PORT}"
echo -e "SOCKS5 代理: socks5://${PROXY_HOST}:${SOCKS_PORT}"

# 测试HTTP代理
_test_connection "$TEST_URL" "http" "${PROXY_HOST}:${HTTP_PORT}"

# 测试SOCKS5代理
_test_connection "$TEST_URL" "socks5" "${PROXY_HOST}:${SOCKS_PORT}"

_proxy_status_check
}

# 禁用代理
proxy_off() {
unset http_proxy https_proxy all_proxy no_proxy

echo -e "${YELLOW}正在关闭代理并测试直连...${NC}"

# 测试直连
_test_connection "$DIRECT_TEST_URL" ""

_proxy_status_check
}

# 状态切换
proxy_toggle() {
if [ -n "$http_proxy" ]; then
proxy_off
else
proxy_on
fi
}

# 帮助信息
proxy_help() {
echo -e "${YELLOW}代理快捷命令:${NC}"
echo -e "${YELLOW}当前配置:${NC}"
echo -e " 代理地址: ${CYAN}${PROXY_HOST}${NC}"
echo -e " HTTP端口: ${CYAN}${HTTP_PORT}${NC}"
echo -e " SOCKS端口: ${CYAN}${SOCKS_PORT}${NC}"
echo -e " 测试URL: ${CYAN}${TEST_URL}${NC}"
echo -e " 直连测试URL: ${CYAN}${DIRECT_TEST_URL}${NC}"
}
EOF

三、添加通用配置文件

可能会同时使用 zsh 或者 bash,所以将配置文件作为通用配置文件进行加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 写入通用配置内容
cat > ~/.config/shell_common <<'EOF'
# ====================== Shell 函数加载 ======================
# 加载函数自动加载器
if [ -f ~/.config/shell_functions/function_loader.sh ]; then
source ~/.config/shell_functions/function_loader.sh
fi

# 禁用危险操作
alias rm='echo "Use trash-put instead!"; false'
alias rmdir='echo "Protected directory!"; false'

# 保留逃生通道
alias real-rm='/bin/rm'
alias real-rmdir='/bin/rmdir'
EOF

四、修改并更新~/.bashrc文件

1. 修改bashrc

1
2
3
cat >> ~/.bashrc <<'EOF'
[ -f ~/.config/shell_common ] && source ~/.config/shell_common
EOF

2. 更新bashrc

1
source ~/.bashrc

五、修改并更新~/.zshrc文件

1. 修改zshrc

1
2
3
cat >> ~/.zshrc <<'EOF'
[ -f ~/.config/shell_common ] && source ~/.config/shell_common
EOF

2. 更新zshrc

1
source ~/.zshrc

六、测试函数

在 bash 终端和 zsh 终端分别执行以下命令测试:

1
2
3
4
5
6
# 开启代理
proxy_on
# 关闭代理
proxy_off
# 切换代理状态
proxy_toggle

Shell函数自动加载器开发指南:Bash、Zsh双终端支持+延迟加载实现
https://blog.echo-silence.top/posts/ca15a280.html
作者
极客奶爸
发布于
2025年4月19日
许可协议