Linux Shell 用户输入完全指南:从命令行参数到交互读取
概述
写 Shell 脚本时,要拿用户输入,关键就在于让脚本「能交互、够灵活」。常见的输入场景有这么几种:
- 命令行参数:跑脚本时直接把数据带进去(比如 ./script.sh 10 20);
- 命令行选项:用 -a/-b 这类符号改变脚本的行为(比如 ./script.sh -f log.txt);
- 交互读取:脚本跑起来后主动问你,等你在键盘上输入(比如「请输入用户名:」)。
Bash 自带了一整套工具来处理这些场景,掌握了它们,脚本就能从「写死的逻辑」升级成「可配置、能交互」的实用工具。
一、命令行参数:最基础的输入传递方式
命令行参数就是跑脚本时跟在脚本名后面的「额外数据」,Shell 会自动把它们塞进「位置变量」里,不用额外配置就能用。
1. 位置变量:参数的 “专属容器”
Shell 用 $0~${n} 来表示位置变量,每个变量对应命令行上的一个元素,规矩是这样的:
- $0:脚本自己的名字(带路径,想只要纯脚本名得用 basename 提);
- $1~$9:第 1 到第 9 个命令行参数;
- ${10}~${n}:第 10 个及以后的参数(必须套大括号,不然会被当成 $1 加个 0)。
示例 1:用位置变量计算阶乘
#!/bin/bash
# 脚本功能:计算 $1 对应的阶乘
if [ -n "$1" ]; then
factorial=1
# 循环从 1 到 $1,计算阶乘
for (( number=1; number<=$1; number++ )); do
factorial=$[ $factorial * $number ]
done
echo "The factorial of $1 is $factorial"
else
echo "请传入一个整数参数(如 ./factorial.sh 5)"
fi
执行效果:
./factorial.sh 5 # 传入参数 5
# The factorial of 5 is 120 # 输出 5 的阶乘
示例 2:处理 10 个以上的参数
#!/bin/bash
# 脚本功能:获取第 10、11 个参数并计算乘积
product=$[ ${10} * ${11} ]
echo "第 10 个参数:${10}"
echo "第 11 个参数:${11}"
echo "乘积:$product"
执行效果:
# 传入 12 个参数(1~12)
./param10+.sh 1 2 3 4 5 6 7 8 9 10 11 12
# 第 10 个参数:10
# 第 11 个参数:11
# 乘积:110
2. 参数中的空格:必须用引号包裹
如果参数里带空格(比如 “big world”),直接传的话 Shell 会把它拆成「两个参数」,得用单引号或双引号把它包成一个「整体」。
示例:传递含空格的字符串参数
#!/bin/bash
# 脚本功能:打印传入的字符串
echo "Hello $1, glad to meet you."
错误与正确执行对比:
# 错误:未加引号,“big world”被拆分为两个参数,$1 仅为“big”
./greet.sh big world
# Hello big, glad to meet you.
# 正确:加引号,“big world”被视为一个参数
./greet.sh "big world" # 或 './greet.sh 'big world''
# Hello big world, glad to meet you.
3. 提取纯脚本名:解决 $0 含路径的问题
$0 返回的是脚本的「完整路径」(比如 ./factorial.sh 或者 /home/user/factorial.sh),如果只想要「纯脚本名」,得用 basename 命令把路径剥掉。
示例:提取脚本名并生成日志
#!/bin/bash
# 脚本功能:记录脚本运行时间到日志
script_name=$(basename $0) # 提取纯脚本名(如 factorial.sh)
log_path="$HOME/script_log.txt"
# 将运行时间写入日志
echo "${script_name} 运行于 $(date '+%Y-%m-%d %H:%M:%S')" >> $log_path
echo "日志已记录至 $log_path"
执行效果:
./record_log.sh
# 日志已记录至 /home/user/script_log.txt
cat /home/user/script_log.txt
# record_log.sh 运行于 2024-06-03 15:30:00
二、参数测试与特殊变量:避免脚本崩溃
直接用位置变量时,要是用户没传参数,脚本可能因为「变量是空的」报错。Shell 提供了参数统计和特殊变量,可以提前把参数的合法性检查好。
1. 特殊变量:快速获取参数信息
Shell 定义了 3 个核心特殊变量,用来简化参数处理:
| 特殊变量 | 含义 | 示例(运行 ./script.sh a b c) |
|---|---|---|
| $# | 命令行参数的总个数(不含脚本名 $0) | $# 等于 3 |
| $* | 所有参数视为 “单个字符串” | $* 等于 “a b c” |
| $@ | 所有参数视为 “多个独立字符串” | $@ 等于 “a” “b” “c” |
| ${!#} | 最后一个参数(无需知道参数总数) | ${!#} 等于 “c” |
示例 1:用 $# 检测参数个数
#!/bin/bash
# 脚本功能:计算两个数的和,需传入 2 个参数
if [ $# -ne 2 ]; then
# 参数个数不对时,提示正确用法
echo "用法:$(basename $0) 数字1 数字2"
else
total=$[ $1 + $2 ]
echo "$1 + $2 = $total"
fi
执行效果:
# 未传参数:提示用法
./add.sh
# 用法:add.sh 数字1 数字2
# 传入 2 个参数:正常计算
./add.sh 17 25
# 17 + 25 = 42
示例 2:$* 与 $@ 的核心区别
$* 会把所有参数拼成一个「字符串」,$@ 会保留每个参数的「独立性」,区别只有加了双引号的时候才看得出来:
#!/bin/bash
# 脚本功能:遍历所有参数
echo "=== 使用 \$*(合并为单个字符串) ==="
count=1
for param in "$*"; do
echo "参数 $count:$param"
count=$[ $count + 1 ]
done
echo -e "\n=== 使用 \$@(保留独立参数) ==="
count=1
for param in "$@"; do
echo "参数 $count:$param"
count=$[ $count + 1 ]
done
执行效果(运行 ./loop_params.sh a b c):
=== 使用 $*(合并为单个字符串) ===
参数 1:a b c
=== 使用 $@(保留独立参数) ===
参数 1:a
参数 2:b
参数 3:c
结论:遍历参数时优先用 $@,避免含空格的参数被拆分。
2. shift 命令:“左移” 参数,简化遍历
shift 命令会把所有位置变量「往左挪 N 位」(默认 N=1),$1 被丢掉,$2 变成新的 $1,适合「不知道一共有几个参数」时把参数全遍历一遍。
示例:用 shift 遍历所有参数
#!/bin/bash
# 脚本功能:遍历所有参数并打印
count=1
echo "所有参数:"
# 当 $1 不为空时,持续循环
while [ -n "$1" ]; do
echo "参数 $count:$1"
count=$[ $count + 1 ]
shift # 左移 1 位,$2 变成新的 $1
done
执行效果(运行 ./shift_loop.sh x y z):
所有参数:
参数 1:x
参数 2:y
参数 3:z
进阶:指定 shift 移动的位数
#!/bin/bash
# 脚本功能:跳过前 2 个参数,处理后续参数
echo "原始参数:$*"
shift 2 # 左移 2 位,跳过前 2 个参数
echo "跳过前 2 个参数后,第一个参数:$1"
执行效果(运行 ./shift_2.sh a b c d):
原始参数:a b c d
跳过前 2 个参数后,第一个参数:c
三、命令行选项:改变脚本行为的 “开关”
命令行选项就是带个 - 的单字母(比如 -a、-f),用来「开关某个功能」或者「传个参数值」(比如 -f log.txt 指定日志文件)。Shell 给了 3 种处理选项的办法:case 语句、getopt、getopts。
1. 基础方法:用 case 语句识别选项
适合处理简单选项(比如 -a、-b),核心就是用 case 匹配选项,配合 shift 把参数挨个过一遍。
示例 1:处理无参数的选项
#!/bin/bash
# 脚本功能:识别 -a、-b、-c 选项
while [ -n "$1" ]; do
case "$1" in
-a) echo "找到选项:-a" ;;
-b) echo "找到选项:-b" ;;
-c) echo "找到选项:-c" ;;
*) echo "未知选项:$1" ;; # 匹配未定义的选项
esac
shift # 左移参数,处理下一个
done
执行效果(运行 ./case_options.sh -a -c -d):
./case_options.sh -a -c -d
# 找到选项:-a
# 找到选项:-c
# 未知选项:-d
示例 2:分离选项与普通参数(— 分隔符)
Linux 标准约定:— 是「选项结束符」,- 后面的内容都视为「普通参数」,而非选项。
#!/bin/bash
# 脚本功能:用 -- 分离选项和参数
while [ -n "$1" ]; do
case "$1" in
-a|-b|-c) echo "找到选项:$1" ;;
--) shift; break ;; # 遇到 -- 停止处理选项
*) echo "未知选项:$1" ;;
esac
shift
done
# 处理 -- 后的普通参数
count=1
echo -e "\n普通参数:"
for param in "$@"; do
echo "参数 $count:$param"
count=$[ $count + 1 ]
done
执行效果:
./sep_options_params.sh -a -b -- file1 file2
# 找到选项:-a
# 找到选项:-b
#
# 普通参数:
# 参数 1:file1
# 参数 2:file2
示例 3:处理带参数值的选项(如 -f log.txt)
部分选项需要「额外参数值」(比如 -f 指定文件),得在 case 里把 $2 抠出来当值,再多 shift 一次跳过这个值。
#!/bin/bash
# 脚本功能:处理带参数值的选项(-f 指定文件,-n 指定次数)
while [ -n "$1" ]; do
case "$1" in
-f) file="$2"; echo "指定文件:$file"; shift ;; # 提取 $2 为文件路径
-n) times="$2"; echo "指定次数:$times"; shift ;; # 提取 $2 为次数
-v) echo "启用详细模式" ;;
*) echo "未知选项:$1" ;;
esac
shift
done
执行效果:
./value_options.sh -v -f /var/log/syslog -n 3
# 启用详细模式
# 指定文件:/var/log/syslog
# 指定次数:3
2. 进阶方法:用 getopt 处理复杂选项
getopt 是个外部命令,能自动解析选项(包括把 -ac 这种合并选项拆开)、处理带值的选项,但它不支持带空格的参数值。
核心语法:getopt optstring parameters
- optstring:定义有效选项,需参数值的选项后加 :(如 ab:c 表示 -a 无值,-b 有值,-c 无值);
- parameters:待解析的命令行参数(通常为 $@)。
示例:在脚本中使用 getopt
#!/bin/bash
# 脚本功能:用 getopt 解析选项
# 1. 用 getopt 格式化参数(-q 忽略错误)
set -- $(getopt -q ab:c "$@")
# 2. 遍历格式化后的参数
while [ -n "$1" ]; do
case "$1" in
-a) echo "找到选项:-a" ;;
-b) file="$2"; echo "指定文件:$file"; shift ;;
-c) echo "找到选项:-c" ;;
--) shift; break ;; # 结束选项处理
*) echo "未知选项:$1" ;;
esac
shift
done
# 3. 处理普通参数
echo -e "\n普通参数:$@"
执行效果(支持合并选项 -ac):
./getopt_demo.sh -ac -b test.txt file1
# 找到选项:-a
# 找到选项:-c
# 指定文件:test.txt
#
# 普通参数:file1
3. 推荐方法:用 getopts 处理所有场景
getopts 是 Bash 的内建命令,补上了 getopt 的短板(支持带空格的参数值),而且语法更简洁,是处理选项的首选工具。
核心语法:getopts optstring variable
- optstring:定义有效选项,规则与 getopt 一致(需值的选项后加 :,开头加 : 忽略错误);
- variable:存储当前解析到的选项(自动去除 -,如 -a 存为 a);
- 环境变量:OPTARG 存储选项的参数值,OPTIND 存储当前处理的参数位置。
示例:用 getopts 解析选项
#!/bin/bash
# 脚本功能:用 getopts 处理选项(支持含空格的参数值)
while getopts :ab:c opt; do # : 开头忽略错误,b: 表示 -b 需参数值
case "$opt" in
a) echo "找到选项:-a" ;;
b) echo "指定文件:$OPTARG" ;; # OPTARG 存储 -b 的参数值
c) echo "找到选项:-c" ;;
?) echo "未知选项:$OPTARG" ;; # ? 匹配未定义选项(OPTARG 为选项本身)
esac
done
# 处理普通参数(OPTIND 是选项结束的位置)
shift $[ $OPTIND - 1 ]
echo -e "\n普通参数:"
count=1
for param in "$@"; do
echo "参数 $count:$param"
count=$[ $count + 1 ]
done
执行效果(支持含空格的参数值):
./getopts_demo.sh -a -b "my file.txt" -c data1 data2
# 找到选项:-a
# 指定文件:my file.txt # 正确识别含空格的参数值
# 找到选项:-c
#
# 普通参数:
# 参数 1:data1
# 参数 2:data2
getopts 的最大的好处
- 自动拆合并选项:比如 -ac 会被拆成 -a 和 -c,不用你额外处理;
- 支持带空格的参数值:-b “my file.txt” 能正确解析,OPTARG 就是 my file.txt;
- 参数位置好管理:OPTIND 自动记着下一个要处理的参数位置,方便把选项和普通参数分开。
4. 选项标准化:遵循 Linux 通用约定
为了让脚本更好上手,建议照着 Linux 命令的「选项含义标准」来,别让用户额外记一套逻辑。常见的标准化选项如下:
| 选项 | 通用含义 | 脚本中的应用场景举例 |
|---|---|---|
| -a | 显示所有对象 | ./list.sh -a 列出所有文件(含隐藏文件) |
| -c | 生成计数 | ./count.sh -c log.txt 统计文件行数 |
| -d | 指定目录 | ./backup.sh -d /home/data 指定备份目录 |
| -f | 指定输入文件 | ./parse.sh -f data.csv 指定待解析文件 |
| -h | 显示帮助信息 | ./script.sh -h 打印用法说明 |
| -l | 长格式输出 | ./ls.sh -l 显示文件详细信息(权限、大小等) |
| -n | 非交互模式(批处理) | ./install.sh -n 静默安装(不提示) |
| -v | 详细模式(打印过程) | ./deploy.sh -v 显示部署每一步操作 |
| -x | 排除指定对象 | ./clean.sh -x temp/ 清理时排除 temp 目录 |
示例:为脚本添加 -h 帮助选项
#!/bin/bash
# 脚本功能:带帮助选项的加法脚本
show_help() {
echo "用法:$(basename $0) [选项] 数字1 数字2"
echo "选项:"
echo " -h 显示帮助信息"
echo " -v 启用详细模式"
echo "示例:"
echo " $(basename $0) 10 20 # 计算 10+20"
echo " $(basename $0) -v 10 20 # 显示计算过程"
}
# 解析选项
verbose=0
while getopts :hv opt; do
case "$opt" in
h) show_help; exit 0 ;;
v) verbose=1 ;;
?) echo "未知选项:$OPTARG"; show_help; exit 1 ;;
esac
done
# 处理普通参数(数字1和数字2)
shift $[ $OPTIND - 1 ]
if [ $# -ne 2 ]; then
echo "错误:需传入 2 个数字参数"
show_help
exit 1
fi
# 计算并输出结果
total=$[ $1 + $2 ]
if [ $verbose -eq 1 ]; then
echo "计算过程:$1 + $2 = $total"
else
echo $total
fi
执行效果(查看帮助):
./add_help.sh -h
# 用法:add_help.sh [选项] 数字1 数字2
# 选项:
# -h 显示帮助信息
# -v 启用详细模式
# 示例:
# add_help.sh 10 20 # 计算 10+20
# add_help.sh -v 10 20 # 显示计算过程
四、交互读取:用 read 命令实现用户问答
read 命令就是用来「脚本跑起来后主动等键盘输入」的,支持自定义提示符、超时控制、隐藏输入(比如密码)这些功能,是实现脚本交互的核心工具。
1. 基础用法:读取输入并存储到变量
read 默认从「标准输入(键盘)」读数据,按 Enter 结束,输的内容会存到指定的变量里(没指定变量时就存进 REPLY 这个环境变量)。
示例 1:简单的姓名输入
#!/bin/bash
# 脚本功能:读取姓名并问候
# 方法1:用 echo -n 生成提示符(不换行)
echo -n "请输入您的姓名:"
read name # 读取输入到 name 变量
echo "您好,$name!欢迎使用本脚本。"
# 方法2:用 read -p 直接指定提示符(更简洁)
read -p "请输入您的年龄:" age
echo "您的年龄是 $age 岁,对应的天数约为 $[ $age * 365 ] 天。"
执行效果:
./read_basic.sh
# 请输入您的姓名:张三
# 您好,张三!欢迎使用本脚本。
# 请输入您的年龄:25
# 您的年龄是 25 岁,对应的天数约为 9125 天。
示例 2:未指定变量时用 REPLY
#!/bin/bash
# 脚本功能:用 REPLY 存储输入
read -p "请输入一句话:"
echo "您输入的内容是:$REPLY"
执行效果:
./read_reply.sh
# 请输入一句话:Shell 很强大!
# 您输入的内容是:Shell 很强大!
2. 进阶功能:超时、隐藏输入与字符限制
read 还提供了好几个选项,应付不同的交互场景:
| 选项 | 功能描述 | 示例 |
|---|---|---|
| -t N | 超时时间(N 秒),超时后返回非 0 状态码 | read -t 5 -p “5 秒内输入:” var |
| -s | 隐藏输入(不显示输入内容,适合密码) | read -s -p “输入密码:” pass |
| -n N | 读取 N 个字符后自动结束(无需按 Enter) | read -n 1 -p “是否继续(Y/N)?” ans |
| -p “提示语” | 直接指定提示符(替代 echo -n) | read -p “请输入:” var |
示例 1:超时控制(避免脚本无限等待)
#!/bin/bash
# 脚本功能:5 秒内未输入则超时
if read -t 5 -p "请在 5 秒内输入您的姓名:" name; then
echo -e "\n您好,$name!"
else
echo -e "\n超时未输入,脚本退出。"
exit 1
fi
执行效果(超时场景):
./read_timeout.sh
# 请在 5 秒内输入您的姓名:
# 超时未输入,脚本退出。
示例 2:隐藏输入(密码输入场景)
#!/bin/bash
# 脚本功能:隐藏密码输入
read -s -p "请输入密码:" pass
echo -e "\n密码长度为:${#pass} 个字符" # ${#pass} 计算字符串长度
执行效果(输入时不显示密码):
./read_password.sh
# 请输入密码: # 输入时无显示
# 密码长度为:8 个字符
示例 3:读取单个字符(快速确认)
#!/bin/bash
# 脚本功能:读取单个字符确认操作
read -n 1 -p "是否删除该文件(Y/N)?" ans
echo # 换行,避免后续输出与提示符重叠
case "$ans" in
Y|y) echo "正在删除文件..." ;;
N|n) echo "取消删除操作。" ;;
*) echo "输入无效,请输入 Y 或 N。" ;;
esac
执行效果(输入 Y 后自动确认):
./read_single_char.sh
# 是否删除该文件(Y/N)?Y
# 正在删除文件...
3. 高级用法:从文件中读取内容
read 不光能读键盘,还能读文件内容——每次调用 read 就读取文件的「一行」,一直读到文件末尾(返回非 0 状态码)为止。
示例:逐行读取文件并处理
#!/bin/bash
# 脚本功能:逐行读取文件并统计行数
file_path="$HOME/test.txt"
if [ ! -f "$file_path" ]; then
echo "文件 $file_path 不存在!"
exit 1
fi
count=1
echo "文件 $file_path 的内容:"
# 用 cat 输出文件内容,通过管道传给 while read(逐行读取)
cat "$file_path" | while read line; do
echo "第 $count 行:$line"
count=$[ $count + 1 ]
done
echo "文件总行数:$[ $count - 1 ]"
执行效果(假设 test.txt 有 3 行内容):
./read_file.sh
# 文件 /home/user/test.txt 的内容:
# 第 1 行:Hello World
# 第 2 行:Shell 读取文件示例
# 第 3 行:End of file
# 文件总行数:3
五、总结:Shell 用户输入的最佳实践
- 选对输入方式:
- 输入是「固定参数」(比如数字、文件名),用命令行参数(简单高效);
- 输入是「功能开关」(比如要不要开详细模式),用命令行选项(首选 getopts);
- 输入需要「用户确认」(比如密码、操作选择),用 read 交互读取(更安全)。
- 参数和选项要防着点:
- 用 $# 检查参数个数,别让「空参数」把脚本搞挂;
- 变量用双引号包起来(比如 “$1”、“$OPTARG”),免得带空格的输入被拆散;
- 给脚本加个 -h 帮助选项,降低别人用的门槛。
- 交互读取体验优化:
- 用 read -p 代替 echo -n,提示符配置更省事;
- 敏感信息(比如密码)用 read -s 隐藏输入,更安全;
- 要等很久的场景(比如等用户输入)用 read -t 设个超时,免得脚本卡住不动。
把上面这些吃透,你就能写出从「小工具」到「复杂交互脚本」的各种 Shell 程序,应付日常运维、自动化任务基本够用。建议结合实际场景练手(比如写个「带选项的文件备份脚本」「带交互确认的日志清理脚本」),把输入处理的逻辑搞明白。