🔍搜索⌘K

Linux Shell 用户输入完全指南:从命令行参数到交互读取

概述

在编写 Shell 脚本时,获取用户输入是实现 “交互性” 和 “灵活性” 的关键。常见的输入场景包括:

  • 命令行参数:运行脚本时直接传入数据(如 ./script.sh 10 20);
  • 命令行选项:通过 -a/-b 等符号改变脚本行为(如 ./script.sh -f log.txt);
  • 交互读取:脚本运行中主动询问用户,等待键盘输入(如 “请输入用户名:”)。

Bash Shell 提供了一套完整的工具来处理这些场景,掌握它们能让脚本从 “固定逻辑” 升级为 “可配置、可交互” 的实用工具。

一、命令行参数:最基础的输入传递方式

命令行参数是运行脚本时紧跟在脚本名后的 “额外数据”,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 程序,满足日常运维、自动化任务的大部分需求。建议结合实际场景练习(如编写 “带选项的文件备份脚本”“带交互确认的日志清理脚本”),深化对输入处理逻辑的理解。