🔍搜索⌘K

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 的最大的好处

  1. 自动拆合并选项:比如 -ac 会被拆成 -a 和 -c,不用你额外处理;
  2. 支持带空格的参数值:-b “my file.txt” 能正确解析,OPTARG 就是 my file.txt;
  3. 参数位置好管理: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 用户输入的最佳实践

  1. 选对输入方式:
  • 输入是「固定参数」(比如数字、文件名),用命令行参数(简单高效);
  • 输入是「功能开关」(比如要不要开详细模式),用命令行选项(首选 getopts);
  • 输入需要「用户确认」(比如密码、操作选择),用 read 交互读取(更安全)。
  1. 参数和选项要防着点:
  • 用 $# 检查参数个数,别让「空参数」把脚本搞挂;
  • 变量用双引号包起来(比如 “$1”、“$OPTARG”),免得带空格的输入被拆散;
  • 给脚本加个 -h 帮助选项,降低别人用的门槛。
  1. 交互读取体验优化:
  • 用 read -p 代替 echo -n,提示符配置更省事;
  • 敏感信息(比如密码)用 read -s 隐藏输入,更安全;
  • 要等很久的场景(比如等用户输入)用 read -t 设个超时,免得脚本卡住不动。

把上面这些吃透,你就能写出从「小工具」到「复杂交互脚本」的各种 Shell 程序,应付日常运维、自动化任务基本够用。建议结合实际场景练手(比如写个「带选项的文件备份脚本」「带交互确认的日志清理脚本」),把输入处理的逻辑搞明白。