什么是 shell?
真正能够控制计算机硬件(CPU、内存、显示器等)的只有操作系统内核(Kernel)。
由于安全、复杂、繁琐等原因,用户不能直接接触内核(也没有必要),需要另外再开发一个程序,让用户直接使用这个程序;该程序的作用就是接收用户的操作(点击图标、输入命令),并进行简单的处理,然后再传递给内核,这样用户就能间接地使用操作系统内核了。
shell就是linux 命令与 linux 内核间的代理; 对一个纯文本的文件进行解析,然后执行这些功能,也可以说 Shell 脚本就是一系列命令的集合。
语法规范
1 | !/bin/bash |
1 | cd 到该目录下 |
也可以直接 sh test.sh,无须!/bin/bash
变量
shell 中,每一个变量的值都是字符串,无论你给变量赋值时有没有使用引号,值都会以字符串的形式存储。
定义变量
Shell 支持以下三种定义变量的方式:
1 | variable=value |
Shell 变量的命名规范和大部分编程语言都一样:
- 变量名由数字、字母、下划线组成;
- 必须以字母或者下划线开头;
- 不能使用 Shell 里的关键字(通过 help 命令可以查看保留关键字)。
单引号与双引号
- variable 是变量名,value 是赋给变量的值。如果 value 不包含任何空白符(例如空格、Tab 缩进等),那么可以不使用引号;如果 value 包含了空白符,那么就必须使用引号包围起来。
- 以单引号’ ‘包围变量的值时,单引号里面是什么就输出什么,即使内容中有变量和命令(命令需要反引起来)也会把它们原样输出。
- 以双引号” “包围变量的值时,输出时会先解析里面的变量和命令,而不是把双引号中的变量名和命令原样输出。
如果变量的内容是数字,那么可以不加引号;如果真的需要原样输出就加单引号;其他没有特别要求的字符串等最好都加上双引号,定义变量时加双引号是最常见的使用场景。
访问变量
1 | myText="hello world" |
变量名外面的花括号{v}是可选的,加不加都行,加花括号是为了帮助解释器识别变量的边界。
只读变量
使用 readonly 命令可以将变量定义为只读变量,只读变量的值不能被改变。
下面的例子尝试更改只读变量,结果报错:
1 | !/bin/bash |
删除变量
1 | unset variable_name |
变量被删除后不能再次使用;unset 命令不能删除只读变量。
将命令的结果赋予变量
第一种方式把命令用反引号(位于 Esc 键的下方)包围起来,反引号和单引号非常相似,容易产生混淆,所以不推荐使用这种方式;
第二种方式把命令用$()包围起来,区分更加明显,所以推荐使用这种方式。
command 可以只有一个命令,也可以有多个命令,多个命令之间以分号;分隔。
1 | variable=`command` |
如果被替换的命令的输出内容包括多行(也即有换行符),或者含有多个连续的空白符,那么在输出变量时应该将变量用双引号包围,否则系统会使用默认的空白符来填充,这会导致换行无效,以及连续的空白符被压缩成一个。
1 | !/bin/bash |
要注意的是,$() 仅在 Bash Shell 中有效,而反引号可在多种 Shell 中使用。所以这两种命令替换的方式各有特点,究竟选用哪种方式全看个人需求。
位置参数
运行 Shell 脚本文件时我们可以给它传递一些参数,这些参数在脚本文件内部可以使用$n的形式来接收,例如,$1 表示第一个参数,$2 表示第二个参数,依次类推。
给脚本文件传递位置参数
1
2
3!/bin/bash
echo "Language: $1"
echo "URL: $2"运行,并附带参数:
1
2
3
4~]$ cd demo
demo]$ . ./test.sh Shell http://c.biancheng.net/shell/
Language: Shell
URL: http://c.biancheng.net/shell/其中Shell是第一个位置参数,http://c.biancheng.net/shell 是第二个位置参数,两者之间以空格分隔。
给函数传递位置参数
1
2
3
4
5
6
7
8!/bin/bash
定义函数
function func(){
echo "Language: $1"
echo "URL: $2"
}
调用函数
func C++ http://c.biancheng.net/cplus/运行:
1
2~]$ cd demo
demo]$ . ./test.sh Language: C++ URL: http://c.biancheng.net/cplus/
如果参数个数太多,达到或者超过了 10 个,那么就得用${n}的形式来接收了,例如 ${10}、${23}。{ }的作用是为了帮助解释器识别参数的边界,这跟使用变量时加{ }是一样的效果。
特殊变量
1 | 当前脚本的文件名。 |
可以不遵守一般变量的命名规范。
字符串变量
获取字符串长度:
${string_name}
字符串拼接:
1
2
3
4
5
6
7
8
9
10
11
12
13!/bin/bash
name="Shell"
url="http://c.biancheng.net/shell/"
str1=$name$url 中间不能有空格
str2="$name $url" 如果被双引号包围,那么中间可以有空格
str3=$name": "$url 中间可以出现别的字符串
str4="$name: $url" 这样写也可以
str5="${name}Script: ${url}index.html" 这个时候需要给变量名加上大括号
echo $str1
echo $str2
echo $str3
echo $str4
echo $str5字符串截取
1
2
3
4
5
6
7
8start :length} 从 string 字符串的左边第 start 个字符开始,向右截取 length 个字符。 :
start} 从 string 字符串的左边第 start 个字符开始截取,直到最后。 :
0-start :length} 从 string 字符串的右边第 start 个字符开始,向右截取 length 个字符。 :
0-start} 从 string 字符串的右边第 start 个字符开始截取,直到最后。 :
从 string 字符串第一次出现 *chars 的位置开始,截取 *chars 右边的所有字符。
从 string 字符串最后一次出现 *chars 的位置开始,截取 *chars 右边的所有字符。
从 string 字符串第一次出现 *chars 的位置开始,截取 *chars 左边的所有字符。
从 string 字符串最后一次出现 *chars 的位置开始,截取 *chars 左边的所有字符。
数组
Shell 并且没有限制数组的大小,理论上可以存放无限量的数据。Shell 是弱类型的,它并不要求所有数组元素的类型必须相同
1 | nums=(29 100 13 8 91 44) 以()表示,元素之间须空格 |
内建命令
所谓 Shell 内建命令,就是由 Bash 自身提供的命令,而不是文件系统中的某个可执行文件。
内建命令会比外部命令执行得更快,执行外部命令时不但会触发磁盘 I/O,还需要 fork 出一个单独的进程来执行,执行完成后再退出。而执行内建命令相当于调用当前 Shell 进程的一个函数。
alias命令
alisa 用来给命令创建一个别名。
1 | !/bin/bash |
unalias用来删除当前进程别名。
echo命令
用来在终端输出字符串,并在最后默认加上换行符。
echo 命令输出结束后默认会换行,如果不希望换行,可以加上-n参数,如下所示:
1 | !/bin/bash |
默认情况下,echo 不会解析以反斜杠\开头的转义字符。我们可以添加-e参数来让 echo 命令解析转义字符。例如:
1 | ~] echo -e "hello \nworld" |
有了-e参数,我们也可以使用转义字符\c来强制 echo 命令不换行。
read命令
read 是 Shell 内置命令,用来从标准输入中读取数据并赋值给变量。如果没有进行重定向,默认就是从键盘读取用户输入的数据;如果进行了重定向,那么可以从文件中读取数据。
使用 read 命令给多个变量赋值:
1 | !/bin/bash |
只读取一个字符:
1 | !/bin/bash |
在指定时间内输入密码:
1 | !/bin/bash |
exit命令
exit 是一个 Shell 内置命令,用来退出当前 Shell 进程,并返回一个退出状态;
exit 命令可以接受一个整数值作为参数,代表退出状态。如果不指定,默认状态值是 0。
一般情况下,退出状态为 0 表示成功,退出状态为非 0 表示执行失败(出错)了。
使用$?来获取退出状态。
declare命令
declare 和 typeset 都是 Shell 内建命令,它们的用法相同,都用来设置变量的属性。不过 typeset 已经被弃用了,建议使用 declare 代替。
declare [+/-] [aAfFgilprtux] [变量名=变量值]
1 | [name] 列出之前由用户在脚本中定义的函数名称和函数体。 |
将变量声明为整数并进行计算:
1 | !/bin/bash |
运行结果:
40
将变量定义为只读变量:
1 | declare -r n=10 |
显示变量的属性和值:
1 | declare -r n=10 |
运算
通俗地讲,就是将数学运算表达式放在((和))之间。
表达式可以只有一个,也可以有多个,多个表达式之间以逗号,分隔。对于多个表达式的情况,以最后一个表达式的值作为整个 (( )) 命令的执行结果。
可以使用$获取 (( )) 命令的结果,这和使用$获得变量值是类似的。
1 | ((a=1+2**3-4%3)) |
表达式
if else
1 | if condition1 |
如果 condition 成立,那么 then 后边的 statement1 语句将会被执行;否则,执行 else 后边的 statement2 语句。
1 | !/bin/bash |
test命令
test 是 Shell 内置命令,用来检测某个条件是否成立。test 通常和 if 语句一起使用,并且大部分 if 语句都依赖 test。
test 命令也可以简写为[],它的用法为:
[ expression ]
注意[]和expression之间的空格,这两个空格是必须的,否则会导致语法错误。[]的写法更加简洁,比 test 使用频率高。
与数值比较相关的 test 选项
1
2
3
4
5
6num1 -eq num2 判断 num1 是否和 num2 相等。
num1 -ne num2 判断 num1 是否和 num2 不相等。
num1 -gt num2 判断 num1 是否大于 num2 。
num1 -lt num2 判断 num1 是否小于 num2。
num1 -ge num2 判断 num1 是否大于等于 num2。
num1 -le num2 判断 num1 是否小于等于 num2。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17!/bin/bash
read age
if test $age -le 2; then
echo "婴儿"
elif test $age -ge 3 && test $age -le 8; then
echo "幼儿"
elif [ $age -ge 9 ] && [ $age -le 17 ]; then
echo "少年"
elif [ $age -ge 18 ] && [ $age -le 25 ]; then
echo "成年"
elif test $age -ge 26 && test $age -le 40; then
echo "青年"
elif test $age -ge 41 && [ $age -le 60 ]; then
echo "中年"
else
echo "老年"
fi与文件检测相关的 test 选项:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18filename 判断文件是否存在,并且是否为块设备文件。
filename 判断文件是否存在,并且是否为字符设备文件。
filename 判断文件是否存在,并且是否为目录文件。
filename 判断文件是否存在。
filename 判断文件是否存在,井且是否为普通文件。
filename 判断文件是否存在,并且是否为符号链接文件。
filename 判断文件是否存在,并且是否为管道文件。
filename 判断文件是否存在,并且是否为非空。
filename 判断该文件是否存在,并且是否为套接字文件。
filename 判断文件是否存在,并且是否拥有读权限。
filename 判断文件是否存在,并且是否拥有写权限。
filename 判断文件是否存在,并且是否拥有执行权限。
filename 判断文件是否存在,并且是否拥有 SUID 权限。
filename 判断文件是否存在,并且是否拥有 SGID 权限。
filename 判断该文件是否存在,并且是否拥有 SBIT 权限。
filename1 -nt filename2 判断 filename1 的修改时间是否比 filename2 的新。
filename -ot filename2 判断 filename1 的修改时间是否比 filename2 的旧。
filename1 -ef filename2 判断 filename1 是否和 filename2 的 inode 号一致,可以理解为两个文件是否为同一个文件。这个判断用于判断硬链接是很好的方法1
2
3
4
5
6
7
8
9
10!/bin/bash
read filename
read url
if test -w $filename && test -n $url
then
echo $url > $filename
echo "写入成功"
else
echo "写入失败"
fi与字符串判断相关的 test 选项
1
2
3
4
5
6
7str 判断字符串 str 是否为空。
str 判断宇符串 str 是否为非空。
str1 = str2
str1 == str2 =和==是等价的,都用来判断 str1 是否和 str2 相等。
str1 != str2 判断 str1 是否和 str2 不相等。
str1 \> str2 判断 str1 是否大于 str2。\>是>的转义字符,这样写是为了防止>被误认为成重定向运算符。
str1 \< str2 判断 str1 是否小于 str2。同样,\<也是转义字符。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16!/bin/bash
read str1
read str2
检测字符串是否为空
if [ -z "$str1" ] || [ -z "$str2" ]
then
echo "字符串不能为空"
exit 0
fi
比较字符串
if [ $str1 = $str2 ]
then
echo "两个字符串相等"
else
echo "两个字符串不相等"
fi与逻辑运算相关的 test 选项
1
2
3expression1 -a expression 逻辑与,表达式 expression1 和 expression2 都成立,最终的结果才是成立的。
expression1 -o expression2 逻辑或,表达式 expression1 和 expression2 有一个成立,最终的结果就成立。
!expression 逻辑非,对 expression 进行取反。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16!/bin/bash
read str1
read str2
检测字符串是否为空
if [ -z "$str1" -o -z "$str2" ] 使用 -o 选项取代之前的 ||
then
echo "字符串不能为空"
exit 0
fi
比较字符串
if [ $str1 = $str2 ]
then
echo "两个字符串相等"
else
echo "两个字符串不相等"
fi
[[]] 命令
test的升级版,[[ ]] 是 Shell 内置关键字,不是命令,在使用时没有给函数传递参数的过程,所以 test 命令的某些注意事项在 [[ ]] 中就不存在了,具体包括:
- 不需要把变量名用双引号””包围起来,即使变量是空值,也不会出错。
- 不需要、也不能对 >、< 进行转义,转义后会出错。
注意,[[ ]] 剔除了 test 命令的-o和-a选项,你只能使用 || 和 &&。
在 Shell [[ ]] 中,可以使用=~来检测字符串是否符合某个正则表达式,它的用法为:
[[ str =~ regex ]]
1 | !/bin/bash |
有了 [[ ]],你还有什么理由使用 test 或者 [ ],[[ ]] 完全可以替代之,而且更加方便,更加强大。
case in
当分支较多,并且判断条件比较简单时,使用 case in 语句就比较方便了。
1 | case expression in |
1 | printf "Input integer number: " |
case in 的 pattern 部分支持简单的正则表达式,具体来说,可以使用以下几种格式:
1 | 表示任意字符串。 |
1 | !/bin/bash |
while
while 循环是 Shell 脚本中最简单的一种循环,当条件满足时,while 重复地执行一组语句,当条件不满足时,就退出 while 循环。
1 | while condition |
1 | !/bin/bash |
util
unti 循环和 while 循环恰好相反,当判断条件不成立时才进行循环,一旦判断条件成立,就终止循环。
1 | until condition |
1 | !/bin/bash |
util循环使用较少,一般使用while即可。
for
C语言风格的 for 循环的用法如下:
1
2
3
4exp2; exp3))
do
statements
done1
2
3
4
5
6
7!/bin/bash
sum=0
for ((i=1; i<=100; i++))
do
+= i))
done
echo "The sum is: $sum"Python 风格的 for in 循环
1
2
3
4for variable in value_list
do
statements
donevaluelist的写法:
1
2
3
4
5
6
7
8!/bin/bash
sum=0
for n in 1 2 3 4 5 6
do
echo $n
n)) =
done
echo "The sum is "$sum1到100的和:
1
2
3
4
5
6
7!/bin/bash
sum=0
for n in {1..100}
do
n)) =
done
echo $sum打印A-z:
1
2
3
4
5!/bin/bash
for c in {A..z}
do
printf "%c" $c
done打印当前目录所有.sh文件:
1
2
3
4
5!/bin/bash
for filename in $(ls *.sh)
do
echo $filename
done
函数
Shell 函数的本质是一段可以重复使用的脚本代码,这段代码被提前编写好了,放在了指定的位置,使用时直接调取即可。
定义函数
Shell 函数定义的语法格式如下:
1 | Shell 函数定义的语法格式如下: |
如果你嫌麻烦,函数定义时也可以不写 function 关键字,如果写了 function 关键字,也可以省略函数名后面的小括号。
函数调用
调用 Shell 函数时可以给它传递参数,也可以不传递。如果不传递参数,直接给出函数名字即可:
name
如果传递参数,那么多个参数之间以空格分隔:
name param1 param2 param3
1 | !/bin/bash |
你可以将调用放在定义的前面,没有影响。