ZSH 自动读取 macOS 系统代理配置并设置环境变量
和其它 Linux 的 DE 一样,macOS 也支持在「系统偏好设置」中设置 HTTP 代理、HTTPS 代理,但是 macOS 并不会在终端(Terminal、iTerm)的 shell 中自动生效系统代理配置。为了方便日常使用,我决定好好研究一下 macOS 的系统代理。
macOS 系统代理的行为
和 Linux 和 Windows 只有一种系统代理配置不同,macOS 为每一种网络设备(Wi-Fi、Ethernet、Bluetooth PAN 等)维护了独立的网络配置,包括代理配置在内。因此当切换网络设备时,macOS 会使用不同的代理配置;如果同时连接了多个网络设备,则操作系统会优先采用在「系统偏好设置」网络「Service Order」靠上的设备的代理配置:
和大部分 Linux 桌面环境一样,系统偏好设置中的代理设置在 shell session 中是不会生效的,在终端中使用代理、需要手动提供 HTTP_PROXY
、HTTPS_PROXY
和 ALL_PROXY
环境变量。
在终端获取 macOS 系统代理配置
macOS 内置了许多实用的命令行工具,如 xcode-select
用于安装命令行工具和配置 Xcode、build_webkit
用于编译 WebKit、softwareupdate
用于获取系统更新等。在读取系统代理配置方面,macOS 提供了三种方法:
$ system_profiler SPNetworkDataType # 获取完整网络配置信息
$ networksetup -listallnetworkservices # 列举所有网络设备
$ networksetup -getwebproxy Wi-Fi # 获取特定网络设备的系统代理配置
$ scutil --proxy # 获取当前已启用的代理配置,是对 system_profiler 的封装
其中,第三种命令的输出最简洁,适合在 shell 中解析:
$ scutil --proxy
<dictionary> {
ExceptionsList : <array> {
0 : 127.0.0.1
1 : 192.168.0.0/16
2 : 10.0.0.0/8
3 : 172.16.0.0/12
4 : 100.64.0.0/10
5 : 17.0.0.0/8
6 : localhost
7 : *.local
8 : 169.254.0.0/16
9 : 224.0.0.0/4
10 : 240.0.0.0/4
}
ExcludeSimpleHostnames : 1
HTTPEnable : 1
HTTPPort : 6152
HTTPProxy : 127.0.0.1
HTTPSEnable : 1
HTTPSPort : 6152
HTTPSProxy : 127.0.0.1
SOCKSEnable : 1
SOCKSPort : 6153
SOCKSProxy : 127.0.0.1
}
解析 scutil
输出
虽然使用 awk
可以轻易解析上述输出,但是正如我在「我就感觉到快 —— zsh 和 oh my zsh 冷启动速度优化」一文中所介绍的,应避免使用外部进程、尽可能使用 zsh 内置语法。而应对字符串操作,zsh 也已经绰绰有余了。
首先,为了获取代理配置不可避免的要生成一个 scutil
的子进程,为了避免子进程的反复生成,应该把输出缓存下来:
SCUTILS_PROXY=$(scutil --proxy)
接着判断代理是否启用。如果配置了代理,则 xxEnable :
的字段值为 1,反之则字段值为 0 或直接整个字段不存在,因此可以使用 zsh 字符串搜索语法搜索 xxEnable : 1
,以 HTTP 代理为例:
HTTP_PROXY_ENABLED_PATTERN="HTTPEnable : 1"
if (( $SCUTILS_PROXY[(I)$HTTP_PROXY_ENABLED_PATTERN] )); then
# HTTP 代理已启用
fi
(I)
是 zsh 中的字符串从右往左搜索的语法,返回值为找到匹配时的位置;当没有找到匹配时,zsh 会一路搜索到字符串最左侧、最终返回值是 0。因此(I)
常见的用法是配合数值条件(( ))
判断变量是否包含某一字符串,这种写法的性能是[[ ]]
的三倍。
接下来是获取代理的主机名和端口,HTTP 代理使用到的字段是 HTTPProxy
和 HTTPPort
。使用 zsh 的「左端最小匹配截断」语法截取 HTTPProxy
字段内容:
$ echo ${SCUTILS_PROXY#*HTTPProxy : }
127.0.0.1
HTTPSEnable : 1
HTTPSPort : 6152
HTTPSProxy : 127.0.0.1
SOCKSEnable : 1
SOCKSPort : 6153
SOCKSProxy : 127.0.0.1
}
#*
是「左端最小匹配截断」、##*
是「左端最大匹配截断」,此外还有%*
和%%*
,分别是「右端最小匹配截断」和「右端最大匹配截断」
接下来是使用 zsh 的多行字符串语法获取第一行内容、以将右端多余的内容略去:
$ echo ${${SCUTILS_PROXY#*HTTPProxy : }[(f)1]}
127.0.0.1
使用 zsh 内置的
(f)
flag 用于多行字符串的遍历和截断,比调用外部进程head
要快得多
用同样的方法获取端口字段的内容:
$ echo ${${SCUTILS_PROXY#*HTTPSPort : }[(f)1]}
6152
最后拼凑字段、添加到环境变量即可:
if (( $SCUTILS_PROXY[(I)$HTTP_PROXY_ENABLED_PATTERN] )); then
HTTP_PROXY_HOST=${${SCUTILS_PROXY#*HTTPProxy : }[(f)1]}
HTTP_PROXY_PORT=${${SCUTILS_PROXY#*HTTPSPort : }[(f)1]}
export http_proxy="http://${HTTP_PROXY_HOST}:${HTTP_PROXY_PORT}"
export HTTP_PROXY="${https_proxy}"
fi
同理,用相同的方法可以获取到其它代理服务器配置。将代码添加到 .zshrc
中,每次新建一个 shell session 时即可自动读取并添加相关环境变量。
zsh-osx-autoproxy
基于上述介绍的方法,我封装了一个 oh-my-zsh 插件 zsh-osx-autoproxy,启用插件后即可自动获取 HTTP 代理、HTTPS 代理、FTP 代理的设置,并在当前 shell 环境中添加相应环境变量(HTTP_PROXY
、HTTPS_PROXY
、ALL_PROXY
、FTP_PROXY
)。oh-my-zsh 用户可以通过下述命令完成安装:
git clone https://github.com/sukkaw/zsh-osx-autoproxy ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-osx-autoproxy
echo "plugins+=(zsh-osx-autoproxy)" | tee -a .zshrc
新建一个终端会话(或使用 source ~/.zshrc
) 即可生效。