Linux 命令行与 shell 脚本编程大全 14 处理用户输入

介绍 shell 脚本如何接收用户的输入

更多精彩

导览

  1. 运行脚本时可以往脚本中传入命令行参数,例如 ./param.sh 1 2 3
  2. shift 命令可以从右向左移动传入的参数位置,类似于迭代器,shift n 命令可以指定参数移动的位置数,默认为 1
  3. $# 可以在脚本中直接获取传入的参数总数,${!#} 可以在脚本中直接获取传入的最后一个参数
  4. $* 可以在脚本中直接获取传入的所有参数,但获取到的内容是一整个字符串
  5. [email protected] 可以再脚本中直接获取传入的所有参数,而且获取到的内容是可以进行参数遍历的字符串
  6. 运行脚本时可以往脚本中传入选项,例如 ./option.sh -a -b -c
  7. getopt 命令可以用于指定脚本的参数和选项的传入规则,在脚本中的基本语法是 set -- $(getopt ab:cd "[email protected]")
  8. getopts 命令不仅可以指定脚本的参数和选项的传入规则,而且还支持带空格、引号的参数,在脚本中的基本语法是 getopts ab:cd opt
  9. read 命令用于接收用户输入,可以通过指定变量名来分别接收用户的单个输入或多个输入,只需要输入内容使用空格进行分隔
  10. read -p 命令可以简化脚本的输入流程,将输入提示语句和输入接收语句合并为一条语句
  11. read -t 命令可以指定输入超时的时间
  12. read -n 命令可以指定输入允许接收的字符数量,达到字符数量时会自动执行下一步操作
  13. read -s 命令可以隐藏用户输入的内容

14.1 命令行参数

  1. 使用命令行参数是向 shell 脚本传递数据的最基本方式

14.1.1 读取参数

  1. 通过命令行参数传递到 shell 脚本中的数据会被标记为 位置参数( Positional Parameter )
  2. 位置参数的索引从 1 开始,因为 0 表示执行 shell 脚本时用的名称
    • 例如 ./input.sh 1 2 ,那么在 input.sh 中 ,$0 的值是 ./input.sh$1 的值是 1 ,$2 的值是 2
  3. 通过命令行参数传递的参数数量最好在 10 个以下,因为从第 10 个开始就无法直接使用 $1 这样的格式获取参数了,而需要使用 ${10} 这样的格式才能获取到参数
  4. 写一个简单的例子演示一下,如下图
    Linux 命令行与 shell 脚本编程大全 14 处理用户输入

14.1.2 使用 basename 读取脚本名

  1. 虽然通过 $0 可以获取脚本在执行时的名称,但如果脚本在执行时携带了相应的路径,也会出现在 $0 参数中
  2. 使用 basename 就可以过滤掉 $0 参数中脚本名以外的内容,从而得到一个纯净的脚本名称,如下图
    Linux 命令行与 shell 脚本编程大全 14 处理用户输入

14.1.3 测试参数

  1. 因为命令行参数是可选的,在执行脚本时,可以添加也可以不添加,所以在脚本中使用命令行参数时,最好是先判断一下,如下图
    • 在使用 test 命令对命令行参数进行条件判断时,不能直接使用 $1 ,需要使用 "$1" 才能正确获取到参数
      Linux 命令行与 shell 脚本编程大全 14 处理用户输入

14.2 特殊参数变量

14.2.1 参数统计

  1. 使用 $# 可以输出脚本在执行时传入了多少个参数,如下图
    Linux 命令行与 shell 脚本编程大全 14 处理用户输入
  2. 既然 $# 可以返回传入参数的数量,那么理论上使用 ${$#} 就可以获取到最后一个参数的内容,但其实不是
  3. 要快速获取最后一个参数的内容需要通过 ${!#} ,如下图
    Linux 命令行与 shell 脚本编程大全 14 处理用户输入

14.2.2 抓取所有的数据

  1. 使用 $* 可以获取通过命令行传入的所有参数,但这些参数会被作为一个字符整体被保存
    • 将传入的所有参数作为一个值
  2. 使用 [email protected] 也可以获取通过命令行传入的所有参数,这些参数则会被作为一个可以循环的字符串被保存
    • 将传入的每个参数作为单独的值
  3. 写一个简单的例子演示一下,如下图
    • 在对两者采用了相同遍历方式的操作之后,通过 $* 得到的是一个参数,而 [email protected] 得到的是 5 个参数
      Linux 命令行与 shell 脚本编程大全 14 处理用户输入

14.3 使用 shift 命令移动变量

  1. 使用 shift 命令时,默认会将每个参数变量向左移动一个位置,也就是参数 $3 -> $2
    • 类似于 Java 中的迭代器
  2. 而除了参数 $0 ,其他的参数的位置在被后续参数替换后,自身就不复存在了
    • 因为 $0 保存着程序名,所以不会改变
  3. 写一个例子简单演示一下,如下图
    • while 循环中每次获取的参数都是 $1 ,但获取到的内容却能够发生变化
    • 这就是因为每次循环后都通过 shift 命令把后续的参数向左移动了一位
      Linux 命令行与 shell 脚本编程大全 14 处理用户输入

14.3.1 使用 shift 命令移动指定数量的变量

  1. 使用 shift num 命令可以一次移动多个位置的变量,如下图
    Linux 命令行与 shell 脚本编程大全 14 处理用户输入

14.3.2 使用 shift 命令移动多个位置时,传入的参数必须是位置数的倍数

  1. 上从图可知,使用 shift 2 命令将参数的位置移动两位,然后脚本在执行的时候传入了 6 个参数
  2. 这个时候如果只传入 5 个参数,那么脚本就会无限循环,如下图
    Linux 命令行与 shell 脚本编程大全 14 处理用户输入

14.4 处理选项

  1. 执行脚本时,除了能往脚本中传入命令行参数,还能 在单破折号后面跟字母 往脚本中传递 选项

14.4.1 查找选项

14.4.1.1 处理简单选项

  1. 当脚本在执行时传入了选项,通过 case 命令进行判断是最好的处理方式,如下图
    • 为了让传入的多个选项能在一次执行过程中都被执行到,需要先使用一个 while + shift 对选项进行遍历
    • 在每次遍历时都通过 case 命令对选项的具体参数进行判断
    • 同时还通过 *) 提供一个默认判断
      Linux 命令行与 shell 脚本编程大全 14 处理用户输入

14.4.1.2 分离参数和选项

  1. 可以通过 双破折号( – ) 将参数和选项进行分离,这样才执行脚本时就可以同时传入两者
  2. 这里的说法 不是指直接将参数和选项进行混排 ,而是 先传入参数后传入选项,或者先传入选项后传入参数 ,在这两者之间通过双破折号进行分离,如下图
    • 才判断选项的 case 命令中,新增了一个 --) 分支,用于判断分支的存在
    • 当从传入的内容中检索到这个分支时,会先执行一个 shift ,目的是将双破这行这个分隔符跳过去
    • 之后再执行一次 break 是为了跳出 while 循环,进行后续属于参数遍历的 for 循环
    • 可以看到,当直接执行 ./option-param.sh -a -b -e a b e 时,由于没有检测到 --) 分支,所以后续的参数也被识别成了错误的选项
    • 但是当使用双破折号将选项和参数分离后,就能顺利遍历到选项和参数了
      Linux 命令行与 shell 脚本编程大全 14 处理用户输入
  3. 从上述这个例子,其实我们可以思考一下,双破折号真的是作为一个 强语法的特殊符号 来被 shell 识别的吗?很显然不是!
  4. 我们可以对上述脚本稍作修改,就可以得出下图的效果
    • 将上图中的 --) 分支判断部分修改为下图中的 -e) 分支判断
    • 然后执行上图中第一次执行导致错误结果的语句,却可以得到和上图第二次正确结果一致的输出
    • 这说明 双破折号作为参数和选项分隔符只是一个规范上的约定,并不是强语法的特殊符号
      Linux 命令行与 shell 脚本编程大全 14 处理用户输入

14.4.1.3 处理带值的选项

  1. 最复杂的情况就是参数和选项的混合搭配,但是在 shell 脚本中,当参数和选项混排后,对应的参数会被认为是前一个选项的值
  2. 例如 ./option.sh -a str1 -b str2 -c ,这里的 str1 就会被认为是 -a 选项的值,需要在 -a 选项的分支中进行操作
  3. 写一个简单的例子演示一下,如下图
    • -a 后续的参数识别之后会执行一次 shift 命令的目的是为了将参数跳过
      Linux 命令行与 shell 脚本编程大全 14 处理用户输入

14.4.2 使用 getopt 命令

14.4.2.1 基础语法

  1. getopt 命令用于将通过简化形式传入脚本的命令行参数和选项进行规范化
    • 比如在执行脚本时使用 ./option.sh -abc ,如果脚本中存在 getopt 命令,就可以被转换为 -a -b -c
    • 这样就可以被脚本正确的识别,同时用户的输入也得到了简化
  2. 在脚本执行时,会先将这些传入脚本的命令行参数和选项自动转换为适当的格式,让脚本在使用这些参数和选项时更加方便
    • 比如它可以定义哪些字母选项是有效的,哪些字母选项需要指定参数值
  3. getopt 命令可以在命令行执行执行,虽然这样做没有什么实际意义,但是可以帮助我们熟练一下语法,如下图
    • ab:cd 就是通过 getopt 命令指定的参数选项规则,表示当前可以传入 abcd 选项,而且选项 b 后面存在一个参数
    • -ab test -cd 就是实际传入的参数内容,第二行的输出结果就是对这些传入的内容进行格式化后的结果
      Linux 命令行与 shell 脚本编程大全 14 处理用户输入
  4. 默认如果传入的内容和规则不匹配,会抛出错误,可以通过 -q 选项忽略错误,如下图
    Linux 命令行与 shell 脚本编程大全 14 处理用户输入

14.4.2.2 在脚本中使用

  1. getopt 命令自然是要在脚本中使用才有意义,而且在脚本中使用的语法基本都是固定的
  2. getopt 命令是用来规范参数和选项的,所以肯定是要放在脚本的最前端,这个也是毋庸置疑
  3. 一般只需要在脚本前端加上 set -- $(getopt -q ab:cd "[email protected]") 即可
    • ab:cd 是可变化的部分,指的是具体参数和选项的匹配方式
  4. 写一个简单的例子演示一下,如下图
    • 这个例子是在 centOS 环境下运行的,因为 macOS 中的 set 命令无法按预期执行出效果
      Linux 命令行与 shell 脚本编程大全 14 处理用户输入
  5. 上图中 case 命令的 --) 分支判断是必须添加的,否则在执行命令时最后一个分支会被多判断一次
  6. 至于具体是为什么多判断一次,可以在执行脚本前添加 sh -x 来对脚本进行 DEBUG ,如下图
    • 首先在原来的脚本中注释掉 --) 分支
    • 然后在与之前相同的脚本执行语句之前加上 sh -x ,之后就可以很清楚的看到脚本的执行过程
    • 在执行过程的第一处可以看到,传入的 -ab hello -cd 被解析后,最后多出来一个 --
    • 于是在脚本的最后,这个 -- 就被作为传入的选项执行了
    • 这就是为什么如果必须 case 命令添加 --) 分支,并且通过 shift + break 命令跳过的原因
      Linux 命令行与 shell 脚本编程大全 14 处理用户输入

14.4.3 使用更高级的 getopts

  1. getopts 命令是 getopt 命令的复数版本( 其实说是升级版更贴切 )
    • 关键在于 getopts 可以处理 带空格和引号的参数 ,而 getopt 不行
  2. getoptsgetopt 更优秀的地方在于,每次调用时,只会处理一个参数,并且在处理完所有参数后会返回一个非 0 的退出状态码
    • getopt 是将参数和选项生成为一个完整的输出
  3. getopts 也可以忽略错误信息,只需要在参数选项规则之前加上一个冒号即可
  4. 如果某个选项后面需要跟一个参数,这个参数会被存储在 OPTARG 变量中
  5. 还有一个变量是 OPTIND ,用于存放 getopts 当前正在处理的参数位置
  6. 写一个简单的例子统一演示一下,如下图
    Linux 命令行与 shell 脚本编程大全 14 处理用户输入
  7. 可以对比一下两个命令在处理相同内容时的区别,如下图
    • 首先,getopts 在处理参数时可以直接加入到 while 的条件判断中,这让语法变的更简单易懂
    • 其次,getoptscase 判断也更简单,每个分支前不需要使用 -a) ,而是直接 a) 即可
    • 用于跳过 -- 选项的分支判断在 getopts 中也不需要了
    • 最后最关键的是,在 while 的每次循环结束之前,不需要使用 shift 命令来对参数进行位置移动
      Linux 命令行与 shell 脚本编程大全 14 处理用户输入
  8. 如果在参数中添加空格,getopts 也可以很好的处理,如下图
    Linux 命令行与 shell 脚本编程大全 14 处理用户输入
  9. 如果在参数选项中传入了与定义的规范不匹配的选项,会被输出一个问号,如下图
    • 可以看到,-d 选项因为没有找到对应的分支判断,所以被识别为其他选项,但输出内容中可以正确的显示出来
    • 但是 -e 选项因为不在 getopts 定义的规范中,所以最后只输出来一个问号
      Linux 命令行与 shell 脚本编程大全 14 处理用户输入
  10. 如果说选项处理完毕后,还需要处理单独的参数列表,则需要在处理参数之前,使用 shift $[ $OPTIND - 1 ] 来跳过参数之前的所有选项,如下图
    • shift $[ $OPTIND - 1 ] 的意思如果不理解的话,这里简单解释一下
    • 首先,$[ $OPTIND - 1 ] 的意思是用 getopts 当前执行的选项的位置数减一,为什么要减一,因为命令行参数的位置从 0 开始的
    • 所以就相当于是使用 shift 将传入的参数内容中被 getopts 执行过的选项全部跳过
      Linux 命令行与 shell 脚本编程大全 14 处理用户输入
  11. 如果还是不理解,可以将上述脚本的 shift $[ $OPTIND - 1 ] 语句注释后再使用相同命令执行,如下图
    • 可以看到,所有的选项都被当做参数输出了
      Linux 命令行与 shell 脚本编程大全 14 处理用户输入

14.5 将选项标准化

  1. 脚本的选项虽然没有一个强制的语法规范,但有不少建议性规范,如果能遵守这些规范,会让你编写的脚本更通用,其他人在上手使用你的脚本时,学习门槛也更低
  2. 下图中提供一些选项的常用含义
    Linux 命令行与 shell 脚本编程大全 14 处理用户输入

14.6 获得用户输入

14.6.1 使用 read 命令进行基本的读取

  1. 使用 read 命令可以接收用户输入,输入的内容会被放置在后续的变量中,如下图
    • echo -n 表示输入内容后不换行
    • 可以看到,通过键盘输入的 asing1elife 被存入到变量 name 中
      Linux 命令行与 shell 脚本编程大全 14 处理用户输入
  2. 使用 read -p 命令可以实现合并输入前提醒语句的效果,如下图
    • 少了 echo -n 语句,却可以达到一样的输出效果
      Linux 命令行与 shell 脚本编程大全 14 处理用户输入
  3. 如果想讲输入的内容分配给多个变量,只需要将内容使用空格进行分隔,并且在 read 命令后使用多个变量进行接收,如下图
    Linux 命令行与 shell 脚本编程大全 14 处理用户输入
  4. 如果在输入的时候使用空格分隔了输入内容,但是在 read 命令后没有使用数量与之对应的变量分别接收,那么剩余的输入内容都会被直接存放到最后一个变量中,如下图
    • 可以看到,在脚本 read-p.sh 中,只有一个变量 name 用于接收输入内容
    • 那么所有的输入内容都被存放到这个变量中
    • 在脚本 read-multi.sh 中,输入内容通过逗号分隔后有 6 个,但是变量只有 3 个,所以从第 3 个输入开始,之后的所有内容都被存放到了第三个变量中
      Linux 命令行与 shell 脚本编程大全 14 处理用户输入
      Linux 命令行与 shell 脚本编程大全 14 处理用户输入
  5. 如果不想在输入命令的最后显式的指定一个变量用于内容接收,也可以直接使用特殊环境变量 REPLY 来获取输入内容,如下图
    • REPLY 变量其实就相当于一个默认的接收变量被隐式的放在了 read 命令的最后,所以会默认接收所有的输入内容
      Linux 命令行与 shell 脚本编程大全 14 处理用户输入

14.6.2 超时

  1. 为了防止用户一直不输入,而导致脚本的执行流行被阻断,可以使用 read -t second 来指定一个定时器
  2. 当输入时间超过指定的时间后,read 命令会返回一个非 0 的退出状态码,如下图
    Linux 命令行与 shell 脚本编程大全 14 处理用户输入

14.6.2.1 字数控制

  1. 使用 read -nNum 可以控制输入的字符数,例如 read -n1 就是当输入 1 个字符时就开始判断,如下图
    • 当输入一个字符时,不管输入的是什么,也不需要按回车键,脚本都会直接开始分支判断
      Linux 命令行与 shell 脚本编程大全 14 处理用户输入

14.6.3 隐藏方式读取

  1. 使用 read -s 命令可以避免输入的内容出现在显示器上,最典型的例子就是输入密码,如下图
    • 实际上,数据还是会显示,只是文本的颜色被设置成和终端的背景色一致,所以看不出来
      Linux 命令行与 shell 脚本编程大全 14 处理用户输入

14.6.4 从文件中读取

  1. 使用 read 命令可以读取文件中的数据,每次调用都会读取一行文本,当文件中没有剩余内容时,会返回非 0 的退出状态码,如下图
    • 首先使用 cat read-s.sh 读取文件内容
    • 然后通过管道将数据直接传递给 read 命令
    • read 再将每次读取的行为通过 while 进行循环
      Linux 命令行与 shell 脚本编程大全 14 处理用户输入