shell相关3

read命令

read将用户的输入存入一个变量,方便后面的代码使用:

read [-options] [variable...]

options是参数选项,variable是用来保存输入数值的一个或多个变量名。如果没有提供变量名,环境变量REPLY会包含用户输入的一整行数据。
比如:

#!/bin/bash

echo -n "输入一些文本 > "
read text
echo "你的输入:$text"

执行结果如下:

$ bash demo.sh
输入一些文本 > 你好,世界
你的输入:你好,世界

使用read接受多个输入:

#!/bin/bash
echo Please, enter your firstname and lastname
read FN LN  #输入时两个输入用空格隔开
echo "Hi! $LN, $FN !"

如果用户的输入项少于read命令给出的变量数目,那么额外的变量值为空。如果用户的输入项多于定义的变量,那么多余的输入项会包含到最后一个变量中:

#read-single文件
#!/bin/bash
# read-single: read multiple values into default variable
echo -n "Enter one or more values > "
read
echo "REPLY = '$REPLY'"

#执行read-single
$ read-single
Enter one or more values > a b c d
REPLY = 'a b c d'

read命令可以用来读文件:

#!/bin/bash

filename='/etc/hosts'

while read myline
do
  echo "$myline"
done < $filename

上面的例子通过read命令,读取一个文件的内容。done命令后面的定向符<,将文件内容导向read命令,每次读取一行,存入变量myline,直到文件读取完毕。

read的参数:
-t:设置了超时的秒数。如果超过了指定时间,用户仍然没有输入,脚本将放弃等待,继续向下执行。
环境变量TMOUT也可以起到同样作用:

$ TMOUT=3
$ read response

-p:指定用户输入的提示信息。
-a:把用户的输入赋值给一个数组,从零号位置开始:

$ read -a people
alice duchess dodo
$ echo ${people[2]}
dodo

-n:参数指定只读取若干个字符作为变量值,而不是整行读取:

$ read -n 3 letter
abcdefghij
$ echo $letter
abc

-e:参数允许用户输入的时候,使用readline库提供的快捷键,比如自动补全:

#!/bin/bash

echo Please input the path to the file:

read -e fileName

echo $fileName

上面例子中,read命令接受用户输入的文件名。这时,用户可能想使用 Tab 键的文件名“自动补全”功能,但是read命令的输入默认不支持readline库的功能。-e参数就可以允许用户使用自动补全。

read命令读取的值,默认是以空格分隔。可以通过自定义环境变量IFS(内部字段分隔符,Internal Field Separator 的缩写),修改分隔标志。
IFS的默认值是空格、Tab 符号、换行符号,通常取第一个(即空格)。

如下,通过修改IFS的值为:,自动分析/etc/passwd文件:

#!/bin/bash
# read-ifs: read fields from a file

FILE=/etc/passwd

read -p "Enter a username > " user_name
file_info="$(grep "^$user_name:" $FILE)"

if [ -n "$file_info" ]; then
  IFS=":" read user pw uid gid name home shell <<< "$file_info"
  echo "User = '$user'"
  echo "UID = '$uid'"
  echo "GID = '$gid'"
  echo "Full Name = '$name'"
  echo "Home Dir. = '$home'"
  echo "Shell = '$shell'"
else
  echo "No such user '$user_name'" >&2
  exit 1
fi

IFS的赋值命令和read命令写在一行,这样的话,IFS的改变仅对后面的命令生效,该命令执行后IFS会自动恢复原来的值。
<<<是 Here 字符串,用于将变量值转为标准输入,因为read命令只能解析标准输入。

如果IFS设为空字符串,就等同于将整行读入一个变量:

#!/bin/bash
input="/path/to/txt/file"
while IFS= read -r line   #-r:raw 模式,表示不把用户输入的反斜杠字符解释为转义字符
do
  echo "$line"
done < "$input"

注意,上述代码最后一段done < "$input",即done < "/path/to/txt/file",该字符串并不是标准输入,无法传入read,而该字符串表示的文件为标准输入,所以read读取的是对应文件的内容。若改成:

#!/bin/bash
input="/path/to/txt/file"
while IFS= read -r line   #-r:raw 模式,表示不把用户输入的反斜杠字符解释为转义字符
do
  echo "$line"
done <<< "$input"

输出的则是/path/to/txt/file,因为<<<可以将字符串转换为标准输入。

条件判断

if语句结构如下:

if commands; then
  commands
[elif commands; then
  commands...]
[else
  commands]
fi
#or
if commands 
then
  commands
[elif commands
then
  commands...]
[else
  commands]
fi

比如:

if test $USER = "foo"; then     #test用于检测后面语句是否成立
  echo "Hello foo."
else
  echo "You are not foo."
fi

可以直接使用true或者false进行判断:

$ if true; then echo 'hello world'; fi
hello world

$ if false; then echo "It's true."; fi

注意,if关键字后面也可以是一条命令,该条命令执行成功(返回值0),就意味着判断条件成立:

$ if echo 'hi'; then echo 'hello world'; fi
hi
hello world

if后面可以跟任意数量的命令。这时,所有命令都会执行,但是判断真伪只看最后一个命令,即使前面所有命令都失败,只要最后一个命令返回0,就会执行then的部分:

$ if false; true; then echo 'hello world'; fi
hello world

test语句

if结构的判断条件,一般使用test命令,有三种形式:

# 写法一
test expression

# 写法二
[ expression ]

# 写法三     
[[ expression ]]   #只有该形式支持正则判断

第二种和第三种写法,[和]与内部的表达式之间必须有空格。
比如:

$ test -f /etc/hosts
$ echo $?
0   #真

$ [ -f /etc/hosts ]      #与上一种等价
$  echo $?
0   #真

以下为常用的判断文件状态的语句:

[ -a file ]:如果 file 存在,则为true
[ -d file ]:如果 file 存在并且是一个目录,则为true
[ -e file ]:如果 file 存在,则为true
[ -f file ]:如果 file 存在并且是一个普通文件,则为true

比如:

#!/bin/bash

FILE=~/.bashrc

if [ -e "$FILE" ]; then
  if [ -f "$FILE" ]; then
    echo "$FILE is a regular file."
  fi
  if [ -d "$FILE" ]; then
    echo "$FILE is a directory."
  fi
  if [ -r "$FILE" ]; then
    echo "$FILE is readable."
  fi
  if [ -w "$FILE" ]; then
    echo "$FILE is writable."
  fi
  if [ -x "$FILE" ]; then
    echo "$FILE is executable/searchable."
  fi
else
  echo "$FILE does not exist"
  exit 1
fi

注意,上面代码中,$FILE要放在双引号之中,这样可以防止变量$FILE为空,从而出错。因为$FILE如果为空,这时[ -e $FILE ]就变成[ -e ],这会被判断为真。而$FILE放在双引号之中,[ -e "$FILE" ]就变成[ -e "" ],这会被判断为伪。

以下为常用的字符串判断语句:

[ string ]:如果string不为空(长度大于0),则判断为真。
[ -n string ]:如果字符串string的长度大于零,则判断为真。
[ -z string ]:如果字符串string的长度为零,则判断为真。
[ string1 = string2 ]:如果string1和string2相同,则判断为真。
[ string1 == string2 ] 等同于[ string1 = string2 ]。
[ string1 != string2 ]:如果string1和string2不相同,则判断为真。
[ string1 '>' string2 ]:如果按照字典顺序string1排列在string2之后,则判断为真。
[ string1 '<' string2 ]:如果按照字典顺序string1排列在string2之前,则判断为真。

注意,test命令内部的><,必须用引号引起来(或者是用反斜杠转义)。否则,它们会被 shell 解释为重定向操作符。
比如:

#!/bin/bash

ANSWER=maybe

if [ -z "$ANSWER" ]; then
  echo "There is no answer." >&2
  exit 1
fi
if [ "$ANSWER" = "yes" ]; then
  echo "The answer is YES."
elif [ "$ANSWER" = "no" ]; then
  echo "The answer is NO."
elif [ "$ANSWER" = "maybe" ]; then
  echo "The answer is MAYBE."
else
  echo "The answer is UNKNOWN."
fi

注意,字符串判断时,变量要放在双引号之中,比如[ -n "$COUNT" ],否则变量替换成字符串以后,test命令可能会报错,提示参数过多。另外,如果不放在双引号之中,变量为空时,命令会变成[ -n ],这时会判断为真。如果放在双引号之中,[ -n "" ]就判断为伪。

以下为常用的整数判断语句:

[ integer1 -eq integer2 ]:如果integer1等于integer2,则为true。
[ integer1 -ne integer2 ]:如果integer1不等于integer2,则为true。
[ integer1 -le integer2 ]:如果integer1小于或等于integer2,则为true。
[ integer1 -lt integer2 ]:如果integer1小于integer2,则为true。
[ integer1 -ge integer2 ]:如果integer1大于或等于integer2,则为true。
[ integer1 -gt integer2 ]:如果integer1大于integer2,则为true。

比如:

#!/bin/bash

#INT=-5
read INT

if [ -z "$INT" ]; then
  echo "INT is empty." >&2
  exit 1
fi
if [ $INT -eq 0 ]; then
  echo "INT is zero."
else
  if [ $INT -lt 0 ]; then
    echo "INT is negative."
  else
    echo "INT is positive."
  fi
  if [ $((INT % 2)) -eq 0 ]; then
    echo "INT is even."
  else
    echo "INT is odd."
  fi
fi 

上面例子中,先判断变量$INT是否为空,然后判断是否为0,接着判断正负,最后通过求余数判断奇偶。

正则判断语句:

[[ string1 =~ regex ]]

比如:

#!/bin/bash

INT=-5

if [[ "$INT" =~ ^-?[0-9]+$ ]]; then
  echo "INT is an integer."
  exit 0
else
  echo "INT is not an integer." >&2
  exit 1
fi

上面代码中,先判断变量INT的字符串形式,是否满足^-?[0-9]+$的正则模式,如果满足就表明它是一个整数。

逻辑判断:
通过逻辑运算,可以把多个test判断表达式结合起来,创造更复杂的判断。三种逻辑运算AND,OR,和NOT,都有自己的专用符号(逻辑判断需要使用[[...]]):

AND运算:符号&&,也可使用参数-a。
OR运算:符号||,也可使用参数-o。
NOT运算:符号!。

比如:

#!/bin/bash

MIN_VAL=1
MAX_VAL=100

INT=50

if [[ "$INT" =~ ^-?[0-9]+$ ]]; then
  if [[ $INT -ge $MIN_VAL && $INT -le $MAX_VAL ]]; then
    echo "$INT is within $MIN_VAL to $MAX_VAL."
  else
    echo "$INT is out of range."
  fi
else
  echo "INT is not an integer." >&2
  exit 1
fi

使用否定操作符!时,最好用圆括号确定转义的范围:

if [ ! \( $INT -ge $MIN_VAL -a $INT -le $MAX_VAL \) ]; then
    echo "$INT is outside $MIN_VAL to $MAX_VAL."
else
    echo "$INT is in range."
fi

上面例子中,test命令内部使用的圆括号,必须使用引号或者转义,否则会被 Bash 解释。

算术判断:
Bash 还提供了((...))作为算术条件,进行算术运算的判断(无需使用test语句):

if ((3 > 2)); then
  echo "true"
fi

如果算术计算的结果是非零值,则表示判断成立。这一点跟命令的返回值正好相反,需要小心:

$ if ((1)); then echo "It is true."; fi
It is true.
$ if ((0)); then echo "It is true."; else echo "it is false."; fi
It is false.

算术条件((...))也可以用于变量赋值:

$ if (( foo = 5 ));then echo "foo is $foo"; fi
foo is 5

上面例子中,(( foo = 5 ))完成了两件事情。首先把5赋值给变量foo,然后根据返回值5,判断条件为真。
注意,赋值语句返回等号右边的值,如果返回的是0,则判断为假:

$ if (( foo = 0 ));then echo "It is true.";else echo "It is false."; fi
It is false.

比如:

#!/bin/bash

INT=-5

if [[ "$INT" =~ ^-?[0-9]+$ ]]; then
  if ((INT == 0)); then
    echo "INT is zero."
  else
    if ((INT < 0)); then
      echo "INT is negative."
    else
      echo "INT is positive."
    fi
    if (( ((INT % 2)) == 0)); then
      echo "INT is even."
    else
      echo "INT is odd."
    fi
  fi
else
  echo "INT is not an integer." >&2
  exit 1
fi

一些命令的混用:

#测试目录temp是否存在,如果不存在,就会执行第二个命令,创建这个目录
$ [ -d temp ] || mkdir temp

#如果temp子目录不存在,脚本会终止,并且返回值为1
[ ! -d temp ] && exit 1

比如:只有在指定文件里面,同时存在搜索词word1和word2,就会执行if的命令部分:

#! /bin/bash

filename=$1
word1=$2
word2=$3

if grep $word1 $filename && grep $word2 $filename
then
  echo "$word1 and $word2 are both in $filename."
fi

比如:将一个&&判断表达式,改写成对应的if结构

[[ -d "$dir_name" ]] && cd "$dir_name" && rm *

# 等同于

if [[ ! -d "$dir_name" ]]; then
  echo "No such directory: '$dir_name'" >&2
  exit 1
fi
if ! cd "$dir_name"; then
  echo "Cannot cd to '$dir_name'" >&2
  exit 1
fi
if ! rm *; then
  echo "File deletion failed. Check results" >&2
  exit 1
fi

case结构:

case expression in
  pattern )
    commands ;;
  pattern )
    commands ;;
  ...
esac

比如:

#!/bin/bash

echo -n "输入一个1到3之间的数字(包含两端)> "
read character
case $character in
  1 ) echo 1
    ;;
  2 ) echo 2
    ;;
  3 ) echo 3
    ;;
  * ) echo 输入不符合要求
esac

上面例子中,最后一条匹配语句的模式是*,这个通配符可以匹配其他字符和没有输入字符的情况,类似if的else部分。

case的匹配模式可以使用各种通配符:

a):匹配a。
a|b):匹配a或b。
[[:alpha:]]):匹配单个字母。
???):匹配3个字符的单词。
*.txt):匹配.txt结尾。
*):匹配任意输入,通过作为case结构的最后一个模式。

比如:

#!/bin/bash

echo -n "输入一个字母或数字 > "
read character
case $character in
  [[:lower:]] | [[:upper:]] ) echo "输入了字母 $character"
                              ;;
  [0-9] )                     echo "输入了数字 $character"
                              ;;
  * )                         echo "输入不符合要求"
esac

Bash 4.0之前,case结构只能匹配一个条件,然后就会退出case结构。Bash 4.0之后,允许匹配多个条件:

#!/bin/bash
# test.sh

read -n 1 -p "Type a character > "
echo
case $REPLY in
  [[:upper:]])    echo "'$REPLY' is upper case." ;;&
  [[:lower:]])    echo "'$REPLY' is lower case." ;;&
  [[:alpha:]])    echo "'$REPLY' is alphabetic." ;;&
  [[:digit:]])    echo "'$REPLY' is a digit." ;;&
  [[:graph:]])    echo "'$REPLY' is a visible character." ;;&
  [[:punct:]])    echo "'$REPLY' is a punctuation symbol." ;;&
  [[:space:]])    echo "'$REPLY' is a whitespace character." ;;&
  [[:xdigit:]])   echo "'$REPLY' is a hexadecimal digit." ;;&
esac

运行结果如下:

$ test.sh
Type a character > a
'a' is lower case.
'a' is alphabetic.
'a' is a visible character.
'a' is a hexadecimal digit.

可以看到条件语句结尾添加了;;&以后,在匹配一个条件之后,并没有退出case结构,而是继续判断下一个条件。

循环

while循环:

while condition; do
  commands
done

比如:

#!/bin/bash

number=0
while [ "$number" -lt 10 ]; do
  echo "Number = $number"
  number=$((number + 1))
done

while的条件部分可以执行任意数量的命令,但是执行结果的真伪只看最后一个命令的执行结果:

#以下代码运行后,不会有任何输出,因为while的最后一个命令是false。
$ while true; false; do echo 'Hi, looping ...'; done

until循环:
until循环与while循环恰好相反,只要不符合判断条件(判断条件失败),就不断循环执行指定的语句。一旦符合判断条件,就退出循环:

until condition; do
  commands
done

比如:

number=0
until [ "$number" -ge 10 ]; do
  echo "Number = $number"
  number=$((number + 1))
done

until的条件部分也可以是一个命令,表示在这个命令执行成功之前,不断重复尝试:

until cp $1 $2; do
  echo 'Attempt to copy failed. waiting...'
  sleep 5
done

转换为while循环:

while ! cp $1 $2; do
  echo 'Attempt to copy failed. waiting...'
  sleep 5
done

for...in循环:
for...in循环用于遍历列表的每一项:

for variable in list; do
  commands
done

比如:

for i in word1 word2 word3; do
  echo $i
done

列表可以由通配符产生:

for i in *.png; do
  ls -l $i
done

in list的部分可以省略,这时list默认等于脚本的所有参数$@

列表也可以通过子命令产生:

#!/bin/bash

count=0
for i in $(cat ~/.bash_profile); do
  count=$((count + 1))
  echo "Word $count ($i) contains $(echo -n $i | wc -c) characters"
done

利用wc指令我们可以计算文件的Byte数、字数、或是列数,若不指定文件名称、或是所给予的文件名为"-",则wc指令会从标准输入设备读取数据。-c--bytes--chars只显示Bytes数。
echo -n:选项-n表示输出文字后不换行,否则计数会多一个回车符。

for循环还支持 C 语言的循环语法:

for (( expression1; expression2; expression3 )); do
  commands
done

比如:

for (( i=0; i<5; i=i+1 )); do
  echo $i
done

注意,循环条件放在双重圆括号之中。另外,圆括号之中使用变量,不必加上美元符号$
for条件部分的三个语句,都可以省略:

for ((;;))
do
  read var
  if [ "$var" = "." ]; then
    break
  fi
done

上面脚本会反复读取命令行输入,直到用户输入了一个点(.)为止,才会跳出循环。

break语句示例:

#!/bin/bash

for number in 1 2 3 4 5 6
do
  echo "number is $number"
  if [ "$number" = "3" ]; then
    break
  fi
done

#执行结果如下:
number is 1
number is 2
number is 3

continue语句示例:

#!/bin/bash

while read -p "What file do you want to test?" filename
do
  if [ ! -e "$filename" ]; then
    echo "The file does not exist."
    continue
  fi

  echo "You entered a valid file.."
done

上面例子中,只要用户输入的文件不存在,continue命令就会生效,直接进入下一轮循环(让用户重新输入文件名),不再执行后面的打印语句。

select语句:
select结构主要用来生成简单的菜单。它的语法与for...in循环基本一致:

select name
[in list]
do
  commands
done

select语句执行流程:

1.select生成一个菜单,内容是列表list的每一项,并且每一项前面还有一个数字编号。
2.Bash 提示用户选择一项,输入它的编号。
3.用户输入以后,Bash 会将该项的内容存在变量name,该项的编号存入环境变量REPLY。如果用户没有输入,就按回车键,Bash 会重新输出菜单,让用户选择。
4.执行命令体commands。
5.执行结束后,回到第一步,重复这个过程。

比如:

#!/bin/bash
# select.sh

select brand in Samsung Sony iphone symphony Walton
do
  echo "You have chosen $brand"
done

执行时会让用户进行选择:

$ ./select.sh
1) Samsung
2) Sony
3) iphone
4) symphony
5) Walton
#?

如果用户没有输入编号,直接按回车键。Bash 就会重新输出一遍这个菜单,直到用户按下Ctrl + c,退出执行。
如果用户选择1,则结果如下:

#? 1
You have chosen Samsung
#? 

select可以与case结合,针对不同项,执行不同的命令:

#!/bin/bash

echo "Which Operating System do you like?"

select os in Ubuntu LinuxMint Windows8 Windows10 WindowsXP
do
  case $os in
    "Ubuntu"|"LinuxMint")
      echo "I also use $os."
    ;;
    "Windows8" | "Windows10" | "WindowsXP")
      echo "Why don't you try Linux?"
    ;;
    *)
      echo "Invalid entry."
      break
    ;;
  esac
done

执行结果如下:

Which Operating System do you like?
1) Ubuntu
2) LinuxMint
3) Windows8
4) Windows10
5) WindowsXP
#? 1
I also use Ubuntu.
#? 5
Why don't you try Linux?
#? 0
Invalid entry.

函数

Bash 函数定义的语法有两种:

# 第一种
fn() {
  # codes
}

# 第二种
function fn() {
  # codes
}

比如:

hello() {
  echo "Hello $1"   #$1表示函数调用时的第一个参数
}

如何调用:

$ hello world
Hello world

比如:

today() {
  echo -n "Today's date is: "
  date +"%A, %B %-d, %Y"
}

调用:

$ today
Today's date is: Monday, October 17, 2022

删除一个函数,可以使用unset命令:

unset -f functionName

查看当前 Shell 已经定义的所有函数(包括函数名和定义),可以使用declare命令:

$ declare -f

declare命令还支持查看单个函数的定义:

$ declare -f functionName

指输出所有已定义的函数名:

$ declare -F

函数体内可以使用参数变量,获取函数参数。函数的参数变量,与脚本参数变量是一致的:

$1~$9:函数的第一个到第9个的参数。
$0:函数所在的脚本名。
$#:函数的参数总数。
$@:函数的全部参数,参数之间使用空格分隔。
$*:函数的全部参数,参数之间使用变量$IFS值的第一个字符分隔,默认为空格,但是可以自定义。

比如test.sh如下:

#!/bin/bash
# test.sh

function alice {
  echo "alice: $@"
  echo "$0: $1 $2 $3 $4"
  echo "$# arguments"

}

alice in wonderland

运行结果如下:

$ bash test.sh
alice: in wonderland
test.sh: in wonderland
2 arguments

再比如:

function log_msg {
  echo "[`date '+ %F %T'` ]: $@"
}

调用结果如下:

$ log_msg "This is sample log message"
[ 2018-08-16 19:56:34 ]: This is sample log message

return示例:

function func_return_value {
  return 10
}

如何获取函数返回值:

$ func_return_value
$ echo "Value returned by function is: $?"
Value returned by function is: 10

return后面可以不加参数:

function name {
  commands
  return
}

全局变量和局部变量:
Bash 函数体内直接声明的变量,属于全局变量,整个脚本都可以读取。这一点需要特别小心:

# 脚本 test.sh
fn () {
  foo=1
  echo "fn: foo = $foo"
}

fn
echo "global: foo = $foo"

运行结果如下:

$ bash test.sh
fn: foo = 1
global: foo = 1

函数体内不仅可以声明全局变量,还可以修改全局变量:

#! /bin/bash
foo=1

fn () {
  foo=2
}

fn

echo $foo

上面代码执行后,输出的变量$foo值为2。

函数里面可以用local命令声明局部变量:

#! /bin/bash
# 脚本 test.sh
fn () {
  local foo
  foo=1
  echo "fn: foo = $foo"
}

fn
echo "global: foo = $foo"

上面脚本运行结果如下:

$ bash test.sh
fn: foo = 1
global: foo =

上面例子中,local命令声明的$foo变量,只在函数体内有效,函数体外没有定义。

数组

数组的创建方式:

#单个元素赋值:
$ array[0]=val
$ array[1]=val
$ array[2]=val

#一次性赋值:
ARRAY=(value1 value2 ... valueN)
# 等同于
ARRAY=(
  value1
  value2
  value3
)

#指定位置赋值
$ array=(a b c)
$ array=([2]=c [0]=a [1]=b)
$ days=(Sun Mon Tue Wed Thu Fri Sat)
$ days=([0]=Sun [1]=Mon [2]=Tue [3]=Wed [4]=Thu [5]=Fri [6]=Sat)

#只为某些位置赋值(其余位置为空字符串)
names=(hatter [5]=duchess alice)

#使用通配符,将当前目录的所有 MP3 文件,放进一个数组
$ mp3s=( *.mp3 )

#先用declare -a命令声明一个数组,也是可以的
$ declare -a ARRAYNAME

#将用户的命令行输入,存入一个数组
$ read -a dice

读取数组某一成员:

$ echo ${array[i]}     # i 是索引

注意,需要加花括号,否则 Bash 会把索引部分[i]按照原样输出:

$ array[0]=a

$ echo ${array[0]}
a

$ echo $array[0]
a[0]

上面例子中,数组的第一个元素是a。如果不加大括号,Bash 会直接读取$array首成员的值,然后将[0]按照原样输出。

读取全部数组:
@*是数组的特殊索引,表示返回数组的所有成员:

$ foo=(a b c d e f)
$ echo ${foo[@]}
a b c d e f

配合循环可以遍历数组:

for i in "${names[@]}"; do
  echo $i
done

@*是有去别的,提前在是否放置在双引号中:
@不在双引号中,会将数组元素中的空格进行分隔:

$ activities=( swimming "water skiing" canoeing "white-water rafting" surfing )
$ for act in ${activities[@]}; \
do \
echo "Activity: $act"; \
done

Activity: swimming
Activity: water
Activity: skiing
Activity: canoeing
Activity: white-water
Activity: rafting
Activity: surfing

@在双引号中,则不会出现这种问题:

$ for act in "${activities[@]}"; \
do \
echo "Activity: $act"; \
done

Activity: swimming
Activity: water skiing
Activity: canoeing
Activity: white-water rafting
Activity: surfing

*不在双引号中,和@不在双引号中一样:

$ for act in ${activities[*]}; \
do \
echo "Activity: $act"; \
done

Activity: swimming
Activity: water
Activity: skiing
Activity: canoeing
Activity: white-water
Activity: rafting
Activity: surfing

*在双引号中,所有成员会被合并为单个字符串返回:

$ for act in "${activities[*]}"; \
do \
echo "Activity: $act"; \
done

Activity: swimming water skiing canoeing white-water rafting surfing

所以,拷贝一个数组的最方便方法,是使用在双引号中的@

$ hobbies=( "${activities[@]}" )

为数组添加新成员:

$ hobbies=( "${activities[@]}" diving )

数组的默认位置为index=0的位置:

$ declare -a foo
$ foo=A   #赋值不指定位置,默认在0位置
$ echo ${foo[0]}
A

$ foo=(a b c d e f)
$ echo ${foo}   #引用不带下表的数组,默认也是0位置
a
$ echo $foo
a

返回数组长度:

${#array[*]}
${#array[@]}

比如把字符串赋值给100位置的数组元素,这时的数组只有一个元素。:

$ a[100]=foo

$ echo ${#a[*]}
1

$ echo ${#a[@]}
1

如果用这种语法去读取具体的数组成员,就会返回该成员的字符串长度:

$ a[100]=foo
$ echo ${#a[100]}
3

${!array[@]}${!array[*]},可以返回数组的成员序号,即哪些位置是有值的:

$ arr=([5]=a [9]=b [23]=c)
$ echo ${!arr[@]}
5 9 23
$ echo ${!arr[*]}
5 9 23

基于此可以遍历稀疏数组:

arr=(a b c d)

for i in ${!arr[@]};do
  echo ${arr[i]}
done

切分数组,提取数组成员:
${array[@]:position:length}的语法可以提取数组成员

$ food=( apples bananas cucumbers dates eggs fajitas grapes )
$ echo ${food[@]:1:1}
bananas
$ echo ${food[@]:1:3}
bananas cucumbers dates

如果省略长度参数length,则返回从指定位置开始的所有成员:

$ echo ${food[@]:4}
eggs fajitas grapes

数组末尾追加成员,可以使用+=赋值运算符。它能够自动地把值追加到数组末尾:

$ foo=(a b c)
$ echo ${foo[@]}
a b c

$ foo+=(d e f)
$ echo ${foo[@]}
a b c d e f

#也可以直接拼接
$ foo=(${foo[*]} 1 2 3)
$ echo ${foo[*]}
a b c d e f 1 2 3

删除一个数组成员,使用unset命令:

$ foo=(a b c d e f)
$ echo ${foo[@]}
a b c d e f

$ unset foo[2]
$ echo ${foo[@]}
a b d e f

unset与将某一数组元素设置为空值不同。将某个成员设为空值,可以从返回值中“隐藏”这个成员:

$ foo=(a b c d e f)
$ foo[1]=''
$ echo ${foo[@]}
a c d e f

注意,这里是“隐藏”,而不是删除,因为这个成员仍然存在,只是值变成了空值:

$ foo=(a b c d e f)
$ foo[1]=''
$ echo ${#foo[@]}     #数组长度没变
6
$ echo ${!foo[@]}   #1号位置仍然有元素
0 1 2 3 4 5

直接将数组变量赋值为空字符串,相当于“隐藏”数组的第一个成员:

$ foo=(a b c d e f)
$ foo=''
$ echo ${foo[@]}
b c d e f

unset ArrayName可以清空整个数组:

$ unset ARRAY

$ echo ${ARRAY[*]}
<--no output-->

关联数组:
Bash 的新版本支持关联数组。关联数组使用字符串而不是整数作为数组索引。
declare -A可以声明关联数组:

declare -A colors
colors["red"]="#ff0000"
colors["green"]="#00ff00"
colors["blue"]="#0000ff"

关联数组必须用带有-A选项的declare命令声明创建。相比之下,整数索引的数组,可以直接使用变量名创建数组,关联数组就不行。
访问关联数组成员的方式,几乎与整数索引数组相同:

echo ${colors["blue"]}

set & shopt

Bash 执行脚本时,会创建一个子 Shell,Bash 默认给定了这个子Shell环境的各种参数。set命令用来修改子 Shell 环境的运行参数,即定制环境。
显示所有的环境变量和 Shell 函数:

$ set

set -u:碰到不存在的变量会报错。
未设置set -u时如下,输出不存在的变量会输出一个空行(echo输出会在行尾加换行符):

#!/usr/bin/env bash

echo $a
echo bar

#运行结果如下
$ bash script.sh

bar

设置了set -u以后:

#!/usr/bin/env bash
set -u

echo $a
echo bar

#运行结果如下
$ bash script.sh
bash: script.sh:行4: a: 未绑定的变量

- u还有另一种写法-o nounset,两者是等价的:

set -o nounset

set -x用来在运行结果之前,先输出执行的那一行命令:

#!/usr/bin/env bash
set -x

echo bar

上述代码运行结果如下:

$ bash script.sh
+ echo bar
bar

-x还有另一种写法-o xtrace

set -o xtrace

脚本当中如果要关闭命令输出,可以使用set +x

#!/bin/bash

number=1

set -x
if [ $number = "1" ]; then
  echo "Number equals 1"
else
  echo "Number does not equal 1"
fi
set +x

上面的例子中,只对特定的代码段打开命令输出。

如果脚本里面有运行失败的命令(返回值非0),Bash 默认会继续执行后面的命令,这在有些开发环境下是不友好的。

#!/usr/bin/env bash

foo
echo bar

上述代码执行结果如下:

$ bash script.sh
script.sh:行3: foo: 未找到命令
bar

实际开发中,如果某个命令失败,往往需要脚本停止执行,防止错误累积。这时,一般采用下面的写法:

command || exit 1       #如果command出错则exit 1

如果停止执行之前需要完成多个操作,就要采用下面三种写法:

# 写法一
command || { echo "command failed"; exit 1; }

# 写法二
if ! command; then echo "command failed"; exit 1; fi

# 写法三
command
if [ "$?" -ne 0 ]; then echo "command failed"; exit 1; fi

set -e使得脚本只要发生错误,就终止执行:

#!/usr/bin/env bash
set -e

foo
echo bar

上述代码执行结果如下:

$ bash script.sh
script.sh:行4: foo: 未找到命令

set -e根据返回值来判断,一个命令是否运行失败。但是,某些命令的非零返回值可能不表示失败,或者开发者希望在命令失败的情况下,脚本继续执行下去。这时可以暂时关闭set -e,该命令执行结束后,再重新打开set -e

set +e
command1
command2
set -e

还有一种方法是使用command || true,使得该命令即使执行失败,脚本也不会终止执行:

#!/bin/bash
set -e

foo || true
echo bar

上面代码中,true使得这一行语句总是会执行成功,后面的echo bar会执行。
-e还有另一种写法-o errexit

set -o errexit

set -e有一个例外情况,就是不适用于管道命令:

#!/usr/bin/env bash
set -e

foo | echo a
echo bar

执行结果如下:

$ bash script.sh
a
script.sh:行4: foo: 未找到命令
bar

可以看到尽管报错了,但是还是继续执行了管道命令的后半句。
Bash 会把最后一个子命令的返回值,作为整个命令的返回值。也就是说,只要最后一个子命令不失败,管道命令总是会执行成功,因此它后面命令依然会执行,set -e就失效了。

set -o pipefail用来解决这种情况,只要一个子命令失败,整个管道命令就失败,脚本就会终止执行:

#!/usr/bin/env bash
set -eo pipefail

foo | echo a
echo bar

执行结果如下:

$ bash script.sh
a
script.sh:行4: foo: 未找到命令

可以看到,echo bar没有执行。

一旦设置了-e参数,会导致函数内的错误不会被trap命令捕获。-E参数可以纠正这个行为,使得函数也能继承trap命令。
比如:

#!/bin/bash
set -e

trap "echo ERR trap fired!" ERR

myfunc()
{
  # 'foo' 是一个不存在的命令
  foo
}

myfunc

上面示例中,myfunc函数内部调用了一个不存在的命令foo,导致执行这个函数会报错。执行结果如下:

$ bash test.sh
test.sh:行9: foo:未找到命令

从运行结果可以看到,由于设置了set -e,函数内部的报错并没有被trap命令捕获,需要加上-E参数才可以:

#!/bin/bash
set -Eeuo pipefail

trap "echo ERR trap fired!" ERR

myfunc()
{
  # 'foo' 是一个不存在的命令
  foo
}

myfunc

执行上面这个脚本,就可以看到trap命令生效了:

$ bash test.sh
test.sh:行9: foo:未找到命令
ERR trap fired!

set命令还有一些其他参数:

set -n:等同于set -o noexec,不运行命令,只检查语法是否正确。
set -f:等同于set -o noglob,表示不对通配符进行文件名扩展。
set -v:等同于set -o verbose,表示打印 Shell 接收到的每一行输入。
set -o noclobber:防止使用重定向运算符>覆盖已经存在的文件。

上面的-f-v参数,可以分别使用set +fset +v关闭。

上面重点介绍的set命令的几个参数,一般都放在一起使用:

# 写法一
set -Eeuxo pipefail

# 写法二
set -Eeux
set -o pipefail

这两种写法建议放在所有 Bash 脚本的头部。

另一种办法是在执行 Bash 脚本的时候,从命令行传入这些参数:

$ bash -euxo pipefail script.sh

shopt命令:
shopt命令用来调整 Shell 的参数,跟set命令的作用很类似。之所以会有这两个类似命令的主要原因是,set是从 Ksh 继承的,属于 POSIX 规范的一部分,而shopt是 Bash 特有的。
直接输入shopt可以查看所有参数,以及它们各自打开和关闭的状态:

$ shopt

shopt命令后面跟着参数名,可以查询该参数是否打开:

$ shopt globstar
globstar  off

-s:用来打开某个参数:

$ shopt -s optionNameHere

-u:用来关闭某个参数:

$ shopt -u optionNameHere

-q:也是用来查询某个参数是否打开,但不是直接输出查询结果,而是通过命令的执行状态$?表示查询结果。如果状态为0,表示该参数打开;如果为1,表示该参数关闭:

$ shopt -q globstar
$ echo $?   #返回状态为1,表示该参数是关闭的
1

这个用法主要用于脚本,供if条件结构使用。下面例子是如果打开了这个参数,就执行if结构内部的语句:

if (shopt -q globstar); then
  ...
fi

参考:https://wangdoc.com/bash/set.html

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 230,622评论 6 544
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 99,716评论 3 429
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 178,746评论 0 383
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 63,991评论 1 318
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 72,706评论 6 413
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 56,036评论 1 329
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 44,029评论 3 450
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 43,203评论 0 290
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 49,725评论 1 336
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 41,451评论 3 361
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 43,677评论 1 374
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 39,161评论 5 365
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 44,857评论 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 35,266评论 0 28
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 36,606评论 1 295
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 52,407评论 3 400
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 48,643评论 2 380

推荐阅读更多精彩内容