环境变量
环境变量是 Bash 环境自带的变量,进入 Shell 时已经定义好了,可以直接使用。它们通常是系统定义好的,也可以由用户从父 Shell 传入子 Shell。
env命令或printenv命令,可以显示所有环境变量。
$ env
# 或者
$ printenv
下面是一些常见的环境变量。
BASHPID
:Bash 进程的进程 ID。HOME
:用户的主目录。RANDOM
:返回一个0到32767之间的随机数。PWD
:当前工作目录。UID
:当前用户的 ID 编号。USER
:当前用户的用户名。HOST
:当前主机的名称。IFS
:词与词之间的分隔符,默认为空格。PATH
:由冒号分开的目录列表,当输入可执行程序名后,会搜索这个目录列表。BASHOPTS
:当前 Shell 的参数,可以用shopt
命令修改。DISPLAY
:图形环境的显示器名字,通常是:0
,表示 X Server 的第一个显示器。EDITOR
:默认的文本编辑器。LANG
:字符集以及语言编码,比如zh_CN.UTF-8
。PS1
:Shell 提示符。PS2
: 输入多行命令时,次要的 Shell 提示符。SHELL
:Shell 的名字。SHELLOPTS
:启动当前 Shell 的set
命令的参数,参见《set 命令》一章。TERM
:终端类型名,即终端仿真器所用的协议。
echo
参数
(1)-n参数
默认情况下,echo输出的文本末尾会有一个回车符。-n参数可以取消末尾的回车符,使得下一个提示符紧跟在输出内容的后面。
$ echo a;echo b
a
b
$ echo -n a;echo b
ab
**(2)-e参数 ** 参数会解释引号(双引号和单引号)里面的特殊字符(比如换行符\n)。如果不使用-e参数,即默认情况下,引号会让特殊字符变成普通字符,echo不解释它们,原样输出。
$ echo "Hello\nWorld"
Hello\nWorld
# 双引号的情况
$ echo -e "Hello\nWorld"
Hello
World
read
#!/bin/bash
echo -n "输入一些文本 > "
read text
echo "你的输入:$text"
等价于
read -p "输入一些文本 >"
上面例子中,先显示一行提示文本,然后会等待用户输入文本。用户输入的文本,存入变量text,在下一行显示出来。
$ bash demo.sh
输入一些文本 > 你好,世界
你的输入:你好,世界
read
命令除了读取键盘输入,可以用来读取文件。
#!/bin/bash
filename='/etc/hosts'
while read myline
do
echo "$myline"
done < $filename
参数
read
命令的参数如下。
(1)-p 参数
-p
参数指定用户输入的提示信息。
read -p "Enter one or more values > "
echo "REPLY = '$REPLY'"
上面例子中,先显示Enter one or more values >
,再接受用户的输入。
(2)-t 参数
read
命令的-t
参数,设置了超时的秒数。如果超过了指定时间,用户仍然没有输入,脚本将放弃等待,继续向下执行。
#!/bin/bash
echo -n "输入一些文本 > "
if read -t 3 response; then
echo "用户已经输入了"
else
echo "用户没有输入"
fi
上面例子中,输入命令会等待3秒,如果用户超过这个时间没有输入,这个命令就会执行失败。if
根据命令的返回值,转入else
代码块,继续往下执行。
(3)-a 参数
-a
参数把用户的输入赋值给一个数组,从零号位置开始。
$ read -a people
alice duchess dodo
$ echo ${people[2]}
dodo
上面例子中,用户输入被赋值给一个数组people
,这个数组的2号成员就是dodo
。
(4)-n 参数
-n
参数指定只读取若干个字符作为变量值,而不是整行读取。
$ read -n 3 letter
abcdefghij
$ echo $letter
abc
上面例子中,变量letter
只包含3个字母。
(5)-e 参数
-e
参数允许用户输入的时候,使用readline
库提供的快捷键,比如自动补全。具体的快捷键可以参阅《行操作》一章。
#!/bin/bash
echo Please input the path to the file:
read -e fileName
echo $fileName
上面例子中,read
命令接受用户输入的文件名。这时,用户可能想使用 Tab 键的文件名“自动补全”功能,但是read
命令的输入默认不支持readline
库的功能。-e
参数就可以允许用户使用自动补全。
(6)其他参数
-
-d delimiter
:定义字符串delimiter
的第一个字符作为用户输入的结束,而不是一个换行符。 -
-r
:raw 模式,表示不把用户输入的反斜杠字符解释为转义字符。 -
-s
:使得用户的输入不显示在屏幕上,这常常用于输入密码或保密信息。 -
-u fd
:使用文件描述符fd
作为输入。
IFS 变量
read
命令读取的值,默认是以空格分隔。可以通过自定义环境变量IFS
(内部字段分隔符,Internal Field Separator 的缩写),修改分隔标志。
IFS
的默认值是空格、Tab 符号、换行符号,通常取第一个(即空格)。
如果把IFS
定义成冒号(:
)或分号(;
),就可以分隔以这两个符号分隔的值,这对读取文件很有用。
#!/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
设为冒号,然后用来分解/etc/passwd
文件的一行。IFS
的赋值命令和read
命令写在一行,这样的话,IFS
的改变仅对后面的命令生效,该命令执行后IFS
会自动恢复原来的值。如果不写在一行,就要采用下面的写法。
OLD_IFS="$IFS"
IFS=":"
read user pw uid gid name home shell <<< "$file_info"
IFS="$OLD_IFS"
另外,上面例子中,<<<
是 Here 字符串,用于将变量值转为标准输入,因为read
命令只能解析标准输入。
如果IFS
设为空字符串,就等同于将整行读入一个变量。
#!/bin/bash
input="/path/to/txt/file"
while IFS= read -r line
do
echo "$line"
done < "$input"
上面的命令可以逐行读取文件,每一行存入变量line
,打印出来以后再读取下一行。
#字符串操作
字符串长度
my=abcd
$ echo ${#my}
4
子字符串
字符串提取子串的语法如下。
${varname:offset:length}
上面语法的含义是返回变量$varname的子字符串,从位置offset开始(从0开始计算),长度为length。
$ count=frogfootman
$ echo ${count:4:4}
foot
数学计算
f=$((5 * 7))
expr 注意空格
$ expr 3 + 2
5
let命令声明变量时,可以直接执行算术表达式。
$ let foo=1+2 #不能为空格
$ echo $foo
3
#脚本参数
上面例子中,script.sh
是一个脚本文件,word1
、word2
和word3
是三个参数。
脚本文件内部,可以使用特殊变量,引用这些参数。
$0
:脚本文件名,即script.sh
。$1
~$9
:对应脚本的第一个参数到第九个参数。$#
:参数的总数。$@
:全部的参数,参数之间使用空格分隔。$*
:全部的参数,参数之间使用变量$IFS
值的第一个字符分隔,默认为空格,但是可以自定义。
如果脚本的参数多于9个,那么第10个参数可以用${10}
的形式引用,以此类推。
用户可以输入任意数量的参数,利用for
循环,可以读取每一个参数。
#!/bin/bash
for i in "$@"; do
echo $i
done
shift 命令
shift
命令可以改变脚本参数,每次执行都会移除脚本当前的第一个参数($1
),使得后面的参数向前一位,即$2
变成$1
、$3
变成$2
、$4
变成$3
,以此类推。
while
循环结合shift
命令,也可以读取每一个参数。
#!/bin/bash
echo "一共输入了 $# 个参数"
while [ "$1" != "" ]; do
echo "剩下 $# 个参数"
echo "参数:$1"
shift
done
上面例子中,shift
命令每次移除当前第一个参数,从而通过while
循环遍历所有参数。
shift
命令可以接受一个整数作为参数,指定所要移除的参数个数,默认为1
。
shift 3
上面的命令移除前三个参数,原来的$4
变成$1
。
条件判断
if 结构
if
是最常用的条件判断结构,只有符合给定条件时,才会执行指定的命令。它的语法如下。
if commands; then
commands
[elif commands; then
commands...]
[else
commands]
fi
test 命令
if
结构的判断条件,一般使用test
命令,有三种形式。
# 写法一
test expression
# 写法二
[ expression ]
# 写法三
[[ expression ]]
上面三种形式是等价的,但是第三种形式还支持正则判断,前两种不支持。
上面的expression
是一个表达式。这个表达式为真,test
命令执行成功(返回值为0
);表达式为伪,test
命令执行失败(返回值为1
)。注意,第二种和第三种写法,[
和]
与内部的表达式之间必须有空格。
$ test -f /etc/hosts
$ echo $?
0
$ [ -f /etc/hosts ]
$ echo $?
0
上面的例子中,test
命令采用两种写法,判断/etc/hosts
文件是否存在,这两种写法是等价的。命令执行后,返回值为0
,表示该文件确实存在。
实际上,[
这个字符是test
命令的一种简写形式,可以看作是一个独立的命令,这解释了为什么它后面必须有空格。
下面把test
命令的三种形式,用在if
结构中,判断一个文件是否存在。
# 写法一
if test -e /tmp/foo.txt ; then
echo "Found foo.txt"
fi
# 写法二
if [ -e /tmp/foo.txt ] ; then
echo "Found foo.txt"
fi
# 写法三
if [[ -e /tmp/foo.txt ]] ; then
echo "Found foo.txt"
fi
判断表达式
if
关键字后面,跟的是一个命令。这个命令可以是test
命令,也可以是其他命令。命令的返回值为0
表示判断成立,否则表示不成立。因为这些命令主要是为了得到返回值,所以可以视为表达式。
常用的判断表达式有下面这些。
文件判断
以下表达式用来判断文件状态。
[ -a file ]
:如果 file 存在,则为true
。[ -b file ]
:如果 file 存在并且是一个块(设备)文件,则为true
。[ -c file ]
:如果 file 存在并且是一个字符(设备)文件,则为true
。[ -d file ]
:如果 file 存在并且是一个目录,则为true
。[ -e file ]
:如果 file 存在,则为true
。[ -f file ]
:如果 file 存在并且是一个普通文件,则为true
。[ -g file ]
:如果 file 存在并且设置了组 ID,则为true
。[ -G file ]
:如果 file 存在并且属于有效的组 ID,则为true
。[ -h file ]
:如果 file 存在并且是符号链接,则为true
。[ -k file ]
:如果 file 存在并且设置了它的“sticky bit”,则为true
。[ -L file ]
:如果 file 存在并且是一个符号链接,则为true
。[ -N file ]
:如果 file 存在并且自上次读取后已被修改,则为true
。[ -O file ]
:如果 file 存在并且属于有效的用户 ID,则为true
。[ -p file ]
:如果 file 存在并且是一个命名管道,则为true
。[ -r file ]
:如果 file 存在并且可读(当前用户有可读权限),则为true
。[ -s file ]
:如果 file 存在且其长度大于零,则为true
。[ -S file ]
:如果 file 存在且是一个网络 socket,则为true
。[ -t fd ]
:如果 fd 是一个文件描述符,并且重定向到终端,则为true
。 这可以用来判断是否重定向了标准输入/输出/错误。[ -u file ]
:如果 file 存在并且设置了 setuid 位,则为true
。[ -w file ]
:如果 file 存在并且可写(当前用户拥有可写权限),则为true
。[ -x file ]
:如果 file 存在并且可执行(有效用户有执行/搜索权限),则为true
。[ file1 -nt file2 ]
:如果 FILE1 比 FILE2 的更新时间最近,或者 FILE1 存在而 FILE2 不存在,则为true
。[ file1 -ot file2 ]
:如果 FILE1 比 FILE2 的更新时间更旧,或者 FILE2 存在而 FILE1 不存在,则为true
。[ FILE1 -ef FILE2 ]
:如果 FILE1 和 FILE2 引用相同的设备和 inode 编号,则为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
上面代码中,首先确定$ANSWER
字符串是否为空。如果为空,就终止脚本,并把退出状态设为1
。注意,这里的echo
命令把错误信息There is no answer.
重定向到标准错误,这是处理错误信息的常用方法。如果$ANSWER
字符串不为空,就判断它的值是否等于yes
、no
或者maybe
。
注意,字符串判断时,变量要放在双引号之中,比如[ -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
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
,接着判断正负,最后通过求余数判断奇偶。
正则判断
[[ expression ]]
这种判断形式,支持正则表达式。
[[ string1 =~ regex ]]
上面的语法中,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 判断的逻辑运算
通过逻辑运算,可以把多个test
判断表达式结合起来,创造更复杂的判断。三种逻辑运算AND
,OR
,和NOT
,都有自己的专用符号。
AND
运算:符号&&
,也可使用参数-a
。OR
运算:符号||
,也可使用参数-o
。NOT
运算:符号!
。
下面是一个AND
的例子,判断整数是否在某个范围之内。
#!/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
上面例子中,&&
用来连接两个判断条件:大于等于$MIN_VAL
,并且小于等于$MAX_VAL
。
使用否定操作符!
时,最好用圆括号确定转义的范围。
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 还提供了((...))
作为算术条件,进行算术运算的判断。
if ((3 > 2)); then
echo "true"
fi
上面代码执行后,会打印出true
。
注意,算术判断不需要使用test
命令,而是直接使用((...))
结构。这个结构的返回值,决定了判断的真伪。
如果算术计算的结果是非零值,则表示判断成立。这一点跟命令的返回值正好相反,需要小心。
$ 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.
上面例子中,((1))
表示判断成立,((0))
表示判断不成立。
算术条件((...))
也可以用于变量赋值。
$ 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
只要是算术表达式,都能用于((...))
语法,详见《Bash 的算术运算》一章。
普通命令的逻辑运算
如果if
结构使用的不是test
命令,而是普通命令,比如上一节的((...))
算术运算,或者test
命令与普通命令混用,那么可以使用 Bash 的命令控制操作符&&
(AND)和||
(OR),进行多个命令的逻辑运算。
$ command1 && command2
$ command1 || command2
对于&&
操作符,先执行command1
,只有command1
执行成功后, 才会执行command2
。对于||
操作符,先执行command1
,只有command1
执行失败后, 才会执行command2
。
$ mkdir temp && cd temp
上面的命令会创建一个名为temp
的目录,执行成功后,才会执行第二个命令,进入这个目录。
$ [ -d temp ] || mkdir temp
上面的命令会测试目录temp
是否存在,如果不存在,就会执行第二个命令,创建这个目录。这种写法非常有助于在脚本中处理错误。
[ ! -d temp ] && exit 1
上面的命令中,如果temp
子目录不存在,脚本会终止,并且返回值为1
。
下面就是if
与&&
结合使用的写法。
if [ condition ] && [ condition ]; then
command
fi
下面是一个示例。
#! /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
上面的例子只有在指定文件里面,同时存在搜索词word1
和word2
,就会执行if
的命令部分。
下面的示例演示如何将一个&&
判断表达式,改写成对应的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
结构用于多值判断,可以为每个值指定对应的命令,跟包含多个elif
的if
结构等价,但是语义更好。它的语法如下。
case expression in
pattern )
commands ;;
pattern )
commands ;;
...
esac
[[:alpha:]])
:匹配单个字母。???)
:匹配3个字符的单词。*.txt)
:匹配.txt
结尾。*)
:匹配任意输入,通过作为case
结构的最后一个模式。
#!/bin/bash
echo -n "输入一个字母或数字 > "
read character
case $character in
[[:lower:]] | [[:upper:]] ) echo "输入了字母 $character"
;;
[0-9] ) echo "输入了数字 $character"
;;
* ) echo "输入不符合要求"
esac
上面例子中,使用通配符[[:lower:]] | [[:upper:]]
匹配字母,[0-9]
匹配数字。
循环
{start..end}
大括号扩展有一个简写形式{start..end},表示扩展成一个连续序列。比如,{a..z}可以扩展成26个小写英文字母。
$ echo d{a..d}g
dag dbg dcg ddg
$ echo {1..4}
1 2 3 4
$ echo Number_{1..5}
Number_1 Number_2 Number_3 Number_4 Number_5
如果整数前面有前导0,扩展输出的每一项都有前导0。
$ echo {01..5}
01 02 03 04 05
$ echo {001..5}
001 002 003 004 005
for…in 循环
for...in
循环用于遍历列表的每一项。
for variable in list
do
commands
done
for循环
for i in {1..4}
do
echo $i
done
for i in word1 word2 word3; do
echo $i
done
上面例子中,word1 word2 word3
是一个包含三个单词的列表,变量i
依次等于word1
、word2
、word3
,命令echo $i
则会相应地执行三次。
列表可以由通配符产生。
for i in *.png; do
ls -l $i
done
上面例子中,*.png
会替换成当前目录中所有 PNG 图片文件,变量i
会依次等于每一个文件。
列表也可以通过子命令产生。
#!/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
上面例子中,cat ~/.bash_profile
命令会输出~/.bash_profile
文件的内容,然后通过遍历每一个词,计算该文件一共包含多少个词,以及每个词有多少个字符。
for 循环
for
循环还支持 C 语言的循环语法。
for (( expression1; expression2; expression3 )); do
commands
done
上面代码中,expression1
用来初始化循环条件,expression2
用来决定循环结束的条件,expression3
在每次循环迭代的末尾执行,用于更新值。
注意,循环条件放在双重圆括号之中。另外,圆括号之中使用变量,不必加上美元符号$
。
它等同于下面的while
循环。
(( expression1 ))
while (( expression2 )); do
commands
(( expression3 ))
done
下面是一个例子。
for (( i=0; i<5; i=i+1 )); do
echo $i
done
上面代码中,初始化变量i
的值为0,循环执行的条件是i
小于5。每次循环迭代结束时,i
的值加1。
for
条件部分的三个语句,都可以省略。
for ((;;))
do
read var
if [ "$var" = "." ]; then
break
fi
done
上面脚本会反复读取命令行输入,直到用户输入了一个点(.
)为止,才会跳出循环。
while 循环
while
循环有一个判断条件,只要符合条件,就不断循环执行指定的语句。
while condition; do
commands
done
上面代码中,只要满足条件condition
,就会执行命令commands
。然后,再次判断是否满足条件condition
,只要满足,就会一直执行下去。只有不满足条件,才会退出循环。
循环条件condition
可以使用test
命令,跟if
结构的判断条件写法一致。
#!/bin/bash
number=0
while [ "$number" -lt 10 ]; do
echo "Number = $number"
number=$((number + 1))
done
上面例子中,只要变量$number
小于10,就会不断加1,直到$number
等于10,然后退出循环。
while read使用
#!/bin/bash
filename='/etc/hosts'
while read myline
do
echo "$myline"
done < $filename
函数
Bash 函数定义的语法有两种。
# 第一种
fn() {
# codes
}
# 第二种
function fn() {
# codes
}
return 命令
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
}
数组
创建数组
数组可以采用逐个赋值的方法创建。
ARRAY[INDEX]=value
上面语法中,ARRAY
是数组的名字,可以是任意合法的变量名。INDEX
是一个大于或等于零的整数,也可以是算术表达式。注意数组第一个元素的下标是0, 而不是1。
下面创建一个三个成员的数组。
$ array[0]=val
$ array[1]=val
$ array[2]=val
数组也可以采用一次性赋值的方式创建。
ARRAY=(value1 value2 ... valueN)
# 等同于
ARRAY=(
value1
value2
value3
)
读取单个元素
读取数组指定位置的成员,要使用下面的语法。
$ 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
循环,就可以用来遍历数组。
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
上面的例子中,数组activities
实际包含5个成员,但是for...in
循环直接遍历${activities[@]}
,导致返回7个结果。为了避免这种情况,一般把${activities[@]}
放在双引号之中。
$ for act in "${activities[@]}"; \
do \
echo "Activity: $act"; \
done
Activity: swimming
Activity: water skiing
Activity: canoeing
Activity: white-water rafting
Activity: surfing
上面例子中,${activities[@]}
放在双引号之中,遍历就会返回正确的结果。
${activities[*]}
不放在双引号之中,跟${activities[@]}
不放在双引号之中是一样的。
$ for act in ${activities[*]}; \
do \
echo "Activity: $act"; \
done
Activity: swimming
Activity: water
Activity: skiing
Activity: canoeing
Activity: white-water
Activity: rafting
Activity: surfing
${activities[*]}
放在双引号之中,所有成员就会变成单个字符串返回。
$ for act in "${activities[*]}"; \
do \
echo "Activity: $act"; \
done
Activity: swimming water skiing canoeing white-water rafting surfing
所以,拷贝一个数组的最方便方法,就是写成下面这样。
$ hobbies=( "${activities[@]}" )
上面例子中,数组activities
被拷贝给了另一个数组hobbies
。
这种写法也可以用来为新数组添加成员。
$ hobbies=( "${activities[@]}" diving )
上面例子中,新数组hobbies
在数组activities
的所有成员之后,又添加了一个成员。
数组的长度
要想知道数组的长度(即一共包含多少成员),可以使用下面两种语法。
${#array[*]}
${#array[@]}
下面是一个例子。
$ a[100]=foo
$ echo ${#a[*]}
1
$ echo ${#a[@]}
1
上面例子中,把字符串赋值给100
位置的数组元素,这时的数组只有一个元素。
注意,如果用这种语法去读取具体的数组成员,就会返回该成员的字符串长度。这一点必须小心。
$ a[100]=foo
$ echo ${#a[100]}
3
上面例子中,${#a[100]}
实际上是返回数组第100号成员a[100]
的值(foo
)的字符串长度。
提取数组序号
`${!array[@]}`或`${!array[*]}`,可以返回数组的成员序号,即哪些位置是有值的。
$ arr=([5]=a [9]=b [23]=c)
$ echo ${!arr[@]}
5 9 23
$ echo ${!arr[*]}
5 9 23
上面例子中,数组的5、9、23号位置有值。
利用这个语法,也可以通过for
循环遍历数组。
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
上面例子中,${food[@]:1:1}
返回从数组1号位置开始的1个成员,${food[@]:1:3}
返回从1号位置开始的3个成员。
如果省略长度参数length
,则返回从指定位置开始的所有成员。
$ echo ${food[@]:4}
eggs fajitas grapes
上面例子返回从4号位置开始到结束的所有成员。
追加数组成员
数组末尾追加成员,可以使用+=
赋值运算符。它能够自动地把值追加到数组末尾。否则,就需要知道数组的最大序号,比较麻烦。
$ foo=(a b c)
$ echo ${foo[@]}
a b c
$ foo+=(d e f)
$ echo ${foo[@]}
a b c d e f
删除数组
删除一个数组成员,使用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
上面例子中,删除了数组中的第三个元素,下标为2。
将某个成员设为空值,可以从返回值中“隐藏”这个成员。
$ 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[@]}
0 1 2 3 4 5
上面代码中,第二个成员设为空值后,数组仍然包含6个成员。
由于空值就是空字符串,所以下面这样写也有隐藏效果,但是不建议这种写法。
$ foo[1]=
上面的写法也相当于“隐藏”了数组的第二个成员。
直接将数组变量赋值为空字符串,相当于“隐藏”数组的第一个成员。
$ foo=(a b c d e f)
$ foo=''
$ echo ${foo[@]}
b c d e f
上面的写法相当于“隐藏”了数组的第一个成员。
命令提示符
环境变量 PS1
命令提示符通常是美元符号$
,对于根用户则是井号#
。这个符号是环境变量PS1
决定的,执行下面的命令,可以看到当前命令提示符的定义。
$ echo $PS1
Bash 允许用户自定义命令提示符,只要改写这个变量即可。改写后的PS1
,可以放在用户的 Bash 配置文件.bashrc
里面,以后新建 Bash 对话时,新的提示符就会生效。要在当前窗口看到修改后的提示符,可以执行下面的命令。
$ source ~/.bashrc
命令提示符的定义,可以包含特殊的转义字符,表示特定内容。
\a
:响铃,计算机发出一记声音。\d
:以星期、月、日格式表示当前日期,例如“Mon May 26”。\h
:本机的主机名。\H
:完整的主机名。\j
:运行在当前 Shell 会话的工作数。\l
:当前终端设备名。\n
:一个换行符。\r
:一个回车符。\s
:Shell 的名称。\t
:24小时制的hours:minutes:seconds
格式表示当前时间。\T
:12小时制的当前时间。\@
:12小时制的AM/PM
格式表示当前时间。\A
:24小时制的hours:minutes
表示当前时间。\u
:当前用户名。\v
:Shell 的版本号。\V
:Shell 的版本号和发布号。\w
:当前的工作路径。\W
:当前目录名。\!
:当前命令在命令历史中的编号。\#
:当前 shell 会话中的命令数。\$
:普通用户显示为$
字符,根用户显示为#
字符。\[
:非打印字符序列的开始标志。\]
:非打印字符序列的结束标志。
举例来说,[\u@\h \W]\$
这个提示符定义,显示出来就是[user@host ~]$
(具体的显示内容取决于你的系统)。
[user@host ~]$ echo $PS1
[\u@\h \W]\$
改写PS1
变量,就可以改变这个命令提示符。
$ PS1="\A \h \$ "
17:33 host $
注意,$
后面最好跟一个空格,这样的话,用户的输入与提示符就不会连在一起。
颜色
默认情况下,命令提示符是显示终端预定义的颜色。Bash 允许自定义提示符颜色。
使用下面的代码,可以设定其后文本的颜色。
\033[0;30m
:黑色\033[1;30m
:深灰色\033[0;31m
:红色\033[1;31m
:浅红色\033[0;32m
:绿色\033[1;32m
:浅绿色\033[0;33m
:棕色\033[1;33m
:黄色\033[0;34m
:蓝色\033[1;34m
:浅蓝色\033[0;35m
:粉红\033[1;35m
:浅粉色\033[0;36m
:青色\033[1;36m
:浅青色\033[0;37m
:浅灰色\033[1;37m
:白色
举例来说,如果要将提示符设为红色,可以将PS1
设成下面的代码。
PS1='\[\033[0;31m\]<\u@\h \W>\$'
但是,上面这样设置以后,用户在提示符后面输入的文本也是红色的。为了解决这个问题, 可以在结尾添加另一个特殊代码\[\033[00m\]
,表示将其后的文本恢复到默认颜色。
PS1='\[\033[0;31m\]<\u@\h \W>\$\[\033[00m\]'
除了设置前景颜色,Bash 还允许设置背景颜色。
\033[0;40m
:蓝色\033[1;44m
:黑色\033[0;41m
:红色\033[1;45m
:粉红\033[0;42m
:绿色\033[1;46m
:青色\033[0;43m
:棕色\033[1;47m
:浅灰色
下面是一个带有红色背景的提示符。
PS1='\[\033[0;41m\]<\u@\h \W>\$\[\033[0m\] '
环境变量 PS2,PS3,PS4
除了PS1
,Bash 还提供了提示符相关的另外三个环境变量。
环境变量PS2
是命令行折行输入时系统的提示符,默认为>
。
$ echo "hello
> world"
上面命令中,输入hello
以后按下回车键,系统会提示继续输入。这时,第二行显示的提示符就是PS2
定义的>
。
环境变量PS3
是使用select
命令时,系统输入菜单的提示符。
环境变量PS4
默认为+
。它是使用 Bash 的-x
参数执行脚本时,每一行命令在执行前都会先打印出来,并且在行首出现的那个提示符。
比如下面是脚本test.sh
。
#!/bin/bash
echo "hello world"
使用-x
参数执行这个脚本。
$ bash -x test.sh
+ echo 'hello world'
hello world
上面例子中,输出的第一行前面有一个+
,这就是变量PS4
定义的。
EOF写入文件
cat <<EOF >/root/1.txt
hello world
EOF
#if [ $(id -u) != "0" ]; then
if [ $UID != "0" ]; then
echo "根用户才能执行当前脚本"
exit 1
fi