shell编程是linux运维必备技能之一,在mud游戏中我们也可以使用shell减化一些操作,比如编写自动编译驱动的脚本,或者调用外部指令的脚本,如:
#!/bin/bash
if [ ! -d "fluffos" ]; then
git clone https://gitee.com/mudren/fluffos.git
fi
cd fluffos && git pull
if [ -d "build" ]; then
rm -rf build
fi
mkdir build && cd build
cmake -DPACKAGE_DB_MYSQL=1 -DPACKAGE_DB_SQLITE=2 .. && make -j4 install
这里整理一些Shell编程的基础知识,方便有需要的同学。
Shell综合知识
- Linux 上的 Shebang 符号(#!)
- Shell 编程中只有字符串和数字二种基本数据类型,变量声明可用
declare - 变量赋值号=的周围不能有空格(示例:var=value)
- 如果 value 不包含任何空白符(例如空格、Tab 缩进等),那么可以不使用引号
- 变量赋值不加
$,变量使用加$,使用变量推荐加{}
for skill in Ada Coffe Action Java; do
echo "I am good at ${skill}Script"
done
- 表达式和运算符之间必须有空格,如:
expr 1 + 1 - 只读变量:
readonly;删除变量:unset(unset不能删除只读变量) - 字符串可以:单引号、双引号、不要引号,单双之别同php语言。
- 获取字符串长度使用
#,如:${#string},LUA语言是不是学习了shell? - 将命令的结果赋值给变量:
variable=`command`
variable=$(command)
php语言的
执行运算符是不是学习了shell?
- 全局变量导出为环境变量:export var
- 数学计算命令:
((expression)) - test命令:
test expression或[ expression ](推荐使用[[ ]]命令代替)
变量
变量的名字必须以字母或者下划线开头,不能以数字开头;不过位置参数却偏偏是数字,这和变量的命名规则是相悖的,我们将它们视为“特殊变量”。
Shell 变量的作用域可以分为三种:
- 有的变量只能在函数内部使用,这叫做局部变量(local variable);
- 有的变量可以在当前 Shell 进程中使用,这叫做全局变量(global variable);
- 而有的变量还可以在子进程中使用,这叫做环境变量(environment variable)。
要想变量的作用域仅限于函数内部,可以在定义时加上local命令,此时该变量就成了局部变量。LUA的变量是不是学习了shell?
全局变量只在当前 Shell 进程中有效,对其它 Shell 进程和子进程都无效。每个 Shell 进程都有自己的作用域,彼此之间互不影响。在 Shell 中定义的变量,默认就是全局变量。
如果使用export命令将全局变量导出(export var 或 export var=value),那么它就在所有的子进程中也有效了,这称为“环境变量”。创建 Shell 子进程最简单的方式是运行 bash 命令,环境变量只能向下传递而不能向上传递,通过exit命令可以一层一层地退出 Shell。通过 export 导出的环境变量只对当前 Shell 进程以及所有的子进程有效,如果最顶层的父进程被关闭了,那么环境变量也就随之消失了,其它的进程也就无法使用了,所以说环境变量也是临时的。
Shell命令替换
Shell 命令替换是指将命令的输出结果赋值给某个变量。Shell 中有两种方式可以完成命令替换,一种是反引号,一种是$(),使用方法如下:
variable=`commands`
variable=$(commands)
其中,variable 是变量名,commands 是要执行的命令。commands 可以只有一个命令,也可以有多个命令,多个命令之间以分号;分隔。
$() 支持嵌套,反引号不行,但$() 仅在 Bash Shell 中有效,而反引号可在多种 Shell 中使用。
Shell位置参数(命令行参数)
运行 Shell 脚本文件时我们可以给它传递一些参数,这些参数在脚本文件内部可以使用$n的形式来接收,例如,$1 表示第一个参数,$2 表示第二个参数,依次类推。
Shell 函数参数的传递和其它编程语言不同,没有所谓的形参和实参,在定义函数时也不用指明参数的名字和数目。换句话说,定义 Shell 函数时不能带参数,但是在调用函数时却可以传递参数,这些传递进来的参数,在函数内部就也使用$n的形式接收。
#!/bin/bash
#定义函数
function func(){
echo "Language: $1"
echo "URL: $2"
}
#调用函数
func C++ http://c.biancheng.net/shell/
如果参数个数太多,达到或者超过了 10 个,那么就得用${n}的形式来接收了,例如 ${12}、${34}。
Shell的特殊变量
除了 $n,Shell 中还有 $#、$*、$@、$?、$$ 几个特殊变量:
变量 含义
$0 当前脚本的文件名。
$n(n≥1) 传递给脚本或函数的参数。n 是一个数字,表示第几个参数。例如,第一个参数是 $1,第二个参数是 $2。
$# 传递给脚本或函数的参数个数。
$* 传递给脚本或函数的所有参数。
$@ 传递给脚本或函数的所有参数。当被双引号" "包含时,$@ 与 $* 稍有不同。
$? 上个命令的退出状态,或函数的返回值。
$$ 当前 Shell 进程 ID。对于 Shell 脚本,就是这些脚本所在的进程 ID。
关于$*和$@的不同,当它们被双引号" "包含时稍有不同(如果使用 for 循环来逐个输出数据,立即就能看出区别来):
- "$*"会将所有的参数从整体上看做一份数据,而不是把每个参数都看做一份数据。
- "$@"仍然将每个参数都看作一份数据,彼此之间是独立的。
Shell字符串详解
字符串可以由单引号' '包围,也可以由双引号" "包围,也可以不用引号。如果字符串中有变量,不用引号或用双绰号会解析变量。
在 Shell 中获取字符串长度很简单,具体方法如下:
${#string_name}
string_name 表示字符串名字。
Shell字符串拼接
在 PHP 中,使用.即可连接两个字符串,在 JavaScript 中,使用+即可将两个字符串合并为一个。然而,在 Shell 中你不需要使用任何运算符,将两个字符串并排放在一起就能实现拼接,非常简单粗暴。
Shell字符串截取
如果想从字符串的左边开始计数,那么截取字符串的具体格式如下:
${string: start :length}
其中,string 是要截取的字符串,start 是起始位置(从左边开始,从 0 开始计数),length 是要截取的长度(省略的话表示直到字符串的末尾)。
如果想从字符串的右边开始计数,那么截取字符串的具体格式如下:
${string: 0-start :length}
同第 1) 种格式相比,第 2) 种格式仅仅多了0-,这是固定的写法,专门用来表示从字符串右边开始计数。
这里需要强调两点:
- 从左边开始计数时,起始数字是 0(这符合程序员思维);从右边开始计数时,起始数字是 1(这符合常人思维)。计数方向不同,起始数字也不同。
- 不管从哪边开始计数,截取方向都是从左到右。
使用#号可以截取指定字符(或者子字符串)右边的所有字符,具体格式如下:
${string#*chars}
其中,string 表示要截取的字符,chars 是指定的字符(或者子字符串),
*是通配符的一种,表示任意长度的字符串。*chars连起来使用的意思是:忽略左边的所有字符,直到遇见 chars(chars 不会被截取),如果不需要忽略 chars 左边的字符,那么也可以不写*。
注意,以上写法遇到第一个匹配的字符(子字符串)就结束了。如果希望直到最后一个指定字符(子字符串)再匹配结束,那么可以使用##,具体格式为:
${string##*chars}
使用%号可以截取指定字符(或者子字符串)左边的所有字符,具体格式如下:
${string%chars*}
${string%%chars*}
请注意*的位置,因为要截取 chars 左边的字符,而忽略 chars 右边的字符,所以*应该位于 chars 的右侧。其他方面%和#的用法相同。
Shell数组
在 Shell 中,用括号( )来表示数组,数组元素之间用空格来分隔。由此,定义数组的一般形式为:
array_name=(ele1 ele2 ele3 ... elen)
注意,赋值号=两边不能有空格,必须紧挨着数组名和数组元素。
Shell 获取数组中的元素要使用下标[ ],下标可以是一个整数,也可以是一个结果为整数的表达式;当然,下标必须大于等于 0。
${array_name[index]}
其中,array_name 是数组名,index 是下标。
使用@或*可以获取数组中的所有元素,例如:
${nums[*]}
${nums[@]}
Shell获取数组长度
利用@或*,可以将数组扩展成列表,然后使用#来获取数组元素的个数,格式如下:
${#array_name[@]}
${#array_name[*]}
其中 array_name 表示数组名。两种形式是等价的,选择其一即可。
Shell数组合并
拼接数组的思路是:先利用@或*,将数组扩展成列表,然后再合并到一起。具体格式如下:
array_new=(${array1[@]} ${array2[@]})
array_new=(${array1[*]} ${array2[*]})
两种方式是等价的,选择其一即可。
Shell删除数组元素
在 Shell 中,使用 unset 关键字来删除数组元素,具体格式如下:
unset array_name[index]
其中,array_name 表示数组名,index 表示数组下标。
如果不写下标,而是写成下面的形式,那么就是删除整个数组,所有元素都会消失:
unset array_name
Shell关联数组
现在最新的 Bash Shell 已经支持关联数组了。关联数组使用字符串作为下标,而不是整数,这样可以做到见名知意。
不同于普通数组,关联数组必须使用带有-A选项的 declare 命令创建:
declare -A color=(["red"]="#ff0000", ["green"]="#00ff00", ["blue"]="#0000ff")
使用下面的形式可以获取关联数组的所有下标值:
${!array_name[@]}
${!array_name[*]}
shell built-in command
shell builtin 命令
命令 说明
: 扩展参数列表,执行重定向操作
. 读取并执行指定文件中的命令(在当前 shell 环境中)
alias 为指定命令定义一个别名
bg 将作业以后台模式运行
bind 将键盘序列绑定到一个 readline 函数或宏
break 退出 for、while、select 或 until 循环
builtin 执行指定的 shell 内建命令
caller 返回活动子函数调用的上下文
cd 将当前目录切换为指定的目录
command 执行指定的命令,无需进行通常的 shell 查找
compgen 为指定单词生成可能的补全匹配
complete 显示指定的单词是如何补全的
compopt 修改指定单词的补全选项
continue 继续执行 for、while、select 或 until 循环的下一次迭代
declare 声明一个变量或变量类型。
dirs 显示当前存储目录的列表
disown 从进程作业表中刪除指定的作业
echo 将指定字符串输出到 STDOUT
enable 启用或禁用指定的内建shell命令
eval 将指定的参数拼接成一个命令,然后执行该命令
exec 用指定命令替换 shell 进程
exit 强制 shell 以指定的退出状态码退出
export 设置子 shell 进程可用的变量
fc 从历史记录中选择命令列表
fg 将作业以前台模式运行
getopts 分析指定的位置参数
hash 查找并记住指定命令的全路径名
help 显示帮助文件
history 显示命令历史记录
jobs 列出活动作业
kill 向指定的进程 ID(PID) 发送一个系统信号
let 计算一个数学表达式中的每个参数
local 在函数中创建一个作用域受限的变量
logout 退出登录 shell
mapfile 从 STDIN 读取数据行,并将其加入索引数组
popd 从目录栈中删除记录
printf 使用格式化字符串显示文本
pushd 向目录栈添加一个目录
pwd 显示当前工作目录的路径名
read 从 STDIN 读取一行数据并将其赋给一个变量
readarray 从 STDIN 读取数据行并将其放入索引数组
readonly 从 STDIN 读取一行数据并将其赋给一个不可修改的变量
return 强制函数以某个值退出,这个值可以被调用脚本提取
set 设置并显示环境变量的值和 shell 属性
shift 将位置参数依次向下降一个位置
shopt 打开/关闭控制 shell 可选行为的变量值
source 读取并执行指定文件中的命令(在当前 shell 环境中)
suspend 暂停 Shell 的执行,直到收到一个 SIGCONT 信号
test 基于指定条件返回退出状态码 0 或 1
times 显示累计的用户和系统时间
trap 如果收到了指定的系统信号,执行指定的命令
type 显示指定的单词如果作为命令将会如何被解释
typeset 声明一个变量或变量类型。
ulimit 为系统用户设置指定的资源的上限
umask 为新建的文件和目录设置默认权限
unalias 刪除指定的别名
unset 刪除指定的环境变量或 shell 属性
wait 等待指定的进程完成,并返回退出状态码
Shell数学计算
Bash shell 在默认情况下不会区分变量类型,即使你将整数和小数赋值给变量,它们也会被视为字符串,这一点和大部分的编程语言不同。要想让数学计算发挥作用,必须使用数学计算命令,记住一个:(( ))用于整数运算,效率很高,推荐使用。
((表达式))
可以使用$获取
(( ))命令的结果,这和使用$获得变量值是类似的。
(())除了可以进行最基本的加减乘除运算,还可以进行大于、小于、等于等关系运算,以及与、或、非逻辑运算。
Shell if else语句
if condition1
then
statement1
elif condition2
then
statement2
elif condition3
then
statement3
……
else
statementn
fi
或
if condition; then
statement(s)
fi
请注意 condition 后边的分号;,当 if 和 then 位于同一行的时候,这个分号是必须的,否则会有语法错误。
Shell退出状态
每一条 Shell 命令,不管是 Bash 内置命令(例如 cd、echo),还是外部的 Linux 命令(例如 ls、awk),还是自定义的 Shell 函数,当它退出(运行结束)时,都会返回一个比较小的整数值给调用(使用)它的程序,这就是命令的退出状态(exit statu)。
很多 Linux 命令其实就是一个C语言程序,熟悉C语言的读者都知道,main() 函数的最后都有一个return 0,如果程序想在中间退出,还可以使用exit 0,这其实就是C语言程序的退出状态。当有其它程序调用这个程序时,就可以捕获这个退出状态。
if 语句的判断条件,从本质上讲,判断的就是命令的退出状态。按照惯例来说,退出状态为 0 表示“成功”;也就是说,程序执行完成并且没有遇到任何问题。除 0 以外的其它任何退出状态都为“失败”。
#!/bin/bash
((0==0))
echo $?
if ((0==0))
then
echo 真
fi
Shell 的这个部分与你所熟悉的其它编程语言正好相反:在C语言、C++、Java、Python 中,0 表示“假”,其它值表示“真”。
Shell test命令
test 是 Shell 内置命令,用来检测某个条件是否成立。test 通常和 if 语句一起使用,并且大部分 if 语句都依赖 test。test 命令有很多选项,可以进行数值、字符串和文件三个方面的检测。
Shell test 命令的用法为:
test expression
当 test 判断 expression 成立时,退出状态为 0,否则为非 0 值。
test 命令也可以简写为[],它的用法为:
[ expression ]
注意[]和expression之间的空格,这两个空格是必须的,否则会导致语法错误。[]的写法更加简洁,比 test 使用频率高。
文件类型判断
选 项 作 用
-b filename 判断文件是否存在,并且是否为块设备文件。
-c filename 判断文件是否存在,并且是否为字符设备文件。
-d filename 判断文件是否存在,并且是否为目录文件。
-e filename 判断文件是否存在。
-f filename 判断文件是否存在,井且是否为普通文件。
-L filename 判断文件是否存在,并且是否为符号链接文件。
-p filename 判断文件是否存在,并且是否为管道文件。
-s filename 判断文件是否存在,并且是否为非空。
-S filename 判断该文件是否存在,并且是否为套接字文件。
文件权限判断
选 项 作 用
-r filename 判断文件是否存在,并且是否拥有读权限。
-w filename 判断文件是否存在,并且是否拥有写权限。
-x filename 判断文件是否存在,并且是否拥有执行权限。
-u filename 判断文件是否存在,并且是否拥有 SUID 权限。
-g filename 判断文件是否存在,并且是否拥有 SGID 权限。
-k filename 判断该文件是否存在,并且是否拥有 SBIT 权限。
文件比较
选 项 作 用
filename1 -nt filename2 判断 filename1 的修改时间是否比 filename2 的新。
filename -ot filename2 判断 filename1 的修改时间是否比 filename2 的旧。
filename1 -ef filename2 判断 filename1 是否和 filename2 的 inode 号一致,可以理解为两个文件是否为同一个文件。这个判断用于判断硬链接是很好的方法
数值比较相关选项列表
选 项 作 用
num1 -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。
字符串判断相关选项列表
选 项 作 用
-z str 判断字符串 str 是否为空。
-n str 判断宇符串 str 是否为非空。
str1 = str2
str1 == str2 =和==是等价的,都用来判断 str1 是否和 str2 相等。
str1 != str2 判断 str1 是否和 str2 不相等。
str1 \> str2 判断 str1 是否大于 str2。\>是>的转义字符,这样写是为了防止>被误认为成重定向运算符。
str1 \< str2 判断 str1 是否小于 str2。同样,\<也是转义字符。
逻辑运算相关选项列表
选 项 作 用
expression1 -a expression 逻辑与,表达式 expression1 和 expression2 都成立,最终的结果才是成立的。
expression1 -o expression2 逻辑或,表达式 expression1 和 expression2 有一个成立,最终的结果就成立。
!expression 逻辑非,对 expression 进行取反。
有C语言、C++、Python、Java 等编程经验的读者请注意,==、>、< 在大部分编程语言中都用来比较数字,而在 Shell 中,它们只能用来比较字符串,不能比较数字,这是非常奇葩的,大家要习惯。
其次,不管是比较数字还是字符串,Shell 都不支持 >= 和 <= 运算符,切记。
Shell [[]] 命令详解
[[ ]]是 Shell 内置关键字,它和 test 命令类似,也用来检测某个条件是否成立。
test 能做到的,[[ ]] 也能做到,而且 [[ ]] 做的更好;test 做不到的,[[ ]] 还能做到。可以认为 [[ ]] 是 test 的升级版,对细节进行了优化,并且扩展了一些功能。
[[ ]] 的用法为:
[[ expression ]]
当 [[ ]] 判断 expression 成立时,退出状态为 0,否则为非 0 值。注意[[ ]]和expression之间的空格,这两个空格是必须的,否则会导致语法错误。
[[ ]] 是 Shell 内置关键字,不是命令,在使用时没有给函数传递参数的过程,所以 test 命令的某些注意事项在 [[ ]] 中就不存在了,具体包括:
- 不需要把变量名用双引号""包围起来,即使变量是空值,也不会出错。
- 不需要、也不能对 >、< 进行转义,转义后会出错。
在 Shell [[ ]] 中,可以使用=~来检测字符串是否符合某个正则表达式,它的用法为:
[[ str =~ regex ]]
str 表示字符串,regex 表示正则表达式。
Shell case in语句
case expression in
pattern1)
statement1
;;
pattern2)
statement2
;;
pattern3)
statement3
;;
……
*)
statementn
esac
case in 的 pattern 部分支持简单的正则表达式:*、[abc]、[m-n]、|
Shell while循环
while condition
do
statements
done
#!/bin/bash
i=1
sum=0
while ((i <= 100))
do
((sum += i))
((i++))
done
echo "The sum is: $sum"
Shell until循环
until condition
do
statements
done
Shell for循环和for int循环
for((初始化语句; 判断条件; 自增或自减))
do
statements
done
或
for variable in value_list
do
statements
done
in value_list 部分可以省略,省略后的效果相当于 in $@
#!/bin/bash
function func(){
for str
do
echo $str
done
}
func C++ Java Python C#
对 value_list 的说明
1) 直接给出具体的值,多个值之间以空格分隔 2) 给出一个取值范围:{start..end} 3) 使用命令的执行结果,使用反引号``或者$()都可以取得命令的执行结果 4) 使用 Shell 通配符,参考《Linux Shell 通配符(glob 模式)》 5) 使用特殊变量,如 $#、$*、$@、$?、$$ 等
#!/bin/bash
for filename in *.sh
do
echo $filename
done
Shell select in循环
select in 是 Shell 独有的一种循环,用来增强交互性,它可以显示出带编号的菜单,用户输入不同的编号就可以选择不同的菜单,并执行不同的功能。
select variable in value_list
do
statements
done
注意,select 是无限循环(死循环),输入空值,或者输入的值无效,都不会结束循环,只有遇到 break 语句,或者按下 Ctrl+D 组合键才能结束循环。
#!/bin/bash
echo "What is your favourite OS?"
select name in "Linux" "Windows" "Mac OS" "UNIX" "Android"
do
echo $name
done
echo "You have selected $name"
select in 通常和 case in 一起使用,在用户输入不同的编号时可以做出不同的反应。
Shell break和continue跳出循环
使用 while、until、for、select 循环时,如果想提前结束循环(在不满足结束条件的情况下结束循环),可以使用 break 或者 continue 关键字。
在C语言、C++、C#、Python、Java 等大部分编程语言中,break 和 continue 只能跳出当前层次的循环,内层循环中的 break 和 continue 对外层循环不起作用;但是 Shell 中的 break 和 continue 却能够跳出多层循环,也就是说,内层循环中的 break 和 continue 能够跳出外层循环。
Shell break 关键字的用法为:
break n
n 表示跳出循环的层数,如果省略 n,则表示跳出当前的整个循环。
Shell continue 关键字的用法为:
continue n
Shell函数详解
Shell 中的函数和C++、Java、Python、C# 等其它编程语言中的函数类似,只是在语法细节有所差别。
Shell 函数定义的语法格式如下:
function name() {
statements
[return value]
}
对各个部分的说明:
- function是 Shell 中的关键字,专门用来定义函数;
- name是函数名;
- statements是函数要执行的代码,也就是一组语句;
- return value表示函数的返回值,其中 return 是 Shell 关键字,专门用在函数中返回一个值;这一部分可以写也可以不写。
如果你嫌麻烦,函数定义时也可以不写 function 关键字:
name() {
statements
[return value]
}
如果写了 function 关键字,也可以省略函数名后面的小括号:
function name {
statements
[return value]
}
调用 Shell 函数时可以给它传递参数,也可以不传递。如果不传递参数,直接给出函数名字即可:
name
如果传递参数,那么多个参数之间以空格分隔:
name param1 param2 param3
不管是哪种形式,函数名字后面都不需要带括号。
和其它编程语言不同的是,Shell 函数在定义时不能指明参数,但是在调用时却可以传递参数,并且给它传递什么参数它就接收什么参数。
Shell 也不限制定义和调用的顺序,你可以将定义放在调用的前面,也可以反过来,将定义放在调用的后面。