变量
显示所有环境变量:
$ env
# 或者
$ printenv
显示某一个环境变量名:
$ echo $HOME
/root
# 或者
$ printenv HOME
/root
Bash 变量名区分大小写,HOME和home是两个不同的变量。
自定义变量是用户在当前 Shell 里面自己定义的变量,仅在当前 Shell 可用。一旦退出当前 Shell,该变量就不存在了。
显示所有变量(包括环境变量和自定义变量),以及所有的 Bash 函数:
$ set
一些变量赋值:
a=z # 变量 a 赋值为字符串 z
b="a string" # 变量值包含空格,就必须放在引号里面
c="a string and $b" # 变量值可以引用其他变量的值
#echo c --> "a string and a string"
由于$在 Bash 中有特殊含义,把它当作美元符号使用时,一定要非常小心:
$ echo The total is $100.00
The total is 00.00
$ echo The total is \$100.00
The total is $100.00
读取变量的时候,变量名也可以使用花括号{}
包围,比如$a
也可以写成${a}
。这种写法可以用于变量名与其他字符连用的情况:
$ a=foo
$ echo $a_file
$ echo ${a}_file
foo_file
如果变量的值本身也是变量,可以使用${!varname}
的语法,读取最终的值:
$ myvar=USER
$ echo ${myvar}
USER
$ echo ${!myvar}
ruanyf
如果变量值包含连续空格(或制表符和换行符),最好放在双引号里面读取:
$ a="1 2 3"
$ echo $a
1 2 3
$ echo "$a"
1 2 3
unset命令用来删除一个变量:
unset NAME
不存在的 Bash 变量一律等于空字符串,所以即使unset命令删除了变量,还是可以读取这个变量,值为空字符串。
用户创建的变量仅可用于当前 Shell,子 Shell 默认读取不到父 Shell 定义的变量。为了把变量传递给子 Shell,需要使用export命令。这样输出的变量,对于子 Shell 来说就是环境变量。
export
命令用来向子 Shell 输出变量:
NAME=foo
export NAME
#或者
export NAME=value
举例:
# 输出变量 $foo
$ export foo=bar
# 新建子 Shell
$ bash
# 读取 $foo
$ echo $foo
bar
# 修改继承的变量
$ foo=baz
# 退出子 Shell
$ exit
# 读取 $foo,子shell不影响父shell
$ echo $foo
bar
$?
为上一个命令的退出码,用来判断上一个命令是否执行成功。返回值是0,表示上一个命令执行成功;如果不是零,表示上一个命令执行失败:
$ ls doesnotexist
ls: doesnotexist: No such file or directory
$ echo $?
1
$$
为当前 Shell 的进程 ID:
$ echo $$
10662
$_
为上一个命令的最后一个参数:
$ grep dictionary /usr/share/dict/words
dictionary
$ echo $_
/usr/share/dict/words
$!
为最近一个后台执行的异步命令的进程 ID:
$ firefox &
[1] 11064
$ echo $!
11064
上面例子中,firefox是后台运行的命令,$!
返回该命令的进程 ID。
$0
为当前 Shell 的名称(在命令行直接执行时)或者脚本名(在脚本中执行时):
$ echo $0
bash
$-
为当前 Shell 的启动参数:
$ echo $-
himBHs
$#
表示脚本的参数数量,$@
表示脚本的参数值。
如下表示如果变量varname存在且不为空,则返回它的值,否则返回word。比如${count:-0}
表示变量count不存在时返回0:
${varname:-word}
如果变量varname存在且不为空,则返回它的值,否则将它设为word,并且返回word:
${varname:=word}
如果变量名存在且不为空,则返回word,否则返回空值。它的目的是测试变量是否存在:
${varname:+word}
如果变量varname存在且不为空,则返回它的值,否则打印出varname: message,并中断脚本的执行。它的目的是防止变量未定义,比如${count:?"undefined!"}
表示变量count未定义时就中断执行,抛出错误,返回给定的报错信息undefined!:
${varname:?message}
上面四种语法如果用在脚本中,变量名的部分可以用数字1到9,表示脚本的参数:
#1表示脚本的第一个参数。如果该参数不存在,就退出脚本并报错。
filename=${1:?"filename missing."}
declare命令可以声明一些特殊类型的变量,为变量设置一些限制,比如声明只读类型的变量和整数类型的变量:
declare OPTION VARIABLE=value
-i
参数声明整数变量以后,可以直接进行数学运算:
$ val1=12 val2=5
$ declare -i result
$ result=val1*val2
$ echo $result
60
-x
参数等同于export命令,可以输出一个变量为子 Shell 的环境变量:
$ declare -x foo
# 等同于
$ export foo
-r
参数可以声明只读变量,无法改变变量值,也不能unset变量:
$ declare -r bar=1
$ bar=2
bash: bar:只读变量
$ echo $?
1
$ unset bar
bash: bar:只读变量
$ echo $?
1
readonly命令等同于declare -r
,用来声明只读变量,不能改变变量值,也不能unset变量:
$ readonly foo=1
$ foo=2
bash: foo:只读变量
$ echo $?
1
let命令声明变量时,可以直接执行算术表达式:
$ let foo=1+2
$ echo $foo
3
let命令的参数表达式如果包含空格,就需要使用引号:
$ let "foo = 1 + 2"
let可以同时对多个变量赋值,赋值表达式之间使用空格分隔:
$ let "v1 = 1" "v2 = v1++" #先赋值,再自增
$ echo $v1,$v2
2,1
字符串操作
获取字符串长度:
${#varname}
比如:
$ myPath=/home/cam/book/long.file.name
$ echo ${#myPath}
29
字符串提取子串的语法如下:
${varname:offset:length}
比如:
$ count=frogfootman
$ echo ${count:4:4}
foot
需要通过变量名传入,不可直接传入字符串:
# 报错
$ echo ${"hello":2:3}
参数为负数:
$ foo="This string is long."
$ echo ${foo: -5} #注意-5前面有空格,与${variable:-word}区分
long.
$ echo ${foo: -5:2}
lo
$ echo ${foo: -5:-2} #如果length为-2,表示要排除从字符串末尾开始的2个字符,所以返回lon
lon
字符串头部匹配:
# 如果 pattern 匹配变量 variable 的开头,
# 删除最短匹配(非贪婪匹配)的部分,返回剩余部分
${variable#pattern}
# 如果 pattern 匹配变量 variable 的开头,
# 删除最长匹配(贪婪匹配)的部分,返回剩余部分
${variable##pattern}
比如:
$ myPath=/home/cam/book/long.file.name
$ echo ${myPath#/*/}
cam/book/long.file.name
$ echo ${myPath##/*/}
long.file.name
#如果匹配不成功,则返回原始字符串:
$ echo ${myPath#aaa}
/home/cam/book/long.file.name
如果要将头部匹配的部分,替换成其他内容,采用下面的写法:
# 模式必须出现在字符串的开头
${variable/#pattern/string}
# 示例
$ foo=JPG.JPG
$ echo ${foo/#JPG/jpg}
jpg.JPG
同样的,字符串尾部匹配:
# 如果 pattern 匹配变量 variable 的结尾,
# 删除最短匹配(非贪婪匹配)的部分,返回剩余部分
${variable%pattern}
# 如果 pattern 匹配变量 variable 的结尾,
# 删除最长匹配(贪婪匹配)的部分,返回剩余部分
${variable%%pattern}
如果要将尾部匹配的部分,替换成其他内容,采用下面的写法:
# 模式必须出现在字符串的结尾
${variable/%pattern/string}
# 示例
$ foo=JPG.JPG
$ echo ${foo/%JPG/jpg}
JPG.jpg
任意位置匹配:
# 如果 pattern 匹配变量 variable 的一部分,
# 最长匹配(贪婪匹配)的那部分被 string 替换,但仅替换第一个匹配
${variable/pattern/string}
# 如果 pattern 匹配变量 variable 的一部分,
# 最长匹配(贪婪匹配)的那部分被 string 替换,所有匹配都替换
${variable//pattern/string}
比如:
$ path=/home/cam/foo/foo.name
$ echo ${path/foo/bar}
/home/cam/bar/foo.name
$ echo ${path//foo/bar}
/home/cam/bar/bar.name
配合通配符:
$ phone="555-456-1414"
$ echo ${phone/5?4/-}
55-56-1414
下面的语法可以改变变量的大小写:
# 转为大写
${varname^^}
# 转为小写
${varname,,}
比如:
$ foo=heLLo
$ echo ${foo^^}
HELLO
$ echo ${foo,,}
hello
((...))
语法可以进行整数的算术运算:
$ ((foo = 5 + 5))
$ echo $foo
10
这个语法不返回值,命令执行的结果根据算术运算的结果而定。只要算术结果不是0,命令就算执行成功:
$ (( 3 + 2 ))
$ echo $?
0
$ (( 3 - 3 ))
$ echo $?
1
如果要读取算术运算的结果,需要在((...))
前面加上美元符号$((...))
,使其变成算术表达式,返回算术运算的值:
$ echo $((2 + 2))
4
这个语法只能计算整数,否则会报错:
# 报错
$ echo $((1.5 + 1))
bash: 语法错误
$((...))
的圆括号之中,不需要在变量名之前加上$,不过加上也不报错:
$ number=2
$ echo $(($number + 1))
3
如果在$((...))里面使用字符串,Bash 会认为那是一个变量名。如果不存在同名变量,Bash 就会将其作为空值,因此不会报错:
$ echo $(( "hello" + 2))
2
$ echo $(( "hello" * 2))
0
$ hello=1
$ echo $(("hello"+1))
2
可以进行动态替换:
$ foo=hello
$ hello=3
$ echo $(( foo + 2 ))
5
$[...]
是以前的语法,也可以做整数运算,不建议使用:
$ echo $[2+2]
4
bash中的进制:
number:没有任何特殊表示法的数字是十进制数(以10为底)。
0number:八进制数。
0xnumber:十六进制数。
base#number:base进制的数。
比如:
$ echo $((0xff))
255
$ echo $((2#11111111))
255
位运算:
<<:位左移运算,把一个数字的所有位向左移动指定的位。
>>:位右移运算,把一个数字的所有位向右移动指定的位。
&:位的“与”运算,对两个数字的所有位执行一个AND操作。
|:位的“或”运算,对两个数字的所有位执行一个OR操作。
~:位的“否”运算,对一个数字的所有位取反。
^:位的异或运算(exclusive or),对两个数字的所有位执行一个异或操作。
比如:
$ echo $((16>>2))
4
逻辑运算:
$((...))
支持以下逻辑运算:
<:小于
>:大于
<=:小于或相等
>=:大于或相等
==:相等
!=:不相等
&&:逻辑与
||:逻辑或
!:逻辑否
expr1?expr2:expr3:三元条件运算符。若表达式expr1的计算结果为非零值(算术真),则执行表达式expr2,否则执行表达式expr3。
比如:
$ echo $(( (3 > 2) || (4 <= 1) ))
1
$ a=0
$ echo $((a<1 ? 1 : 0))
1
$ echo $((a>1 ? 1 : 0))
0
((...))
可以执行赋值运算:
$ echo $((a=1))
1
$ echo $a
1
如果在表达式内部赋值,可以放在圆括号中,否则会报错:
$ a=0
$ echo $(( a<1 ? (a+=1) : (a-=1) ))
1
逗号,
在$((...))
内部是求值运算符,执行前后两个表达式,并返回后一个表达式的值:
$ echo $((foo = 1 + 2, 3 * 4)) #两个表达式都会执行,然后返回后一个表达式的值12
12
$ echo $foo
3
expr命令支持算术运算,可以不使用((...))
语法,也只支持整数运算:
$ expr 3 + 2
5
$ foo=3
$ expr $foo + 2
5
let命令用于将算术运算的结果,赋予一个变量:
$ let x=2+3
$ echo $x
5
注意,x=2+3
这个式子里面不能有空格,否则会报错。
bash会保留历史操作在文件中,环境变量HISTFILE总是指向这个文件。
$ echo $HISTFILE
/home/me/.bash_history
history命令会输出这个文件的全部内容。用户可以看到最近执行过的所有命令,每条命令之前都有行号。越近的命令,排在越后面:
$ history
...
498 echo Goodbye
499 ls ~
500 cd
输入命令时,按下Ctrl + r
快捷键,就可以搜索操作历史,选择以前执行过的命令。这时键入命令的开头部分,Shell 就会自动在历史文件中,查询并显示最近一条匹配的结果,这时按下回车键,就会执行那条命令。
!e
表示找出操作历史之中,最近的那一条以e开头的命令并执行。Bash 会先输出那一条命令echo Goodbye,然后直接执行:
$ echo Hello World
Hello World
$ echo Goodbye
Goodbye
$ !e
echo Goodbye
Goodbye
由于!string语法会扩展成以前执行过的命令,所以含有!的字符串放在双引号里面,必须非常小心:
$ echo "hello!" #双引号中!后无字符
hello!
$ echo "I say:\"hello!\"" #双引号中!后有字符
bash: !\: event not found
#需要对!转义:
$ echo "I say:\"hello\!\""
I say:"hello\!"
上面的命令会报错,原因是感叹号后面是一个反斜杠,Bash 会尝试寻找,以前是否执行过反斜杠开头的命令,一旦找不到就会报错。解决方法就是在感叹号前面,也加上反斜杠。
在history中显示时间戳:
$ export HISTTIMEFORMAT='%F %T '
$ history
1 2013-06-09 10:40:12 cat /etc/issue
2 2013-06-09 10:40:12 clear
上面代码中,%F
相当于%Y - %m - %d
,%T
相当于%H : %M : %S
。
环境变量HISTSIZE设置保存历史操作的数量:
$ export HISTSIZE=10000
上面命令设置保存过去10000条操作历史。
如果不希望保存本次操作的历史,可以设置HISTSIZE等于0:
export HISTSIZE=0
如果HISTSIZE=0
写入用户主目录的~/.bashrc
文件,那么就不会保留该用户的操作历史。如果写入/etc/profile
,整个系统都不会保留操作历史。
环境变量HISTIGNORE可以设置哪些命令不写入操作历史:
export HISTIGNORE='pwd:ls:exit'
上面示例设置,pwd
、ls
、exit
这三个命令不写入操作历史。
操作历史的每一条记录都有编号。知道了命令的编号以后,可以用感叹号 + 编号执行该命令。如果想要执行.bash_history里面的第8条命令,可以像下面这样操作:
$ !8
history命令的-c参数可以清除操作历史:
$ history -c
cd -
命令可以返回前一次的目录:
# 当前目录是 /path/to/foo
$ cd bar
# 重新回到 /path/to/foo
$ cd -
如果希望记忆多重目录,可以使用pushd命令和popd命令。它们用来操作目录堆栈:
pushd命令的用法类似cd命令,可以进入指定的目录,并将该目录放入堆栈:
$ pushd dirname
第一次使用pushd命令时,会将当前目录先放入堆栈,然后将所要进入的目录也放入堆栈,位置在前一个记录的上方。以后每次使用pushd命令,都会将所要进入的目录,放在堆栈的顶部。
popd命令不带有参数时,会移除堆栈的顶部记录,并进入新的栈顶目录(即原来的第二条目录)。
比如:
# 当前处在主目录,堆栈为空
$ pwd
/home/me
# 进入 /home/me/foo
# 当前堆栈为 /home/me/foo /home/me
$ pushd ~/foo
# 进入 /etc
# 当前堆栈为 /etc /home/me/foo /home/me
$ pushd /etc
# 进入 /home/me/foo
# 当前堆栈为 /home/me/foo /home/me
$ popd
# 进入 /home/me
# 当前堆栈为 /home/me
$ popd
# 目录不变,当前堆栈为空
$ popd
这两个命令的参数如下:
-n
的参数表示仅删除堆栈顶部的记录,不改变目录,执行完成后还停留在当前目录:
$ popd -n
这两个命令还可以接受一个整数作为参数,该整数表示堆栈中指定位置的记录(从0开始)。pushd命令会把这条记录移动到栈顶,同时切换到该目录;popd则从堆栈中删除这条记录,不会切换目录:
# 将从栈顶算起的3号目录(从0开始)移动到栈顶,同时切换到该目录
$ pushd +3
# 将从栈底算起的3号目录(从0开始)移动到栈顶,同时切换到该目录
$ pushd -3
# 删除从栈顶算起的3号目录(从0开始),不改变当前目录
$ popd +3
# 删除从栈底算起的3号目录(从0开始),不改变当前目录
$ popd -3
dirs命令可以显示目录堆栈的内容,一般用来查看pushd和popd操作后的结果:
$ dirs
~/foo/bar ~/foo ~
栈顶(最晚入栈的目录)在最左边,栈底(最早入栈的目录)在最右边。
dirs -c
:清空目录栈。
脚本一般以Shebang行开头:
#!/bin/sh
# 或者
#!/bin/bash
如果bash解释器不在/bin目录,也可以这么写:
#!/usr/bin/env bash
解释:env
命令会返回环境变量。当执行 env python
时,它其实会去 env | grep PATH
里(也就是 /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin)这几个路径里去依次查找名为 python 的可执行文件。
脚本入门
如果将脚本放在环境变量$PATH
指定的目录中,就不需要指定路径了。因为 Bash 会自动到这些目录中,寻找是否存在同名的可执行文件。
建议在主目录新建一个~/bin子目录,专门存放可执行脚本,然后把~/bin
加入$PATH
:
export PATH=$PATH:~/bin
可以将这一行加到~/.bashrc
文件里面,然后重新加载一次.bashrc,这个配置就可以生效了:
$ source ~/.bashrc
以后不管在什么目录,直接输入脚本文件名,脚本就会执行:
$ script.sh
Bash 脚本中,#
表示注释。
调用脚本可以带参数:
$ script.sh word1 word2 word3
脚本文件内部,可以使用特殊变量,引用这些参数:
$0:脚本文件名,即script.sh。
$1~$9:对应脚本的第一个参数到第九个参数。
$#:参数的总数。
$@:全部的参数,参数之间使用空格分隔。
$*:全部的参数,参数之间使用变量$IFS值的第一个字符分隔,默认为空格,但是可以自定义。
如果脚本的参数多于9个,那么第10个参数可以用${10}的形式引用,以此类推。
比如script.sh如下:
#!/bin/bash
# script.sh
echo "全部参数:" $@
echo "命令行参数数量:" $#
echo '$0 = ' $0
echo '$1 = ' $1
echo '$2 = ' $2
echo '$3 = ' $3
运行结果如下:
$ ./script.sh a b c
全部参数:a b c
命令行参数数量:3
$0 = script.sh
$1 = a
$2 = b
$3 = c
如果多个参数放在双引号里面,视为一个参数。
$ ./script.sh "a b"
上面例子中,Bash 会认为"a b"是一个参数,$1会返回a b。注意,返回时不包括双引号。
shift命令:
shift命令可以改变脚本参数,每次执行都会移除脚本当前的第一个参数($1
),使得后面的参数向前一位,即$2
变成$1
、$3
变成$2
、$4
变成$3
,以此类推:
#!/bin/bash
#while循环结合shift命令,也可以读取每一个参数
echo "一共输入了 $# 个参数"
while [ "$1" != "" ]; do
echo "剩下 $# 个参数"
echo "参数:$1"
shift
done
shift命令的默认参数为1,如上。也可以shift 3
,表示移除前三个参数,原来的$4
变成$1
。
getopts命令
getopts命令用在脚本内部,用于取出脚本所有的带有前置连词线-
的参数(配置项参数):
getopts optstring name
第一个参数optstring是字符串,给出脚本所有的连词线参数。比如,某个脚本可以有三个配置项参数-l
、-h
、-a
,其中只有-a
可以带有参数值,而-l
和-h
是开关参数,那么getopts的第一个参数写成lha:
,顺序不重要。注意,a后面有一个冒号,表示该参数带有参数值。getopts的第二个参数name是一个变量名,用来保存当前取到的配置项参数,即l、h或a。
比如使用getopts命令配合while循环处理参数:
while getopts 'lha:' OPTION; do
case "$OPTION" in
l)
echo "linuxconfig"
;;
h)
echo "h stands for h"
;;
a)
avalue="$OPTARG" #如果某个连词线参数带有参数值,比如-a foo,那么处理a参数的时候,环境变量$OPTARG保存的就是参数值。
echo "The value provided is $OPTARG"
;;
?) #如果用户输入了没有指定的参数(比如-x),那么OPTION等于?。
echo "script usage: $(basename $0) [-l] [-h] [-a somevalue]" >&2
exit 1
;;
esac
done
shift "$(($OPTIND - 1))" #变量$OPTIND在getopts开始执行前是1,然后每次执行就会加1。
basename
命令用于去掉文件名的目录和后缀。
变量$OPTIND
在getopts开始执行前是1,然后每次执行就会加1。等到退出while循环,就意味着连词线参数全部处理完毕。这时,$OPTIND - 1
就是已经处理的连词线参数个数,使用shift命令将这些参数移除,保证后面的代码可以用$1
、$2
等处理命令的主参数。
-
和--
开头的参数,会被 Bash 当作配置项解释。那如果需要创建文件名为--test
,需使用配置项参数终止符--
,表示后面的参数都是实体参数:
$ touch -- --test
比如:
$ myPath="-l"
$ ls -- $myPath
ls: 无法访问'-l': 没有那个文件或目录
如果想在文件里面搜索--hello
,这时也要使用参数终止符--
:
$ grep -- "--hello" example.txt
如果不用参数终止符,grep
命令就会把--hello
当作配置项参数,从而报错。
exit
命令用于终止当前脚本的执行,并向 Shell 返回一个退出值。exit
命令后面可以跟参数,该参数就是退出状态:
# 退出值为0(成功)
$ exit 0
# 退出值为1(失败)
$ exit 1
比如:
if [ $(id -u) != "0" ]; then
echo "根用户才能执行当前脚本"
exit 1
fi
id -u
命令返回用户的 ID,一旦用户的 ID 不等于0(根用户的 ID),脚本就会退出,并且退出码为1,表示运行失败。
命令执行结束后,会有一个返回值。0表示执行成功,非0(通常是1)表示执行失败。环境变量$?
可以读取前一个命令的返回值:
cd /path/to/somewhere
if [ "$?" = "0" ]; then
rm *
else
echo "无法切换目录!" 1>&2
exit 1
fi
也可以使用if直接判断命令执行结果:
if cd /path/to/somewhere; then
rm *
else
echo "Could not change directory! Aborting." 1>&2
exit 1
fi
正常的执行脚本文件,是类似于bash *.sh
,其实是调用子shell。而source
命令不会调用子shell,所以常用于加载配置文件:
比如:
#!/bin/bash
# test.sh
echo $foo
使用不同方式调用:
# 当前 Shell 新建一个变量 foo
$ foo=1
# 打印输出 1
$ source test.sh
1
# 打印输出空字符串 #这一步如果想要输出1则需要在之前export foo=1
$ bash test.sh
source命令的另一个用途,是在脚本内部加载外部库:
#!/bin/bash
source ./lib.sh
function_from_lib
source有一个简写形式,可以使用一个点.
来表示:
$ . .bashrc
alias
命令用来为一个命令指定别名,这样更便于记忆:
alias NAME=DEFINITION
比如:
alias search=grep
alias
也可以用来为长命令指定一个更短的别名。下面是通过别名定义一个today的命令:
$ alias today='date +"%A, %B %-d, %Y"'
$ today
星期一, 一月 6, 2020
alias定义的别名也可以接受参数,参数会直接传入原始命令(其实就是个替换的过程):
$ alias echo='echo It says: '
$ echo hello world
It says: hello world
直接调用alias命令,可以显示所有别名:
$ alias
unalias
命令可以解除别名:
$ unalias lt