shell处理命令行选项和参数

  • 原创
  • Madman
  • /
  • 2018-04-25 09:16
  • /
  • 0
  • 372 次阅读

shell处理命令行选项和参数-min.png

Synopsis: shell编程时经常要处理用户输入,除了使用read命令获取数据外,最直接的是从命令行参数和选项处来获取用户输入。命令行参数允许用户运行脚本时直接从命令行输入数据,脚本通过位置参数来取回命令行参数并将它们赋值给位置变量。case结合shift命令基本上能处理大部分情形的选项和参数,但是不能正确处理多个短选项合并的情形。外部命令getopt和内置命令getopts可以很好的处理命令行选项,但不支持长选项,且getopt不能正确处理选项后面跟带空格的参数值

1. 命令行参数

1.1 位置变量

向shell脚本传递数据的最基本方式是使用命令行参数(command line parameters),bash shell将在命令行中输入的所有参数(以空格分隔)赋值给一些特殊变量,这些变量称为位置变量(positional parameter),其中$0为shell执行的程序的名称,$1为第一个参数,$2为第二个参数,依次类推,直到$9为第九个参数。在第9个变量之后,必须使用大括号将变量括起来,如${10}

1. 有两个命令行参数,$1被赋值10,$2被赋值20
[root@CentOS ~]# ./test.sh 10 20

2. 传递包含空格的参数值,必须使用引号(单引号和双引号均可),$1被赋值Hello World
[root@CentOS ~]# ./test.sh "Hello World"

在shell脚本中使用命令行参数前,要对参数进行检查,确保参数变量中确实存在数据,否则脚本可能会产生错误:

1. 脚本内容
#!/bin/bash

if [ -n "$1" ]; then
  echo "hello $1, glad to meet you."
else
  echo "Sorry, you didn't identify yourself."
fi

2. 测试
[root@CentOS ~]# ./test.sh
Sorry, you didn't identify yourself.
[root@CentOS ~]# ./test.sh wangy
hello wangy, glad to meet you.

或者使用[[ ]],里面的参数变量可以不用双引号包括起来:

1. 脚本内容
#!/bin/bash

if [[ -n $1 ]]; then
  echo "hello $1, glad to meet you."
else
  echo "Sorry, you didn't identify yourself."
fi

2. 测试
[root@CentOS ~]# ./test.sh
Sorry, you didn't identify yourself.
[root@CentOS ~]# ./test.sh wangy
hello wangy, glad to meet you.

如果条件中使用[ ],且参数没有使用双引号,那么当用户不传参数的时候,$1为空,这时就会变成if [ -n ];,结果是错误的,而有双引号时是if [ -n "" ];

1. 脚本内容
#!/bin/bash

if [ -n $1 ]; then
  echo "hello $1, glad to meet you."
else
  echo "Sorry, you didn't identify yourself."
fi

2. 测试
[root@CentOS ~]# ./test.sh
hello , glad to meet you.  # 错误
[root@CentOS ~]# ./test.sh wangy
hello wangy, glad to meet you.

或者,可以加一个辅助字符串来进行比较,如if [ x$1 != x ]

1. 脚本内容
#!/bin/bash

if [ x$1 != x ]; then
  echo "hello $1, glad to meet you."
else
  echo "Sorry, you didn't identify yourself."
fi

2. 测试
[root@CentOS ~]# ./test.sh
Sorry, you didn't identify yourself.
[root@CentOS ~]# ./test.sh wangy
hello wangy, glad to meet you.

1.2 参数计数

特殊变量$#中存储了执行脚本时包含的命令行参数的个数,最后一个命令行参数值不是${$#},因为不能在大括号中使用美元符号$,必须将它换成感叹号!,即${!#}

1. 脚本内容
#!/bin/bash

if [ $# -ne 2 ]; then
  echo "Usage: test.sh a b"
else
  total=$[ $1 + $2 ]
  echo "The total is $total"
fi

2. 测试
[root@CentOS ~]# ./test.sh
Usage: test.sh a b
[root@CentOS ~]# ./test.sh 10
Usage: test.sh a b
[root@CentOS ~]# ./test.sh 10 20
The total is 30

1.3 获取所有参数

特殊变量$*$@都包含所有命令行参数,它们不被双引号" "包括时是一样的。不带双引号时,两种都不建议使用,因为参数包含空格或通配符,它们就可能意外中断

1. 脚本内容
#!/bin/bash

vim $*  # 这里换成vim $@也一样

2. 测试,假设要打开当前目录下的foo.txt、bar.txt、hello\ world.txt这三个文件,最后一个文件名有空格,但是参数传递给脚本后,被识别成要打开foo.txt、bar.txt、hello、world.txt四个文件,这是错误的(vim打开多个文件,可以用:ls查看打开的文件列表)
[root@CentOS ~]# ./test.sh foo.txt bar.txt "hello world.txt"

但是当它们被双引号包括时是有区别的,"$*"把所有命令行参数当作一个字串,每个参数的值由IFS特殊变量的第一个字符分隔,即"$1c$2c...",其中cIFS变量值的第一个字符(默认是空格)。如果IFS为空IFS=,参数之间直接连接,即"$1$2..."。如果IFS未设置unset IFS,则参数由空格分隔。

"$@"把命令行参数当作多个字串,每个参数都会扩展为单独的字串,即"$1" "$2" ...。如果双引号扩展出现在字串中,则第一个参数的扩展与原始字串的开始部分连接,最后一个参数的扩展连接到原始字串的最后部分。

$*$@"$*""$@"分别被扩展成几个字串?

1. 脚本内容
#!/bin/bash

function main {
   echo 'MAIN sees ' $# ' args'
}

main $*
main $@

main "$*"
main "$@"

2. 测试
[root@CentOS ~]# ./test.sh foo bar "hello world"
MAIN sees  4  args  # 四个参数,分别是foo、bar、hello、world
MAIN sees  4  args  # 四个参数,分别是foo、bar、hello、world
MAIN sees  1  args  # 一个参数,即"foo bar hello world"(引号不算在内)
MAIN sees  3  args  # 三个参数,分别是foo、bar、"hello world"(引号不算在内),只有它是正确的

如果需要分开成多个字串,请使用"$@"; 如果需要将参数当作一个字串传给其它程序如grep,请使用"$*"

1. 脚本内容
#!/bin/bash

echo "*** \$* and \$@ are not put into double quotes(\" \") ***"
count=1
for param in $*; do
  echo "\$* Parameters #$count = '$param'"
  count=$[ $count + 1 ]
done

count=1
for param in $@; do
  echo "\$@ Parameters #$count = '$param'"
  count=$[ $count + 1 ]
done
echo "---------------------------"

echo "*** IFS is set to '+fuck' ***"
IFS="+fuck"
count=1
for param in "$*"; do
  echo "\"\$*\" Parameters #$count = '$param'"
  count=$[ $count + 1 ]
done

count=1
for param in "$@"; do
  echo "\"\$@\" Parameters #$count = '$param'"
  count=$[ $count + 1 ]
done
echo "---------------------------"

echo "*** IFS is set to null ***"
IFS=
count=1
for param in "$*"; do
  echo "\"\$*\" Parameters #$count = '$param'"
  count=$[ $count + 1 ]
done

count=1
for param in "$@"; do
  echo "\"\$@\" Parameters #$count = '$param'"
  count=$[ $count + 1 ]
done
echo "---------------------------"

echo "*** IFS is unset ***"
unset IFS
count=1
for param in "$*"; do
  echo "\"\$*\" Parameters #$count = '$param'"
  count=$[ $count + 1 ]
done

count=1
for param in "$@"; do
  echo "\"\$@\" Parameters #$count = '$param'"
  count=$[ $count + 1 ]
done

2. 测试
[root@CentOS ~]# ./test.sh foo bar "hello world"
*** $* and $@ are not put into double quotes(" ") ***
$* Parameters #1 = 'foo'
$* Parameters #2 = 'bar'
$* Parameters #3 = 'hello'
$* Parameters #4 = 'world'
$@ Parameters #1 = 'foo'
$@ Parameters #2 = 'bar'
$@ Parameters #3 = 'hello'
$@ Parameters #4 = 'world'
---------------------------
*** IFS is set to '+fuck' ***
"$*" Parameters #1 = 'foo+bar+hello world'
"$@" Parameters #1 = 'foo'
"$@" Parameters #2 = 'bar'
"$@" Parameters #3 = 'hello world'
---------------------------
*** IFS is set to null ***
"$*" Parameters #1 = 'foobarhello world'
"$@" Parameters #1 = 'foo'
"$@" Parameters #2 = 'bar'
"$@" Parameters #3 = 'hello world'
---------------------------
*** IFS is unset ***
"$*" Parameters #1 = 'foo bar hello world'
"$@" Parameters #1 = 'foo'
"$@" Parameters #2 = 'bar'
"$@" Parameters #3 = 'hello world'

注意: 当没有位置参数时,"$@"$@都展开为空(即它们被移除),而"$*"是空的单词(即""

[root@CentOS ~]# ./test.sh
*** $* and $@ are not put into double quotes(" ") ***
---------------------------
*** IFS is set to '+fuck' ***
"$*" Parameters #1 = ''
---------------------------
*** IFS is set to null ***
"$*" Parameters #1 = ''
---------------------------
*** IFS is unset ***
"$*" Parameters #1 = ''

1.4 移位命令shift

shift命令能够改变命令行参数的相对位置,默认将每个参数变量左移一个位置(可以为shift命令提供要移动的位置数目来实现多位移变化)。于是,变量$3的值移给变量$2,变量$2的值移给变量$1,而变量$1的值被丢弃(注意$0的值和程序名称都保持不变)。

在不清楚命令行传递多少个参数的情况下,shift命令是迭代命令行参数的另一种好办法。可以先对第一个参数进行操作,然后对参数进行一次左移,再对第一个参数进行操作。

1. 脚本内容
#!/bin/bash

count=1
while [ -n "$1" ]; do
  echo "Parameter #$count = $1"
  count=$[ $count + 1 ]
  shift
done

2. 测试
[root@CentOS ~]# ./test.sh apple oragne pear
Parameter #1 = apple
Parameter #2 = oragne
Parameter #3 = pear

2. 命令行选项

短选项是由一个破折号引导的单个字母,长选项是由两个破折号引导的字符串,选项用来改变命令的行为。从表面上看,命令行选项和参数一样跟随在脚本名称之后,可以像处理命令行参数一样处理命令行选项。选项后面也可以指定参数值

1. -a、-b、-c都是短选项,且它们没有附加的参数值
# ./test.sh -a -b -c

2. 效果同上,多个短选项可以写在一起
# ./test.sh -abc

3. -a选项后面需要指定参数值,arg是选项-a的参数值。而-b和-c不需要
# ./test.sh -a arg -b -c

4. --a-long和--c-long都是长选项,但--c-long后面需要指定参数值(长选项后面如果跟参数既可以使用等号=连接,也可以使用空格)
# ./test.sh --a-long -b --c-long=/tmp

5. 使用双破折号将命令行中选项和参数分开,先用while循环,当$1等于--时,shift命令将--从参数变量中移除,并用break提前跳出此循环。再用for循环剩余的$@,此时全是命令行参数
# ./test.sh -c -a -b -- test1 test2 test3

2.1 case+shift找出选项

(1) 处理简单选项

可以像处理命令行参数一样,使用shift命令处理命令行选项,在抽取每个位置参数时,使用case语句判断位置参数是否符合选项格式:

1. 脚本内容
#!/bin/bash

while [ -n "$1" ]; do
  case "$1" in
    -a) echo "Found the -a option" ;;
    -b) echo "Found the -b option" ;;
    -c) echo "Found the -c option" ;;
    *) echo "$1 is not an option" ;;
  esac
  shift
done

2. 测试
[root@CentOS ~]# ./test.sh -a -b -c -d
Found the -a option
Found the -b option
Found the -c option
-d is not an option

无论选项在命令行中以什么顺序出现,此方法都有效:

[root@CentOS ~]# ./test.sh -d -c -a
-d is not an option
Found the -c option
Found the -a option

(2) 从参数中分离选项

Linux中可以使用特殊字符双破折号--来将命令行中的选项和普通参数分开,它指明选项结束和普通参数开始的位置,发现双破折号后,脚本就能安全的将剩余的命令行参数作为参数而不是选项处理:

1. 脚本内容
#!/bin/bash

while [ -n "$1" ]; do
  case "$1" in
    -a) echo "Found the -a option" ;;
    -b) echo "Found the -b option" ;;
    -c) echo "Found the -c option" ;;
    --) shift
        break ;;
    *) echo "$1 is not an option" ;;
  esac
  shift
done

count=1
for param in "$@"; do
  echo "Parameter #$count: $param"
  count=$[ $count + 1 ]
done

2. 测试
[root@CentOS ~]# ./test.sh -c -a -b test1 test2 test3
Found the -c option
Found the -a option
Found the -b option
test1 is not an option
test2 is not an option
test3 is not an option

# 使用双破折号将命令行中选项与参数分开
[root@CentOS ~]# ./test.sh -c -a -b -- test1 test2 test3
Found the -c option
Found the -a option
Found the -b option
Parameter #1: test1
Parameter #2: test2
Parameter #3: test3

(3) 处理带值的选项

有些选项需要附加参数值,要求脚本必须能够检测命令选项何时需要附加参数值,并能够正确获取该选项的参数值:

1. 脚本内容
#!/bin/bash

while [ -n "$1" ]; do
  case "$1" in
    -a) echo "Found the -a option" ;;
    -b) shift  # shift之前$1的值是-b,左移一位后,$1是-b选项的参数值
        echo "Found the -b option, with parameter value $1" ;;
    -c) echo "Found the -c option" ;;
    --) shift
        break ;;
    *) echo "$1 is not an option" ;;
  esac
  shift
done

count=1
for param in "$@"; do
  echo "Parameter #$count: $param"
  count=$[ $count + 1 ]
done

2. 测试
[root@CentOS ~]# ./test.sh -a -b test1 -d
Found the -a option
Found the -b option, with parameter value test1
-d is not an option

# 改变选项的顺序
[root@CentOS ~]# ./test.sh -b test1 -a -d
Found the -b option, with parameter value test1
Found the -a option
-d is not an option

(4) 缺陷

但是此方法也有局限性,当试图在一个位置参数中合并多个选项时,就会产生问题:

[root@CentOS ~]# ./test.sh -b test1 -ac
Found the -b option, with parameter value test1
-ac is not an option

2.2 使用getopt命令

getopt命令可以接受任意形式的命令行选项和参数列表,并自动将这些选项和参数转换为适当的格式。

(1) 命令格式

getopt options optstring parameters

其中,选项字符串(optstring)是getopt命令处理的关键,它定义命令行中的有效选项字母,同时它还指明了哪此选项字母需要参数值(在每个需要参数值的选项字母后面放置一个冒号:)。

getopt命令根据定义的选项字符串解析提供的参数列表(parameters):

[root@CentOS ~]# getopt ab:cd -a -b test1 -cd test2 test3
 -a -b test1 -c -d -- test2 test3

示例中选项字符串ab:cd定义了a、b、c和d四个有效选项字母,且选项b需要参数值。getopt命令根据选项字符串optstring对后面的参数列表parameters进行解析,并且自动将-cd分隔成两个不同的选项,并插入双破折号来分隔命令行中的额外参数

如果提供的参数列表parameters中包含了不在选项字符串optstring中指定的选项,默认会生成一个错误消息:

[root@CentOS ~]# getopt ab:cd -a -b test1 -cde test2 test3
getopt: invalid option -- 'e'
 -a -b test1 -c -d -- test2 test3

getopt命令的-q选项可以忽略这个错误,要注意的是,getopt的选项options必须位于选项字符串optstring的前面,不然会被当作要解析的参数列表parameters:

[root@CentOS ~]# getopt -q ab:cd -a -b test1 -cde test2 test3
 -a -b 'test1' -c -d -- 'test2' 'test3'

# 这是错误的
[root@CentOS ~]# getopt ab:cd -q -a -b test1 -cde test2 test3
getopt: invalid option -- 'q'
getopt: invalid option -- 'e'
 -a -b test1 -c -d -- test2 test3

(2) 在shell脚本中使用getopt

在脚本中使用getopt命令的格式类似如下:

set -- $(getopt -q ab:cd "$@")

首先,原始的命令行参数与选项会被getopt命令格式化,其返回值被传递给set命令,再结合set命令的--选项,最终的命令行参数是被getopt替换后的参数。比如,原始参数是-a -b test1 -cd test2 test3,那么使用getopt和set命令替换后的参数是-a -b test1 -c -d -- test2 test3

在前面步骤建立的脚本中添加一行:

#!/bin/bash

set -- $(getopt -q ab:cd "$@")
while [ -n "$1" ]; do
  case "$1" in
    -a) echo "Found the -a option" ;;
    -b) shift
        echo "Found the -b option, with parameter value $1" ;;
    -c) echo "Found the -c option" ;;
    --) shift
        break ;;
    *) echo "$1 is not an option" ;;
  esac
  shift
done

count=1
for param in $@; do
  echo "Parameter #$count: $param"
  count=$[ $count + 1 ]
done

现在能够正确识别合并后的短选项:

[root@CentOS ~]# ./test.sh -ac
Found the -a option
Found the -c option

所有的原始功能还能顺利工作:

[root@CentOS ~]# ./test.sh -a -b test1 -cd test2 test3
Found the -a option
Found the -b option, with parameter value 'test1'
Found the -c option
-d is not an option
Parameter #1: 'test2'
Parameter #2: 'test3'

(3) 缺陷

但是,getopt不能正确处理带空格的参数值

[root@CentOS ~]# ./test.sh -a -b "hello world" -cd "test2 test3" test4
Found the -a option
Found the -b option, with parameter value 'hello
world' is not an option
Found the -c option
-d is not an option
Parameter #1: 'test2
Parameter #2: test3'
Parameter #3: 'test4'

2.3 内置命令getopts

(1) 命令格式

getopts optstring variable

其中,选项字符串(optstring)getopt中的类似,它定义命令行中的有效选项字母,同时它还指明了哪此选项字母需要参数值(在每个需要参数值的选项字母后面放置一个冒号:),如果要去掉错误消息的话,可以在optstring最前面加一个冒号:

getopt一次性将所有命令行选项和参数解析成一个字串不同的是,getopts它每被调用一次,只处理一个命令行上检测到的参数,并且将当前参数保存在命令行中定义的variable中,注意getopts命令解析命令行选项时,它会移除选项开头的单破折号,所以variable中没保存破折号。结合while循环,getopts可以解析命令行所有选项。

1. 脚本内容
#!/bin/bash

while getopts :ac opt
do
    case $opt in
        a) echo "Found the -a option" ;;
        c) echo "Found the -c option" ;;
        *) echo "Unknown option: $opt" ;;
    esac
done

2. 测试
[root@CentOS ~]# ./test.sh -ac
Found the -a option
Found the -c option

# 找到的所有未定义的选项统一输出成问号
[root@CentOS ~]# ./test.sh -ac -d -e
Found the -a option
Found the -c option
Unknown option: ?
Unknown option: ?

# 如果省略:ac最前面的冒号
[root@CentOS ~]# ./test.sh -ac -d -e
Found the -a option
Found the -c option
./param.sh: illegal option -- d
Unknown option: ?
./param.sh: illegal option -- e
Unknown option: ?

(2) OPTARG

getopts命令会用到两个环境变量,如果选项需要跟一个参数值,OPTARG环境变量就会保存这个参数值。

1. 脚本内容
#!/bin/bash

while getopts :ab:c opt
do
    case $opt in
        a) echo "Found the -a option" ;;
        b) echo "Found the -b option, with value $OPTARG" ;;
        c) echo "Found the -c option" ;;
        *) echo "Unknown option: $opt" ;;
    esac
done

2. 测试
[root@CentOS ~]# ./test.sh -abtest1 -c
Found the -a option
Found the -b option, with value test1
Found the -c option
[root@CentOS ~]# ./test.sh -a -b "hello world" -c -d
Found the -a option
Found the -b option, with value hello world  # 可以正确处理带空格的参数值
Found the -c option
Unknown option: ?

(3) OPTIND

OPTIND环境变量保存了参数列表中getopts正在处理的参数位置,在getopts处理每个选项时,它会将OPTIND环境变量值增加1(从1开始计数)。在getopts完成处理时,你可以结合shift命令来移动参数,如shift $[ $OPTIND - 1 ],这样就移除了所有刚处理过的选项(包括选项带的参数值),只剩下待处理的其它命令行参数了:

1. 脚本内容
#!/bin/bash

while getopts :ab:cd opt
do
    case $opt in
        a) echo "Found the -a option" ;;
        b) echo "Found the -b option, with value $OPTARG" ;;
        c) echo "Found the -c option" ;;
        d) echo "Found the -d option" ;;
        *) echo "Unknown option: $opt" ;;
    esac
done
shift $[ $OPTIND -1 ]

count=1
for param in "$@"
do
  echo "Parameter #$count: '$param'"
  count=$[ $count + 1 ]
done

2. 测试
[root@CentOS ~]# ./test.sh -a -b "test1 test2" -d test3 test4
Found the -a option
Found the -b option, with value test1 test2
Found the -d option
Parameter #1: 'test3'
Parameter #2: 'test4'

3. 总结

  • getoptgetopts不支持长选项
  • getopt不能正确处理带空格的参数值
  • case+shift可以处理长选项,也可以处理带空格的参数值,但是不支持多个短选项合并
  • 如果只使用短选项,建议用getopts;如果想提供长、短选项(长选项字面意义明显),且不需要合并多个短选项,建议用case+shift
while [ $# -gt 0 ]; do
    case "$1" in
        --help | -h) usage ;;
        --team_device | -d) shift; team_device=$1 ;;
        --team_type | -t) shift; team_type=$1 ;;
        --team_ports | -p) shift; team_ports=$1 ;;
        *) shift ;;
    esac
    shift
done
未经允许不得转载: LIFE & SHARE - 王颜公子 » shell处理命令行选项和参数

分享

作者

作者头像

Madman

如果博文内容有误或其它任何问题,欢迎留言评论,我会尽快回复; 或者通过QQ、微信等联系我

0 条评论

暂时还没有评论.

发表评论前请先登录