跳到主要内容

· 阅读需 3 分钟

前言

Zerotier 使用体验并不好,配置复杂,P2P 打洞耗时长,成功率低。最近体验了一下 Tailscale,感觉不错。

Tailscale 客户端配置简单;子网路由更简单,不需要配置客户端防火墙;打洞速度快,成功率高。

但 Tailscale 也有缺点:

  1. 免费版限制 20 台设备,1 个子网路由;
  2. 不能自定义虚拟网络网段。

官网注册

Tailscale 1 官方网站可以注册并登录。登录之后可以看到网页控制台。

接入客户端

Tailscale 下载页面 2 可以下载各个平台的客户端。支持 macOS、iOS、Windows、Linux 和 Android。

使用客户端登录同一账号之后即可加入网络。

或者是在网页控制台中的设置里,找到「Keys」->「Auth keys」,创建一个有时限的密钥,客户端可以凭此密钥登录,而不用使用账号登录。

此方法适用于命令行界面,用法如下。

sudo tailscale up --authkey tskey-abcdef1432341818

子网路由

客户端可以将自己所在局域网暴露给其他客户端。

sudo tailscale up --advertise-routes=192.168.123.0/24

之后在网页控制台中开启该客户端建议的子网路由即可。其它客户端就能直接访问该局域网的资源,就跟物理连接在该局域网一样。

开源替代品

headscale 3 是 Tailscale 服务端的开源实现。

headscale 兼容 Tailscale 的客户端,除了 iOS 设备之外,你可以使用 Tailscale 的客户端连接上 headscale 控制器。

参考资料

· 阅读需 2 分钟

前言

在使用爱国上网的过程中,IP 地址经常在不同国家间变动。默认情况下 Google 搜索的结果会偏好当前 IP 属地的语言,这就导致在使用日本 IP 时会有一堆日文结果看不懂。

你可以在搜索参数中加上gws_rd=cr来组织这样的默认行为,并且加上gl=cn来强制使用中文。

使用

搜索时使用如下请求,%s替换为搜索词。gl参数可以改为任意国家代码。

https://www.google.com/search?q=%s&pws=0&gl=cn&gws_rd=cr

在 Chrome 中设置

在 Chrome 搜索引擎的设置中添加一个新的搜索引擎,然后将其设为默认搜索引擎。

image-20220703002722383

参考资料

· 阅读需 1 分钟

前言

某些情况下路由器会丢弃长时间不活跃的 TCP 连接,通过正确配置可以防止 SSH 连接闲置后意外被断开。

可以配置客户端或者服务端定期发送心跳包来保持连接活跃;选择其一配置即可。

客户端配置

修改~/.ssh/config文件。

Host *
ServerAliveInterval 60
ServerAliveCountMax 5

服务端配置

修改/etc/ssh/sshd_config文件。

ClientAliveInterval 60
ClientAliveCountMax 5

重启 SSH 服务。

systemctl restart sshd

参考资料

· 阅读需 3 分钟

前言

OpenWrt 是一个用于嵌入式设备的 Linux 发行版,通常作为路由器的操作系统。

OpenWrt_CN

OpenWrt_CN 1 中收录了一些适合国内用户的发行版。

我主要在虚拟机中使用 OpenWrt,所以选择了 ImmortalWrt 2,它的优点是简洁,出厂附带的软件包非常少,内置软件源和该发行版对齐发行,比较稳定;并且我常用的软件都能在软件源中找到。

安装

更改局域网 IP 地址

使用下面的命令编辑网络配置文件,修改其中br-lan的部分。

vi /etc/config/network

使用下面的命令重启网络服务。

service restart network

更多帮助可以参考 官方文档 3

包管理器 opkg

OpenWrt 使用 opkg 4 作为包管理器。

opkg 软件源配置在/etc/opkg/distfeeds.conf/etc/opkg/customfeeds.conf中;设置官方软件源的镜像设置前者即可。

下面是 opkg 常用的命令。

opkg update
opkg upgrade <>
opkg install <>
opkg remove <>
opkg list <>
opkg list-installed
opkg list-upgradable
opkg info <>

安装软件示例

某些时候连接不上官方软件源,需要爱国上网;可以使用 SSH 把本地的代理端口带过去。

ssh root@192.168.233.4 -R 7890:localhost:7890

之后在 SSH 会话中设置代理。

export https_proxy=http://127.0.0.1:7890 http_proxy=http://127.0.0.1:7890 all_proxy=socks5://127.0.0.1:7890

下面的命令用于安装一些常用软件。

# 更新索引
opkg update

# openclash
opkg install luci-app-openclash

# zerotier
opkg install luci-i18n-zerotier-zh-cn

# tailscale
opkg install tailscale

# wireguard
# 安装完之后重启才能生效
opkg install luci-proto-wireguard luci-i18n-wireguard-zh-cn

· 阅读需 4 分钟

前言

通过 libpam-google-authenticator 为 SSH 启用多重要素验证,在使用密码或者密钥登录之后还要输入一个基于时间变化的密码才能登录,增加服务器安全性。

本文中使用 Debian 11 系统作为例子。

安装 libpam-google-authenticator

安装 libpam-google-authenticator。

apt update
apt install libpam-google-authenticator -y

运行设置向导。

google-authenticator

记住 secret key,导入到支持 TOTP 的软件里生成密码。

Do you want authentication tokens to be time-based (y/n) y
Your new secret key is: ****************

在下一步中验证密码,密码是从密码生成器中得到的。

Enter code from app (-1 to skip): *******

下面有备用密码,记录下来,当你的密码生成器丢失的时候有用,每个密码可以用一次。

Your emergency scratch codes are:
********
********
********
********
********

更新配置文件。

Do you want me to update your "/root/.google_authenticator" file? (y/n) y

设置最大时间误差 30 秒,默认可以使用前一个、现在的和后一个验证码通过认证。

By default, a new token is generated every 30 seconds by the mobile app.
In order to compensate for possible time-skew between the client and the server,
we allow an extra token before and after the current time. This allows for a
time skew of up to 30 seconds between authentication server and client. If you
experience problems with poor time synchronization, you can increase the window
from its default size of 3 permitted codes (one previous code, the current
code, the next code) to 17 permitted codes (the 8 previous codes, the current
code, and the 8 next codes). This will permit for a time skew of up to 4 minutes
between client and server.
Do you want to do so? (y/n) y

设置每 30 秒最多重试 3 次。

If the computer that you are logging into isn't hardened against brute-force
login attempts, you can enable rate-limiting for the authentication module.
By default, this limits attackers to no more than 3 login attempts every 30s.
Do you want to enable rate-limiting? (y/n) y

配置 SSH 服务器

修改/etc/pam.d/sshd文件。

nano /etc/pam.d/sshd

注释掉@include common-auth,并加入新行。

# Standard Un*x authentication.
# @include common-auth
auth required pam_google_authenticator.so

修改/etc/ssh/sshd_config文件。

nano /etc/ssh/sshd_config

修改以下几项。其中AuthenticationMethods指定了先使用密钥方式登录,再要求验证额外密码。

ChallengeResponseAuthentication yes
PasswordAuthentication no
AuthenticationMethods publickey,keyboard-interactive

重启 ssh 服务器。

systemctl restart sshd

SSH 服务器启动错误排查

首先停止 sshd 服务。

systemctl stop sshd

手动启动 sshd,指定-d参数,该参数会让 sshd 打印出更多错误信息。

/usr/bin/sshd -d

根据打印出的信息排查错误。错误修复之后再通过服务启动 sshd。

systemctl start sshd

参考资料

· 阅读需 3 分钟

前言

SSH 除了能连接远程服务器外,还可以做端口转发的工作,并且利用安全隧道来保障通信安全。

动态转发

动态转发指的是 SSH 在本机暴露一个 SOCKS5 端口,通过此端口进行的所有通信都转发到远程服务器,通过远程服务器访问外网。通过这种方式可以实现一个简易代理。

ssh -D local-port tunnel-host -N
  • -D,动态转发;
  • -N,不打开 Shell,只进行端口转发。

下面这条命令的用途是通过本机2121端口的流量都会被转发到tunnel-host,然后通过tunnel-host再发送该流量。

ssh -D 2121 tunnel-host -N

可以写在 ssh_config(~/.ssh/config)中。

DynamicForward <tunnel-host>:<local-port>

本地转发

本地转发指的是 SSH 在本机暴露一个端口,通过此端口的请求都会转发到远程服务器,然后远程服务器会将请求转发到目标服务器的目标端口。

ssh -L local-port:target-host:target-port tunnel-host

下面这条命令的用途是,当访问本地2121端口时,流量会被转发到tunnel-host,再由tunnel-host把流量发送到www.example.com:80

ssh -L 2121:www.example.com:80 tunnel-host -N

可以写在 ssh_config(~/.ssh/config)中。

Host test.example.com
LocalForward client-IP:client-port server-IP:server-port

远程转发

远程转发指的是在远程主机监听某个端口,通过该端口的流量都转发到本机,再通过本机发送该流量。

ssh -R remote-port:target-host:target-port -N remotehost

下面这条命令的作用是通过远程主机8080端口的流量都转发到本地主机的80端口。

ssh -R 8080:localhost:80 -N my.public.server

可以写在 ssh_config(~/.ssh/config)中。

Host remote-forward
HostName test.example.com
RemoteForward remote-port target-host:target-port

参考资料

· 阅读需 4 分钟

前言

SDKMAN 是一系列 Bash 脚本,可以用来管理各个版本的 JDK。在 M1 芯片的 Mac 中可以很方便地安装 ARM 版本的 JDK,并在各个版本之间切换。

SDKMAN 也可以用来安装其他工具,如 Maven、Gradle 等,但是目前我用不上,我只用它来管理 JDK。

安装

SDKMAN 1 是一系列 Bash 脚本,所以有 Bash 环境的系统就能运行 SDKMAN,比如 Linux、macOS。

执行下面的安装脚本来安装 SDKMAN。改脚本会把 SDKMAN 的相关脚本下载至~/.sdkman文件夹中。之后它会修改你的 Shell 启动文件,将 SDKMAN 的初始化命令放入其中。

curl -s "https://get.sdkman.io" | bash

如果你的 Shell 启动文件(.zshrc/.bashrc/...)没有被附加 SDKMAN 的初始化命令,你可以手动放入以下命令。

source "$HOME/.sdkman/bin/sdkman-init.sh"

使用

重新打开一个新的 Shell 会话,如果你正确完成了安装过程,此时你可以执行命令sdk

# 列出可以安装的 JDK 发行版
sdk ls java
================================================================================
Available Java Versions for macOS ARM 64bit
================================================================================
Vendor | Use | Version | Dist | Status | Identifier
--------------------------------------------------------------------------------
Corretto | | 18.0.1 | amzn | | 18.0.1-amzn
| | 17.0.3.6.1 | amzn | | 17.0.3.6.1-amzn
| | 11.0.15.9.1 | amzn | | 11.0.15.9.1-amzn
| | 8.332.08.1 | amzn | | 8.332.08.1-amzn
Gluon | | 22.1.0.1.r17 | gln | | 22.1.0.1.r17-gln
| | 22.1.0.1.r11 | gln | | 22.1.0.1.r11-gln
GraalVM | | 22.1.0.r17 | grl | installed | 22.1.0.r17-grl
| | 22.1.0.r11 | grl | | 22.1.0.r11-grl
Java.net | | 20.ea.4 | open | | 20.ea.4-open
| | 20.ea.3 | open | | 20.ea.3-open
| | 19.ea.29 | open | | 19.ea.29-open
| | 19.ea.28 | open | | 19.ea.28-open
| | 18.0.1.1 | open | | 18.0.1.1-open
Liberica | | 18.0.1.1.fx | librca | | 18.0.1.1.fx-librca
| | 18.0.1.1 | librca | | 18.0.1.1-librca
| | 17.0.3.1.fx | librca | | 17.0.3.1.fx-librca
| | 17.0.3.1 | librca | | 17.0.3.1-librca
| | 11.0.15.1.fx | librca | | 11.0.15.1.fx-librca
| | 11.0.15.1 | librca | | 11.0.15.1-librca
| | 8.0.333.fx | librca | | 8.0.333.fx-librca
| | 8.0.333 | librca | | 8.0.333-librca
Microsoft | | 17.0.3 | ms | | 17.0.3-ms
| | 11.0.15 | ms | | 11.0.15-ms
Oracle | | 18.0.1 | oracle | | 18.0.1-oracle
| | 17.0.3 | oracle | | 17.0.3-oracle
SapMachine | | 18.0.1.1 | sapmchn | | 18.0.1.1-sapmchn
| | 17.0.3 | sapmchn | | 17.0.3-sapmchn
| | 17.0.3.0.1 | sapmchn | | 17.0.3.0.1-sapmchn
| | 17.0.2 | sapmchn | | 17.0.2-sapmchn
Semeru | | 18.0.1.1 | sem | | 18.0.1.1-sem
| | 17.0.3 | sem | | 17.0.3-sem
| | 11.0.15 | sem | | 11.0.15-sem
Temurin | | 18.0.1 | tem | | 18.0.1-tem
| | 17.0.3 | tem | | 17.0.3-tem
| | 11.0.15 | tem | | 11.0.15-tem
Zulu | | 18.0.1 | zulu | | 18.0.1-zulu
| | 18.0.1.fx | zulu | | 18.0.1.fx-zulu
| | 17.0.3 | zulu | installed | 17.0.3-zulu
| | 17.0.3.fx | zulu | | 17.0.3.fx-zulu
| | 11.0.15 | zulu | installed | 11.0.15-zulu
| | 11.0.15.fx | zulu | | 11.0.15.fx-zulu
| >>> | 8.0.332 | zulu | installed | 8.0.332-zulu
| | 8.0.332.fx | zulu | | 8.0.332.fx-zulu
================================================================================
Omit Identifier to install default version 17.0.3-tem:
$ sdk install java
Use TAB completion to discover available versions
$ sdk install java [TAB]
Or install a specific version by Identifier:
$ sdk install java 17.0.3-tem
Hit Q to exit this list view
================================================================================
(END)
# 安装 zulu jdk 8.0.332
sdk install java 8.0.332-zulu
# 把 8.0.332-zulu 加入环境变量
sdk use java 8.0.332-zulu
# 查看现在使用的哪个 JDK
sdk current java
# 卸载 8.0.332-zulu
sdk uninstall java 8.0.332-zulu

· 阅读需 8 分钟

前言

之前使用 ohmyzsh 作为 zsh 的插件管理器,但是在使用过程中发现启动速度太慢了。在终端启动后仍需花费 1 至 2 秒才能看见命令提示符,并且我开启的插件数量也不多,没有什么优化思路。

近期我寻找到 ohmyzsh 的替代品 zinit,在经过一番体验之后感觉良好,启动速度非常快,并且我之前在 ohmyzsh 中需要的功能都能实现。

备份 Shell 启动文件

折腾之前先备份一下目前的 Shell 启动文件,如.zshrc.zprofile文件,在折腾坏了之后能快速恢复。

在这两个文件备份完毕之后,清空这两个文件的内容。

安装 zinit

在 zsh 中执行下面的命令来下载 zinit 1 的安装脚本并执行。

bash -c "$(curl --fail --show-error --silent --location https://raw.githubusercontent.com/zdharma-continuum/zinit/HEAD/scripts/install.sh)"

这个安装脚本会克隆 zinit 的仓库至~/.local/share/zinit/zinit.git;并且更新你的.zshrc文件,在其中附加上 zinit 的启动命令,以下内容是安装脚本放入.zshrc文件的内容。

# ~/.zshrc

### Added by Zinit's installer
source "$HOME/.local/share/zinit/zinit.git/zinit.zsh"
autoload -Uz _zinit
(( ${+_comps} )) && _comps[zinit]=_zinit
### End of Zinit's installer chunk

之后打开一个新的 zsh 会话,zinit 就生效了。

在以后的使用过程中可以使用下面的命令来更新 zinit。

zinit self-update

安装插件

zinit 引入插件的语法和 ohmyzsh 的语法不一样。

ohmyzsh 在引入自带插件的时候只需直接声明在plugins变量中即可;在引入第三方插件的时候首先需要自己下载插件到~/.oh-my-zsh/custom/plugins目录中,然后再在plugins中声明。

zinit 在引入插件的时候不需要自己手动下载,通过zinit命令声明之后在下次 zinit 加载之后自动下载插件。

zinit 引入插件的语法有两种。确保这些命令处于 zinit 初始化命令之后即可。

  • 通过load加载的插件会启用分析功能,你可以通过zinit report [plugin]命令来查看插件的加载过程;
  • 通过light加载的插件不启用分析功能,性能比load好。

其中repo/plugin代表了 Github 仓库的用户名和仓库名。

zinit load  <repo/plugin> # Load with reporting/investigating.
zinit light <repo/plugin> # Load without reporting/investigating.

这是我加载的一些插件。

其中,zdharma-continuum/fast-syntax-highlighting确实比 oh-my-posh 中的zsh-syntax-highlighting流畅不少。

# ~/.zshrc

zinit light zdharma-continuum/fast-syntax-highlighting
zinit light zsh-users/zsh-completions
zinit light zsh-users/zsh-autosuggestions
zinit light sunlei/zsh-ssh
zinit light ael-code/zsh-colored-man-pages
zinit light MichaelAquilina/zsh-you-should-use

你还可以通过snippet命令来加载一个或多个脚本文件。使用这个命令可以加载 ohmyzsh 仓库中的插件,因为这些插件处于仓库的子目录中。

在 snippet 命令之后你可以加载本地或远程的脚本,直接写本地文件地址或 URL 即可。

对于常用的仓库你还可以使用别名,可以使用的别名有:

  • PZT::,Prezto
  • PZTM::,Prezto module
  • OMZ::,Oh My Zsh
  • OMZP::,OMZ plugin
  • OMZL::,OMZ library
  • OMZT::,OMZ theme

下面是我从 ohmyzsh 中加载的插件。

# ~/.zshrc

zinit snippet OMZP::safe-paste
zinit snippet OMZP::sudo

某些插件不是单文件的,在使用过程中还需要加载其它文件,此时你就需要加载多个文件。

你可以在zinit snippet命令之前使用zinit ice命令。zinit ice命令是对下一句zinit命令的描述,只对下一句命令生效。zinit ice不仅仅可以用来描述zinit snippet命令。

比如在加载插件z的时候,除了加载z.plugin.zsh之外,该文件还加载了z.sh,此时就需要把整个子目录下载到本地。

使用zinit ice svn表示下一句zinit命令使用 svn 下载整个子文件夹到本地,Github 兼容 svn 协议。这样在使用 z 插件的时候就不会出错了。

# ~/.zshrc

zinit ice svn
zinit snippet OMZP::z

使用下面的命令来更新插件。

zinit update --parallel

安装主题

这里我使用了 powerlevel10k2 主题,原因就是它启动非常快。直接使用 zinit 加载主题。

其中zinit ice depth"1"中的depth是传递给git clone的参数。

# ~/.zshrc

zinit ice depth"1"
zinit light romkatv/powerlevel10k

重新开启一个 zsh 会话你就可以看到 powerlevel10k 的初始化向导。根据向导选择你喜欢的样式即可,该向导会自动修改你的.zshrc文件,加入相关的启动命令。

如果 powerlevel10k 的初始化向导没有出现或者你想重新配置 powerlevel10k,运行以下命令即可。

p10k configure

其他优化思路

除了替换掉 ohmyzsh 之外,还有很多优化 zsh 启动时间的思路。

如果你正在使用 nvm 作为你的 Node.js 版本管理工具,你可以考虑停止使用 nvm,寻找其他替代品代替它,因为它实在是太影响启动速度了,你可以使用 fnm,兼容.nvmrc配置文件;

在 Shell 启动文件中尽量不要执行外部命令,如brew --prefix命令,因为你不会经常更改 Homebrew 的安装位置,所以直接使用brew --prefix的结果替换掉该命令。

尽量少使用eval命令,比如在引入 Homebrew 的时候会使用eval "$(/opt/homebrew/bin/brew shellenv)",你可以直接将/opt/homebrew/bin/brew shellenv命令的结果写入启动文件而不是使用eval命令。比如使用下面的命令替代eval命令。

export HOMEBREW_PREFIX="/opt/homebrew";
export HOMEBREW_CELLAR="/opt/homebrew/Cellar";
export HOMEBREW_REPOSITORY="/opt/homebrew";
export PATH="/opt/homebrew/bin:/opt/homebrew/sbin${PATH+:$PATH}";
export MANPATH="/opt/homebrew/share/man${MANPATH+:$MANPATH}:";
export INFOPATH="/opt/homebrew/share/info:${INFOPATH:-}";
FPATH="/opt/homebrew/share/zsh/site-functions:${FPATH}"

前后对比

使用time命令对比一下分别使用 ohmyzsh 和使用 zinit 的 zsh 加载速度。

# ohmyzsh
/usr/bin/time zsh -i -c exit
0.37 real 0.18 user 0.12 sys

# zinit
/usr/bin/time zsh -i -c exit
0.12 real 0.07 user 0.03 sys

最终发现两者实际差距并不大,提升体验大部分是 powerlevel10k 主题的功劳,你在启动 zsh 之后的瞬间就能看见命令提示符。

zinit 还可以配置 Turbo Mode,可以让插件懒加载,对于加载时间长的插件非常有用。不过我在使用中并没有配置懒加载,即使这样,zinit 的加载速度还是比 ohmyzsh 快的。

暂时就先用 zinit 和 powerlevel10k 主题的组合吧,感觉不错。

参考资料

· 阅读需 2 分钟

前言

折腾 Shell 的时候搞坏了 PATH 环境变量,导致所有命令都找不到。

临时修复

在 Shell 中执行以下命令来在此次会话中设置 PATH。这样一些基础工具就能工作了。

export PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin

排查错误

从 Shell 启动文件中排查错误,看看最近修改了什么,以及什么命令可能会影响到 PATH 变量。

如果你使用 Bash,你可能需要排查下面的文件。

  • /etc/profile
  • /etc/bash.bashrc
  • /etc/profile.d/*
  • ~/.profile
  • ~/.bashrc

如果你使用 Zsh,你可能需要排查下面的文件。

  • /etc/zprofile
  • /etc/zshrc
  • ~/.zprofile
  • ~/.zshrc

参考资料

· 阅读需 3 分钟

前言

最近发现 zsh 启动速度慢,每次启动后要过 2 秒左右才完全启动。虽然在完全启动之前也可以输入命令,但是还是让人不爽。

使用time命令

time zsh -i -c exit
zsh -i -c exit  0.36s user 0.25s system 88% cpu 0.688 total

使用zprof模块

~/.zshrc文件中的开头写入zmodload zsh/zprof,在结尾写入zprof,启动 zsh 之后就能看到哪些加载项影响了启动速度。

# ~/.zshrc

# 首行加入
zmodload zsh/zprof

# 中间省略
# ...

# 末行加入
zprof
num  calls                time                       self            name
-----------------------------------------------------------------------------------
1) 1 91.87 91.87 38.76% 78.87 78.87 33.28% nvm_auto
2) 1 60.75 60.75 25.63% 60.75 60.75 25.63% is_update_available
3) 2 22.79 11.39 9.62% 22.79 11.39 9.62% compaudit
4) 2 80.84 40.42 34.11% 20.08 10.04 8.47% (anon)
5) 1 35.70 35.70 15.06% 12.91 12.91 5.45% compinit
6) 1 10.37 10.37 4.38% 10.28 10.28 4.34% _zsh_highlight_load_highlighters
7) 1 13.00 13.00 5.48% 7.54 7.54 3.18% nvm_rc_version
8) 1 5.38 5.38 2.27% 5.38 5.38 2.27% nvm_echo
9) 1 5.05 5.05 2.13% 5.05 5.05 2.13% _zsh_highlight_bind_widgets
10) 1 4.88 4.88 2.06% 4.88 4.88 2.06% __sdkman_export_candidate_home
11) 1 3.86 3.86 1.63% 3.86 3.86 1.63% __sdkman_prepend_candidate_to_path
12) 1 1.36 1.36 0.57% 1.36 1.36 0.57% regexp-replace
13) 1 0.75 0.75 0.32% 0.75 0.75 0.32% colors
14) 7 0.65 0.09 0.28% 0.65 0.09 0.28% add-zsh-hook
15) 3 0.52 0.17 0.22% 0.52 0.17 0.22% bashcompinit
16) 4 0.51 0.13 0.21% 0.51 0.13 0.21% is-at-least
17) 4 0.28 0.07 0.12% 0.28 0.07 0.12% compdef
18) 2 0.30 0.15 0.13% 0.15 0.08 0.06% complete
19) 8 0.12 0.02 0.05% 0.12 0.02 0.05% is_plugin
20) 1 5.46 5.46 2.30% 0.07 0.07 0.03% nvm_err
21) 2 0.05 0.02 0.02% 0.05 0.02 0.02% is_theme
22) 1 91.91 91.91 38.78% 0.04 0.04 0.02% nvm_process_parameters
23) 2 0.03 0.02 0.01% 0.03 0.02 0.01% env_default
24) 2 0.02 0.01 0.01% 0.02 0.01 0.01% __sdkman_echo_debug
25) 1 0.02 0.02 0.01% 0.02 0.02 0.01% detect-clipboard
26) 1 0.01 0.01 0.01% 0.01 0.01 0.01% nvm_is_zsh

参考资料