Linux环境变量与Shell加载机制深度解析
1. 项目概述为什么搞懂环境变量和 Shell 是 Linux 生存的第一课在 Linux 系统里你敲下的每一个命令——ls、python3、git commit甚至只是按 Tab 键自动补全路径——背后都有一套看不见却无处不在的“空气系统”在默默支撑环境变量variabel lingkungan和Shell 解释器。它们不是某个高级功能的附属品而是整个用户态操作系统的呼吸中枢。我带过几十期 Linux 实操训练营发现一个铁律90% 的新手卡点根本不是语法写错而是command not found却死活找不到 PATH 在哪改是ModuleNotFoundError却不知道 PYTHONPATH 没生效是脚本在终端能跑双击桌面图标就报错——全因环境变量在不同 Shell 启动方式下加载逻辑完全不同。标题里这句印尼语 “Cara Membaca dan Mengatur Variabel Lingkungan dan Shell pada Linux”直译是“Linux 下读取与配置环境变量及 Shell 的方法”但它的真正分量是这是你从“会用 Linux”跃升到“真正掌控 Linux”的临界点。它不涉及内核编译也不需要写驱动但一旦吃透你就能一眼看穿adb shell sh /sdcard/android/data/com.omarea.vtools/up.sh为何能绕过常规权限限制明白.env python文件为何在虚拟环境中优先级高于系统全局设置也能立刻判断mvn -t the java_home environment variable is not defined correctly这类报错该去/etc/profile还是~/.bashrc里修。这不是命令记忆题而是一套操作系统级的“上下文感知能力”。本文不堆砌env和printenv的 man 手册原文而是带你像拆解一台机械表一样一层层拨开 Shell 启动时的变量加载链路实测每一步的输出差异标注哪些修改立即生效、哪些必须重启终端、哪些甚至要登出重登录——所有结论都来自我在 Ubuntu 22.04、CentOS 7、Kali 2023 和国产麒麟 V10 上反复验证的现场记录。2. 核心机制拆解Shell 启动时的环境变量加载链路图谱2.1 Shell 的两种本质身份登录 Shell 与非登录 Shell很多教程一上来就教export VARvalue却从不解释为什么你在终端里执行了 export新开一个终端又没了根本原因在于 Shell 有两种启动模式它们加载配置文件的路径完全独立。这不是 Linux 的 bug而是精心设计的安全隔离机制。登录 ShellLogin Shell指你通过 SSH 远程登录、或在图形界面中打开终端后首次输入用户名密码进入的 Shell。它的核心任务是“初始化用户工作环境”因此会严格按顺序读取一系列全局和用户级配置文件。典型触发场景ssh userhost、CtrlAltF2切换到 TTY 登录、GNOME Terminal 首次启动取决于终端模拟器设置。非登录 ShellNon-login Shell指在已有会话中新开的子 Shell比如在终端里再执行bash、运行一个 Shell 脚本如./deploy.sh、或 IDE 内置终端。它的设计哲学是“轻量继承”只加载最精简的配置以保证执行效率。提示用shopt login_shell命令可实时查看当前 Shell 是否为登录 Shell。返回login_shell on即为登录 Shelloff则为非登录 Shell。这个命令本身就能帮你快速定位问题根源——比如你发现~/.bashrc里的 alias 不生效先运行它如果显示off那问题必然出在非登录 Shell 的加载逻辑上。2.2 登录 Shell 的四级加载链路从系统到用户的完整传递登录 Shell 的配置加载不是随机的而是一条有严格先后顺序、支持覆盖的“信任链”。我把它拆解为四个层级每一层都可能被下一层覆盖层级文件路径加载时机关键特性实操影响L1系统级全局配置/etc/profile登录 Shell 启动时最先读取所有用户共享通常设置PATH、umask等基础变量修改此处会影响所有用户需sudo权限但普通用户无法编辑故日常配置应避开此层L2系统级扩展配置/etc/profile.d/*.sh/etc/profile执行末尾for循环调用模块化设计各软件包如 Java、Python可独立安装自己的.sh文件例如conda安装后会在/etc/profile.d/conda.sh中写入初始化代码这就是conda env能全局生效的底层原因L3用户级主配置~/.bash_profile或~/.profileL1/L2 执行完毕后若存在则加载用户专属优先级高于 L1/L2~/.bash_profile优先于~/.profile关键避坑点很多国产 Linux 发行版如麒麟、UOS默认只创建~/.profile而用户误以为该改~/.bashrc导致环境变量不生效L4用户级交互配置~/.bashrc仅当 Shell 为交互式interactive且非登录时才加载包含alias、function、PS1提示符等不包含export全局变量这是最大误区来源~/.bashrc里的export JAVA_HOME/usr/lib/jvm/java-11-openjdk-amd64在登录 Shell 中根本不会执行除非你在~/.bash_profile里显式source ~/.bashrc我曾在 Kali Linux 上复现过一个经典故障用户安装完 CUDA 11.3 后在~/.bashrc里添加了export LD_LIBRARY_PATH/usr/local/cuda-11.3/lib64:$LD_LIBRARY_PATH结果nvidia-smi正常但nvcc --version报错。原因正是 Kali 默认使用~/.bash_profile作为登录 Shell 主配置而~/.bashrc未被 source。解决方案不是删掉~/.bashrc而是在~/.bash_profile末尾追加一行[ -f ~/.bashrc ] source ~/.bashrc。这个逻辑确保只有~/.bashrc存在时才加载避免脚本错误。2.3 非登录 Shell 的极简加载逻辑为什么脚本里要手动 source非登录 Shell如执行./script.sh的加载逻辑极其精简它默认不读取任何配置文件完全继承父进程的环境变量。这意味着如果你在登录 Shell 中已通过export设置了MY_VARhello那么./script.sh里echo $MY_VAR一定能输出hello但如果你在~/.bashrc里写了export MY_VARworld而该文件未被登录 Shell 加载如前述麒麟系统未 source那么./script.sh继承的是空值更危险的是某些脚本如up.sh会显式指定解释器#!/bin/sh此时它启动的是 POSIX Shell而非 Bash~/.bashrc对它完全无效。注意adb shell sh /sdcard/android/data/com.omarea.vtools/up.sh这个命令链里adb shell启动的是 Android 的 Ash ShellBusyBox它根本不认~/.bashrc。所以up.sh必须在脚本内部export所有依赖变量或通过sh -c export VARval; ./up.sh方式传入。这是嵌入式 Linux 开发者必须刻进 DNA 的常识。3. 实操要点解析读取、设置、验证环境变量的七种真实场景3.1 读取变量env、printenv、$VAR三者的本质区别初学者常混淆这三个命令以为只是写法不同。实际上它们解决的是三个完全不同的问题env命令它是一个独立的程序位于/usr/bin/env作用是“在干净的环境里执行另一个命令”。当你运行env | grep PATH本质是启动了一个新进程该进程的环境变量是当前 Shell 的完整副本然后grep在这个副本里搜索。env最大价值在于调试环境污染比如env -i bash可启动一个完全空白环境的 Bash用来验证某个命令是否真的不依赖外部变量。printenv命令它是 Shell 内置命令的封装通常为/usr/bin/printenv功能是“打印指定变量的值”。printenv PATH输出/usr/local/bin:/usr/bin:/bin而printenv不带参数则列出所有变量。它的优势是安全可靠即使变量名包含空格或特殊字符如printenv JAVA_HOME也不会像$JAVA_HOME那样被 Shell 展开失败。$VAR语法这是 Shell 的变量展开机制发生在命令行解析阶段。echo $PATH的执行流程是Shell 先将$PATH替换为实际值/usr/local/bin:...再把echo和替换后的字符串一起交给execve()系统调用。因此$语法的致命弱点是它无法处理变量名动态生成的场景。比如你想打印JAVA_HOME_8和JAVA_HOME_11写echo $JAVA_HOME_$VERSION是错的因为 Shell 会先找JAVA_HOME_这个变量为空再拼接$VERSION。正确做法是echo ${JAVA_HOME_$VERSION}用花括号明确边界或eval echo \$JAVA_HOME_$VERSION不推荐有安全风险。我实测过一个案例某 Python 项目要求根据ENVIRONMENT变量选择不同.env文件dev.env/prod.env。开发者写了source .env_${ENVIRONMENT}结果在ENVIRONMENTprod时失败。原因就是source是 Shell 内置命令不支持$展开。最终方案是source $(printf .env_%s $ENVIRONMENT)用命令替换确保路径正确生成。3.2 设置变量export、declare -x、set -a的适用边界设置变量不是简单写VARvalue就完事必须理解其作用域和生命周期VARvalue无 export创建的是局部变量仅在当前 Shell 进程内有效。执行VARtest; echo $VAR输出test但bash -c echo $VAR输出空。这种写法适合临时计算如count$(ls | wc -l)。export VARvalue这是最常用的方式将变量标记为“导出到子进程环境”。export本质是调用putenv()系统调用让后续fork()出的子进程能继承该变量。注意export本身不改变变量值只改变其导出属性。VARvalue; export VAR和export VARvalue效果完全相同。declare -x VARvalueBash 特有的声明方式功能与export完全等价但支持类型约束。例如declare -x -i NUM100强制NUM为整数后续NUMabc会被静默转为0。在编写健壮的 Shell 脚本时declare -x比裸export更安全。set -a这是一个开关指令开启后所有后续的变量赋值包括VARvalue都会自动export。常用于批量导入配置文件set -a; source .env; set a。set a关闭自动导出避免污染后续环境。实操心得在~/.bashrc中设置export是低效的。因为~/.bashrc每次打开新终端都会重新执行重复export没有意义。更优方案是用declare -x声明并赋值或直接在~/.bash_profile中集中管理。对于 Python 项目.env文件应由python-dotenv库在应用启动时加载而非靠 Shellexport否则systemd服务或 Docker 容器中会失效。3.3 永久生效的三大落地策略何时改哪里一次到位永久生效不是“随便找个文件写 export”而是根据变量用途选择最匹配的加载层级策略一全局通用变量如JAVA_HOME、CUDA_HOME修改位置/etc/profile.d/xxx.sh理由所有用户都需要且由系统包管理器统一维护。例如为 CUDA 11.3 创建/etc/profile.d/cuda113.sh# /etc/profile.d/cuda113.sh export CUDA_HOME/usr/local/cuda-11.3 export PATH$CUDA_HOME/bin:$PATH export LD_LIBRARY_PATH$CUDA_HOME/lib64:$LD_LIBRARY_PATH执行sudo chmod x /etc/profile.d/cuda113.sh后所有用户下次登录即生效。优势升级 CUDA 时只需修改此文件无需触碰用户家目录。策略二用户专属变量如ANDROID_HOME、GOPATH修改位置~/.bash_profile首选或~/.profile理由用户级配置避免权限问题。在~/.bash_profile末尾添加# ~/.bash_profile export ANDROID_HOME$HOME/Android/Sdk export PATH$ANDROID_HOME/platform-tools:$PATH # 确保加载 ~/.bashrc 中的 alias 和函数 [ -f ~/.bashrc ] source ~/.bashrc注意不要在~/.bash_profile里写source ~/.bashrc后再写export因为~/.bashrc可能已定义同名变量导致覆盖。应将export放在source之前。策略三会话级临时变量如DEBUG1、PUPPETEER_SKIP_DOWNLOADtrue修改位置直接在终端执行export或写入~/.bashrc并source理由这类变量常随项目切换硬编码到全局配置反而混乱。例如puppeteer_skip_download场景在项目根目录创建dev-env.sh# dev-env.sh export PUPPETEER_SKIP_DOWNLOADtrue export NODE_ENVdevelopment然后在终端运行source dev-env.sh。退出项目时执行unset PUPPETEER_SKIP_DOWNLOAD NODE_ENV即可清理比改配置文件更灵活。4. 核心环节实现从诊断到修复的完整工作流4.1 诊断阶段五步精准定位环境变量失效根源当遇到command not found或variable undefined时按以下顺序排查每步耗时不超过 10 秒Step 1确认当前 Shell 类型ps -p $$ # 输出类似 12345 pts/0 00:00:00 bash # 其中 bash 即 Shell 名$$ 是当前进程 PID shopt login_shell # 显示 on/offStep 2检查变量是否已定义printenv | grep -i java\|cuda\|android # 全局搜索关键词 echo $JAVA_HOME # 直接展开看是否为空Step 3追溯变量定义位置# 查找所有可能定义 JAVA_HOME 的文件 grep -r JAVA_HOME /etc/profile* ~/.bash* ~/.profile 2/dev/null # 输出示例/etc/profile.d/java.sh:export JAVA_HOME/usr/lib/jvm/java-11-openjdk-amd64Step 4验证配置文件加载状态# 检查 ~/.bash_profile 是否被读取 echo DEBUG: ~/.bash_profile loaded /tmp/debug.log # 重启终端检查 /tmp/debug.log 是否有该行 # 若无则说明登录 Shell 未加载此文件可能是发行版默认用 ~/.profileStep 5模拟子进程环境# 启动一个干净的 Bash测试变量是否继承 env -i bash -c echo $JAVA_HOME; which java # 若输出为空证明变量未被 export若输出路径但 which 失败证明 PATH 未更新我曾帮一位湖南大学学生解决hnu shell lab的PATH问题他把export PATH$HOME/bin:$PATH写在~/.bashrc但实验要求在sh下运行。sh不认~/.bashrc解决方案是创建~/.profile并写入相同内容因为sh登录时会读取~/.profile。4.2 修复阶段针对八类高频故障的实操方案故障一mvn -t报错JAVA_HOME not defined correctly根因Maven 检查JAVA_HOME是否指向 JDK 根目录含bin/java而非 JRE。修复# 先确认 JDK 路径 update-java-alternatives -l # 列出所有 JDK # 假设输出java-1.11.0-openjdk-amd64 1101 /usr/lib/jvm/java-11-openjdk-amd64 # 修改 /etc/profile.d/maven.sh echo export JAVA_HOME/usr/lib/jvm/java-11-openjdk-amd64 | sudo tee /etc/profile.d/maven.sh echo export PATH$JAVA_HOME/bin:$PATH | sudo tee -a /etc/profile.d/maven.sh sudo chmod x /etc/profile.d/maven.sh source /etc/profile.d/maven.sh故障二conda env install cuda113后nvcc不识别根因Conda 环境的nvcc在envs/cuda113/bin/但该路径未加入PATH。修复# 激活环境后手动添加 conda activate cuda113 export PATH$CONDA_PREFIX/bin:$PATH # 永久化在 conda 环境的 activation.d 中创建脚本 mkdir -p $CONDA_PREFIX/etc/conda/activate.d echo export PATH$CONDA_PREFIX/bin:$PATH $CONDA_PREFIX/etc/conda/activate.d/env_vars.sh故障三adb shell pm grant权限不生效根因Android 的pm grant需在shell用户上下文中执行而adb shell默认是shell用户但某些定制 ROM 会降权。修复# 强制以 root 执行需设备已 root adb root adb shell pm grant com.accessibilitymanager android.permission.write_sec # 或切换到正确的用户 ID adb shell run-as com.accessibilitymanager pm grant android.permission.write_sec故障四WSL 提示 “Windows Subsystem for Linux must be updated”根因WSL2 内核版本过旧与 Windows 主机不兼容。修复# 在 Windows PowerShell管理员中执行 wsl --update # 若失败手动下载最新内核 # https://learn.microsoft.com/en-us/windows/wsl/install-manual#downloading-distributions # 然后在 WSL 中更新 PATH echo export PATH/mnt/c/Users/$USER/AppData/Local/Microsoft/WindowsApps:$PATH ~/.bashrc source ~/.bashrc故障五set puppeteer_skip_download在 Windows CMD 有效Linux 失效根因Windows CMD 的set是会话级而 Linux Shell 的set是 Shell 内置命令不导出变量。修复# Linux 正确写法 export PUPPETEER_SKIP_DOWNLOADtrue # 或在 package.json 中 scripts: { dev: PUPPETEER_SKIP_DOWNLOADtrue node app.js }故障六linux找不到大文件路径根因find命令未指定-size参数或路径错误。修复# 查找大于 100MB 的文件 find /home -type f -size 100M -ls 2/dev/null | head -20 # 优化排除 /proc /sys 等虚拟文件系统 find /home -path /home/* -prune -o -type f -size 100M -print故障七落入initramfs紧急shell根因系统启动时无法挂载根文件系统常见于/etc/fstab错误或磁盘 UUID 变化。修复# 在 initramfs shell 中 ls /dev/sd* # 查看可用磁盘 blkid # 查看 UUID # 假设根分区是 /dev/sda2UUID 为 xxx exit # 退出 initramfs触发重新挂载 # 启动后立即修复 fstab sudo nano /etc/fstab # 将 UUIDold_uuid 改为 UUIDxxx sudo update-initramfs -u # 更新 initramfs故障八shell echo $(pgrep -f )返回空根因pgrep -f需要匹配完整命令行而$(...)中的空格会导致参数截断。修复# 正确写法用引号包裹整个 pgrep 命令 pid$(pgrep -f python3 server.py) # 或更健壮用 pidof pid$(pidof -x server.py)5. 常见问题与排查技巧实录来自十年一线踩坑的独家笔记5.1 为什么source ~/.bashrc后alias ll仍不生效现象在~/.bashrc中写了alias llls -la执行source ~/.bashrc后ll命令报command not found。真相alias是 Shell 内置功能但~/.bashrc默认被# If not running interactively, dont do anything的守卫代码屏蔽。打开~/.bashrc找到这段# If not running interactively, dont do anything case $- in *i*) ;; *) return;; esac这段代码的意思是如果当前 Shell 不是交互式$-不含i字符则直接return跳过后续所有内容包括alias定义。而source ~/.bashrc是在当前交互式 Shell 中执行$-包含i所以应该生效。但如果~/.bashrc被其他脚本如~/.bash_profile以非交互方式source就会触发return。终极解法删除或注释掉这段守卫代码或确保source总是在交互式 Shell 中执行。更优雅的做法是将alias移到~/.bash_aliases并在~/.bashrc中显式source ~/.bash_aliases。5.2export PATH被覆盖的隐形杀手/etc/environment文件现象在~/.bash_profile中export PATH$HOME/bin:$PATH但echo $PATH里没有$HOME/bin。真相Ubuntu/Debian 系发行版有一个隐藏配置文件/etc/environment它采用KEYVALUE格式无export关键字由 PAM 模块在登录时直接注入优先级高于所有 Shell 配置文件。查看它cat /etc/environment # 可能输出PATH/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games # 注意这里没有 $HOME/bin且是绝对路径无法引用变量修复/etc/environment不支持变量展开所以不能在这里写$HOME/bin。解决方案是删除/etc/environment中的PATH行不推荐可能影响系统服务在~/.profile中export PATH因为~/.profile在/etc/environment之后加载可以追加最佳实践在~/.profile中写export PATH$HOME/bin:$PATH确保$HOME被正确展开。5.3conda env与virtualenv的环境变量战争现象激活 conda 环境后which python指向 conda 路径但python -c import os; print(os.environ.get(PATH))输出的PATH却不含 conda 路径。真相Conda 的activate脚本不仅修改PATH还修改CONDA_DEFAULT_ENV、CONDA_PREFIX等变量并通过conda.sh中的conda activate函数动态重写PATH。但 Python 进程启动时os.environ读取的是进程创建时的快照而conda activate是在 Shell 层面修改Python 进程内的PATH是继承自父 Shell 的旧值。验证conda activate myenv echo $PATH # 显示 conda 路径 python -c import os; print(os.environ[PATH]) # 可能不包含 conda 路径修复这不是 Bug而是设计。conda activate的目标是让which、command等 Shell 命令能找到 conda 的二进制文件Python 内部的PATH无关紧要。若需在 Python 中获取 conda 路径应读取os.environ.get(CONDA_PREFIX)。5.4 国产 Linux 系统麒麟、UOS的 CMA 连续内存不足问题现象麒麟系统提示CMA: Failed to reserve 256 MiB导致 GPU 加速失效。真相CMAContiguous Memory Allocator是 Linux 内核为 GPU 分配连续物理内存的机制。cma256M参数需在内核启动时通过 GRUB 传递而非环境变量。修复步骤编辑/etc/default/grubsudo nano /etc/default/grub # 修改 GRUB_CMDLINE_LINUX 行 GRUB_CMDLINE_LINUXcma512M splash quiet更新 GRUBsudo update-grub # 麒麟系统可能需sudo grub2-mkconfig -o /boot/grub2/grub.cfg重启生效。关键点这不是 Shell 环境变量问题而是内核参数切勿尝试用export CMA512M解决——内核根本看不到用户态的export。5.5shell:522205fd8-5dfb-447d-801a-d0b52f2e83e1这类 UUID 是什么现象在日志或错误信息中看到类似shell:522205fd8-5dfb-447d-801a-d0b52f2e83e1的字符串。真相这不是环境变量而是Shell 进程的唯一标识符UUID由某些监控工具如 Datadog、New Relic或容器运行时如 Docker注入用于追踪 Shell 会话生命周期。它通常通过SHELL_SESSION_ID或自定义变量如DD_TRACE_SHELL_ID暴露。验证echo $SHELL_SESSION_ID # 可能为空因非标准变量 # 查看所有以 SHELL_ 开头的变量 env | grep ^SHELL_应对这类 UUID 无需手动设置它是监控系统自动注入的。若需禁用查阅对应监控工具文档通常在 agent 配置中关闭shell_tracing。5.6hnu计算机系统shell实验中的陷阱execve与fork的环境继承现象HNU 实验要求用 C 语言实现简易 Shell在execve(/bin/ls, argv, envp)时envp参数传NULL导致ls无法解析~。真相execve的第三个参数envp是环境变量数组若传NULL子进程将获得一个空环境HOME变量丢失ls ~中的~无法展开。修复// C 代码中正确传递环境变量 extern char **environ; execve(/bin/ls, argv, environ); // 传入全局 environ 数组 // 或显式构造 char *my_env[] {PATH/usr/bin:/bin, HOME/home/user, NULL}; execve(/bin/ls, argv, my_env);延伸fork()创建的子进程会完全复制父进程的内存空间包括所有环境变量所以fork后execve传environ是最安全的。6. 进阶实战用 Shell 脚本自动化环境诊断与修复6.1 编写env-diag.sh一键生成环境健康报告将前述诊断步骤封装为脚本每次遇到问题只需运行./env-diag.sh#!/bin/bash # env-diag.sh - Linux 环境变量健康诊断工具 set -euo pipefail LOGFILEenv-diag-$(date %Y%m%d-%H%M%S).log echo 环境诊断报告 $(date) $LOGFILE echo 【1. Shell 基础信息】 $LOGFILE echo PID: $$ $LOGFILE echo Shell: $(ps -p $$ -o comm) $LOGFILE echo Login Shell: $(shopt -q login_shell echo yes || echo no) $LOGFILE echo Interactive: $(echo $- | grep -q i echo yes || echo no) $LOGFILE echo -e \n【2. 关键变量检查】 $LOGFILE for var in JAVA_HOME CUDA_HOME ANDROID_HOME PATH; do value$(printenv $var 2/dev/null || echo (not set)) echo $var $value $LOGFILE done echo -e \n【3. PATH 路径分析】 $LOGFILE IFS: read -ra PATH_ARRAY $PATH for i in ${!PATH_ARRAY[]}; do path${PATH_ARRAY[$i]} if [ -d $path ]; then statusOK perms$(stat -c %A $path 2/dev/null | cut -c2-3) if [[ $perms ! rx ]]; then statusPERMISSION_DENIED fi else statusNOT_FOUND fi echo $i: $path [$status] $LOGFILE done echo -e \n【4. 配置文件检查】 $LOGFILE for file in /etc/profile ~/.bash_profile ~/.profile ~/.bashrc; do if [ -f $file ]; then echo $file: $(wc -l $file) lines, last modified $(stat -c %y $file | cut -d -f1) $LOGFILE # 检查是否包含 export if grep -q export.* $file 2/dev/null; then echo - Contains export statements $LOGFILE fi fi done echo -e \n【5. 子进程环境测试】 $LOGFILE env -i bash -c echo Clean env PATH: $PATH $LOGFILE echo 诊断完成报告已保存至 $LOGFILE使用方法chmod x env-diag.sh ./env-diag.sh # 查看报告 less env-diag-2024*.log6.2 构建env-fix.sh智能修复常见配置错误基于诊断报告自动修复典型问题#!/bin/bash # env-fix.sh - 智能环境修复脚本谨慎使用建议先备份 set -euo pipefail BACKUP_DIR$HOME/.env-backup-$(date %Y%m%d) mkdir -p $BACKUP_DIR # 修复 ~/.bash_profile 缺失 source ~/.bashrc 的问题 if [ -f ~/.bash_profile ] ! grep -q source.*\.bashrc ~/.bash_profile; then echo 【修复】在 ~/.bash_profile 中添加 source ~/.bashrc cp ~/.