shell 脚本编程基础
shell 编程概述
shell 本身是一个用 C 语言编写的程序,是连接 用户 和 UNIX/Linux 内核 的桥梁。
一个简单的shell脚本 ShowHello.sh (Linux脚本一般使用 .sh 作为文件后缀名):
#! /bin/bash
#ShowHello
#To show hello to somebody
echo -n "Enter your name:"
read name
echo "Hello, $name"- 第一行
#! /bin/bash这是一个特殊的标记(称为 shebang),告诉系统这个脚本需要用/bin/bash这个程序来执行,是 shell 脚本的标准开头。通常放在脚本第一行,从而调用相应的解释程序予以执行 - 第二行
#ShowHello和第三行#To show hello to somebody以#开头的行是注释,不会被执行,用来解释脚本的功能 - 第四行
echo -n "Enter your name:"echo 是输出文字的命令,-n 选项表示输出后不自动换行。这行的效果是在屏幕上显示 "Enter your name:",光标会停在冒号后面等待输入 - 第五行
read nameread 命令用来接收用户从键盘输入的内容,这里把输入的内容保存到变量 name 中。比如你输入 "Tom",那么 name 这个变量就代表 "Tom" - 第六行
echo "Hello, $name"再次使用 echo 输出内容,$name 表示取出变量 name 中保存的值。如果之前输入了 "Tom",这行就会输出 "Hello, Tom"
这个shell脚本还不能运行,因为它没有 x 权限,无法作为应用程序执行。为这个脚本的所有者 u 添加 x 权限 $ chomd u+x ShowHello.sh
Linux 中,创建文件默认是不可执行的文件,需要修改文件的权限,为文件增加可执行权限,后在命令行模式下直接输入该文件的相对路径 ./文件名,即可运行
$ vim ShowHello.sh
$ ./ShowHello.sh
-bash: ./ShowHello.sh: Permission denied
$ ls -l
total 8
-rw-r--r-- 1 d111kc d111kc 108 Jul 31 20:23 ShowHello.sh
-rw-r--r-- 1 d111kc d111kc 0 Jul 30 21:05 note1.txt
drwxr-xr-x 2 d111kc d111kc 4096 Jul 30 21:04 test1
$ chmod u+x ShowHello.sh
$ ./ShowHello.sh
Enter your name:Mike
Hello, Mikeshell 的种类
shell脚本的第一行需要指出该脚本的解释程序,即shell的种类。Linux系统提供了多种不同的shell。
查看Linux系统提供的shell种类:
$ cat /etc/shells
# /etc/shells: valid login shells
/bin/sh
/usr/bin/sh
/bin/bash
/usr/bin/bash
/bin/rbash
/usr/bin/rbash
/usr/bin/dash
/usr/bin/tmux最常见的shell有Bourne shell(简称sh)、C shell(简称csh)、Korn shell(简称ksh)和Bourne-again shell(简称bash)。默认的shell是bash,它具有以下优点:
- 命令记忆功能 通过键盘上的上下箭头可以快速输入之前输入过的命令
- Tab自动补全
- 设置别名功能 例如
$ alias ll='ls -l'可以将ls -l的别名设置为ll
shell 脚本的运行
有3中方式执行shell脚本:
方式一
将shell脚本的权限设置为可执行,然后在提示符下直接运行。脚本文件的编辑完成后,利用chmod命令修改脚本文件的权限为可执行,如前面的示例:
$ chmod u+x ShowHello.sh
$ ./ShowHello.sh在运行该脚本时,必须给出该脚本的相对路径(如 ./ShowHello.sh,这个相对路径不能省略 ./)或绝对路径。在执行系统命令时,只需要给出命令文件名。因为命令和用户的可执行脚本文件是否能够执行,与环境变量PATH有关。
查看环境变量PATH $ echo $PATH
环境变量PATH的值决定了shell将到哪些目录中寻找命令或可执行程序,当用户运行一个命令或可执行程序时,Linux在这些目录下按顺序依次搜寻。由于用户的可执行脚本是放在用户家目录下的,所以直接输入 ShowHello.sh,系统无法找到该文件。
通过这种方式运行脚本,脚本文件第一行的#!/bin/bash一定要写对,好让系统查找到正确的解释器。
方式二
以脚本名作为参数传递给shell程序。
$ bash ShowHello.sh
Enter your name:Mike
Hello, Mike这种执行方式就是运行bash,将脚本 ShowHello.sh 作为参数传递给bash。通过这种方式运行脚本,不需要在脚本文件的第一行指定解释器信息,并且脚本名后面还可以带参数。
方式三(不推荐使用)
将脚本以输入重定向的方式传递给bash。
运行bash程序,以输入重定向的方式让bash从给定文件中读入命令行,并进行相应处理。
d111kc@D111KC:~$ bash < ShowHello.sh
Enter your name:d111kc@D111KC:~$这种方式的脚本后面是不能带参数的
shell 基础
shell脚本是文本文件
通配符
文件名的扩展称为通配(Globbing)
*匹配任意字符(不包括路径分隔符/)的 0 次或多次出现?匹配任意单个字符(不包括/)[]将一组字符列表括起来,其作用是匹配该列表中的任意一个字符。例如,space.[co]匹配space.c或者space.o,[Hh]*匹配以“H"或“h”开头的文件名。[]中无论有几个字符,都只代表某一个字符。在[]中,可以使用-指定字符的范围,例如[0-9]匹配任何0至9的数字。[^]或[!]为了匹配不在列表中的字符,需要在列表的开头加一个^或者!,代表反向选择。例如,[^Hh]*和[!Hh]*匹配以非“H"或“h”开头的文件名。{stringl,string2,...}匹配 sringl或 string2(或更多)中的一个字符串。{}告诉shell依次使用每种string形成一个单独的文件名进行匹配,大括号扩展只适用于bash、tcsh和Cshell,不适用于Kornshell和FreeBSDshell。例如,用户希望查看目录/home/student,/home/cs1,/home/re中所有文件的名称,$ ls /home/student /home/cs1 /home/re
使用通配符命令就比较简单:$ ls /home/{student,cs1,re}大括号中的逗号前面和后面都不能有空格
转义字符
shell默认支持转义字符,如果是特殊字符想要表示字面意义需要在前面添加 \ 转义
引号
shell 中引用字符有 3 种:单引号'',双引号""和反引号`
- 单引号引用一串字符,所有字符都是普通字符
单引号
被单引号括起来的所有字符都是普通字符,就算特殊字符也不再有特殊含义。在某些时候,会希望使用个别特殊字符,如$来引用变量的值,例如:
$ echo 'the directory is $HOME; the userid is $USER'
the directory is $HOME; the userid is $USER很显然这样的输出并不是我们所需要的。这时命令就不能正常工作,在不使用转义字符 \(若命令中存在多个特殊字符,使用转义字符“\”会让命令可读性变差)时,双引号可以很好地解决这个问题。
双引号
由双引号括起来的字符(除$、反引号和转义字符 \ 外)均作为普通字符。“$”“\”和倒引号是拥有特殊含义的,“$”代表引用变量的值,而反引号代表引用命令。在上述例题中,采用双引号可以使 $ 保留特殊含义
d111kc@D111KC:~$ echo 'the directory is $HOME; the userid is $USER'
the directory is $HOME; the userid is $USER
d111kc@D111KC:~$ echo "the directory is $HOME; the userid is $USER"
the directory is /home/d111kc; the userid is d111kc$ name=d111kc
$ echo '$name'
$name
$ echo "$name"
d111kc如果需要在双引号中间输出“$”和倒引号,则要在符号前加转义符“\”
反引号
由反引号括起来的字符串被shell解释为命令行,shell会先执行该命令,并以它的标准输出结果取代整个倒引号部分。
$ today=`date`
$ echo "Today is $today"
Today is Thu Jul 31 21:45:01 CST 2025重定向符
shell重定向分为两种,一种是输入重定向,另一种是输出重定向。从字面上理解,输入/输出重定向就是改变输入与输出方向。
每个程序都有输入和输出,那么程序应该能够从任何源接受输入,并可以向任何目标输出。例如,一个排序程序可以选择从键盘输入文本,从已有文件中读取文本,或者读入另一个程序的输出结果。同样,排序程序应该能够在显示器上显示输出,将输出写入文件或者将输出发送给另一个程序做进一步处理。
这样的设计思想有一个突出的优点就是,当编写程序时,可以依靠Linux处理输入/输出,程序员不必考虑输入/输出的各种情况,只需处理输入/输出的内容,当程序真正开始运行时,shell会将程序连接到用户希望使用的输入和输出设备上。在程序员眼里,一切设备皆文件,他以一种通用的方式编写程序的读取和写入。为了实现这一思想,UNIX(Linux)的开发入员设计了一种读/写数据的通用方法,这些功能被称为标准I/O
在默认情况下,大多数程序都会从键盘读取输入,并将输出到显示器。因此在登录时,shell会自动地将程序的输入源设置为键盘,称为标准输入;设置程序运行的正确结果输出到显示器,称为标准输出;将程序运行中的出错信息输出位置也设置为显示器,称为标准错误。标准输入、标准输出和标准错误都以文件的方式存在,通常缩写为stdin、stdout和stderr。
每次输入命令,都可以告诉shell在此命令执行期间重新设置输入和输出的方向,这正是UNIX(Linux)功能强大的地方。
使用输出重定向符 > ,改变内容输出的位置:
$ ls
ShowHello.sh bash name note1.txt test1
$ ls > filelist.txt
$ cat filelist.txt
ShowHello.sh
bash
filelist.txt
name
note1.txt
test1上面的代码 ls > filelist.txt 将 ls 命令输出的内容到文件 filelist.txt 中
输入/输出方向就是数据的流动方向。输入方向就是数据从哪里流向程序。数据默认从键盘流向程序,如果改变了它的方向,数据就从其他地方流入,这就是输入重定向。输出方向就是数据从程序流向哪里。数据默认从程序流向显示器,如果改变了它的方向,数据就流向其他地方,这就是输出重定向。
常用的输入/输出重定向符:
- 标准输入重定向
command < file将文件作为命令的输入command < 分界符从标准输入中读入,直到遇见分界符才停止
- 标准错误输入重定向
command 2 > file以覆盖的方式,把command的错误信息输出到file文件中command2 >> file以追加的方式,把command的错误信息输出到file文件中
- 标准输出重定向
command > file以覆盖的方式,把command的正确输出结果输出到file文件中command >> file以追加的方式,把command的正确输出结果输出到file文件中
输入重定向符号 <
输入重定向符 < 的作用是把命令(或可执行文件)的标准输入重新定向到指定文件,如:
$ wc -l < ShowHello.sh
6
$ wc -l ShowHello.sh
6 ShowHello.sh使用 wc -l 统计 ShowHello.sh这个文件有几行,是否使用输入重定向符并没有太大的差异。
输入重定向符号 <<
与输出重定向 >> 不同,输入重定向符 << 不代表追加的意思。它的作用是使用特定的分界符作为命令输入的结束标志,而不使用默认的Ctrl+D组合键。
$ wc -l << a
> 123
> 456
> abc
> a
3不停的输入,直到输入字符 a 之后按回车,才结束输入。上面的 > 表示命令没有结束时的换行
输出重定向符 >
输出重定向符 > 的作用是把命令(或可执行文件)的标准输出重新定向到指定文件(或其他输出目标)。这样,该命令的结果就不在屏幕上显示,而是写入指定文件中(或其他输出目标)。例如:
$ ls
ShowHello.sh bash filelist.txt hello.txt name note1.txt test1
d111kc@D111KC:~$ ls > list.txt
d111kc@D111KC:~$ cat list.txt
ShowHello.sh
bash
filelist.txt
hello.txt
list.txt
name
note1.txt
test1将 ls 的输出结果写入 list.txt 中(这个文件不存在时,会自动创建。文件存在时,会覆盖原文件)。
为了避免原文件内容被覆盖,可以使用输出追加重定向符 >>
输出追加重定向符 >>
和 > 类似,不覆写原文件,而是在原文件的末尾追加新内容。
文件描述符
Linux中一切皆文件,包括标准输入设备(键盘)和标准输出设备(显示器)在内的所有计算机硬件都是文件。在每个进程中,为了表示和区分已经打开的文件,Linux会给每个文件分配一个ID,这个ID就是一个整数,被称为文件描述符(FileDescriptor)。Linux程序在执行任何形式的I/O操作时,都是在读或者写一个文件描述符。在编写程序时,程序员只关注文件的内容。一个文件描述符只是一个和打开的文件相关联的整数,它的背后可能是一个硬盘上的普通文件、FIFO、管道、终端、键盘、显示器,甚至是一个网络连接。在进程创建的时候,系统为每个进程自动打开3个标准文件(即标准输入stdin、标准输出stdout和错误输出stderr),它们默认都是打开
的,其文件描述符分别为0、1、2。在重定向的过程中,0、1、2这3个文件描述符可以直接使用。
标准输出和标准错误输出虽然对应的设备都是显示器,但是它们是两个文件
命令执行操作符
多条命令可以在一行中出现,用命令执行操作符连接,则将按如下规则执行命令:
顺序执行 ;
如果使用 ; 连接多条命令,那么这些命令会依次执行,各命令之间没有任何逻辑关系,也就是说,不论哪条命令报错了,后面的命令仍会依次执行。
$ ls; date; pwd
ShowHello.sh bash filelist.txt hello.txt list.txt name note1.txt test1
Fri Aug 1 21:31:32 CST 2025
/home/d111kc它与写成多行的形式是等价的。
逻辑与 &&
如果使用 && 连接多条命令,那么这些命令之间就有逻辑关系了。只有第一条命令正确执行了,&& 连接的第二条命令才会执行。
逻辑或 ||
使用 || 连接多条命令,则只有前一条命令执行错误,后一条命令才能执行。
小括号()和大括号{}
当用户登录后启动shell时,这个shell是交互式的。当以这种方式工作时,shell是用户界面,当它执行完一条命令后,接着显示另一个提示符,始终在前台等待用户交互。还有一种shell是非交互的,当在交互shell中运行一个外部命令或者执行一个脚本时,交互shell会创建一个子shell,这个shell在不需要输入的情况下一次性处理所有的命令,运行结束后子shell进程结束。
小括号() 和大括号 {} 都可以将若干命令括起来,组合在一起,在逻辑上被视为一条命令。使用时在格式上应注意,左括号后面应有一个空格,右括号之前应有一个分号 ;。大括号也可以包括若干单独占一行的命令,大括号中的命令表必须用分号或者换行符终止。
成组命令也可以用小括号括起来,在成组命令用小括号括起来时,左括号后不必有空格,右括号之前也不需要加上分号。在用于成组命令的执行时,小括号和大括号的主要区别在于:小括号执行成组命令时,重新开启一个子shell来执行;而大括号在执行成组命令时,是在当前shell中执行
管道符、后台命令符和注释符
在shell中,管道符|用来连接多条命令,如“命令1丨命令2”。每个命令执行时都有一个独立的进程,前一个命令的输出是下一个命令的输入。这里需要注意,命令1必须有正确输出,而命令2必须可以处理命令1的输出结果;而且命令2只能处理命令1的正确输出,而不能处理错误输出。
为了使这些进程能够在后台运行,可以在命令后面加上&,实现后台运行。后台命令符&使得在shell中实现多道进程运行成为可能:一个进程在前台运行,其他进程在后台运行。前台进程在执行过程中,用户和进程可以发生交互一一响应用户输入数据,进程处理数据并输出结果,这种方式就是前台方式。后台进程在运行过程中,与用户无交互,即不响应用户的输入和中断控制信号。
适合在后台运行的命令是一些执行时间较长且无交互的命令,如编译程序、费时的排序程序及一些shell脚本。在后台运行作业时要注意,在后台运行的命令一样会将结果输出到屏幕上,干扰用户的工作。如果放在后台运行的作业会产生大量的输出,最好把它的输出重定向到某个文件中。
shell 编程
shell语法和C语言类似。
变量
shell变量分为两类:用户自定义变量和系统预定义的特殊变量。
用户自定义变量
变量名:用户自定义的变量是最普通的shell变量。变量名以字母或下画线开头,由字母、数字和下划线_组成。注意,数字不能做变量的开头符,大写字母通常是系统默认的变量,用户自定义变量通常用小写字母。
变量赋值:变量命名后,需要赋值,未赋值的变量其值为空。变量赋值的一般形式为:变量名=值。注意,变量与变量内容以等号=来连接。赋值时等号两边不能有空格。如果出现空格,赋值语句执行失败,变量未被赋值,依然为空。如果字符串中一定包括空格,字符串要加上引号。
变量引用:在程序中使用变量的值时,要在变量名前面加上$符,表示使用变量的值。例如:
$ name=d111kc
$ echo $name
d111kc
$ echo name
name在下述情况下,变量的引用会出现歧义:
$ dir=/home/student
$ cat $dir01/file1.txt在/home目录下,是以student开头,加学号的学生账户,所以上述命令的本意是要显示/home/student01/file1.txt文件,但是shell无法区别变量名是dir还是dir01,在这种情况下,shell默认到第一个非法变量名字符为止,第一个非法变量名字符是/,所以shell认为变量名为dir01,而dir01
未赋值,所以为空,命令执行失败。在这种情况下,要正确引用变量,需要用大括号将该变量名括起来,如下所示:
$ cat ${dir}01/filel.txt
除了用大括号外,还可以用引号引用变量,上述命令还可以表达成:
$ cat "$dir"01/filel.txt需要注意的是,大括号一定不要错写成小括号,$()等价于倒引号,其作用是把括号内命令的执行结果赋值给变量,如
$ working_dir=$(pwd)
$ echo $working_dir
/home/d111kc
$ echo `pwd`
/home/d111kc
数组的定义和使用与变量类似。所谓数组,就是相同数据类型的元素按一定顺序排列的集合,也就是把有限个类型相同的变量用一个名字命名,然后用编号区分它们的变量集合,把这个名字称为数组名,把编号称为下标。组成数组的各个变量被称为数组的元素。但是bash仅支持一维数组(不支持多维数组),并且没有限定数组的大小。数组下标从0开始,不能使用负数下标。
在bash中,用小括号 () 来表示包裹数组,数组元素用“空格”分隔。
$ langs=(html css javascript)
$ echo ${langs[*]}
html css javascript
$ langs[0]=python
$ langs[1]=c
$ echo ${langs[*]}
python c javascript还可以用declare命令显式声明一个数组,而不赋值,其形式是 declare -a langs 这时langs就是一个数组名,而不会被解释成变量了。
通过下标获取数组元素的值 ${数组名[下标]},如果没有给出数组元素的下标,则数组名表示下标为0的数组元素。
获取整个数组的值 ${数组名[*]} 或 ${数组名[@]}
$ langs=(html css javascript)
$ echo ${langs[0]}
html
$ echo $langs
html
$ echo ${langs[*]}
html css javascript
$ echo ${langs[@]}
html css javascript${langs[*]} 和 ${langs[@]} 的区别就是,${langs[*]} 将整个数组扩展为一
个词(即字符串),这个词由以空格分开的各个数组元素组成;${langs[@]} 将整个数组扩展为多个词,每个数组元素是一个词。例如:
$ langs=(html css javascript)
$ for i in "${langs[*]}"; do echo $i; done
html css javascript
$ for i in "${langs[@]}"; do echo $i; done
html
css
javascript代码对数组进行了遍历,do 表示循环体的开始,done 表示循环体的结束。
系统预定义变量
系统预定义变量是在shell中已定义好的变量,系统预定义变量不能被用户重新定义。
常用的预定义变量:
$?上一条命令执行后的返回值(也称返回码,退出状态等)$$当前进程的进程号(PID)$!上一个后台命令对应的进程号(PID)$-当前在运行shell程序的选项$#命令行上参数的个数$*,$@命令行上实际给出的所有实参
$? 是上一条命令执行后的返回值,它是一个十进制数。大多数的shell命令(除了let命令计算表达式的值等)执行成功后,返回值为0;执行失败,则返回非0值(不同的值代表不同的错误)
$ pwd
/home/d111kc
$ echo $?
0$$ 表示当前进程的进程号。每个进程都有唯一的进程号(PID)
$ echo $$
300新开一个终端再次运行上面的命令时,输出的值会发生改变。
$! 是上一个后台命令对应的进程号
$- 是当前在运行shell程序的选项。shell命令的一般格式是:命令名 [选项] [参数] 登录后启动的交互式shell也是一个程序,它和上述命令一样有选项(默认不带参数),它的选项可以通过执行 echo $-显示出来:
$ echo $-
himBHs位置参数变量
位置参数变量也属于预定义变量。
在Linux的命令行中,当一条命令或脚本执行时,后面可以跟多个参数,使用位置参数变量来表示这些参数。其中,$0 代表命令行本身,$1 代表第1个参数,依次类推。当参数个数超过10个时,就要用大括号把这个数字括起来,例如,${10};代表第10个参数,${14}则代表第14个参数。举个例子:
$ cat filel file2在这里,cat对应于$0,file1对应于$1,file2对应于$2
常用的位置参数变量:
$nn为数字,$0代表命令本身,$1到$9代表第1—9个参数,10以上的参数需要用大括号包含,如${10}$*代表命令行中所有的参数,把所有的参数看成一个整体$@代表命令行中所有的参数,把每个参数区别对待$#命令行上参数的个数
位置参数变量用于向命令或程序脚本传递信息,比如要写一个做加法计算的脚本 add.sh,在编写脚本的时候,并不知道add脚本将要处理的数据对象,所以编写代码的时候,以位置参数变量来代替将要处理的数据对象:
$ vim add.sh
$ cat add.sh
#! /bin/bash
let num1=$1
let num2=$2
let sum=num1+num2
echo $sum
$ chmod u+x add.sh
$ ./add.sh 1 2
3上面的 add.sh 脚本可以带两个参数,脚本根据它们在命令上的位置来识别并予以处理,所以称之为位置参数。
上面的脚本必须传入两个参数,如果想要使用不定长参数,脚本就需要用到循环语句,循环语句需要确定循环的次数,这时候就需要应用$#来解决这个问题,$#代表命令行上参数的个数,但不包含shell脚本名本身,即不包括$0的参数的个数。
除了使用$#来控制循环次数,还可以使用shift来解决这个问题。shift命令移动位置参数,每执行一次shift命令,就把命令行上的实参向左移动一位。注意shift命令不能将$O移走,shift命令执行后,新$1的值是原$2的值,新$2的值是原$3的值,以此类推。例如可以传入不定长参数的加法运算脚本 add2.sh:
$ vim add2.sh
$ cat add2.sh
#! /bin/bash
let sum=0
while [ $1 ]
do
let num1=$1
let sum=num1+sum
shift
done
echo $sum
$ chmod u+x add2.sh
$ ./add2.sh 1 3 5 7 9
25位置参数除了通过脚本运行时的实参传递赋值外,还可以利用set命令赋值。
环境变量
环境变量也是系统预定义变量。环境变量是全局变量,而用户自定义变量是局部变量。用户自定义变量只在当前的shell中生效,而环境变量会在当前shell和这个shell的所有子shell中生效。
环境变量是写入相应的配置文件的,在执行过程中修改环境变量修改的是进程内存中的变量值,而没有写入配置文件,所以当前shell一旦终止,这个环境变量的修改就会结束,而只有写入配置文件才会在所有shell中生效。
在Linux中一般通过环境变量配置操作系统的环境,如提示符、查找命令的路径、用户家目录等,这些系统默认的环境变量的变量名是固定的,一些环境变量为只读变量,只读变量的值通常是在登录过程中定义的,意味着不能改变这些变量的值;而另一些为非只读变量,可以修改。
环境变量名一般都大写,便于区分。
在bash中可以用env命令列出已经定义的所有环境变量:
$ env
SHELL=/bin/bash
WSL2_GUI_APPS_ENABLED=1
WSL_DISTRO_NAME=Ubuntu
NAME=D111KC
PWD=/home/d111kc
LOGNAME=d111kc
HOME=/home/d111kc
LANG=C.UTF-8
WSL_INTEROP=/run/WSL/320_interop
LS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=00:su=37;41:sg=30;43:ca=00:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arc=01;31:*.arj=01;31:*.taz=01;31:*.lha=01;31:*.lz4=01;31:*.lzh=01;31:*.lzma=01;31:*.tlz=01;31:*.txz=01;31:*.tzo=01;31:*.t7z=01;31:*.zip=01;31:*.z=01;31:*.dz=01;31:*.gz=01;31:*.lrz=01;31:*.lz=01;31:*.lzo=01;31:*.xz=01;31:*.zst=01;31:*.tzst=01;31:*.bz2=01;31:*.bz=01;31:*.tbz=01;31:*.tbz2=01;31:*.tz=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.war=01;31:*.ear=01;31:*.sar=01;31:*.rar=01;31:*.alz=01;31:*.ace=01;31:*.zoo=01;31:*.cpio=01;31:*.7z=01;31:*.rz=01;31:*.cab=01;31:*.wim=01;31:*.swm=01;31:*.dwm=01;31:*.esd=01;31:*.avif=01;35:*.jpg=01;35:*.jpeg=01;35:*.mjpg=01;35:*.mjpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.svg=01;35:*.svgz=01;35:*.mng=01;35:*.pcx=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.m2v=01;35:*.mkv=01;35:*.webm=01;35:*.webp=01;35:*.ogm=01;35:*.mp4=01;35:*.m4v=01;35:*.mp4v=01;35:*.vob=01;35:*.qt=01;35:*.nuv=01;35:*.wmv=01;35:*.asf=01;35:*.rm=01;35:*.rmvb=01;35:*.flc=01;35:*.avi=01;35:*.fli=01;35:*.flv=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.yuv=01;35:*.cgm=01;35:*.emf=01;35:*.ogv=01;35:*.ogx=01;35:*.aac=00;36:*.au=00;36:*.flac=00;36:*.m4a=00;36:*.mid=00;36:*.midi=00;36:*.mka=00;36:*.mp3=00;36:*.mpc=00;36:*.ogg=00;36:*.ra=00;36:*.wav=00;36:*.oga=00;36:*.opus=00;36:*.spx=00;36:*.xspf=00;36:*~=00;90:*#=00;90:*.bak=00;90:*.crdownload=00;90:*.dpkg-dist=00;90:*.dpkg-new=00;90:*.dpkg-old=00;90:*.dpkg-tmp=00;90:*.old=00;90:*.orig=00;90:*.part=00;90:*.rej=00;90:*.rpmnew=00;90:*.rpmorig=00;90:*.rpmsave=00;90:*.swp=00;90:*.tmp=00;90:*.ucf-dist=00;90:*.ucf-new=00;90:*.ucf-old=00;90:
WAYLAND_DISPLAY=wayland-0
LESSCLOSE=/usr/bin/lesspipe %s %s
TERM=xterm-256color
LESSOPEN=| /usr/bin/lesspipe %s
USER=d111kc
DISPLAY=:0
SHLVL=1
XDG_RUNTIME_DIR=/run/user/1000/
WSLENV=
XDG_DATA_DIRS=/usr/local/share:/usr/share:/var/lib/snapd/desktop
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/usr/lib/wsl/lib:/mnt/e/workstation/bin/:/mnt/c/Program Files/Common Files/Autodesk Shared/:/mnt/c/Program Files/Microsoft SQL Server/120/Tools/Binn/:/mnt/c/Windows/system32:/mnt/c/Program Files/nodejs/:/mnt/d/Bandizip/:/mnt/c/Program Files/Go/bin:/mnt/d/hugo_extended_withdeploy_0.143.0_windows-amd64:/mnt/c/WINDOWS/system32:/mnt/c/WINDOWS:/mnt/c/WINDOWS/System32/Wbem:/mnt/c/WINDOWS/System32/WindowsPowerShell/v1.0/:/mnt/c/WINDOWS/System32/OpenSSH/:/mnt/c/Program Files/Git/cmd:/mnt/e/dev/python/python3.12/Scripts/:/mnt/e/dev/python/python3.12/:/mnt/c/Program Files/MySQL/MySQL Server 8.0/bin:/mnt/c/Program Files/PowerShell/7/:/mnt/c/Program Files/Docker/Docker/resources/bin:/mnt/c/Program Files/MySQL/MySQL Shell 8.0/bin/:/mnt/c/Users/86183/AppData/Local/Microsoft/WindowsApps:/mnt/c/Users/86183/AppData/Roaming/Python/Python312/Scripts:/mnt/d/PyCharm/PyCharm Community Edition 2024.3.1.1/bin:/mnt/c/Users/86183/AppData/Roaming/npm:/mnt/d/ffmpeg-7.1-full_build/ffmpeg-7.1-full_build/bin:/mnt/c/Users/86183/go/bin:/mnt/c/Users/86183/AppData/Local/Programs/Microsoft VS Code/bin:/mnt/d/vscode/Microsoft VS Code/bin:/snap/bin
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus
HOSTTYPE=x86_64
PULSE_SERVER=unix:/mnt/wslg/PulseServer
_=/usr/bin/env常用环境变量:
HOME 用户家目录
$ echo $HOME
/home/d111kcPATH shell查找命令的目录列表。PATH变量包含带冒号分界符的字符串,这些字符串代表存放系统命令的绝对路径。通常,用户会在家目录下建一个bin目录,存放自己编写的所有可执行命令,把这个目录加人PATH变量中,如下:$ PATH=$PATH:$HOME/bin即在当前的命令查找路径下增加一个目录$HOME/bin,这时用户就可以像使用系统命令一样使用自已编写的命令,而不用再给出相对路径./sh01。需要注意的是,$HOME/bin一定要放在PATH的最后面,因为系统是严格按照PATH变量值中的路径顺序来查找命令的,也就是说PATH变量值中的路径顺序决定了先从哪个目录查找
PS1 shell的主提示符。主提示符是在 shell准备接受命令时显示的字符串。PS1定义主提示符是怎样构成的。
$ echo $PS1
\[\e]0;\u@\h: \w\a\]${debian_chroot:+($debian_chroot)}\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$“\u”表示当前用户的用户名;“\h”显示主机名的第一个字段(简称),如locallhost;“\W”显示完整的工作目录,~ 代表家目录在PS1中常用的转义字符及其含义如下:
\$:提示字符,如果是root用户,提示符为#,普通用户则为$。\t:显示时间为24小时格式,如“HH:MM:SS”- \T:显示时间为12小时格式
- \H:显示完整的主机名
- \s:所用shell名称
- \v:bash的版本号。
PWD 当前工作目录的绝对路径。
SHELL 当前使用的shell。它指出shell解释程序放在什么地方。
$ echo $SHELL
/bin/bash环境变量和用户自定义变量的作用域不同,是通过export命令来设置的。用户自定义变量是局部变量,仅限于自身范围,不能自动传给子进程。如果希望父进程在创建子进程时将变量传递给子进程,须用export命令将这些变量送人进程转出区,进程转出区的变量将被子进程继承,从而使得用户自定义变量成为全局变量。
export命令的一般使用形式是:$ export变量名
变量通过export命令声明即可被子进程继承。用户自定义变量都是通过export设置为全局变量的。用户自定义变量只能在当前shell中有效,而export命令声明的变量在当前shell和所有子 shell 中有效。
$ name=d111kc
$ echo $name
d111kc
$ bash
To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.
$ echo $name
这时name变量为空,因为第一句赋值是在父shell中执行的,在执行了bash语句后,处于子shell,在子shell中未对name变量赋值,所以name变量为空,下面继续执行如下命令:
$ exit
exit
$ export name
$ echo $name
d111kc
$ bash
To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.
$ echo $name
d111kc这时显示name变量的值为zhang,因为在父shell中已经通过export声明了变量name,子shell继承该变量,所以该变量值为d111kc
变量的查询和删除
用户可以定义变量,在不需要使用该变量的时候也可以注销该变量,注销变量用unset命令,其一般形式是:$ unset 变量名
d111kc@D111KC:~$ name=d111kc
d111kc@D111KC:~$ echo $name
d111kc
d111kc@D111KC:~$ unset name
d111kc@D111KC:~$ echo $name
d111kc@D111KC:~$环境变量的删除方法和用户自定义变量是一样的,都使用unset命令。在注销变量之前,通常需要查询系统内已有的变量列表,查询所有变量的命令有set、env和export。三者都可以查询系统当前已有的变量列表,set的用法如下,env和export用法与set类似。
$ set
三者的区别就是:set可以查询所有的变量,包括局部变量和全局变量;env命令可以查询所有的环境变量,包括本进程及“祖先进程”所设置的全局(环境)变量;export命令可以显示本进程利用export命令所输出的全部变量。
数值运算
在在shell中所有的变量默认都是“字符串型”。也就是说,如果不手动指定变量的类型,那么所有的数值都是不能进行运算的。
$ a=11
$ b=12
$ c=$a+$b
$ echo $c
11+12如果需要进行数值运算,则可以采用以下三种方法中的任意一种:
使用declare声明变量类型
使用declare命令就可以声明变量的类型。declare命令格式如下:$ declare [选项] 变量名
选项:
-给变量设定类型属性,配合字母使用+取消变量的类型属性,配合字母使用-a将变量声明为数组型-i将变量声明为整数型-r将变量声明为只读变量-x将变量声明为环境变量-p显示指定变量被声明的类型
$ a=11
$ b=12
$ declare -i c=$a+$b
$ echo $c
23数组与变量类似,变量和数组都是用来保存数据的,只是变量只能被赋予一个数据值,一旦重复赋值,后一个值就会覆盖前一个值;而数组可以被赋予一组相同类型的数据值。
$ declare -a name
name会被认为是数组,虽然还未赋值,也不会被视为变量。如果没有把name变量声明为数组型,只要在定义变量时采用了变量名[下标]的格式,这个变量就会被系统视为数组型了,不用强制声明。
declare命令还可以把变量声明为环境变量,它和export命令的作用是一样的。例如:$ declare -x test=123表示把变量test声明为环境变量。
使用expr和let命令
进行数值运算的第二种方法是使用expr命令,这个命令没有declare命令那么复杂。命令如下:
$ a=11
$ b=12
$ c=$(expr $a + $b)
$ echo $c
23注意使用expr命令进行运算时,“+”号左右两侧必须有空格,否则运算不执行。
let命令和expr命令类似,let比expr更常用。用法如下:
$ a=11
$ b=12
$ let c=$a+$b
$ echo $c
23使用$(())运算
凡是用let命令的地方都可以用 $(()) 取代,但其中只能包含一个算术表达式
$ a=11
$ b=12
$ c=$(( $a + $b ))
$ echo $c
23if条件语句
if语句用于条件控制结构,其一般格式为:
if 测试条件
then 命令1
else 命令2
fi其中,if、then、else 和fi是关键字。
条件测试有两种常用形式:一种是用test命令if test condition;另一种是用一对方括号将测试条件括起来 if [condition]。这两种形式是完全等价的。[ ] 内部左右两端需要有空格。
条件测试常用来测试文件的属性、做字符串的比较和数值的比较。有关文件测试运算符的形式及其功能如下:
-r若文件存在且用户可读,则测试条件为真-w若文件存在且用户可写,则测试条件为真-x若文件存在且用户可执行,则测试条件为真-f若文件存在且是普通文件,则测试条件为真-d若文件存在且是目录文件,则测试条件为真-b若文件存在且是块设备文件,则测试条件为真-c若文件存在且是字符设备文件,则测试条件为真-s若文件存在且文件长度大于0,则测试条件为真-e该文件是否存在
有关字符串测试运算符的形式及其功能如下:
-z s1若字符串s1的长度为0,则测试条件为真-n s1若字符串s1的长度大于0,则测试条件为真sl = s2若s1等于s2,则测试条件为真,“=”前后应有空格s1 != s2若s1不等于s2,则测试条件为真
数值比较运算:
n1 -eq n2若n1等于n2,则测试条件为真n1 -ne n2若n1不等于n2,则测试条件为真n1 -lt n2若n1小于n2,则测试条件为真n1 -le n2若n1小于或等于n2,则测试条件为真n1 -gt n2若n1大于n2,则测试条件为真n1 -ge n2若n1大于或等于n2,则测试条件为真
if语句使用fi结尾,和一般语言使用大括号结尾不同。
if条件语句的判断思路是测试条件判断式是否成立,如果成立,则执行“then”中的命令;如果不成立,则执行“else”中的命令。
else部分还可以嵌套if,用关键字“elif”代替“elseif”,其一般格式如下:
if [ 条件 ]
then 命令1
elif [ 条件 ]
then 命令2
else 命令3
ficase 多分支条件语句
case语句和if语句一样都是多分支条件语句,不过和多分支if条件语句不同的是,case语句只能判断一种条件关系,而if语句可以判断多种条件关系。
case语句的语法如下:
case $变量名 in
"值1") 命令
命令;;
“值2") 命令
命令;;
...
*) 命令
命令;;
esaccase语句会取出变量中的值,然后与语句体中的值逐一比较。如果数值符合,则执行对应的程序;如果数值不符,则依次比较下一个值;如果所有的值都不符合,则执行*) 的命令,*可以缺省。
case语句以“case”开头,以“esac”结尾。
在每个分支程序之后要以;;(双分号)结尾,代表该程序段结束。
$ vim yesno.sh
$ chmod u+x yesno.sh
$ ./yesno.sh
输入是或否(y/n): y
Your answer is yes!
yes
$ ./yesno.sh
输入是或否(y/n): n
Your answer is no!
no
$ ./yesno.sh
输入是或否(y/n): 5
Your choice is error!
$ cat yesno.sh
#! /bin/bash
echo -n "输入是或否(y/n): "
read answer
case $answer in
"y") echo "Your answer is yes!"
echo "yes";;
"n") echo "Your answer is no!"
echo "no";;
*) echo "Your choice is error!";;
esacwhile 循环语句
shell有多种循环形式,包括while循环、until循环、for循环和selectin循环。此外,还需要了解continue和break关键字,以便在不满足结束条件的时候跳出循环。
while循环是shell脚本中最简单的一种循环,当条件满足时,while重复地执行一组语句,当条件不满足时,则退出while循环。while循环的用法如下:
while condition
do
statements
donecondition表示判断条件,statements表示要执行的语句(可以只有一条,也可以有多条),do和
done都是shell 中的关键字。
while循环的执行流程为:先对condition进行判断,如果该条件成立,则进入循环,执行while循环体中的语句,也就是do和done之间的语句。这样就完成了一次循环。每一次执行到done的时候都会重新判断condition是否成立,如果成立,则进人下一次循环,继续执行do和done之间的语句,如果不成立,则结束整个while循环,执行done后面的其他shell代码。如果一开始condition就不成立,那么程序就不会进人循环体,do和done之间的语句就没有执行的机会。
注意,在while循环体中必须有相应的语句使得condition越来越趋近于“不成立”,只有这样才能最终退出循环,否则while就成了死循环,会一直执行下去,永无休止。
while语句和if语句中的条件判断用法是一样的,可以使用test或[]命令,也可以使用 (())。
例如:计算1-100的整数之和:
$ vim sum.sh
$ chmod u+x sum.sh
$ ./sum.sh
the sum is 5050
$ cat sum.sh
#! /bin/bash
i=1
sum=0
while [ "$i" -le 100 ]
do
(( sum += i ))
(( i++ ))
done
echo "the sum is $sum"until 循环
until循环和while循环恰好相反,当判断条件不成立时才进行循环,一旦判断条件成立终止循环。until循环的用法如下:
until condition
do
statements
donefor 循环
for循环有两种使用形式,一种是算术表达式方式,另一种是值表方式。算术表达式方式的for循环用法如下:
for ((expl;exp2;exp3))
do
statements
doneexpl、exp2、exp3是3个表达式,其中exp2是判断条件,for循环根据exp2的结果来决定是否继续下一次循环;statements是循环体语句,可以有一条,也可以有多条;do和done是shell中的关键字。
它的运行过程为:步骤一,执行expl;步骤二,执行exp2,如果它的判断结果是成立的,则执行循环体中的语句,否则结束整个for循环;步骤三,执行完循环体后再执行exp3;步骤四,重复执行步骤二和三,直到exp2的判断结果不成立,则结束循环。在上面的步骤中,步骤二和步骤三会重复执行,for语句的主要作用就是不断执行步骤二和三。exp1仅在第一次循环时执行,以后都不会再执行,可以认为这是一个初始化语句。exp2一般是一个关系表达式,决定了是否还要继续下次循环,称为“循环条件”。exp3很多情况下是一个带有自增或自减运算的表达式,以使循环条件逐渐变得“不成立”
使用for循环计算 1 + 2 + ... + 100:
$ vim sumfor.sh
$ chmod u+x sumfor.sh
$ ./sumfor.sh
the sum is 5050
$ cat sumfor.sh
#! /bin/bash
sum=0
for (( i=0; i<=100; i++ ))
do
(( sum+=i ))
done
echo "the sum is $sum"for循环中的expl(初始化语句)、exp2(循环条件)和exp3(自增或自减)都是可选项,都可以省略(但分号;必须保留)。
值表方式的for循环的用法如下:
for variable in value_list
do
statements
donevariable表示变量,value_list表示取值列表,in是shell中的关键字。每次循环都会从value_list中取出一个值赋给变量variable,然后进入循环体(do和done之间的部分),执行循环体中的statements。直到取完value_list中的所有值,循环就结束了。invalue_list部分可以省略,省略后的效果相当于in $@($*),变量依次取位置参数的值,然后执行循环体内的命令,直至所有的位置参数取完为止。例如:
$ vim forloop.sh
$ chmod u+x forloop.sh
$ ./forloop.sh
1
2
3
4
5
the sum is 15
$ cat forloop.sh
#! /bin/bash
sum=0
for n in 1 2 3 4 5
do
echo $n
(( sum+=n ))
done
echo "the sum is $sum"下面是缺省value_list的情况,这时变量将匹配位置参数,如下:
$ vim forloop.sh
$ chmod u+x forloop.sh
$ ./forloop.sh python c html css javascript
python
c
html
css
javascript
$ cat forloop.sh
#! /bin/bash
for str
do
echo $str
doneselect in 循环
select in是shell独有的一种循环,非常适合终端这样的交互场景,是C语言所没有的。selectin循环用来增强交互性,它可以显示出带编号的菜单,用户输人不同的编号就可以选择不同的菜单,并执行不同的功能。select in循环的用法如下:
select variable in value_list
do
statements
donevariable表示变量,value_list表示取值列表,in是shell中的关键字。select in和for in的语法是相似的。
$ vim selectin.sh
$ chmod u+x selectin.sh
$ ./selectin.sh
what's is your favorite os?
1) linux
2) windows
3) mac
4) android
#? 1
you select linux
#? 4
you select android
#?#? 用来提示用户输人菜单编号;Ctrl+D组合键,它的作用是结束select in循环。select语句取值列表value_list中的内容会以菜单的形式显示出来,用户输人菜单编号,就表示选中了某个值,这个值就会赋给变量variable,然后再执行循环体中的statements(do和done之间的部分)。每次循环时select都会要求用户输人菜单编号,并使用环境变量PS3的值作为提示符,PS3的默认值为#?,修改PS3的值就可以修改提示符。如果用户输人的菜单编号不在范围之内,例如上面输入的9,就会给variable赋一个空值;如果用户输人一个空值(什么也不输人,直接回车),会重新显示一遍菜单。需要注意,select是无限循环(死循环),输人空值,或者输入的值无效,都不会结束循环,只有遇到break语句,或者按下Ctrl+D组合键才能结束循环。
select in通常和case in一起使用,在用户输人不同的编号时可以做出不同的反应。修改上面的代码,加人case in语句:
$ ./selectin.sh
what's is your favorite os?
1) linux
2) windows
3) mac
4) android
#? 2
my pc is win11
$ cat selectin.sh
#! /bin/bash
echo "what's is your favorite os?"
select name in "linux" "windows" "mac" "android"
do
case $name in
"linux")
echo "linux is opensource"
break;;
"windows")
echo "my pc is win11"
break;;
"mac")
echo "i want to buy a macbook in the future"
break;;
"android")
echo "i have an android phone"
break;;
esac
donebreak 和 continue
使用while、until、for、select循环时,如果想提前结束循环(在不满足结束条件的情况下结束循环),可以使用关键字break或者continue。
在C语言中,break和continue只能跳出当前或本次循环,内层循环中的break和continue对外层循环不起作用;但是shell中的break和continue却能够跳出多层循环,也就是说,内层循环中的break和continue能够跳出外层循环。不过在实际应用中,break和continue一般只用来跳出当前层次的循环,很少有需要跳出多层循环的情况。
break关键字
break关键字的用法为:
break nn表示跳出循环的层数,如果省略n,则表示跳出整个循环。break关键字通常和if语句一起使用,即在满足条件时便跳出循环。
continue关键字
continue关键字的用法为:
continue nn表示循环的层数,如果省略n,则表示continue只对当前层次的循环语句有效,遇到continue会跳过本次循环,忽略本次循环的剩余代码,直接进人下一次循环。如果带上n,比如n的值为2,那么continue对内层和外层循环语句都有效,不但内层会跳过本次循环,外层也会跳过本次循环,其效果相当于内层循环和外层循环同时执行了不带n的continue。continue关键字通常也和if语句一起使用,即在满足条件时便跳出循环。
shell函数
shell函数的本质是一段可以重复使用的脚本代码,这段代码被提前编写好了,放在了指定的位置,使用时直接调取即可。shell中的函数和C语言中的函数类似,只是在语法细节上有所差别。
函数定义
shell函数定义的语法格式如下:
function name() {
statements
[ return value ]
}function是shell中的关键字,专门用来定义函数;name是函数名;statements是函数要执行的代码,也就是一组语句;returnvalue表示函数的返回值,其中return是shell的关键字,专门用在函数中返回一个值,这一部分可以写也可以不写。由一包围的部分称为函数体,调用一个函数,实际上就是执行函数体中的代码。
函数定义时也可以不写function关键字,如下:
name() {
statements
[ return value ]
}如果写了function关键字,也可以省略函数名后面的小括号,如下:
function name {
statements
[ return value ]
}建议使用标准的写法,这样能够做到“见名知意”。
调用shell函数时可以给它传递参数,也可以不传递。如果不传递参数,直接给出函数名字即可:
name如果传递参数,那么多个参数之间以空格分隔:
name param1 param2 param3不管是哪种形式,函数名字后面都不需要带括号。
和其他编程语言不同的是,shell函数在定义时不能指明参数,但是在调用时却可以传递参数,并且给它传递什么参数它就接收什么参数。shell也不限制定义和调用的顺序,可以将定义放在调用的前面,也可以将定义放在调用的后面。
函数参数
和C语言等大部分编程语言不同,shell中的函数在定义时不能指明参数,但是在调用时却可以传递参数。函数参数是shell位置参数的一种,在函数内部可以使用$n来接收,例如,$1表示第一个参数,$2表示第二个参数,依次类推。除了$n,还有另外3个比较重要的变量:$#可以获取传递的参数的个数;$@或者$*可以一次性获取所有的参数(用法同shell位置参数$*和$@)。