shell 是一种脚本语言。
脚本:本质是一个文件,文件里面存放的是 特定格式的指令,系统可以使用脚本解析器 翻译或解析 指令 并执行(它不需要编译)。
举个例子,我想实现这样的操作:
1)进入到/tmp/目录;
2)列出当前目录中所有的文件名;
3)把所有当前的文件拷贝到/root/目录下;
4)删除当前目录下所有的文件。
简单的4步在shell窗口中需要你敲4次命令,按4次回车。这样是不是很麻烦?当然这4步操作非常简单,如果是更加复杂的命令设置需要几十次操作呢?那样的话一次一次敲键盘会很麻烦。
所以不妨把所有的操作都记录到一个文档中,然后去调用文档中的命令,这样一步操作就可以完成。其实这个文档呢就是shell脚本了,只是这个shell脚本有它特殊的格式。
shell 是一个用 C 语言编写的程序,它是用户使用 Linux 的桥梁。Shell 既是一种命令语言,又是一种程序设计语言。(应用程序 解析 脚本语言)。
Shell脚本能帮助我们很方便的去管理服务器,因为我们可以指定一个任务计划定时去执行某一个shell脚本实现我们想要需求。
这对于Linux系统管理员来说是一件非常值得自豪的事情。现在的139邮箱很好用,发邮件的同时还可以发一条邮件通知的短信给用户,利用这点,我们就可以在我们的Linux服务器上部署监控的shell脚本,比如网卡流量有异常了或者服务器web服务器停止了就可以发一封邮件给管理员,同时发送给管理员一个报警短信这样可以让我们及时的知道服务器出问题了。
Linux 的内核、shell、基础命令程序,都是C语言编写的
yum install gcc -y # 安装gcc编译
c语言脚本hello.c
# include
void main(){
printf("hello world!\n");
}
执行c语言脚本
gcc hello.c # 默认编译生成a.out文件
ls
a.out hello hello.c master.tar.gz test.sh
./a.out # 执行编译之后的a.out文件
hello world!
gcc hello.c -o hello # -o表示编译成为自己想要的文件名字
ls
a.out hello hello.c master.tar.gz test.sh
./hello # 运行c脚本
hello world!
Shell 编程跟 JavaScript、php 编程一样,只要有一个能编写代码的文本编辑器和一个能解释执行的脚本解释器就可以了。
Linux 的 Shell 种类众多,常见的有:
#! 告诉系统其后路径所指定的程序即是解释此脚本文件的 Shell 程序。
打开文本编辑器(可以使用 vi/vim 命令来创建文件),新建一个文件 test.sh,扩展名为 sh(sh代表shell),扩展名并不影响脚本执行,见名知意就好,如果你用 php 写 shell 脚本,扩展名就用 php 好了。
shell脚本通常都是以.sh 为后缀名的,这个并不是说不带.sh这个脚本就不能执行,只是大家的一个习惯而已。
有一个问题需要约定一下,凡是自定义的脚本建议放到
/usr/local/sbin/
目录下,这样做的目的是:一来可以更好的管理文档。
二来以后接管你的管理员都知道自定义脚本放在哪里,方便维护。
【shell脚本的基本结构以及如何执行】
查看自己Linux系统的默认解析:echo $SHELL
echo $SHELL
/bin/bash
实例
#!/bin/bash
echo "Hello World !"
1、定义以开头:#!/bin/bash
单个"#"号代表注释当前行第一步:编写脚本文件
2、加上可执行权限
chmod +x xxxx.sh
默认我们用vim编辑的文档是不带有执行权限的,所以需要加一个执行权限,那样就可以直接使用’./filename’ 执行这个脚本了。
3、运行
三种执行方式 (./xxx.sh
|bash xxx.sh
| . xxx.sh
)
./xxx.sh
:先按照 文件中#!
指定的解析器解析
#!
指定指定的解析器不存在,才会使用系统默认的解析器bash xxx.sh
:指明先用bash解析器解析
使用
sh
命令去执行一个shell脚本的时候是可以加-x选项来查看这个脚本执行过程的,这样有利于我们调试这个脚本哪里出了问题。sh -x return_test.sh + sayhello junma + '[' junma ']' + echo hello junma hello junma + echo 0 0 + sayhello jinlong + '[' jinlong ']' + echo hello jinlong hello jinlong + echo 0 0 + sayhello + '[' '' ']' + echo 'give me a name please!' give me a name please! + return 1 + echo 1 1
. xxx.sh
直接使用默认解析器解析
三种执行情况:(重要)
#!
是一个约定的标记,它告诉系统这个脚本需要什么解释器来执行,即使用哪一种 Shell。
\#!
用来声明脚本由什么shell解释,否则使用默认shell
echo
命令用于向窗口输出文本。
shell脚本是一种脚本语言,我们只需使用任意文本编辑器,按照语法编写相应程序,增加可执行权限,即可在安装shell命令解释器的环境下执行。
你有没有用过这样的命令/etc/init.d/iptables restart
,其实前面的/etc/init.d/iptables
文件其实就是一个shell脚本,为什么后面可以跟一个”restart”? 这里就涉及到了shell脚本的预设变量。实际上,shell脚本在执行的时候后边是可以跟变量的,而且还可以跟多个。不妨、写一个脚本,看看就会明白了。
# test.sh
#!/bin/bash
sum=$[$1+$2]
echo $sum
执行过程如下:
sh -x test.sh 19 20
sum=39
echo 39
39
在脚本中,你会不会奇怪,哪里来的$1
和$2
,这其实就是shell脚本的预设变量,其中$
1的值就是在执行的时候输入的$1
,而$2
的值就是执行的时候输入的$2
,当然一个shell脚本的预设变量是没有限制的,这回你明白了吧。另外还有一个$0
,不过它代表的是脚本本身的名字。不妨把脚本修改一下。
# test.sh
#!/bin/bash
sum=$[$1+$2]
echo "$0 $1 $2"
执行过程如下:
sh test.sh 20 20
test.sh 20 20
每个命令执行后都会有对应的进程退出状态码,用来表示该进程是否是正常退出。
所以,在命令行中,在shell脚本中,经常会使用特殊变量$?
判断最近一个前台命令是否正常退出。
通常情况下,如果$?
的值:
为0,表示进程成功执行,即正常退出
非0,表示进程未成功执行,即非正常退出
但非0退出状态码并不一定表示错误,也可能是正常逻辑的退出。
另外,在shell脚本中,所有条件判断(比如if语句、while语句)都以0退出状态码表示True,以非0退出状态码为False。
exit
命令可用于退出当前shell进程,比如退出当前shell终端、退出shell脚本,等等。
exit [N]
exit
可指定退出状态码N,如果省略N,则默认退出状态码为0,即表示正确退出。
在命令的结尾使用&
符号,可以将这个命令放入后台执行。
命令放入后台后,会立即回到shell进程,shell进程会立即执行下一条命令(如果有)或退出。
使用$!
可以获取最近一个后台进程的PID。
sleep 20 &
[1] 14968
echo $!
14968
使用wait
命令可以等待后台进程(当前shell进程的子进程)完成:
wait [n1 n2 n3 ...]
不给定任何参数时,会等待所有子进程(即所有后台进程)完成。
sleep 5 &
[2] 15316
[1] Done sleep 20
wait
echo haha
haha
shell中有多种组合多个命令的方式。
cmd1;cmd2
cmd1 && cmd2
cmd1 || cmd2
&&
和||
可以随意结合# cmd1正确退出后执行cmd2,cmd2正确退出后执行cmd3
cmd1 && cmd2 && cmd3...
# cmd1正确退出则执行cmd2,cmd1不正确退出会执行cmd3
# cmd1正确退出,但cmd2不正确退出,也会执行cmd3
cmd1 && cmd2 || cmd3
# cmd1正确退出会执行cmd3
# cmd1不正确退出会执行cmd2,cmd2正确退出会执行cmd3
cmd1 || cmd2 && cmd3
# 小括号组合的多个命令是在子shell中执行
# 即会先创建一个新的shell进程,再执行里面的命令
(cmd1;cmd2;cmd3)
# 大括号组合的多个命令是在当前shell中执行
# 大括号语法特殊,要求:
# 1.开闭括号旁边都有空白,否则语法解析错误(解析成大括号扩展)
# 2.写在同一行时,每个cmd后都要加分号结尾
# 3.多个命令可分行书写,不要求分号结尾
{ cmd1;cmd2;cmd3; }
{
cmd1
cmd2
cmd3
}
顾名思义,变量就是其值可以变化的量。变量名是指向一片用于存储数据的内存空间。变量有局部变量,环境变量之分。
bash中基本数据类型只有字符串类型,连数值类型都没有(
declare -i
可强制声明数值类型)。
shell变量是一种弱类型的变量,不像在C语言中,变量必须要先声明再使用。
shell中变量名区分大小写。
变量名=变量值
注意:
=
两边不能有空格!
如:
name=Kitty
变量替换是指在命令开始执行前,shell会先将变量的值替换到引用变量的位置处。
例如:
a="hello"
echo $a world
在echo命令开始执行前,shell会取得变量a的值hello,并将它替换到命令行的$a
处。于是,在echo命令开始执行时,命令行已经变成:
echo hello world
除了变量替换,Shell还会做其它替换:
学习Shell如何做命令行解析很重要,如果不掌握命令行解析,当遇到命令行语法错误后很可能会花掉大量无谓的时间去调试命令。而掌握命令行解析后,就会对命令生命周期了如执掌,不敢说一次就能写对所有命令行,但能节省大量调试时间,对写命令行和写脚本的能力也会上升一个层次。
$变量名
如:
$name
unset 变量名
如:
unset name
readonly num=10 # readonly只读
echo "num=$num"
num=10
num=200
-bash: num: readonly variable
env
NVM_INC=/www/server/nvm/versions/node/v16.13.2/include/node
XDG_SESSION_ID=463
HOSTNAME=lw
NVM_CD_FLAGS=
TERM=xterm
SHELL=/bin/bash
HISTSIZE=1000
SSH_CLIENT=183.230.190.196 63586 22
SSH_TTY=/dev/pts/0
NVM_DIR=/www/server/nvm
USER=root
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=01;05;37;41:su=37;41:sg=30;43:ca=30;41: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:*.Z=01;31:*.dz=01;31:*.gz=01;31:*.lrz=01;31:*.lz=01;31:*.lzo=01;31:*.xz=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:*.jpg=01;35:*.jpeg=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:*.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:*.axv=01;35:*.anx=01;35:*.ogv=01;35:*.ogx=01;35:*.aac=01;36:*.au=01;36:*.flac=01;36:*.mid=01;36:*.midi=01;36:*.mka=01;36:*.mp3=01;36:*.mpc=01;36:*.ogg=01;36:*.ra=01;36:*.wav=01;36:*.axa=01;36:*.oga=01;36:*.spx=01;36:*.xspf=01;36:
MAIL=/var/spool/mail/root
PATH=/www/server/nvm/versions/node/v16.13.2/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin
PWD=/root
LANG=en_US.UTF-8
HISTCONTROL=ignoredups
SHLVL=1
HOME=/root
LOGNAME=root
SSH_CONNECTION=183.230.190.196 63586 172.23.217.49 22
NVM_BIN=/www/server/nvm/versions/node/v16.13.2/bin
LESSOPEN=||/usr/bin/lesspipe.sh %s
XDG_RUNTIME_DIR=/run/user/0
LE_WORKING_DIR=/root/.acme.sh
_=/usr/bin/env
source 脚本文件
source FileName
作用:在当前bash环境下读取并执行FileName中的命令。
注:该命令通常用命令“
.
”来替代。如:
source .bash_rc
与. .bash_rc
是等效的。
source
在当前bash环境下执行命令,而scripts是启动一个子shell来执行命令。这样如果把设置环境变量(或alias等等)的命令写进scripts中,就只会影响子shell,无法改变当前的BASH,所以通过文件(命令列)设置环境变量时,要用source命令。
可以在终端中读取:
echo "$USER"
root
在其他sh脚本读取:
#!/bin/bash
echo "$USER"
root
注意事项:
1、变量名只能包含英文字母下划线,不能以数字开头
1_num=10 # 错误
num_1=20 # 正确
2、等号两边不能直接接空格符,若变量中本身就包含了空格,则整个字符串都要用双引号、或单引号括起来
3、双引号 单引号的区别
如果想在PATH变量中 追加一个路径写法如下:(重要!!!!)
export PATH=$PATH:/需要添加的路径
{num:-val}
如果num存在,整个表达式的值为num,否则为val
echo ${name:-是是是}
是是是
{num:=val}
如果num存在,整个表达式的值为num,否则为val,同时将num的值赋值为val
echo ${name:=是是是} # 100
是是是
echo $name
是是是
字符串的串联操作,直接将两段数据连接在一起即可,不需要任何操作符。
echo "junma""jinlong"
junma jinlong
echo 1234 5678
1234 5678
${#str}
str="一从大地起风雷,便有精生白骨堆。僧是愚氓犹可训,妖为鬼蜮必成灾。"
echo ${#str}
32
${str:i}
str="一从大地起风雷,便有精生白骨堆。僧是愚氓犹可训,妖为鬼蜮必成灾。"
echo ${str:16}
僧是愚氓犹可训,妖为鬼蜮必成灾。
${str:i:length}
str="一从大地起风雷,便有精生白骨堆。僧是愚氓犹可训,妖为鬼蜮必成灾。"
echo ${str:0:16}
一从大地起风雷,便有精生白骨堆。
${str/old/new}
str="一从大地起风雷,便有精生白骨堆。僧是愚氓犹可训,妖为鬼蜮必成灾。"
echo ${str/,/|}
一从大地起风雷|便有精生白骨堆。僧是愚氓犹可训,妖为鬼蜮必成灾。
${str//old/new}
str="一从大地起风雷,便有精生白骨堆。僧是愚氓犹可训,妖为鬼蜮必成灾。"
echo ${str//,/---}
一从大地起风雷---便有精生白骨堆。僧是愚氓犹可训---妖为鬼蜮必成灾。
文件测试运算符用于检测 Unix 文件的各种属性。
其他检查符:
-S
: 判断某文件是否 socket。-L
: 检测文件是否存在并且是一个符号链接。#!/bin/bash
# author:菜鸟教程
# url:www.runoob.com
file="/var/www/runoob/test.sh"
if [ -r $file ]
then
echo "文件可读"
else
echo "文件不可读"
fi
if [ -w $file ]
then
echo "文件可写"
else
echo "文件不可写"
fi
if [ -x $file ]
then
echo "文件可执行"
else
echo "文件不可执行"
fi
if [ -f $file ]
then
echo "文件为普通文件"
else
echo "文件为特殊文件"
fi
if [ -d $file ]
then
echo "文件是个目录"
else
echo "文件不是个目录"
fi
if [ -s $file ]
then
echo "文件不为空"
else
echo "文件为空"
fi
if [ -e $file ]
then
echo "文件存在"
else
echo "文件不存在"
fi
test
命令:用于测试字符串、文件状态和数字
test
命令有两种格式:
test
命令或功能等价的Bash内置命令[ ]
可以做条件测试;此外,还可以使用[[]]
来做条件测试,甚至let、$[]、$(())
也可以做条件测试。
test condition
[ condition ]
[ ]
echo $?
1
test
echo $?
1
没有任何测试内容时,直接返回false。
#!/bin/bash
read -p "请输入一个文件名:" fileName
请输入一个文件名:test.sh
test -e $fileName
# [ -e $fileName ] # 和上面read -p "请输入一个文件名:" fileName等效
echo $?
0 # 0表示该文件存在,非0表示不存在
条件表达式 | 含义 |
---|---|
-e file | 文件是否存在(exist) |
-f file | 文件是否存在且为普通文件(file) |
-d file | 文件是否存在且为目录(directory) |
-b file | 文件是否存在且为块设备block device |
-c file | 文件是否存在且为字符设备character device |
-S file | 文件是否存在且为套接字文件Socket |
-p file | 文件是否存在且为命名管道文件FIFO(pipe) |
-L file | 文件是否存在且是一个链接文件(Link) |
条件表达式 | 含义 |
---|---|
-r file | 文件是否存在且当前用户可读 |
-w file | 文件是否存在且当前用户可写 |
-x file | 文件是否存在且当前用户可执行 |
-s file | 文件是否存在且大小大于0字节,即检测文件是否非空文件 |
-N file | 文件是否存在,且自上次read后是否被modify |
条件表达式 | 含义 |
---|---|
file1 -nt file2 | (newer than)判断file1是否比file2新 |
file1 -ot file2 | (older than)判断file1是否比file2旧 |
file1 -ef file2 | (equal file)判断file1与file2是否为同一文件 |
#!/bin/bash
test -z $yn
echo $? #0
0
read -p "please input y/n:" yn
please input y/n:[ -z $yn ]
echo "1:$?"
1:1
[ $yn = "y" ]
echo "2:$?"
2:0
#!/bin/bash
test -z $yn
echo $? #0
0
#!/bin/bash
read -p "请输入第一个字符串:" str1
请输入第一个字符串:abc
read -p "请输入第二个字符串:" str2
请输入第二个字符串:def
test $str1 = $str2
echo $?
1
read -p "请输入第一个字符串:" str1
请输入第一个字符串:123
read -p "请输入第二个字符串:" str2
请输入第二个字符串:123
[ $str1 = $str2 ]
echo $?
0
#!/bin/bash
read -p "请输入第一个数值:" data1
请输入第一个数值:19990221
read -p "请输入第二个数值:" data2
请输入第二个数值:2135139861
test $data1 -eq $data2
echo "相等:$?"
相等:1
[ $data1 -ge $data2 ]
echo "大于等于:$?"
大于等于:1
[ $data1 -lt $data2 ]
echo "小于:$?"
小于:0
test -e /home && test -d /home && echo "true"
true
test "aaa" = "aaa" || echo "not equal" && echo "equal"
equal
test 2 -lt 3 && test 5 -gt 3 && echo "equal"
equal
# test.sh文件同时具有r和x权限时,才为true
test -r test.sh -a -x test.sh
echo $?
0
# test.sh文件同时具有r或x权限时,就传回true
test -r test.sh -o -x test.sh
echo $?
0
# 当test.sh文件不具有x时,回传true
test ! -x test.sh
echo $?
1
格式一:
if [条件1]; then
执行第一段程序
else
执行第二段程序
fi
#################
格式二:
if [条件1]; then
执行第一段程序
elif [条件2];then
执行第二段程序
else
执行第三段程序
fi
# test.sh(vim编辑test.sh脚本——`vi test.sh`)
#!/bin/bash
read -p "请输入y继续:" yn
if [ $yn = "y" ]; then
echo "继续执行"
else
echo "停止执行"
fi
sh test.sh
请输入y继续:y
继续执行
sh test.sh
请输入y继续:n
停止执行
案例:字符串比较是否为 null
# test.sh
#!/bin/bash
a=""
if [ -n $a ]
then
echo "-n $a : 字符串长度不为 0"
else
echo "-n $a : 字符串长度为 0"
fi
sh test.sh
-n : 字符串长度为0
注意:是 $a 这里应该加上双引号,否则 -n $a 的结果永远是 true。
案例:判断当前路径下有没有文件夹,有就进入创建文件,没有 就创建文件夹,再进入创建文件。
# test.sh
#!/bin/bash
read -p "请输入文件夹的名字:" dirName
# 判断文件夹是否存在
if [ -e $dirName ]; then
echo "$dirName 是存在的 即将进入文件夹"
cd $dirName
echo "即将创建文件名为test.c"
touch test.c
else # 不存在
echo "该文件夹 是 不存在的 即将创建该文件夹"
mkdir $dirName
echo "进入$dirName里面"
cd $dirName
echo "即将创建文件test.c"
touch test.c
fi
sh test.sh
请输入文件夹的名字:test01
test01 是存在的 即将进入文件夹
即将创建文件名为test.c
ls /
bin boot dev etc home lib lib64 lost+found media mnt opt patch proc root run sbin srv sys tmp usr var www
pwd
/root
ls /root/
master.tar.gz myfile test01 test.sh
ls /root/test01/
test.c
Linux 删除文件夹和文件的命令
-r 就是向下递归,不管有多少级目录,一并删除
-f 就是直接强行删除,不作任何提示的意思删除文件夹实例:
rm -rf /root/test01
将会删除
/root/test01
目录以及其下所有文件、文件夹删除文件使用实例:
rm -f /var/log/httpd/access.log
将会强制删除
/var/log/httpd/access.log
这个文件
案例:
# test.sh
#!/bin/bash
read -p "请输入一个文件夹的名字:" dirName
if [ -e $dirName ]; then
cd $dirName
else
mkdir $dirName
cd $dirName
fi
read -p "请输入y创建文件,n直接退出" yes
if [ $yes = "y" ]; then
read -p "请输入文件名:" fileName
touch $fileName
elif [ $yes = "n" ]; then
echo "退出了!"
fi
sh test.sh
输入一个文件夹的名字:test01
请输入y创建文件,n直接退出y
请输入文件名:test1
ls /root/test01
test1
case脚本常用于编写系统服务的启动脚本,例如/etc/init.d/iptables
中就用到了,你不妨去查看一下。
# test.sh
#!/bin/bash
read -p "请输入yes/no:" choice
case $choice in
yes | y* | Y* )
echo "输入了yes"
;;
no | n* | N* )
echo "输入了no"
;; # break
* ) # default
echo "输入了其他"
;;
esac
sh test.sh
请输入yes/no:yes
输入了yes
sh test.sh
请输入yes/no:no
输入了no
sh test.sh
请输入yes/no:Ni
输入了no
bash中基本数据类型只有字符串类型,连数值类型都没有(
declare -i
可强制声明数值类型)。
# test.sh
#!/bin/bash
declare -i sum=0
declare -i i=0
for (( i=0;i<=100;i++ ))
do
sum=$sum+$i;
done
echo "sum=$sum"
sh test.sh
sum=5050
# test.sh
#!/bin/bash
declare -i sum=0
declare -i i=0
for i in 1 2 3 4 5 6 7 8 9 10
do
sum=$sum+$i;
done
echo "sum=$sum"
sh test.sh
sum=55
# test.sh
#!/bin/bash
declare -i i
declare -i s
while [ "$i" != "101" ]
do
s+=i;
i=i+1;
done
echo "The count is $s"
sh test.sh
The count is 5050
# test.sh
#!/bin/bash
declare -i i
declare -i s
until [ "$i" = "101" ]
do
s+=i;
i=i+1;
done
echo "The count is $s"
sh test.sh
The count is 5050
在shell脚本中,函数一定要写在最前面,不能出现在中间或者最后,因为函数是要被调用的,如果还没有出现就被调用,肯定是会出错的。
Shell函数可以当作命令一样执行,它是一个或多个命令的组合结构体。通常,可以为每个功能定义一个函数,该函数中包含实现这个功能相关的所有命令和逻辑。
因为可以组合多个命令,并且定义之后就可以直接在当前Shell中调用,所以函数具有一次定义多次调用且代码复用的功能。
shell函数的定义风格有下面几种:
function func_name {CMD_LIST}
func_name() {CMD_LIST}
function func_name() {CMD_LIST}
所有函数在使用前必须定义,必须将函数放在脚本开始部分,直至shell解释器首次发现它时,才可以使用
函数定义后,可以直接使用函数名来调用函数,同时还可以向函数传递零个或多个参数。
# 不传递参数
func_name
# 传递多个参数
func_name arg1 arg2 arg3
在函数中,那些位置变量将有特殊的意义:
$1、$2、$3...
:传递给函数的第一个参数保存在$1
中,第二个参数保存在$2
中…
$@
和$*
:保存了所有参数,各参数使用空格分隔
不用双引号包围时,两者没区别
双引号包围时,$@
的各个元素都被双引号包围,$*
的所有元素一次性被双引号包围
例如,定义一个函数专门用来设置和代理相关的变量:
# proxy_test.sh
#!/bin/bash
proxy_addr=127.0.0.1:8118
function proxy_set {
local p_addr=$1
export http_proxy=$p_addr
export https_proxy=$p_addr
export ftp_proxy=$p_addr
}
# 调用函数
proxy_set $proxy_addr
# 各代理变量已设置
echo $http_proxy
echo $https_proxy
echo $ftp_proxy
上面在函数定义的代码中使用了local
,它可以用在函数内部表示定义一个局部变量,局部变量在函数执行完毕后就消失,不会影响函数外部的环境。
bash proxy_test.sh
127.0.0.1:8118
127.0.0.1:8118
127.0.0.1:8118
另外,函数中可以使用return语句来定义函数的返回值,每当执行到函数内的return时,函数就会终止执行,直接退出函数。在shell中,函数的返回值其实就是退出状态码。
return [N]
如果不指定N,则默认退出状态码为0。
例如:
# return_test.sh
#!/bin/bash
function sayhello {
[ "$1" ] || { echo "give me a name please!"; return 1; }
echo hello $1
}
sayhello "junma"; echo $?
sayhello "jinlong"; echo $?
sayhello ; echo $?
sh return_test.sh
hello junma
0
hello jinlong
0
give me a name please!
1
# test.sh
#!/bin/bash
function my_max()
{
if [ $1 -gt $2 ]; then
return $1
else
return $2
fi
}
read -p "请输入数值1:" data1
read -p "请输入数值2:" data2
# 函数调用
my_max $data1 $data2
echo "$data1和$data2的最大值为:$?"
sh test.sh
输入数值1:25
输入数值2:34
5和34的最大值为:34
案例:函数分文件
# test.sh
#!/bin/bash
function my_max()
{
if [ $1 -gt $2 ]; then
return $1
else
return $2
fi
}
# _sh.sh
#!/bin/bash
# 导入函数
source test.sh
read -p "请输入数值1:" data1
read -p "请输入数值2:" data2
# 调用函数
my_max $data1 $data2
# $? 只有一个字节
echo "$data1和data2的最大值:$?"
使用sh test.sh
报错:_sh.sh: line 3: source: test.sh: file not found
,但bash test.sh
却没有报错。
sh _sh.sh
_sh.sh: line 3: source: test.sh: file not found
# 脚本运行过程中,没有找到文件test.sh sh不等于bash!
#################################################
bash _sh.sh
请输入数值1:23
请输入数值2:32
23和data2的最大值:32
一般情况下,在Linux中执行一个bash的脚本,建议使用bash去执行,就是为了避免这种奇怪问题的产生。
# _sh.sh
#!/bin/bash
# 导入函数
source ./test.sh
# 给source提供了一个该文件的路径,虽然是相对路径,但bash是可以根据脚本本身执行的位置来找到test.sh的
read -p "请输入数值1:" data1
read -p "请输入数值2:" data2
# 调用函数
my_max $data1 $data2
# $? 只有一个字节
echo "$data1和data2的最大值:$?"
但如果
source test.sh
写成source ./test.sh
,给source提供了一个该文件的路径
使用反引号或$()
可以执行命令替换。
推荐用 $() 代替 ``
`cmd`
$(cmd)
命令替换是指先执行cmd,将cmd的输出结果替换到$()
或反引号位置处。
例如:
id root
echo `id root`
echo $(id root)
在echo命令执行前,会先执行id命令,id命令的执行结果:
id root
uid=0(root) gid=0(root) groups=0(root)
所以会将结果uid=0(root) gid=0(root) groups=0(root)
替换$(id root)
。于是,echo命令开始执行时,命令行已经变成了:
echo uid=0(root) gid=0(root) groups=0(root)
$[]
或$(())
或let命令可以做算术运算。
let
是单独的命令,不能写在其它命令行中。
a=3
let a=a+1
echo $a
$[]
和$(())
可以写在命令行内部,shell在解析命令行的时候,会对它们做算术运算,然后将运算结果替换到命令行中。
a=33
echo $[a+3]
echo $((a+3))
因为变量替换先于算术替换,所以,使用变量名或引用变量的方式都可以:
a=333
echo $[$a+3]
echo $(($a+3))
管道的用法:
cmd1 | cmd2 | cmd3...
每个竖线代表一个管道。上面命令行表示 cmd1 的标准输出会放进管道,cmd2 会从管道中读取进行处理,cmd2 的标准输出会放入另一个管道,cmd3 会从这个管道中读取数据进行处理。后面还可以接任意数量的管道。
shell管道是shell中最值得称赞的功能之一,它以非常简洁的形式实现了管道的进程间通信方式,个人认为dhell处理文本数据的半壁江山都来自于竖线形式的管道。像其它编程语言,打开管道后还要区分哪个进程写管道、哪个进程读管道,为了安全,每个进程还要关闭不用的读端或写端,总之就是麻烦,而Shell的管道非常简洁,竖线左边的就是写管道的,竖线右边的就是读管道的。
例如:
ps aux | grep 'sshd' root 5049 0.0 0.1 155088 5996 ? Ss 17:51 0:07 sshd: root@pts/2 root 17562 0.0 0.0 112816 976 pts/2 S+ 20:13 0:00 grep --color=auto sshd root 25830 0.0 0.1 112984 4344 ? Ss Jan27 0:00 /usr/sbin/sshd -D
ps(Process Status)
命令产生的数据(标准输出)会写进管道,只要管道内一有数据,grep
命令就从中读取数据进行处理。
ps
查看 Linux 中当前运行的进程的命令。能列出系统中运行的进程,包括进程号、命令、CPU使用量、内存使用量等示例:
ps -a # 列出所有运行中/激活进程 ps -ef | grep # 列出需要进程 ps -aux # 显示进程信息,包括无终端的(x)和针对用户(u)的进程:如USER, PID, %CPU, %MEM等
将上面的代码保存为 test.sh,并 cd 到相应目录:
chmod +x ./test.sh #使脚本具有执行权限
./test.sh #执行脚本
注意,一定要写成 ./test.sh,而不是 test.sh,运行其它二进制的程序也一样,直接写 test.sh,linux 系统会去 PATH 里寻找有没有叫 test.sh 的,而只有 /bin, /sbin,/usr/bin,/usr/sbin 等在 PATH 里,你的当前目录通常不在 PATH 里,所以写成 test.sh 是会找不到命令的,要用 ./test.sh
告诉系统说,就在当前目录找。
这种运行方式是,直接运行解释器,其参数就是 shell 脚本的文件名,如:
/bin/sh test.sh
/bin/php test.php
这种方式运行的脚本,不需要在第一行指定解释器信息,写了也没用。
Shell 的 echo
指令与 PHP 的 echo
指令类似,都是用于字符串的输出。命令格式:
echo string
您可以使用echo
实现更复杂的输出格式控制。
echo "It is a test"
这里的双引号完全可以省略,以下命令与上面实例效果一致:
echo It is a test
echo "\"It is a test\""
结果将是:
"It is a test"
同样,双引号也可以省略。
read
命令从标准输入中读取一行,并把输入行的每个字段的值指定给 shell 变量
#!/bin/bash
read name
Hello World!
echo "$name is a test"
Hello World! is a test
以上代码保存为 test.sh,name 接收标准输入的变量,结果将是:
1、编辑sh文件
vi test.sh
(i:插入 | esc:退出insert模式 | wq + 回车:保存退出)
read name
echo "$name is a test"
2、保存退出
敲击esc, 然后输入:wq
,回车退出
3、添加可执行权限,当然默认就是可执行的
chmod +x test.sh
4、运行文件
sh test.sh
5、输入值,显示结果
sh test.sh
OK #标准输入
OK It is a test #输出
echo -e "Is a test one \n" # -e 开启转义
Is a test one
# 此处是空行
echo "Is a test two \n"
Is a test two \n
#!/bin/sh
echo -e "OK! \c" # -e 开启转义 \c 不换行
OK! [root@lw ~]#
echo "It is a test"
It is a test
echo "Hello Linux" > myfile
cat myfile # cat(英文全拼:concatenate)命令用于连接文件并打印到标准输出设备上
Hello Linux
>
重定向输出到某个位置,替换原有文件的所有内容。
>>
重定向追加到某个位置,在原有文件的末尾添加内容。
<
重定向输入某个位置文件。
2>
重定向错误输出。
2>>
重定向错误追加输出到文件末尾。
&>
混合输出错误的和正确的都输出。
echo "$name\""
Hello World!"
echo '$name\"'
$name\"
echo `date`
注意: 这里使用的是反引号 `, 而不是单引号 '。
结果将显示当前日期
Fri Jan 28 10:22:47 CST 2022
该shell脚本中用到了date
这个命令,它的作用就是用来打印当前系统的时间。
其实在shell脚本中date
使用率非常高。有几个选项常在shell脚本中用
%Y表示年
注意%y
和%Y
的区别
比如2022年,对于%y
是22
,而对于%Y
是2022
date "+%Y-%m/%d %H:%M:%S"
2022-01/29 22:04:33
date "+%y-%m/%d %H:%M:%S"
22-01/29 22:04:50
%m表示月
%d表示日期
%H表示小时
%M表示分钟
%S表示秒
date "+%Y-%m/%d %H:%M:%S"
2022-01/29 22:05:11
-d
选项也是经常要用到的,它可以打印n天前或者n天后的日期,当然也可以打印n个月/年前或者后的日期。
date "+%Y-%m/%d"
2022-01/29
date -d "+2022 year" "+%Y-%m/%d" # 年份加2022
4044-01/29
date -d "+5 month" "+%Y-%m/%d" # 月份加5
2022-06/29
星期几也是常用的
date +%w
6
echo
显示带颜色,需要使用参数-e
echo -e "\033[字背景颜色;文字颜色m字符串\033[0m"
例如:
echo -e "\033[41;37m TonyZhang \033[0m"
# 其中41的位置代表底色, 37的位置是代表字的颜色
注:
1、字背景颜色和文字颜色之间是英文的;
2、文字颜色后面有个m
3、字符串前后可以没有空格,如果有的话,输出也是同样有空格
使用方式:
\e[显示方式;前景色;背景色m
或
\033[显示方式;前景色;背景色m
### 显示方式
0(默认值)、1(高亮)、22(非粗体)、4(下划线)、24(非下划线)、5(闪烁)、25(非闪烁)、7(反显)、27(非反显)
### 前景色
30(黑色)、31(红色)、32(绿色)、 33(黄色)、34(蓝色)、35(洋红)、36(青色)、37(白色)
### 背景色
40(黑色)、41(红色)、42(绿色)、 43(黄色)、44(蓝色)、45(洋红)、46(青色)、47(白色)
控制码:控制字符是打开某种样式,输出完成时需要再关闭样式才能使terminal恢复到原来状态:
printf("\e[32m%s\e[0m\n", "hello world");
\033[0m 关闭所有属性
\033[1m 设置高亮度
\033[4m 下划线
\033[5m 闪烁
\033[7m 反显
\033[8m 消隐
\033[30m----\33[37m 设置前景色
\033[40m----\33[47m 设置背景色
\e[0m
将颜色重新置回
特效可以叠加,需要使用“;”隔开,例如:闪烁+下划线+白底色+黑字为 \033[5;4;47;30m闪烁+下划线+白底色+黑字为\033[0m
\033[nA 光标上移n行
\033[nB 光标下移n行
\033[nC 光标右移n行
\033[nD 光标左移n行
\033[y;xH 设置光标位置
\033[2J 清屏
\033[K 清除从光标到行尾的内容
\033[s 保存光标位置
\033[u 恢复光标位置
\033[?25l 隐藏光标
\033[?25h 显示光标
#!/bin/bash
#下面是字体输出颜色及终端格式控制
#字体色范围:30-37
echo -e "\033[30m 黑色字 \033[0m"
echo -e "\033[31m 红色字 \033[0m"
echo -e "\033[32m 绿色字 \033[0m"
echo -e "\033[33m 黄色字 \033[0m"
echo -e "\033[34m 蓝色字 \033[0m"
echo -e "\033[35m 紫色字 \033[0m"
echo -e "\033[36m 天蓝字 \033[0m"
echo -e "\033[37m 白色字 \033[0m"
#字背景颜色范围:40-47
echo -e "\033[40;37m 黑底白字 \033[0m"
echo -e "\033[41;30m 红底黑字 \033[0m"
echo -e "\033[42;34m 绿底蓝字 \033[0m"
echo -e "\033[43;34m 黄底蓝字 \033[0m"
echo -e "\033[44;30m 蓝底黑字 \033[0m"
echo -e "\033[45;30m 紫底黑字 \033[0m"
echo -e "\033[46;30m 天蓝底黑字 \033[0m"
echo -e "\033[47;34m 白底蓝字 \033[0m"
#控制选项说明
#\033[0m 关闭所有属性
#\033[1m 设置高亮度
#\033[4m 下划线
echo -e "\033[4;31m 下划线红字 \033[0m"
#闪烁
echo -e "\033[5;34m 红字在闪烁 \033[0m"
#反影
echo -e "\033[8m 消隐 \033[0m "
#\033[30m-\033[37m 设置前景色
#\033[40m-\033[47m 设置背景色
#\033[nA光标上移n行
#\033[nB光标下移n行
echo -e "\033[4A 光标上移4行 \033[0m"
#\033[nC光标右移n行
#\033[nD光标左移n行
#\033[y;xH设置光标位置
#\033[2J清屏
#\033[K清除从光标到行尾的内容
echo -e "\033[K 清除光标到行尾的内容 \033[0m"
#\033[s 保存光标位置
#\033[u 恢复光标位置
#\033[?25| 隐藏光标
#\033[?25h 显示光标
echo -e "\033[?25l 隐藏光标 \033[0m"
echo -e "\033[?25h 显示光标 \033[0m"
read
命令一个一个词组地接收输入的参数,每个词组需要使用空格进行分隔;如果输入的词组个数大于需要的参数个数,则多出的词组将被作为整体为最后一个参数接收。
测试文件 test.sh 代码如下:
read firstStr secondStr
echo "第一个参数:$firstStr; 第二个参数:$secondStr"
Vim删除所有内容
命令为:
ggdG
其中,
gg
为跳转到文件首行;dG
为删除光标所在行以及其下所有行的内容。再细讲,
d
为删除,G
为跳转到文件末尾行。
执行测试:
sh test.sh
test1 test2 test3 test4 test5 test6
第一个参数:test1; 第二个参数:test2 test3 test4 test5 test6
实例,文件 test.sh:
read -p "请输入一段文字:" -n 6 -t 5 -s password
echo -e "\npassword is $password"
参数说明:
read -p
选项类似echo的作用sh test.sh
请输入一段文字:
password is 1asesw
tar命令是Linux中常用的解压与压缩命令
tar [-cxtzjvfpPN] 文件与目录 ....
参数:
-c :建立一个压缩文件的参数指令(create 的意思);
-x :解开一个压缩文件的参数指令!
-t :查看 tarfile 里面的文件!
特别注意,在参数的下达中, c/x/t 仅能存在一个!不可同时存在!
因为不可能同时压缩与解压缩。
-z :是否同时具有 gzip 的属性?亦即是否需要用 gzip 压缩?
-j :是否同时具有 bzip2 的属性?亦即是否需要用 bzip2 压缩?
-v :压缩的过程中显示文件!这个常用,但不建议用在背景执行过程!
-f :使用档名,请留意,在 f 之后要立即接档名喔!不要再加参数!
例如使用『 tar -zcvfP tfile sfile』就是错误的写法,要写成
『 tar -zcvPf tfile sfile』才对喔!
-p :使用原文件的原来属性(属性不会依据使用者而变)
-P :可以使用绝对路径来压缩!
-N :比后面接的日期(yyyy/mm/dd)还要新的才会被打包进新建的文件中!
注意:
1、--exclude=file1 而不是 --exclude file1
2、要排除一个目录是--exclude=dir1而不是--exclude=dir1/
范例一:将整个 /etc
目录下的文件全部打包成为 /tmp/etc.tar
tar -cvf /tmp/etc.tar /etc # 仅打包,不压缩!
tar -zcvf /tmp/etc.tar.gz /etc # 打包后,以 gzip 压缩
tar -jcvf /tmp/etc.tar.bz2 /etc # 打包后,以 bzip2 压缩
# 特别注意,在参数 f 之后的文件档名是自己取的,我们习惯上都用 .tar 来作为辨识。
# 如果加 z 参数,则以 .tar.gz 或 .tgz 来代表 gzip 压缩过的 tar file
# 如果加 j 参数,则以 .tar.bz2 来作为附档名
# 上述指令在执行的时候,会显示一个警告讯息:
# 『tar: Removing leading `/' from member names』那是关于绝对路径的特殊设定。
范例二:查阅上述 /tmp/etc.tar.gz
文件内有哪些文件?
# 由於我们使用 gzip 压缩,所以要查阅该 tar file 内的文件时,就得要加上 z 这个参数了!这很重要的!
tar -ztvf /tmp/etc.tar.gz # 参数t查看tar file里面的文件!
范例三:将 /tmp/etc.tar.gz
文件解压缩在 /usr/local/src
目录下
cd /usr/local/src
tar -zxvf /tmp/etc.tar.gz # 参数x——解压
# 在预设的情况下,我们可以将压缩档在任何地方解开的!以这个范例来说,我先将工作目录变换到 /usr/local/src 底下,并且解开 /tmp/etc.tar.gz,则解开的目录会在 /usr/local/src/etc
# 另外,如果您进入 /usr/local/src/etc,则会发现,该目录下的文件属性与 /etc/ 可能会有所不同
范例四:在 /tmp
底下,只将 /tmp/etc.tar.gz
内的 etc/passwd
解开
cd /tmp
tar -zxvf /tmp/etc.tar.gz etc/passwd
# 可以通过 tar -ztvf 来查阅 tarfile 内的文件名称,如果单只要一个文件,就可以通过这个方式来下达!
# 注意!etc.tar.gz 内的根目录 / 是被拿掉了
范例五:将 /etc/
内的所有文件备份下来,并且保存其权限
tar -zxvpf /tmp/etc.tar.gz /etc
# -p 的属性是很重要的,尤其是当您要保留原本文件的属性时!
范例六:在 /home
当中,比 2005/06/01 新的文件才备份
tar -N '2005/06/01' -zcvf home.tar.gz /home
范例七:备份 /home
,/etc
,但不要 /home/dmtsai
tar --exclude /home/dmtsai -zcvf myfile.tar.gz /home/* /etc
范例八:将 /etc/
打包后直接解开在 /tmp
底下,而不产生文件
cd /tmp
tar -cvf - /etc | tar -xvf -
# 这个动作有点类似 cp -r /etc /tmp 依旧是有其有用途的!
# 要注意的地方在于输出档变成 - ,而输入档也变成 - ,又有一个 | 存在
# 这分别代表 standard output, standard input 与管线命令