shell 脚本编程详解
shell 脚本(shell script),是一种为 shell 编写的脚本程序,一般文件后缀为 .sh。
脚本解释器
#! 是一个约定的标记,它告诉系统这个脚本需要什么解释器来运行,即:使用哪一种 Shell。#!被称为shebang(也称为 Hashbang),例如使用 bash:#! /bin/bash
新建一个 test.sh 的文件,内容如下:
#!/bin/bash
echo "Hello World!"运行 shell 脚本
第一种方式:作为可执行程序
1、当前 test.sh 是没有可执行权限的,首先使脚本文件具有执行权限。
# 使脚本文件具有执行权限
chmod +x ./test.sh2、执行脚本
# 执行脚本,需注意要加目录的标识
./test.sh
# 也可以用 source 来执行脚本,跟上面的写法是等价的,但是不需要脚本具有执行权限
source ./test.sh注意:一定要写成 ./test.sh ,而不是 test.sh 。运行其他二进制的程序也是一样,直接写 test.sh,Linux 系统会去 PATH 中寻找有没有叫 test.sh 的,而只有 /bin, /sbin, /usr/bin, /usr/sbin 等在 PATH 中。你的当前目录通常不在 PATH 中,所以写成 test.sh 是找不到命令的,要用./test.sh 告诉系统,就在当前目录找。
通过这种方式运行 bash 脚本,第一行一定要写对,好让系统(Shell 程序)查找到正确的解释器。如果是使用标准默认的 Shell,可以省去第一行。
扩展:source script.sh 和 ./script.sh 有什么区别?
source script.sh 和 ./script.sh 有什么区别?这两种情况 script.sh 都会在 bash 会话中被读取和执行,不同点在于哪个会话执行这个命令。
对于 source 命令来说,命令是在当前的 bash 会话中执行的,因此当 source 执行完毕,对当前环境的任何更改(例如更改目录或是定义函数)都会留存在当前会话中。
单独运行 ./script.sh 时,当前的 bash 会话将启动新的 bash 会话(实例),并在新实例中运行命令 script.sh。因此,如果 script.sh 更改目录,新的 bash 会话(实例)会更改目录,但是一旦退出并将控制权返回给父 bash 会话,父会话仍然留在先前的位置(不会有目录的更改)。
同样,如果 script.sh 定义了要在终端中访问的函数,需要用 source 命令在当前 bash 会话中定义这个函数。否则,如果你运行 ./script.sh,只有新的 bash 会话(进程)才能执行定义的函数,而当前的 shell 不能。
第二种方式:作为解释器参数
直接运行解释器,其参数就是 Shell 脚本的文件名。
这种方式运行的脚本,不需要在第一行指定解释器信息,写了也没用。
语法
1、注释
单行注释:以
#开头,到行尾结束。多行注释:以
:<<EOF开头,到EOF结束
如果有段代码要频繁的注释和取消注释,可以用花括号括起来,定义成一个函数,没有地方调用这个函数,这块代码就不会执行,达到了和注释一样的效果。
2、变量
变量类型
局部变量:局部变量是仅在某个脚本内部有效的变量,它们不能被其他的程序和脚本访问。函数中能声明函数内部的局部变量。
环境变量:环境变量是从父进程中继承而来的变量,对当前 Shell 会话内所有的程序和脚本都可见。创建它们跟创建局部变量类似,但使用的是
export、declare -x关键字,shell 脚本也可以定义环境变量,但一般自己定义的环境变量关掉当前 Shell 就失效了,后面有介绍怎么让环境变量持久化。shell 变量(系统变量):shell 变量是由 shell 程序设置的特殊变量,对所有 Shell 会话有效。shell 变量中有一部分是环境变量,有一部分是局部变量,这些变量保证了 shell 的正常运行。
变量语法
1、声明变量
可以使用等号操作符为变量赋值:varName=value,varName 是变量名,value 是赋值给变量的值。
变量名的命名规则:
只能使用英文字母,数字、下划线
中间不能有空格,可以使用下划线,如果有空格,必须使用单引号或双引号
不能使用标点符号
不能使用 shell 关键字
注意:varName=value的等号两边没有空格,变量值如果有空格,需要用引号包住。
2、访问变量
访问变量的语法形式为:${varName} 和 $varName,变量名外面的花括号是可选的,加不加都行,加花括号是为了帮助解释器识别变量的边界(推荐加花括号)。
因为 Shell 使用空白字符来分隔单词,所以上面的例子中需要加上花括号来告诉 Shell 这里的变量名是 fruit,而不是 fruits
注意:使用单引号时,变量不会被扩展(expand),仍依照原样显示。这意味着 echo '$var'会显示 $var。使用双引号时,如果 $var 已经定义过,那么 echo "$var"会显示出该变量的值,如果没有定义过,则什么都不显示。
3、只读变量
使用 readonly 命令可以将变量定义为只读变量,只读变量的值不能被改变
4、删除变量
使用 unset 命令可以删除变量,变量被删除后不能再次使用。unset 命令不能删除只读变量

Shell 特殊变量(系统变量)
上面讲过变量名的命名规则,但是还有一些包含其他字符的变量有特殊含义,这样的变量被称为特殊变量。
$0
当前脚本的文件名
$n
传递给脚本或函数的参数。n 是一个数字,表示第几个参数。例如,第一个参数是$1
$#
传递给脚本或函数的参数个数
$*
传递给脚本或函数的所有参数
$@
传递给脚本或函数的所有参数,被双引号("")包含时,与$*稍有不同
$FUNCNAME
函数名称(仅在函数内值)
$?
上个命令的退出状态,或函数的返值
$-
显示 shell 使用的当前选项(flag),后面扩展中检测是否为交互模式时会用到
$$
当前 Shell 进程 ID。对于 Shell 脚本,就是这些脚本所在的进程 ID
$!
最后一个后台运行的进程 ID 号
$HOME
当前由用户主目录
$PATH
全局命令的搜索路径
$PS1
命令提示符
命令行参数:运行脚本时传递给脚本的参数称为命令行参数,命令行参数用
$n表示。
执行命令:./test.sh Linux Shell,结果为:
$? 可以获取上一个命令的退出状态。所谓退出状态,就是上一个命令执行后的返回结果。退出状态是一个数字,一般情况下,大部分命令执行成功会返回 0,失败会返回 1。$?也可以表示函数的返回值。
3、字符串
字符串引号
shell 字符串可以使用单引号 '' ,也可以使用双引号"" , 也可以不用引号。
单引号:不识别变量,单引号中间不能出现单独的单引号(使用转义字符转义也不行),可以成对出现实现字符串拼接。
双引号:可以识别变量,双引号中可以出现用转义字符转义的双引号
反引号:执行一段命令
设置一个字符串变量,下面的都是对这个变量的操作
${#var}:获得变量值的长度
${#var}:获得变量值的长度${var:x:x}:通过索引位置截取子字符串
${var:x:x}:通过索引位置截取子字符串${var#}、${var##}:删除字符串左侧的值
${var#}、${var##}:删除字符串左侧的值${var%}、${var%%}:删除字符串右侧的值
${var%}、${var%%}:删除字符串右侧的值${var:-word}:如果变量 var 为空、没有定义或已被删除(unset),那么返回 word,但不改变 var 的值。
${var:-word}:如果变量 var 为空、没有定义或已被删除(unset),那么返回 word,但不改变 var 的值。${var:=word}:如果变量 var 为空、没有定义或者已被删除,那么返回 word,并将 var 的值设置为 word。
${var:=word}:如果变量 var 为空、没有定义或者已被删除,那么返回 word,并将 var 的值设置为 word。${var:?message}:如果变量 var 为空、没有定义或者已被删除,那么将消息 message 送到标准错误输出。
${var:?message}:如果变量 var 为空、没有定义或者已被删除,那么将消息 message 送到标准错误输出。可以用来检测变量 var 是否可以被正常赋值。若此替换出现在 shell 脚本中,那么脚本将停止运行。
${var:+word}:如果变量 var 被定义,那么返回 word,但不改变 var 的值。
${var:+word}:如果变量 var 被定义,那么返回 word,但不改变 var 的值。数组
数组是可以存储多个值的变量,这些值可以单独引用,也可以作为整个数组来引用。数组的下标从 0 开始,下标可以是整数或算数表达式,其值应该大于等于 0。
创建数组
访问数组
访问单个元素
访问数组的所有元素
${colors[*]}和${colors[@]}有些细微的差别,在将数组中的每个元素单独一行输出的时候,有没有被引号包住会有不同的差别,在引号内,${colors[@]}将数组中的每个元素扩展为一个单独的参数,数组元素中的空格得以保留。
访问数组部分元素
数组的长度
数组中添加元素
数组中删除元素
完整的代码示例:
运算符
Shell 中有很多运算符,包括算数运算符、关系运算符、布尔运算符、字符串运算符和文件测试符。
算数运算符
原生 bash 不支持简单的数学运算,较为常用的是借助 expr 来实现数学运算。
算数运算符列表,变量 a 是 10 变量 b 是 50
+
加法
expr ${a} + ${b} 结果为 60
-
减法
expr ${b} - ${a} 结果为 40
*
乘法
expr ${a} \* ${b} 结果为 500
/
除法
expr ${b} / ${a} 结果为 5
%
取余
expr ${b} % ${a} 结果为 0
=
赋值
a=$b 就是正常的变量赋值
示例代码如下:
注意:
表达式和运算符之间要有空格,例如
1+1是错误的,必须写成1 + 1完整的表达式要用反引号 ` 包住
条件表达式要放在方括号之间,并且要有空格,例如
[${a}==${b}]是错误的,必须写成[ ${a} == ${b} ]
条件运算符(关系运算符)
关系运算符只支持数字,不支持字符串,除非字符串的值是数字。
关系运算符列表,变量 a 是 10 变量 b 是 50
-eq
检测两个数是否相等,相等返回 true
[ ${a} -eq ${b} ] 返回 false
-ne
检测两个数是否不相等,不相等返回 true
[ ${a} -ne ${b} ] 返回 true
-gt
检测左边的数是否大于右边的数,如果是,返回 true
[ ${a} -gt ${b} ] 返回 false
>
跟 -gt 一样,不过因为兼容性问题,可能要在 [[]] 表达式中使用
[[ ${a} > ${b} ]] 返回 false
-lt
检测左边的数是否小于右边的数,如果是,返回 true
[ ${a} -lt ${b} ] 返回 true
<
跟 -lt 一样,不过因为兼容性问题,可能要在 [[]] 表达式中使用
[[ ${a} < ${b} ]] 返回 true
-ge
检测左边的数是否大于等于右边的数,如果是,返回 true
[ ${a} -ge ${b} ] 返回 false
-le
检测左边的数是否小于等于右边的数,如果是,返回 true
[ ${a} -le ${b} ] 返回 true
实例代码如下:
条件运算符(布尔运算符、逻辑运算符、字符串运算符)
条件运算符列表,变量 a 是 10, 变量 b 是 50,变量 x 是 "abc",变量 y 是 "efg"
!
非运算
[ ! false ] 返回 true
-o
或运算
[ ${a} -eq 10 -o ${b} -eq 100 ] 返回 true
||
跟 -o 类似,逻辑的 OR,不过需要使用 [[]] 表达式
[[ ${a} -eq 10 || ${b} -eq 100 ]] 返回 true
-a
与运算
[ ${a} -eq 10 -a ${b} -eq 50 ] 返回 true
&&
跟 -a 类似,逻辑的 AND,不过需要使用 [[]] 表达式
[[ ${a} -eq 10 && ${b} -eq 50 ]] 返回 true
=
检测两个数字或字符串是否相等,相等返回 true
[ ${a} = ${b} ] 返回 false
!=
检测两个数字或字符串是否相等,不相等返回 true
[ ${a} != ${b} ]返回 true
==
相等。比较两个数字或字符串,如果相等返回 true(不推荐使用,有兼容性问题)
[ ${a} == ${b} ] 返回 false
-z
检测字符串长度是否为 0,为 0 返回 true
[ -z ${x} ] 返回 false
-n
检测字符串长度是否不为 0,不为 0 返回 true
[ -n ${x} ] 返回 true
var
检测变量是否存在或不为空,存在或不为空返回 true
[ $s ] 返回 false

代码示例如下:
文件目录判断运算符
文件目录判断运算符列表
-f filename
判断文件是否存在,当 filename 存在且是普通文件时(既不是目录,也不是设备文件)返回 true
-d pathname
判断目录是否存在,当 pathname 存在且是目录时返回 true
-e pathname
判断【某个东西】是否存在,当 pathname 指定的文件或目录存在时返回 true
-a pathname
同上,已经过时,而且使用的时候还有另外一个与的逻辑,容易混淆
-s filename
判断是否是一个非空文件,当 filename 存在并且文件大小大于 0 时返回 true
-r pathname
判断是否可读,当 pathname 指定的文件或目录存在并且可读时返回 true
-x pathname
判断是否可执行,当 pathname 指定的文件或目录存在并且可执行时返回 true
-w pathname
判断是否可写,当 pathname 指定的文件或目录存在并且可写时返回 true
-b filename
判断是否是一个块文件,当 filename 存在且是块文件时返回 true
-c filename
判断是否是一个字符文件,当 filename 存在且是字符文件时返回 true
-L filename
判断是否是一个符号链接,当 filename 存在且是符号链接时返回 true
-u filename
判断文件是否设置 SUID 位,SUID 是 Set User ID
-g filename
判断文件是否设置 SGID 位,SGID 是 Set Group ID

示例代码如下:
条件语句
在条件语句中,由 [] 或 [[]] 包起来的表达式被称为检测命令或基元。
if...fi 语句
if...else 经常跟 test 命令结合使用,test命令用于检查某个条件是否成立,与方括号[]类似(它们两个在/usr/bin 中是用软连接指向的)。
if...else...fi
if...elif...fi
case...esac
case...esac 与其他语言中的 switch...case 类似,是一种多分支选择结构。
case 语句匹配一个值或一个模式,如果匹配成功,执行想匹配的命令。适用于需要面对很多情况,分别要采取不同的措施的情况。
注意:可以在 ) 前用 | 分割多个模式。
循环语句
bash 中有四种循环:for , while , until , select
for 循环
语法中的列表是一组值(数字、字符串)组成的序列,每个值通过空格分隔。这些值还可以是通配符或大括号扩展,例如 *.sh 和 {1..5}。
while 循环
while 循环会不断的检测一个条件,只要这个条件返回 true,就执行一段命令。被检测的条件跟 if 中的一样。while 也可用于从输入文件中读取数据。
until 循环
until 循环是检测一个条件,只要条件是 false 就会一直执行循环,直到条件条件为 true 时停止。它跟 while 正好相反。
select 循环
select 循环的语法跟 for 循环基本一致。它帮助我们组织一个用户菜单。
select 会打印列表的值以及他们的序列号到屏幕上,之后会提示用户选择,用户通常看到的提示是 $? ,用户输入相应的信号,选择的结果会被保存到变量中。
break 命令
break 命令允许跳出所有循环(终止执行后面的所有循环)。在嵌套循环中 break 命令后面还可以跟一个整数,表示跳出几层循环。
continue 命令
continue 命令跟 break 命令类似,只有一点差别,它不会跳出所有循环,仅仅跳出当前循环。同样,continue 后面也可以跟一个数字,表示跳出第几层循环。
函数
shell 函数必须先定义后使用,调用函数仅使用其函数名即可。
函数定义时,
function关键字可有可无函数返回值:可以显式的使用 return 语句,返回值类型只能为整数(0-255)。如果不加 return 语句,会默认将最后一条命令运行结果作为返回值。
函数返回值在调用该函数后,通过
$?获得。在函数内部声明变量一般会使用
local进行限定在当前作用域中生效,或者使用 unset 进行变量撤销
参数
位置参数是在调用一个函数并传给它参数时创建的变量,见上文 Shell 特殊变量。
输入输出重定向
Unix 命令默认从标准输入设备(stdin)获取输入,将结果输出到标准输出设备(stdout)显示。一般情况下,标准输入设备就是键盘,标准输出设备就是显示器。

输入输出流
shell 接收输入,并以字符序列或字符流的形式产生输出。这些流能被重定向到文件或另一个流中。
一般情况下,每个 Unix/Linux 命令都会打开三个文件:标准输入文件、标准输出文件、标准错误文件,三个文件描述符:
0
stdin
标准输入
1
stdout
标准输出
2
stderr
标准错误输出
重定向
重定向让我们可以控制一个命令的输入来自哪里,输出结果到什么地方。
输出重定向:命令的输出不仅可以是显示器,还可以很容的转义到文件,这被称为输出重定向。
输入重定向:使 Unix 命令也可以从文件获取输入,这样本来要从键盘获取输入的命令会转移到文件读取内容。
有一个文件是 test.sh,用两种方式输出文件的行数
第一个例子会输出文件名,第二个不会,因为它仅仅知道从标准输入读取的内容。
以下操作符在控制流的重定向时会被用到:
>
重定向输出
>>
将输出已追加的方式重定向
>&
将两个输出文件合并,需要在命令的最后使用
2>
错误重定向
<&
将两个输入文件合并
<
重定向输入
<<
Here 文档语法(见下文扩展),将开始标记 tag 和结束标记 tag 之间的内容作为输入
<<<
Here 字符串
如果希望 stderr 重定向到 file,可以这样写:
如果希望将 stdout 和 stderr 合并后重定向的 file,可以这样写:
&[n] 代表是已经存在的文件描述符,&1 代表输出 &2 代表错误输出 &- 代表关闭与它绑定的描述符
如果希望 stdin 和 stdout 都重定向,可以这样写:
如果希望执行某个命令,但又不希望在屏幕上显示输出结果,那么可以将输出重定向到 /dev/null。
/dev/null 是一个特殊的文件,写入到它的内容都会被丢弃,如果尝试从该文件读取内容,那么什么也读不到。但是 /dev/null 文件非常有用,将命令的输出重定向到它,会起到"禁止输出"的效果。
加载外部脚本
像其他语言一样,Shell 也可以加载外部脚本,将外部脚本的内容合并到当前脚本。shell 中加载外部脚本有两种写法。
两种方式效果相同,简单起见,一般使用点号(.),但是!注意点号(.)和文件名中间有一个空格
Debug
全局 Debug
shell 提供了用于 debug 脚本的工具。如果想采用 debug 模式运行某脚本,可以在其 shebang 中使用一个特殊的选项。(有些 shell 不支持)
或者在执行 Bash 脚本的时候,从命令行传入这些参数
局部 Debug
有时我们只需要 debug 脚本的一部分。这种情况下,使用 set 命令会很方便。这个命令可以启用或禁用选项。 使用 - 启用选项,使用 + 禁用选项。
1、用来在运行结果之前,先输出执行的那一行命令
2、执行脚本时,如果遇到不存在的变量会报错,并停止执行。(默认是忽略报错的)
顺便说一下,如果命令行下不带任何参数,直接运行set,会显示所有的环境变量和 Shell 函数。
3、执行脚本时,发生错误就终止执行。(默认是继续执行的)
set -e 根据返回值来判断,一个命令是否运行失败。但是,某些命令的非零返回值可能不表示失败,或者开发者希望在命令失败的情况下,脚本继续执行下去。这时可以暂时关闭 set +e,该命令执行结束后,再重新打开 set -e。
4、管道命令执行失败,脚本终止执行
管道命令就是多个子命令通过管道运算符(|)组合成为一个大的命令。Bash 会把最后一个子命令的返回值,作为整个命令的返回值。最后一个子命令不失败,管道命令就总是会执行成功的,因此 set -e 会失效,后面的命令会继续执行。
set -o pipefail 用来解决这个情况,搭配 -e 只要一个子命令失败,整个管道命令就会失败,脚本就会终止执行。
上面的命令可以放在一起使用:
AI
LangCommand is a local inference command-line tool powered by llama.cpp that transforms natural language descriptions into executable shell commands.
https://github.com/guoriyue/LangCommand
参考文档
set -eux
cd /home/project/wanquan-ai
if [ -d "frontend" ]; then rm -rf ./frontend echo "frontend 已删除!" fi
if [ -d "backend" ]; then rm -rf ./frontend echo "backend 已删除!" fi
echo "拉取 frontend 代码!" git clone --depth=1 https://${GIT_USERNAME}:${GIT_PASSWORD}@codeup.aliyun.com/676147a1721f4dc7be6c8c9c/wanquan-ai-frontend.git frontend
echo "拉取 backend 代码!" git clone --depth=1 https://${GIT_USERNAME}:${GIT_PASSWORD}@codeup.aliyun.com/676147a1721f4dc7be6c8c9c/wanquan-ai-backend.git
if [ -d "backend" ]; then cd backend docker-compose up --build -d else echo "没有 backend !" fi
echo "删除所有为 none 的镜像!" docker rmi $(docker images | grep "none" | awk '{print $3}')
Last updated