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

导览

命令行参数、read

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

tolal=$[ $1 * $2 ]

echo "The first parameter is $1"
echo "The first parameter is $2"
echo "The total value is $total"
image.png

如果文本字符串,参数中包含空格,必须要用引号(单引号或双引号均可)

14.1.2 使用 basename 读取脚本名

  1. 虽然通过 $0 可以获取脚本在执行时的名称,但如果脚本在执行时携带了相应的路径,也会出现在 $0 参数中
  2. 使用 basename 就可以过滤掉 $0 参数中脚本名以外的内容,从而得到一个纯净的脚本名称,如下图
[ttxie@41 part14]$ cat basename.sh 
#!/bin/bash

name=$(basename $0)

echo "The script name $name"

if [ $name = "addem" ]
then
 total=$[ $1 + $2 ]
elif [ $name = "multem" ]
then
 total=$[ $1 * $2 ]
fi

echo "The calculated value is $total"
image.png

14.1.3 测试参数

  1. 因为命令行参数是可选的,在执行脚本时,可以添加也可以不添加,所以在脚本中使用命令行参数时,最好用-n先判断一下,如下图
    • 在使用 test 命令对命令行参数进行条件判断时,不能直接使用 $1 ,需要使用 "$1" 才能正确获取到参数
[ttxie@41 part14]$ cat input.test.sh 
#!/bin/bash

if [ -n "$1" ]
then
 echo "Hello $1 "
else
echo "sorry, you did not identify yourself"
fi
image.png

14.2 特殊参数变量

14.2.1 参数统计

  1. 使用 $# 可以输出脚本在执行时传入了多少个参数,如下图,其中-ne 意思是不等于
[ttxie@41 part14]$ cat param-count.sh
#/bin/bash

if [ $# -ne 2 ]
then
   echo "Usage : param-count.sh a b"
else
   total=$[ $1 + $2 ]
   echo "The total is $total"
fi
image.png
  1. 既然 $# 可以返回传入参数的数量,那么理论上使用 ${$#} 就可以获取到最后一个参数的内容,但其实不是
  2. 要快速获取最后一个参数的内容需要通过 ${!#} ,如下图
#!/bin/bash

echo "The last parameter was ${$#}"
echo "The last parameter was ${!#}"
image.png

14.2.2 抓取所有的数据

  1. 使用 $* 可以获取通过命令行传入的所有参数,但这些参数会被作为一个字符整体被保存
    • 将传入的所有参数作为一个值
  2. 使用 $@ 也可以获取通过命令行传入的所有参数,这些参数则会被作为一个可以循环的字符串被保存
    • 将传入的每个参数作为单独的值
  3. 写一个简单的例子演示一下,如下图
    • 在对两者采用了相同遍历方式的操作之后,通过 $* 得到的是一个参数,而 $@ 得到的是 5 个参数
[ttxie@41 part14]$ cat get-all-param.sh
#!/bin/bash

count1=1

for param in "$*"
do
  echo "\$* parameter #$count1 = $param"
  count1=$[ $count1 + 1 ]
done

echo
count2=1

for param in "$@"
do
  echo "\$@ parameter #$count2 = $param"
  count2=$[ $count2 + 1 ]
done
image.png

14.3 使用 shift 命令移动变量

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

    • 这就是因为每次循环后都通过 shift 命令把后续的参数向左移动了一位

      image

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

  1. 使用 shift num 命令可以一次移动多个位置的变量,如下图

    image

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

  1. 上从图可知,使用 shift 2 命令将参数的位置移动两位,然后脚本在执行的时候传入了 6 个参数

  2. 这个时候如果只传入 5 个参数,那么脚本就会无限循环,如下图

    image

14.4 处理选项

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

14.4.1 查找选项

14.4.1.1 处理简单选项

  1. 当脚本在执行时传入了选项,通过 case 命令进行判断是最好的处理方式,如下图
    • 为了让传入的多个选项能在一次执行过程中都被执行到,需要先使用一个 while + shift 对选项进行遍历

    • 在每次遍历时都通过 case 命令对选项的具体参数进行判断

    • 同时还通过 *) 提供一个默认判断

      image

14.4.1.2 分离参数和选项

  1. 可以通过 双破折号( -- ) 将参数和选项进行分离,这样才执行脚本时就可以同时传入两者

  2. 这里的说法 不是指直接将参数和选项进行混排 ,而是 先传入参数后传入选项,或者先传入选项后传入参数 ,在这两者之间通过双破折号进行分离,如下图

    • 才判断选项的 case 命令中,新增了一个 --) 分支,用于判断分支的存在

    • 当从传入的内容中检索到这个分支时,会先执行一个 shift ,目的是将双破这行这个分隔符跳过去

    • 之后再执行一次 break 是为了跳出 while 循环,进行后续属于参数遍历的 for 循环

    • 可以看到,当直接执行 ./option-param.sh -a -b -e a b e 时,由于没有检测到 --) 分支,所以后续的参数也被识别成了错误的选项

    • 但是当使用双破折号将选项和参数分离后,就能顺利遍历到选项和参数了

      image
  3. 从上述这个例子,其实我们可以思考一下,双破折号真的是作为一个 强语法的特殊符号 来被 shell 识别的吗?很显然不是!

  4. 我们可以对上述脚本稍作修改,就可以得出下图的效果

    • 将上图中的 --) 分支判断部分修改为下图中的 -e) 分支判断

    • 然后执行上图中第一次执行导致错误结果的语句,却可以得到和上图第二次正确结果一致的输出

    • 这说明 双破折号作为参数和选项分隔符只是一个规范上的约定,并不是强语法的特殊符号

      image

14.4.1.3 处理带值的选项

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

      image

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 就是实际传入的参数内容,第二行的输出结果就是对这些传入的内容进行格式化后的结果

      image
  4. 默认如果传入的内容和规则不匹配,会抛出错误,可以通过 -q 选项忽略错误,如下图

    image

14.4.2.2 在脚本中使用

  1. getopt 命令自然是要在脚本中使用才有意义,而且在脚本中使用的语法基本都是固定的

  2. getopt 命令是用来规范参数和选项的,所以肯定是要放在脚本的最前端,这个也是毋庸置疑

  3. 一般只需要在脚本前端加上 set -- $(getopt -q ab:cd "$@") 即可

    • ab:cd 是可变化的部分,指的是具体参数和选项的匹配方式
  4. 写一个简单的例子演示一下,如下图

    • 这个例子是在 centOS 环境下运行的,因为 macOS 中的 set 命令无法按预期执行出效果

      image
  5. 上图中 case 命令的 --) 分支判断是必须添加的,否则在执行命令时最后一个分支会被多判断一次

  6. 至于具体是为什么多判断一次,可以在执行脚本前添加 sh -x 来对脚本进行 DEBUG ,如下图

    • 首先在原来的脚本中注释掉 --) 分支

    • 然后在与之前相同的脚本执行语句之前加上 sh -x ,之后就可以很清楚的看到脚本的执行过程

    • 在执行过程的第一处可以看到,传入的 -ab hello -cd 被解析后,最后多出来一个 --

    • 于是在脚本的最后,这个 -- 就被作为传入的选项执行了

    • 这就是为什么如果必须 case 命令添加 --) 分支,并且通过 shift + break 命令跳过的原因

      image

14.4.3 使用更高级的 getopts

  1. getopts 命令是 getopt 命令的复数版本( 其实说是升级版更贴切 )

    • 关键在于 getopts 可以处理 带空格和引号的参数 ,而 getopt 不行
  2. getoptsgetopt 更优秀的地方在于,每次调用时,只会处理一个参数,并且在处理完所有参数后会返回一个非 0 的退出状态码

    • getopt 是将参数和选项生成为一个完整的输出
  3. getopts 也可以忽略错误信息,只需要在参数选项规则之前加上一个冒号即可

  4. 如果某个选项后面需要跟一个参数,这个参数会被存储在 OPTARG 变量中

  5. 还有一个变量是 OPTIND ,用于存放 getopts 当前正在处理的参数位置

  6. 写一个简单的例子统一演示一下,如下图

    image
  7. 可以对比一下两个命令在处理相同内容时的区别,如下图

    • 首先,getopts 在处理参数时可以直接加入到 while 的条件判断中,这让语法变的更简单易懂

    • 其次,getoptscase 判断也更简单,每个分支前不需要使用 -a) ,而是直接 a) 即可

    • 用于跳过 -- 选项的分支判断在 getopts 中也不需要了

    • 最后最关键的是,在 while 的每次循环结束之前,不需要使用 shift 命令来对参数进行位置移动

      image
  8. 如果在参数中添加空格,getopts 也可以很好的处理,如下图

    image
  9. 如果在参数选项中传入了与定义的规范不匹配的选项,会被输出一个问号,如下图

    • 可以看到,-d 选项因为没有找到对应的分支判断,所以被识别为其他选项,但输出内容中可以正确的显示出来

    • 但是 -e 选项因为不在 getopts 定义的规范中,所以最后只输出来一个问号

      image
  10. 如果说选项处理完毕后,还需要处理单独的参数列表,则需要在处理参数之前,使用 shift $[ $OPTIND - 1 ] 来跳过参数之前的所有选项,如下图

*   `shift $[ $OPTIND - 1 ]` 的意思如果不理解的话,这里简单解释一下
*   首先,`$[ $OPTIND - 1 ]` 的意思是用 `getopts` 当前执行的选项的位置数减一,为什么要减一,因为命令行参数的位置从 0 开始的
*   所以就相当于是使用 `shift` 将传入的参数内容中被 `getopts` 执行过的选项全部跳过

    ![image](//upload-images.jianshu.io/upload_images/14012057-0690b535d2b70d73.png?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp)
  1. 如果还是不理解,可以将上述脚本的 shift $[ $OPTIND - 1 ] 语句注释后再使用相同命令执行,如下图
*   可以看到,所有的选项都被当做参数输出了

    ![image](//upload-images.jianshu.io/upload_images/14012057-7c7f9072bc6dd894.png?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp)

14.5 将选项标准化

  1. 脚本的选项虽然没有一个强制的语法规范,但有不少建议性规范,如果能遵守这些规范,会让你编写的脚本更通用,其他人在上手使用你的脚本时,学习门槛也更低

  2. 下图中提供一些选项的常用含义

    image

14.6 获得用户输入

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

  1. 使用 read 命令可以接收用户输入,输入的内容会被放置在后续的变量中,如下图
    • echo -n 表示输入内容后不换行
    • 可以看到,通过键盘输入的 asing1elife 被存入到变量 name 中
[ttxie@41 part14]$ cat read.sh 
#!/bin/bash

echo -n "Enter your name:"
read name
echo "hellp $name"
image.png
  1. 使用 read -p 命令可以实现合并输入前提醒语句的效果,如下图
    • 少了 echo -n 语句,却可以达到一样的输出效果
[ttxie@41 part14]$ cat read-p.sh
#!/bin/bash

read -p "Enter your age:" age
days=$[ $age * 365 ]
echo "That makes you over $days days old"
image.png
  1. 如果想讲输入的内容分配给多个变量,只需要将内容使用空格进行分隔,并且在 read 命令后使用多个变量进行接收,如下图
[ttxie@41 part14]$ cat read-multi.sh 
#!/bin/bash

read -p "请依次输入你的名字、性别、年龄(使用空格分隔):" name gender age

echo "你的名字:$name , 你的性别是:$gender , 年龄是:$age"
image.png
  1. 如果在输入的时候使用空格分隔了输入内容,但是在 read 命令后没有使用数量与之对应的变量分别接收,那么剩余的输入内容都会被直接存放到最后一个变量中,如下图

    • 可以看到,在脚本 read-p.sh 中,只有一个变量 name 用于接收输入内容

    • 那么所有的输入内容都被存放到这个变量中

    • 在脚本 read-multi.sh 中,输入内容通过逗号分隔后有 6 个,但是变量只有 3 个,所以从第 3 个输入开始,之后的所有内容都被存放到了第三个变量中

      image
      image
  2. 如果不想在输入命令的最后显式的指定一个变量用于内容接收,也可以直接使用特殊环境变量 REPLY 来获取输入内容,如下图

    • REPLY 变量其实就相当于一个默认的接收变量被隐式的放在了 read 命令的最后,所以会默认接收所有的输入内容
[ttxie@41 part14]$ cat read-REPLY.sh 
#!/bin/bash

read -p "Enter your name:"

echo "hello $REPLY"
image.png

14.6.2 超时

  1. 为了防止用户一直不输入,而导致脚本的执行流行被阻断,可以使用 read -t second 来指定一个定时器
  2. 当输入时间超过指定的时间后,read 命令会返回一个非 0 的退出状态码,如下图
[ttxie@41 part14]$ cat read-time.sh 
#!/bin/bash

if read -t 5 -p "Enter your name:"
then
  echo "HELLO, $REPLY"
else
  echo "sorry, too slow"
fi
image.png

14.6.2.1 字数控制

  1. 使用 read -nNum 可以控制输入的字符数,例如 read -n1 就是当输入 1 个字符时就开始判断,如下图
    • 当输入一个字符时,不管输入的是什么,也不需要按回车键,脚本都会直接开始分支判断
[ttxie@41 part14]$ cat read-nNum.sh 
#!/bin/bash

read -n1 -p "Do you want to continue [Y/N]:"

case $REPLY in
Y | y ) echo
        echo "fine,continue";;
N | n ) echo
        echo "ok, bye";;
esac

echo "END of the script"
image.png

14.6.3 隐藏方式读取

  1. 使用 read -s 命令可以避免输入的内容出现在显示器上,最典型的例子就是输入密码,如下图
    • 实际上,数据还是会显示,只是文本的颜色被设置成和终端的背景色一致,所以看不出来
[ttxie@41 part14]$ cat read-s.sh 
#!/bin/bash

read -s -p "请输入你的密码:" password
echo
read -n1 -p "你的密码是$password吗?[Y/N]"
echo

case $REPLY in
  Y | y) echo “ok,你的密码是$password” ;;
  N | n) echo "密码错误" ;;
  * ) echo "错误操作" ;;
esac
image.png

14.6.4 从文件中读取

  1. 使用 read 命令可以读取文件中的数据,每次调用都会读取一行文本,当文件中没有剩余内容时,会返回非 0 的退出状态码,如下图
    • 首先使用 cat read-s.sh 读取文件内容
    • 然后通过管道将数据直接传递给 read 命令
    • read 再将每次读取的行为通过 while 进行循环
[ttxie@41 part14]$ cat read-file.sh
#!/bin/bash

count=1

cat read-s.sh |while read line
do
  echo "第$count 行:$line"
  count=$[ $count + 1 ]
done
image.png

部分内容转载来自:
作者:asing1elife
链接:https://www.jianshu.com/p/71d1a165ffae
来源:
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

你可能感兴趣的:(【Linux 命令行与 shell 脚本编程大全】 14 处理用户输入)