Shell基础与运行环境

Shell 基础与运行环境

Shell 是用户与操作系统内核之间的命令解释层;Bash 是 Linux 中最常见的 Shell 实现之一。本篇先建立脚本执行、变量作用域和启动环境的整体认识,后续再进入字符串、数组、流程控制与文本处理。

阅读路线:基础与运行环境字符串、数组与运算符流程控制、函数与重定向文本处理工具与综合练习自动推送脚本

Shell、终端与 Bash

  • 终端(Terminal):负责显示输入输出,是与 Shell 交互的窗口。
  • Shell:读取命令、展开变量、建立管道并启动程序。
  • Bash:Shell 的一种具体实现,常见路径是 /bin/bash
  • Shell 脚本:按顺序保存 Shell 命令的文本文件,通常使用 .sh 后缀。

可以先确认当前环境:

代码块BASH · 4 行收起展开
echo $SHELL       # 登录时配置的默认 Shell
echo $0           # 当前 Shell 或脚本名称
bash --version
command -v bash      # Bash 可执行文件的位置

$SHELL 表示账户的默认 Shell,不一定等于当前正在运行的解释器;判断当前脚本应优先结合 $0BASH_VERSION 等信息。

第一个 Bash 脚本

新建 hello.sh:

代码块BASH · 4 行收起展开
#!/usr/bin/env bash

name=${1:-Shell}
printf 'Hello, %s!\n' $name

第一行称为 shebang,用于告诉系统应由哪个解释器执行脚本。/usr/bin/env bash 会从当前 PATH 中寻找 Bash,跨环境时通常比写死 /bin/bash 更灵活。

代码块BASH · 2 行收起展开
chmod +x hello.sh
./hello.sh World

如果脚本在 Windows 和 Linux 间复制后出现 bad interpreter 或行尾带有 ^M,通常是 CRLF 行尾导致的,应转换为 LF。

脚本的执行方式

执行方式是否需要执行权限运行位置适用场景
bash script.sh新 Bash 进程明确使用 Bash 解释脚本
./script.sh新进程按 shebang 正常执行脚本
source script.sh当前 Shell加载环境变量、函数或别名
. script.sh当前 Shellsource 的 POSIX 简写

子进程不能直接修改父进程的工作目录和普通变量。因此下面两种方式结果不同:

代码块BASH · 2 行收起展开
bash change-dir.sh    # 脚本中的 cd 不影响当前终端
source change-dir.sh  # cd 和变量修改保留在当前终端

只有确实需要改变当前 Shell 状态的配置脚本才应使用 source,普通任务脚本通常应独立执行。

命令组合与退出状态

Linux 命令结束后会返回退出状态码:0 表示成功,非 0 表示失败。

代码块BASH · 2 行收起展开
mkdir demo
echo $?

常见组合方式:

代码块BASH · 4 行收起展开
command1 ; command2   # 无论 command1 是否成功,都执行 command2
command1 && command2  # command1 成功才执行 command2
command1 || command2  # command1 失败才执行 command2
command1 | command2   # 将前一个命令的标准输出交给后一个命令

例如:

代码块BASH · 2 行收起展开
mkdir -p build && cd build
grep 'ERROR' app.log | sort | uniq -c

变量与引用

定义和读取变量

变量赋值时,等号两侧不能有空格:

代码块BASH · 5 行收起展开
project_name='shell-demo'
count=3

echo $project_name
echo ${project_name}-${count}

推荐用 ${变量名} 明确变量边界,并在普通字符串场景中使用双引号,避免空格、通配符和空值造成意外拆分。

单引号、双引号与不加引号

代码块BASH · 4 行收起展开
name='World'
echo '$name'          # 原样输出 $name
echo $name          # 展开变量
echo $name            # 会发生单词拆分和路径名展开,不建议默认使用
  • 单引号 '...':内容按字面值处理,不展开变量和命令。
  • 双引号 ...:展开变量和命令替换,但保留整体参数边界。
  • 不加引号:还可能发生单词拆分和通配符展开,最容易产生隐蔽问题。

命令替换

使用 $(...) 获取命令输出:

代码块BASH · 3 行收起展开
current_dir=$(pwd)
today=$(date +%F)
printf '目录:%s,日期:%s\n' $current_dir $today

相比旧式反引号,$(...) 更易读,也更容易嵌套。

Shell 变量与环境变量

普通变量只存在于当前 Shell;使用 export 后,它才会作为环境变量传递给后续创建的子进程。

代码块BASH · 4 行收起展开
app_mode='dev'
export app_mode

bash -c 'echo $app_mode'

也可以只为单次命令提供环境变量:

代码块BASH · 1 行收起展开
APP_MODE=prod ./start.sh

常用管理命令:

代码块BASH · 5 行收起展开
export NAME=value     # 导出环境变量
readonly NAME=value   # 创建只读变量
unset NAME            # 删除变量
env                    # 查看环境变量
declare -p NAME       # 查看变量的准确声明

位置参数与特殊变量

执行 ./deploy.sh api prod 时,脚本可以通过特殊变量读取调用信息:

变量含义
$0脚本名称
$1$2第 1、2 个位置参数
$#参数数量
$@保持参数边界的全部参数
$?上一条命令的退出状态
$$当前 Shell 的进程 ID
$!最近一个后台任务的进程 ID
代码块BASH · 8 行收起展开
#!/usr/bin/env bash

printf '脚本:%s\n' $0
printf '参数数量:%s\n' $#

for arg in $@; do
  printf '参数:%s\n' $arg
done

遍历调用参数时应使用 $@;$* 会更容易丢失原始参数边界。

PATH 与命令查找

Shell 会按照 PATH 中从左到右的目录顺序查找命令:

代码块BASH · 3 行收起展开
printf '%s\n' $PATH | tr ':' '\n'
command -v java
type cd
  • command -v:判断命令最终会解析到哪里。
  • type:还能识别内置命令、函数和别名。
  • 当前目录默认不在 PATH 中,因此执行当前脚本通常要写 ./script.sh
  • 不建议把 . 放入公共 PATH,否则可能误执行当前目录中的同名恶意文件。

临时追加目录:

代码块BASH · 1 行收起展开
export PATH=$HOME/bin:$PATH

需要长期生效时,应把它写入合适的 Bash 启动文件,而不是每次手动执行。

登录、交互与启动文件

理解 Bash 环境时要区分两个维度:

  • 交互式 / 非交互式:是否正在等待用户输入命令。
  • 登录 / 非登录:是否作为一次登录会话启动。
场景常见启动文件
登录 Shell/etc/profile,再读取 ~/.bash_profile~/.bash_login~/.profile 中第一个存在的文件
交互式非登录 Shell/etc/bash.bashrc(发行版相关)与 ~/.bashrc
普通非交互脚本通常不会自动读取 ~/.bashrc;可按需使用 BASH_ENV

常见做法是在 ~/.bash_profile 中主动加载 ~/.bashrc:

代码块BASH · 3 行收起展开
if [[ -f ~/.bashrc ]]; then
  source ~/.bashrc
fi

推荐的职责划分:

  • ~/.profile~/.bash_profile:登录时只需设置一次的环境变量。
  • ~/.bashrc:别名、提示符、补全和交互函数。
  • 项目脚本:项目自身所需的变量与逻辑,不依赖个人终端配置。

判断当前 Bash 类型:

代码块BASH · 2 行收起展开
[[ $- == *i* ]] && echo '交互式 Shell'
shopt -q login_shell && echo '登录 Shell'

调试与安全执行

代码块BASH · 2 行收起展开
bash -n script.sh   # 只检查语法,不执行
bash -x script.sh   # 展开并打印执行过程

较严谨的脚本可以从下面的模板开始:

代码块BASH · 9 行收起展开
#!/usr/bin/env bash
set -Eeuo pipefail

main() {
  local target=${1:-default}
  printf '处理目标:%s\n' $target
}

main $@
  • -e:未处理的失败命令会终止脚本。
  • -u:读取未定义变量时终止脚本。
  • pipefail:管道中任一命令失败,整条管道即视为失败。
  • -E:让错误陷阱在函数和子 Shell 等场景中继续生效。

这些选项会改变错误处理语义,使用时仍应显式处理允许失败的命令,例如放入 if 判断或搭配清晰的兜底逻辑。

常见错误速查

问题原因修正方式
name = value 报错赋值等号两侧有空格改为 name=value
变量为空或被拆成多个参数展开时未加双引号默认写成 $name
./script.sh: Permission denied没有执行权限chmod +x script.sh
修改变量后当前终端无变化脚本在子进程执行需要改变当前环境时使用 source
command not found命令不在 PATH,或当前目录未写 ./command -v 检查路径
bad interpreter / ^MWindows CRLF 行尾转为 Linux LF 行尾

基础练习

  1. 编写 hello.sh,接收一个姓名参数;没有参数时使用默认值。
  2. 编写 check-command.sh,用 command -v 判断指定命令是否存在,并返回不同状态码。
  3. 分别用 bash script.shsource script.sh 修改变量、切换目录,观察父子 Shell 的差异。
  4. 打印 $@ 中的每个参数,验证带空格参数在加引号和不加引号时的区别。

掌握这些基础后,再进入字符串展开、数组、运算符和流程控制,就能明确每段语法究竟发生在当前 Shell、子进程还是命令展开阶段。

延伸阅读