Linux Shell快速入门

前言

开始使用Ubuntu操作系统,使用起来感觉挺顺,但是对于Shell脚本了解不多,以下是一个简单的shell入门总结。

不同的类Unix系统可能使用不同的Shell程序, 这里所有的例子都是基于Bash(Bourne Again Shell)

脚本语言,既然冠之以“语言”,就说明它跟其他C/C++等编译语言在形式上是完全一样的,有变量,有函数,有if,else,while等条件分支,只是脚本语言是解释性的执行:碰到一句,解释一句,执行。

写一个脚本看一看

运行脚本之前,需要做三件事情:

  • 写一个脚本
  • 使Shell有权限执行该脚本
  • 把脚本放在Shell可以找到的地方

打开文本编辑器,输入下列文本,保存为my_scripts.sh:

1
2
3
4
#!/bin/bash
# this is my first shell script

echo "hello shell scripts"

第一脚本就写成了。运行脚本之前,你可能需要修改脚本的权限:

1
chmod 755 my_scripts

输入下面命令执行脚本:

1
./my_scripts.sh

基础

变量

shell脚本提供了不少环境变量来获取系统信息,如用户名,主机名,时间等:

1
$HOSTNAME,$USER,$DATE

这些变量是全局的,在任何时候都可以使用。但,要在脚本中使用的变量(变量无需声明,直接使用即可),却并不具备这样的全局性,例如,有如下脚本 myvar.sh:

1
2
3
4
#! /bin/sh
echo "MY_VAR is: $MY_VAR"
MY_VAR= "hi, there"
echo "MY_VAR is : $MY_VAR"

执行如下脚本命令:

1
2
3
4
$ MY_VAR=hello
$ ./myvar.sh
MY_VAR is:
MY_VAR is: hi, there

输出:

1
2
MY_VAR is:
MY_VAR is: hi, there

因而,变量MY_VAR并不具有全局的作用域,如果要像环境变量一样使用该变量,必须将其export:

1
2
3
4
5
$ MY_VAR=hello
$ export MY_VAR
$ ./myvar.sh
MY_VAR is:
MY_VAR is: hi, there

这样输出就变了:

1
2
MY_VAR is: hello
MY_VAR is: hi, there

附几个比较特殊的变量:

  • $$ 该变量对应的PID(进程ID)
  • $? 上一个脚本命令的退出条件值

数组

跟其他语言相似, Bash也支持数组, 数据元素之间通过一个IFS(Input Field Separator, 默认是一个空格字符)来分割的. 一般,声明一个数据有如下三个方式:

1
2
3
4
5
6
7
8
9
10
11
12
13

fruits[0]=apple
fruits[1]=pear
fruits[2]=orange

#or

fruits=(apple pear orange)

#or

declare -a arry

遍历数组中的元素, 引用单个元素:

1
2
3

echo ${fruits[1]}

引用所有元素:

1
2
3
4
5
6
7

echo ${fruits[*]}

#or

echo ${fruits[@]}

如果只是引用一个数组中的一部分,可以如下操作, 其中第一个数字表示开始的位置, 第二个数字表示需要引用的数组的长度.

1
2
3
4
5


echo "${fruits[@]:0:2}" # 打印数组前两个值


如何在数组中添加或者删除元素了? 实现起来并不麻烦.

1
2
3
4
5
6
7
8

fruits=(Banana "${fruits[@]}" Cherry)
echo ${fruits[@]} # Banana Apple pear orange Cherry

# 删除元素
unset 'fruits[0]' # 删除第一个元素
echo ${fruits[@]}

如果我们要确定数组中元素的数量以及某个元素的长度可以通过参数扩展(shell的几种扩展形式在下文中可以参考下文的阐述)的方式来获取:

1
2
3
4
5
6
7

a[99]=aha

echo ${#a[@]} #获取元素数量, 这里为1

echo ${#a[99]} #获取第99个元素长度,为3

需要注意的是,Bash中的数据未初始化的元素为空不作为数组长度计入,这与编程语言不一样。由于数组中肯能存在空隙,如果要知道哪些元素是存在的,同样需要通过参数扩展来遍历:

1
2
3
4
5
6
7
8
9
10
11
12
13

foo=([1]=a [3]=b [5]=c [7]=d)

#输出元素 a b c d
for i in "${foo[@]}"; do
echo $i
done

#输出非空元素索引 1 3 5 7
for i in "${!foo[@]}"; do
echo $i
done

函数

1
function fcn_name(){ ... }

那么,怎么知道函数的参数了?很简单,$1对应第一个参数,$2对应第二个参数,以此类推,$0则表示执行脚本本身的名字,另外有几个个比较特殊的变量:

  • $# 函数的参数个数(执行脚本的参数)
  • $@ 除了脚本名外所有的参数,$1 $2 ....
1
2
3
4
5
6
7
 
#! /bin/sh
while [ "$#" -gt "0" ]
do
echo "\$1 is $1"
shift
done

不是还有递归吗?Shell脚本同样可以实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#! /bin/sh

factor()
{
if [ "$1" -gt "1" ]; then
i= 'expr $1 - 1'
j='factor $i'
k='expr $1 \* $j'
echo $k
else
echo 1
fi
}

while:
do
echo "enter a number"
read x
factor $x
done

语句

多举几个例子就看懂了。

if/else 条件判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# first form
if condition ; then
commands
fi

# Second form
if condition ; then
commands
else
commands
fi

# Third form
if condition ; then
commands
elif condition ; then
commands
fi

循环

Bash中支持forwhile(until)两种形式的循环, 先来看看for循环:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#! /bin/sh

for i in 1 2 3 5 9
do
echo "number $i"
done

#! /bin/sh
INPUT_STR=hello
while [ "$INPUT_STR" != "bye" ]
do
echo "Please type something (bye to quit)"
read INPUT_STR
echo "you just typed: $INPUT_STR"
done

上述ffor循环可以写成C风格的形式:

1
2
3
4
5
6
7
#! /bin/sh

for (( i = 1; i < 9; i++ ))
do
echo "number $i"
done

while循环(与if命令一样,while也是根据命令列表的退出状态值来判断是否继续执行循环):

1
2
3
4
5
6
7
8
9
10

#!/bin/bash

cnt = 1

while [[ "$cnt" -le 5 ]]; do
echo "$cnt"
cnt = ((cnt + 1))
done

untilwhile大同小异, 只不过while是在退出状态值不为0时结束循环,而until与之相反。上述例子如果用until可以写成:

1
2
3
4
5
6
7
8
9
10

#!/bin/bash

cnt = 1

until [[ "$cnt" -gt 5 ]]; do
echo "$cnt"
cnt = ((cnt + 1))
done

Case

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#! /bin/sh
echo "please say something to me..."

while:
do
read INPUT_STR
case $INPUT_STR in
hello)
echo "hello you too!"
;;
bye)
echo "See you again!"
break;
;;
*)
echo "fail to understand"
;;
esac
done

echo
echo "that's the all my folks!"

退出条件

应用执行是否成功的标志,是一个0~255之间的整数,0表示应用没有发生错误,执行成功;其他任何值都表示发生了错误。

$? 即可查看一个命令是否执行成功。

例如判断某个文件是否存在:

1
2
3
4
5
6
#! /bin/bash
if [ -f .bash_profile ]; then
echo "You have a .bash_profile. Things are fine."
else
echo "Yikes! You have no .bash_profile!"
fi

通配符

在使用Shell过程中要用到大量文件名, 因此它提供了一种特殊字符, 帮助找快速指定一组文件名. 这种特殊字符叫做通配符(wildcard). 使用通配符的过程也称为”通配符匹配”(globbing). 在命令ls/cp/mkdir/find中都可以使用通配符来实现操作多个文件的目的.下表列出了常用的通配符:

通配符 含义
* 匹配任意多个字符
? 匹配任意单个字符
[chars] 匹配属于字符集合chars中的任意单个字符
[!chars] 匹配不属于字符集合chars中的任意单个字符
[[:class:]] 匹配字符类class中的任意单个字符

常见的字符类有[:alnum:](单个字母数字), [:alpha:](单个字母), [:digit]的那个数字, [:lower:]小写字母, [:upper:]大写字母

利用通配符, 可以构建出复杂的文件名匹配条件:

模式 匹配
* 所有文件(不包含隐藏文件)
g* g开头的任意文件
b*.txt b开头, 扩展名为.txt的文件
Data??? 以Data开头并紧接3个字符的文件
[abc]* 以a/b/c中任意字符开头的文件
backup.[0-9]{3} 以backup.开头并紧接3个数字的文件
[[:upper:]]* 以大写字母开头的文件
*[[:lower:]123] 以小写字母或1/2/3任意数字结尾的文件
[![:digit:]]* 不以数字开头的文件

扩展

每次执行Shell命令时, Bash会对命令中的某些符号, 比如前面说的*/?等通配符, 进行替换操作. 这一处理过程被称为扩展(expansion). 经过扩展, 我们执行的命令在被Shell执行之前会被展开成其他内容. 比如:

1
2
3

echo *

会把当前文件目录的文件都展示出来. 实际在执行echo之前, *已经被替换扩展成了其他内容. Shell中存在如路径名扩展/算术扩展等多种扩展形式:

  • 路径名扩展

通配符的工作机制被称为路径名扩展, 比如:

1
2
3
4
5
6
7
8
9

echo D*

echo *s

echo [[:upper:]]*

echo /usr/*/share

  • 浪纹线扩展

浪纹线~被扩展为同用户名的主目录. 如果未执行用户名, 则扩展为当前用户的目录:

1
2
3
4
5
echo ~

#如果存在用户foo, 则扩展结果为/home/foo
echo ~foo

  • 算术扩展

Shell通过算术扩展来执行算术运算-这样我们可以把Shell当作一个简单的计算器来使用了:

1
2
3
4
5
6
7
8
9

echo $((2 * 2))

echo $((10 + 3))

echo $((10 % 3))

echo $((3 ** 5))

算术扩展还支持嵌套:

1
2
3

echo $(($((5 ** 2)) * 3))

  • 花括号扩展

花括号扩展{}允许创建多个文本字符串; 花括号表达式本身可以是逗号分割的字符串列表, 也可以是整数区间或单个字符, 但不能包括未经引用的空白字符, 例如:

1
2
3
4
5
6
7

echo front-{A,B,C}-back

echo Number_{1..5}

echo {Z..A}

花括号扩展可以用来创建多个类似模式的文件夹, 比如我们要创建一个以年月时间为名称的系列文件夹:

1
2
3

mkdir {2020-2021}-{01..12}

  • 参数扩展

通过参数扩展可以将变量扩展为对应内容:

1
2
3
4
5
6
7
8
9

a="Alpah"

echo $a

echo $USER

echo $(which cp)

Shell通过引用可以有选择的禁止扩展: 第一种引用类型是双引号, 在双引号引用中, Shell中使用的特殊字符都将失去其特殊含义($, \, 除外`), 就是说路径名扩展, 浪纹线扩展, 花括号扩展全部失效, 但是参数扩展/算术扩展仍然可用; 第二种类型是单引号, 单引用会禁止所有扩展.

重定向

一般程序会将执行的状态或信息发送到标准输出(stdout)或者标准错误(stderr), 默认情况下标准输出/标准错误都会直接输出到屏幕, 如果要将其导入到文件则需要使用I/O重定向功能. Bash中提供了三种重定向:

  • 标准输出重定向
1
2
3
4
5

ls -al > ~/ls-output.txt # 将标准输出写入文件(覆盖已有内容)

ls -al >> ~/ls-output.txt # 将输出追加到文件

  • 标准输入重定向
1
2
3

cat < ~/ls-output.txt

  • 标准错误重定向: 标准错误没有专门的重定向符号, 需要使用文件描述符2来引用
1
2
3
4
5
6
7
8
9

ls -al /usr/bin 2> ls-error.txt

#将标准输出/标准错误保存到一个文件
ls -al /usr/bin > ls-output.txt 2>&1

#较新版本的Bash可以用更简单的方式来实现标准输出/错误同时重定向到文件
ls -al /usr/bin &> ls-output.txt

命令索引

Expression Description Example
& run the previous command in the background ls &
&& logical AND if [ "$foo" -ge "0" ] && [ "$foo" -le "9" ]
or logical OR if [ "$foo" -ge "0" ] or [ "$foo" -le "9" ]
^ start of line grep "^foo
$ end of line grep "foo$
= string equlity if [ "$foo" = "bar" ]
! logical NOT if [ "$foo" != "bar" ]
$$ pid of current shell echo "PID=$$
$! pid of last background command ls & echo "PID of ls = $!
&? exit status of last command ls; echo "ls returned code $?
$0 name of current command echo "I am $0"
$1 name of 1st parameter echo "first argument is $1
$9 name of 9th parameter echo "nith argument is $9
$@ all of current commands’ parameters(preserving whitespace/quoting) echo "my arguments are $@
$* all of current commands’ parameters(not preserving whitespace/quoting) echo "my arguments are $*
-d file True if file is a directory if [ -d /bin ]
-e file True if file exists if [ -e /home/bin/my.text ]
-f file True if file exists and is a regular file if [ -f /bin/fs]
-L file True if file is symbolic link if [ -L /bin/fs ]
-r file True if file is readable if [ -r /bin/fs ]
-w file True if file is writable if [ -w /bin/fs ]
-x file True if file is executable if [ -x /bin/fs ]
f1 -nt f2 True if f1 is newer than f2(modification time) if [ "@f1" -nt "$f2" ]
f1 -ot f2 True if f1 is older than f2 if [ "@f1" -ot "$f2" ]
-z string True if string is empty if [ -z "$f00" ]
-n string True if string is not empty if [ -n "$f00" ]
str1=str2 True if str1 equal str2 if [ "$foo" = "bar" ]
str1!=str2 True if str1 not equal to str2 if [ "$foo" != "bar" ]
cd - 将工作目录切换到上一个工作目录 cd /xxx/xxxx; cd -

Markdown显示原因: or实际为 ||

参考资料