初识Shell
初识shell
Shell定义
-
shell的含义
操作系统 内核
shell的英文含义是“壳”;这是相对于内核来说的,因为它是建立在内核的基础上,面向于用户的一种表现形式。
Shell的分类
Linux中默认的Shell是/bin/bash,流行的Shell有ash、bash、ksh、csh、zsh等,不同的Shell都有自己的特点以及用途。
1、Bash
大多数Linux系统默认使用的Shell,bash Shell是Bourne Shell 的一个免费版本,它是最早的Unix Shell,bash还有一个特点,可以通过help命令来查看帮助。包含的功能几乎可以涵盖Shell所具有的功能,所以一般的Shell脚本都会指定它为执行路径。
2、Csh
C Shell 使用的是“类C”语法,csh是具有C语言风格的一种Shell,其内部命令有52个,较为庞大。目前使用的并不多,已经被/bin/tcsh所取代。
3、Ksh
Korn Shell 的语法与Bourne Shell相同,同时具备了C Shell的易用特点。许多安装脚本都使用ksh,ksh 有42条内部命令,与bash相比有一定的限制性。
4、TCsh
tcsh是csh的增强版,与C Shell完全兼容。
5、Sh
是一个快捷方式,已经被/bin/bash所取代。
6、Nologin
指用户不能登录
7、Zsh
目前Linux里最庞大的一种 zsh。它有84个内部命令,使用起来也比较复杂。一般情况下,不会使用该Shell。
Shell的适用场景
- 自动化批量系统初始化程序 (update,软件安装,时区设置,安全策略...)
- 自动化批量软件部署程序 (LAMP,LNMP,Tomcat,LVS,Nginx)
- 应用管理程序 (KVM,集群管理扩容,MySQL,DELLR720批量RAID)
- 日志分析处理程序(PV, UV, 200, !200, top 100, grep/awk)
- 自动化备份恢复程序(MySQL完全备份/增量 + Crond)
- 自动化管理程序(批量远程修改密码,软件升级,配置更新)
- 自动化信息采集及监控程序(收集系统/应 用状态信息,CPU,Mem,Disk,Net,TCP Status,Apache,MySQL)
- 配合Zabbix信息采集(收集系统/应用状态信息,CPU,Mem,Disk,Net,TCP Status,Apache,MySQL)
- 自动化扩容(增加云主机-->业务上线)
- Zabbix监控CPU 80%+|-50% Python API AWS/EC2(增加/删除云主机) + Shell Script(业务上线)
- 俄罗斯方块,打印三角形,打印圣诞树,打印五角星,运行小火车,坦克大战,排序算法实现
- Shell可以做任何事(一切取决于业务需求)
1.别名
- alisa 命令
alisa //查看系统当前的所有别名
alisa h5="head -5" //定义新的别名,输入效果相同
unalisa h5 //取消别名“head -5”的定义
-
如果想要文件永久生效,只需要将使用的别名写入对应用户或系统的 bashrc 文件中
-
如果定义的别名与将要使用的命令冲突,可以添加 \ 使别名失效
\cp -rf /etc/hosts
2.快捷键
快捷键 | 作用 |
---|---|
*ctrl+a | 把光标移动到命令行开头。如果我们输入的命令过长,想要把光标移动到命令行开头时使用。 |
*ctrl+e | 把光标移动到命令行结尾。 |
*ctrl+c | 强制终止当前的命令。 |
*ctrl+l | 清屏,相当于clear命令。 |
*ctrl+u | 删除或剪切光标之前的命令。我输入了一行很长的命令,不用使用退格键一个一个字符的删除,使用这个快捷键会更加方便 |
*ctrl+k | 删除或剪切光标之后的内容。 |
ctrl+y | 粘贴ctrl+U或ctul+K剪切的内容。 |
*ctrl+r | 在历史命令中搜索,按下ctrl+R之后,就会出现搜索界面,只要输入搜索内容,就会从历史命令中搜索。 |
*ctrl+d | 退出当前终端。 |
ctrl+z | 暂停,并放入后台。这个快捷键牵扯工作管理的内容,我们在系统管理章节详细介绍。 |
ctrl+s | 暂停屏幕输出。 |
ctrl+q | 恢复屏幕输出。 |
ctrl+左右箭头 | 单词之间快速移动光标 |
3.前后台作业控制
- bash 单一终端界面下,经常需要管理或者完成多个作业,如一边执行编译,一边实现数据备份...等其他任务,所有上述的工作可以在一个bash内实现,在同一个终端窗口完成
- 通过 jobs 方式来管理作业,当前终端的作业在其他终端不可见。
常用的作业命令
-
command & 直接让作业进入后台运行
[root@localhost ~]# ls /etc/passwd & [1] 6328 [root@localhost ~]# /etc/passwd [1]+ Done ls --color=auto /etc/passwd
-
ctrl+z 将当前作业切换到后台
[root@localhost ~]# vim test.txt #进入vim编辑器按下ctrl+z后 [1]+ Stopped vim test.txt [root@localhost ~]#
-
jobs 查看后台作业状态
[root@localhost ~]# jobs [1]+ Stopped vim test.txt
-
fg %n 让后台运行的作业n切换到前台来(n是[1]+ Stopped 内的[]数字)
[root@localhost ~]#fg %1 //因为之前的进程[1]是“vim test.txt” vim test.txt //所以执行的就是编辑test.txt
-
bg %n 让指定的作业n在后台运行
[root@localhost ]# jobs [2]- Stopped find /u02 -type f -size +100000k [3]+ Stopped find / -type f -size +100000k # 运行后台中暂停的作业(bg命令) # 前面有2个job处于stopped状态,现在我们让其在后台运行,直接输入bg命令则缺省的job继续运行,否则输入job编号,运行指定的job [root@localhost ~]# bg %2 # 输入bg %2之后,可以看到原来的命令后被追加了& [2]- find /u02 -type f -size +100000k &
-
kill %n 移除指定的作业n
[root@localhost ~]# jobs [1]+ Stopped vim test.txt [root@localhost ~]# kill -9 %1 //kill -9 强制杀死 [1]+ Stopped vim test.txt [root@localhost ~]# jobs //查看后台发现作业[1]的状态是Killed [1]+ Killed vim test.txt
"n" 为jobs命令查看到的job编号,不是进程id。
每一个job会有一个对应的job编号,编号在当前的终端从1开始分配。
job 编号的使用样式为[n],后面可能会跟有 "+" 号或者 "-" 号,或者什么也不跟。
"+" 号表示最近的一个job,
"-" 号表示倒数第二个被执行的Job。"+" 号与 "-" 号会随着作业的完成或添加而动态发生变化。
4.输入输出重定向
- 输入重定向:数据从哪流向程序。默认数据是从键盘流向程序,如果改变了它的方向,数据从其他地方流入,就是输入重定向
- 输出重定向:数据从程序流向哪。默认数据是从程序流向显示器,如果改变了它的方向,数据就流向其他地方,这就是输出重定向
1.文件描述符
文件描述符 | 文件名 | 类型 | 硬件 |
---|---|---|---|
0 | stdin | 标准输入文件 | 键盘 |
1 | stdout | 标准正确输出文件 | 显示器 |
2 | stderr | 标准错误输出文件 | 显示器 |
2.输出重定向
类 型 | 符 号 | 作 用 |
---|---|---|
标准输出重定向 | command >file | 以覆盖的方式,把 command 的正确输出结果输出到 file 文件中。 |
command >>file | 以追加的方式,把 command 的正确输出结果输出到 file 文件中。 | |
标准错误输出重定向 | command 2>file | 以覆盖的方式,把 command 的错误信息输出到 file 文件中。 |
command 2>>file | 以追加的方式,把 command 的错误信息输出到 file 文件中。 | |
正确输出和错误信息同时保存 | command >file 2>&1 | 以覆盖的方式,把正确输出和错误信息同时保存到同一个文件(file)中。 |
command >>file 2>&1 | 以追加的方式,把正确输出和错误信息同时保存到同一个文件(file)中。 | |
command >file1 2>file2 | 以覆盖的方式,把正确的输出结果输出到 file1 文件中,把错误信息输出到 file2 文件中。 | |
command >>file1 2>>file2 | 以追加的方式,把正确的输出结果输出到 file1 文件中,把错误信息输出到 file2 文件中。 | |
command >file 2>file | 【不推荐】这两种写法会导致 file 被打开两次,引起资源竞争,所以 stdout 和 stderr 会互相覆盖, | |
command >>file 2>>file |
-
输出重定向中,> 代表的是覆盖,>> 代表的是追加。
-
输出重定向的完整写法其实是 fd>file 或者 fd>>file ,其中 fd 表示文件描述符,如果不写,默认为 1,也就是标准输出文件。
-
当文件描述符为 1 时,一般都省略不写,如上表所示;当然,如果你愿意,也可以将 command >file 写作 command 1>file,但这样做是多此一举。
-
当文件描述符为大于 1 的值时,比如 2,就必须写上。
-
需要重点说明的是,fd 和 >之间不能有空格,否则 Shell 会解析失败;> 和 file 之间的空格可有可无。为了保持一致,习惯在 > 两边都不加空格。
输出重定向
-
将 echo 命令的输出结果以追加的方式写入到 demo.txt 文件中。
[root@localhost ~]# echo $(date) >> demo.txt #将输入结果以追加的方式重定向到文件 [root@localhost ~]# cat demo.txt Fri Feb 14 19:18:40 CST 2019
-
将
ls -l
命令的输出结果重定向到文件中[root@localhost ~]# ls -l #先预览一下输出结果 total 4 -rw-------. 1 root root 1526 Mar 30 2019 anaconda-ks.cfg [root@localhost ~]# ls -l >demo.txt #重定向 [root@localhost ~]# cat demo.txt #查看文件内容 total 4 -rw-------. 1 root root 1526 Mar 30 2019 anaconda-ks.cfg
错误输出重定向例子
-
命令正确执行是没有错误信息的,我们必须刻意地让命令执行出错,如下所示:
[root@localhost ~]# ls java #先预览一下错误信息 ls: cannot access java: No such file or directory [root@localhost ~]# ls java 2>err.log #重定向 [root@localhost ~]# cat err.log #查看文件 ls: cannot access java: No such file or directory
正确输出和错误信息同时保存
[root@localhost ~]# ls -l >out.log 2>&1
[root@localhost ~]# ls java >>out.log 2>&1
[root@localhost ~]# cat out.log
total 8
-rw-------. 1 root root 1526 Mar 30 2019 anaconda-ks.cfg
-rw-r--r-- 1 root root 50 Feb 14 20:15 err.log
-rw-r--r-- 1 root root 0 Feb 14 20:15 out.log
ls: cannot access java: No such file or directory
-
out.log 的最后一行是错误信息,其它行都是正确的输出结果。
-
上面的实例将正确结果和错误信息都写入同一个文件中,建议把正确结果和错误信息分开保存到不同的文件中
[root@localhost ~]# ls -l >>out.log 2>>err.log
/dev/null (黑洞)文件
如果不想把命令的输出结果保存到文件,也不想把命令的输出结果显示到屏幕上,干扰命令的执行,可以把命令的所有结果重定向到 /dev/null 文件中
[root@localhost ~]# ls -l &>/dev/null
可以把 /dev/null 当成 Linux 系统的垃圾箱,任何放入垃圾箱的数据都会被丢弃,不能恢复。
3.输入重定向
符号 | 说明 |
---|---|
command <file | 将 file 文件中的内容作为 command 的输入。 |
command <<END | 从标准输入(键盘)中读取数据,直到遇见分界符 END 才停止(分界符可以是任意的字符串,用户自己定义)。 |
command file2 | 将 file1 作为 command 的输入,并将 command 的处理结果输出到 file2。 |
输入重定向例子:
cat >> ifcfg-eth2 << EOF
TYPE=Ethernet
BOOTPROTO=none
NAME=ens160
DEVICE=ens160
ONBOOT=yes
IPADDR=192.168.26.200
PREFIX=24
GATEWAY=192.168.26.2
DNS1=192.168.26.2
EOF
5.管道 | tee管道
1.管道 |
-
管道,从一头进去,从另一头出来。
-
在Shell中,管道将一个程序的标准输出作为另一个程序的标准输入,就像用一根管子将一个程序的输出连接到另一个程序的输入一样。
-
管道的符号是 |,下面的程序将 man 的标准输出作为 less 的标准输入,以实现翻页的功能:
[root@localhost ~]# man ls | less
2.tee
-
有时候我们想要同时将程序的输出显示在屏幕上(或进入管道)和保存到文件中,这个时候可以使用
tee
。 -
tee 程序的输出和它的输入一样,但是会将输入内容额外的保存到文件中:
[root@localhost ~]# cat /etc/passwd | tee hello.txt
tee 程序将 cat 程序的输出显示在屏幕上,并且在 hello.txt 文件中保留了副本。需要注意的是,如果 tee 命令中指定的文件已经存在,那么它将会被覆盖,使用 -a 选项在文件末尾追加内容(而不是覆盖)
[root@localhost ~]# cat hello.txt | tee -a hello.txt.bk
6.命令排序
1.&& || 具备逻辑判断
- command1 && command2 只有在 command1 成功执行后才会执行 command2;
- command1 || command2在 command1 没有成功执行时执行command2。
2. ; (分号) 不具备逻辑判断
[root@localhost ~]# cd /usr/local;cat test.txt
7.通配符
常见通配符
字符 | 含义 | 实例 |
---|---|---|
* | 匹配0个或多个任意字符 | a*b,a与b之间可以有任意长度的字符,也可以没有。例如:aabcb,ab,azxcb... |
? | 匹配一个任意字符 | a?b,a与b之间必须但也只能存在一个字符,该字符可以是任意字符。例如:aab,abb,acb... |
[list] | 匹配list中的任意单个字符,一个[]只匹配一个位置的字符 | a[xyz]b,a与b之间必须但也只能存在一个字符,该字符只能是x或y或z。例如:axb,ayb,azb |
[!list] | 匹配除list中的任意单个字符 | a[!a-z]b,a与b之间必须但也只能存在一个字符,该字符不能是小写字母。例如:aAb,a0b... |
[c1-c2] | 匹配c1-c2间的任意单个字符 | a[0-1]b,a与b之间必须但也只能存在一个字符,该字符只能是数字。例如:a0b,a1b... |
{string1,string2,...} | 匹配string1、string2等中的一个字符串 | a{abc,xyz,opq}b,a与b之间必须但也只能存在一个字符串,字符串只能是abc或xyz或opq。例如:aabcb,axyzb,aopqb... |
{1..3} | 匹配1到3 |
[root@localhost ~]# ls /etc/*.conf
/etc/asound.conf /etc/kdump.conf /etc/man_db.conf /etc/sudo-ldap.conf
/etc/chrony.conf /etc/krb5.conf /etc/mke2fs.conf /etc/sysctl.conf
/etc/dracut.conf /etc/ld.so.conf /etc/nsswitch.conf /etc/vconsole.conf
/etc/e2fsck.conf /etc/libaudit.conf /etc/resolv.conf /etc/yum.conf
/etc/fuse.conf /etc/libuser.conf /etc/rsyslog.conf
/etc/GeoIP.conf /etc/locale.conf /etc/sestatus.conf
/etc/host.conf /etc/logrotate.conf /etc/sudo.conf
[root@localhost ~]# ls /etc/???.conf
/etc/yum.conf
[root@localhost ~]# touch file{1,2,3}
[root@localhost ~]# ls file*
file1 file2 file3
[root@localhost ~]# ls file[123]
file1 file2 file3
bash初始化
1.bash环境变量文件
centos下的环境变量文件
/etc/profile 使用范围:所有账户
/etc/bashrc 使用范围:所有账户
~/.bashrc 使用范围:~所代表的账户
~/.bash_profile 使用范围:~所代表的账户
~/.bash_logout
login shell
登录的时候需要输入用户名
nologin shell
登录的时候不需要输入用户
- /etc/profile
- 全局(公有)配置,不管是哪个用户,登录时都会读取该文件。
- /etc/bashrc
- 它也是全局(公有)的
- bash 执行时,不管是何种方式,都会读取此文件
- ~/.profile
-
若 bash 是以 login 方式执行时,读取 ~/.bash_profile,若它不存在,则读取 ~/.bash_login,若前两者不存在,读取 ~/.profile。
-
图形模式登录时,此文件将被读取,即使存在 ~/.bash_profile 和 ~/.bash_login。
-
- ~/.bash_login
- 若 bash 是以 login 方式执行时,读取 ~/.bash_profile,若它不存在,则读取 ~/.bash_login,若前两者不存在,读取 ~/.profile。
- ~/.bash_profile
- 只有 bash 是以 login 形式执行时,才会读取此文件。通常该配置文件还会配置成去读取 ~/.bashrc。
- ~/.bashrc
- 当 bash 是以 non-login 形式执行时,读取此文件。若是以 login 形式执行,则不会读取此文件。
- ~/.bash_logout
- 注销时,且是 longin 形式,此文件才会读取。在文本模式注销时,此文件会被读取,图形模式注销时,此文件不会被读取。
2、Bash 环境变量加载
-
图形模式登录时,顺序读取:/etc/profile 和 ~/.profile
-
图形模式登录后,打开终端时,顺序读取:/etc/bash.bashrc 和 ~/.bashrc
-
文本模式登录时,顺序读取:/etc/bash.bashrc,/etc/profile 和 ~/.bash_profile
-
从其它用户 su 到该用户,则分两种情况:
- 如果带 -l 参数(或-参数,--login 参数),如:su -l username,则 bash 是 login 的,它将顺序读取以下配置文件:/etc/bash.bashrc,/etc/profile 和~/.bash_profile。
- 如果没有带 -l 参数,则 bash 是 non-login 的,它将顺序读取:/etc/bashrc 和 ~/.bashrc
-
注销时,或退出 su 登录的用户,如果是 longin 方式,那么 bash 会读取:~/.bash_logout
- 执行自定义的 Shell 文件时,若使用 bash -l a.sh 的方式,则 bash 会读取行:/etc/profile 和 ~/.bash_profile,若使用其它方式,如:bash a.sh,./a.sh,sh a.sh(这个不属于bash Shell),则不会读取上面的任何文件。
- 上面的例子凡是读取到 ~/.bash_profile 的,若该文件不存在,则读取 ~/.bash_login,若前两者不存在,读取 ~/.profile。
Shell 脚本规范
1、风格规范
开头有“蛇棒”
所谓shebang其实就是在很多脚本的第一行出现的以”#!”开头的注释,他指明了当我们没有指定解释器的时候默认的解释器,一般可能是下面这样:
#!/bin/sh
除了 bash 之外,可以用下面的命令查看本机支持的解释器:
[root@qfdeu ~]# cat /etc/shells
/bin/sh
/bin/bash
/usr/bin/sh
/usr/bin/bash
- 直接使用 ./a.sh 来执行这个脚本的时候,如果没有shebang,就会默认用 $Shell指定的解释器,否则就会用 shebang 指定的解释器。
- 上面这种写法可能不太具备适应性,一般我们会用下面的方式来指定:
#!/usr/bin/env bash
2、注释(重点)
注释的意义不仅在于解释用途,而在于告诉我们注意事项,就像是一个 README。
具体的来说,对于Shell脚本,注释一般包括下面几个部分:
- shebang
- 脚本的参数
- 脚本的用途
- 脚本的注意事项
- 脚本的写作时间,作者,版权等
- 各个函数前的说明注释
- 一些较复杂的单行命令注释
3、参数要规范(重点)
这一点很重要,当脚本需要接受参数的时候,一定要先判断参数是否合乎规范,并给出合适的回显,方便使用者了解参数的使用。
至少得判断下参数的个数
if [[ $# != 2 ]];then
echo "Parameter incorrect."
exit 1
fi
4、变量
一般情况下会将一些重要的环境变量定义在开头,确保这些变量的存在。
source /etc/profile
export PATH=”/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin:/apps/bin/”
- 这种定义方式有一个很常见的用途,最典型的应用就是,当本地安装了很多 java 版本时,可能需要指定一个java来用。这时就会在脚本开头重新定义JAVA_HOME以及PATH变量来进行控制。
- 一段好的代码通常是不会有很多硬编码在代码里的“魔数”的。如果一定要有,通常是用一个变量的形式定义在开头,然后调用的时候直接调用这个变量,这样方便日后的修改。
5、缩进
-
正确的缩进非常重要,尤其是在写函数时,否则在阅读时很容易把函数体跟直接执行的命令搞混。
-
常见的缩进方法主要有”soft tab”和”hard tab”两种:
- 所谓soft tab就是使用n个空格进行缩进(n通常是2或4)
- 所谓hard tab当然就是指真实的”\t”字符12
-
对于if和for语句之类的,最好不要把then,do这些关键字单独写一行,这样看上去比较丑。
6、命名有标准
所谓命名规范,基本包含下面这几点:
- 文件名规范,以.sh结尾,方便识别
- 变量名字要有含义,不要拼错
- 统一命名风格,写 Shell 一般用小写字母加下划线
7、编码要统一
在写脚本的时候尽量使用 UTF-8 编码,能够支持中文等一些奇奇怪怪的字符。不过虽然能写中文,但是在写注释以及打log的时候还是尽量英文,毕竟很多机器还是没有直接支持中文的,打出来可能会有乱码。
8、日志和回显
- 日志的重要性不必多说,能够方便回头纠错,在大型的项目里是非常重要的。
- 如果这个脚本是供用户直接在命令行使用的,那么最好还要能够在执行时实时回显执行过程,方便用户掌控。
- 为了提高用户体验,会在回显中添加一些特效,比如颜色啊,闪烁啊之类的。
9、密码要移除
不要把密码硬编写在脚本里,尤其是当脚本托管在类似 Github 这类平台中时。
10、太长要分行
在调用某些程序的时候,参数可能会很长,这时候为了保证较好的阅读体验,我们可以用反斜杠(续行符)来分行:
./configure \
–prefix=/usr \
–sbin-path=/usr/sbin/nginx \
–conf-path=/etc/nginx/nginx.conf
11、代码有效率(重点)
在使用命令的时候要了解命令的具体做法,尤其当数据处理量大的时候,要时刻考虑该命令是否会影响效率。
比如下面的两个sed命令:
[root@qfdeu ~]# sed -n '1p' file
[root@qfdeu ~]# sed -n '1p;1q' file
作用一样,都是获取文件的第一行。但是第一条命令会读取整个文件,而第二条命令只读取第一行。当文件很大的时候,仅仅是这样一条命令不一样就会造成巨大的效率差异。
当然,这里只是为了举一个例子,这个例子真正正确的用法应该是使用head -n1 file命令
勤用双引号
- 几乎所有的大佬都推荐在使用”$”来获取变量的时候最好加上双引号。
- 不加上双引号在很多情况下都会造成很大的麻烦,
#!/bin/sh
#已知当前文件夹有一个a.sh的文件
var="*.sh"
echo $var
echo "$var"
运行结果如下:
a.sh
*.sh
可以解释为它执行了下面的命令
echo *.sh
echo "*.sh"
在很多情况下,在将变量作为参数的时候,一定要注意上面这一点,仔细体会其中的差异。上面只是一个非常小的例子,实际应用的时候由于这个细节导致的问题实在是太多了
12、学会查路径
-
很多情况下,会先获取当前脚本的路径,然后以这个路径为基准,去找其他的路径。通常我们是直接用 pwd 以期获得脚本的路径。
不过其实这样是不严谨的,pwd 获得的是当前Shell的执行路径,而不是当前脚本的执行路径。
-
正确的做法应该是下面这两种:
script_dir=$(cd $(dirname $0) && pwd)
script_dir=$(dirname $(readlink -f $0 ))
应当先cd进当前脚本的目录然后再pwd,或者直接读取当前脚本的所在路径。
13、代码要简短
这里的简短不单单是指代码长度,而是只用到的命令数。原则上我们应当做到,能一条命令解决的问题绝不用两条命令解决。这不仅牵涉到代码的可读性,而且也关乎代码的执行效率。
- 最经典的例子如下:
[root@qfdeu ~]# cat /etc/passwd | grep root
[root@qfdeu ~]# grep root /etc/passwd
- cat 命令最为人不齿的用法就是这样,用的没有任何意义,明明一条命令可以解决,非得加根管道
14、使用新写法
AT&T bell unix FREE BSD emacs vim
这里的新写法不是指有多厉害,而是指可能更希望使用较新引入的一些语法,更多是偏向代码风格的、
- 尽量使用func( ){ }来定义函数,而不是func{ }
- 尽量使用[[ ]]来代替[ ]
- 尽量使用$()将命令的结果赋给变量,而不是反引号。
- 在复杂的场景下尽量使用printf代替echo进行回显
15、其他小技巧
- 路径尽量保持绝对路径,不容易出错,如果非要用相对路径,最好用./修饰
- 优先使用bash的变量替换代替awk sed,这样更加简短
- 简单的if尽量使用&& ||,写成单行。比如[[ x > 2]] && echo x
- 当export变量时,尽量加上子脚本的namespace,保证变量不冲突
- 会使用trap捕获信号,并在接受到终止信号时执行一些收尾工作
- 使用mktemp生成临时文件或文件夹
- 利用/dev/null过滤不友好的输出信息
- 会利用命令的返回值判断命令的执行情况
- 使用文件前要判断文件是否存在,否则做好异常处理
- 不要处理ls后的数据(比如ls -l | awk '{ print $8 }'),
- ls的结果非常不确定,并且平台有关
- 读取文件时不要使用for loop而要使用while read
Shell 脚本调试
Shell脚本的语法调试,使用bash的相关参数进行调试
sh [参数] 文件名.sh
- -n 不要执行script,仅查询语法的问题
- -v 在执行script之前,先将script的内容输出到屏幕上
- -x 将使用的脚本的内容输出到屏幕,该参数经常被使用
#-v的示例:
[root@qfdeu ~]# sh -v demo.sh
module () { eval `/usr/bin/modulecmd bash $*`
}
#!/bin/bash
case $1 in
"one")
echo "you input number is one"
;;
"two")
echo "you input number is twp"
;;
*)
echo "you input number is other"
;;
esac
you input number is other
#-x的示例:
[root@qfdeu ~]# sh -x demo.sh
+ case $1 in
+ echo 'you input number is other'
you input number is other
[root@qfdeu ~]# sh -vx demo.sh
脚本运行方式
Linux中Shell脚本的执行通常有4种方式,分别为工作目录执行,绝对路径执行,sh执行,Shell环境执行。
1、工作目录执行
工作目录执行,指的是执行脚本时,先进入到脚本所在的目录(此时,称为工作目录),然后使用 ./脚本方式执行
[root@qfdeu ~]# ./test.sh
Hello Shell
2、绝对路径执行
[root@qfdeu ~]# /home/tan/scripts/test.sh
Hello Shell
3、bash 执行
#在子shell中执行,不需要脚本的可执行权限
[root@qfdeu ~]# sh test.sh
Hello Shell
[root@qfdeu ~]# bash test.sh
Hello Shell
4、Shell 环境执行
Shell环境执行,指的是在当前的Shell环境中执行,可以使用 . 接脚本 或 source 接脚本
不需要脚本的可执行权限,在当前shell中执行
[root@qfdeu ~]# . test.sh
Hello Shell
[root@qfdeu ~]# source test.sh
Hello Shell
Shell 变量
1、自定义变量
定义变量:
变量名=变量值
变量名只能由数字字母和下划线组成,必须以字母或下划线开头,区分大小写
例子:
ip1=192.168.2.115
引用变量:
变量名 或 {变量名}
查看变量:
echo $变量名
set
所有变量:包括自定义变量和环境变量
取消变量:
unset 变量名
作用范围:
仅在当前Shell中有效
2、环境变量
设置永久生效的方法:放在四个用户登录脚本内部
定义环境变量:
- 方法一 export back_dir2=/home/backup
- 方法二 export back_dir1 将自定义变量转换成环境变量
-
引用环境变量:变量名 或 {变量名}
-
查看环境变量:echo $变量名 env 例如 env | grep back_dir2
-
取消环境变量:unset 变量名
-
变量作用范围:在当前Shell和子Shell有效
3、位置变量
1 2 3 4 5 6 7 8 9 {10}
4、预定义变量
$0 脚本名(自己本身)
**\* 所有的参数** "*" 会将所有的参数作为一个整体,以"1 2 … $n"的形式输出所有参数
@ 所有的参数 "@" 会将各个参数分开,以"1" "2" … "$n" 的形式输出所有参数
$# 参数的个数
$$
当前进程的PID
$!
上一个后台进程的PID
$?
上一个命令的返回值 0表示成功
示例1:
# vim test.sh
echo "第2个位置参数是$2"
echo "第1个位置参数是$1"
echo "第4个位置参数是$4"
echo "所有参数是: $*"
echo "所有参数是: $@"
echo "参数的个数是: $#"
echo "当前进程的PID是: $$"
echo '$1='$1
echo '$2='$2
echo '$3='$3
echo '$*='$*
echo '$@='$@
echo '$#='$#
echo '$$='$$
示例2:
# vim ping.sh
#!/bin/bash
ping -c2 $1 &>/dev/null
if [ $? = 0 ];then
echo "host $1 is ok"
else
echo "host $1 is fail"
fi
[root@qfdeu ~]# chmod a+x ping.sh
[root@qfdeu ~]# ./ping.sh 192.168.2.25
*和@区别
- 当*和@没有被引用的时候,它们确实没有什么区别,都会把位置参数当成一个个体。最好不要使用,因为如果位置参数中带有空格或者通配符的情况下,可能结果会和想要的不一样
- "\*" 会把所有位置参数当成一个整体(或者说当成一个字符串),如果没有位置参数,则"*"为空,如果有两个位置参数并且IFS为空格时,"*"相当于"1 $2"
- "@" 会把所有位置参数当成一个单独的字段,如果没有位置参数(#为0),则"@"展开为空(不是空字符串,而是空列表),如果存在一个位置参数,则"@"相当于"1",如果有两个参数,则"@"相当于"1" "2"等等
[root@server busybox]# cat a.sh
for i in $@
do
echo $i
done
for i in $*
do
echo $i
done
for i in "$@"
do
echo $i
done
for i in "$*"
do
echo $i
done
5、变量赋值
显式赋值
-
变量名=变量值
-
示例:
ip1=192.168.1.251
school="BeiJing 1000phone"
today1=`date +%F`
today2=$(date +%F)
read 从键盘读入变量值
-
read 变量名
-
read -p "提示信息: " 变量名
-
read -t 5 -p "提示信息: " 变量名
-
read -n 2 变量名
-
read -s -p ... # -s是隐藏用户输入
-
read: 用法:read [-ers] [-a 数组] [-d 分隔符] [-i 缓冲区文字] [-n 读取字符数] [-N 读取字符数] [-p 提示符] [-t 超时] [-u 文件描述符] [名称 ...]
[root@qfdeu ~]# vim first.sh
back_dir1=/var/backup
read -p "请输入你的备份目录: " back_dir2
echo $back_dir1
echo $back_dir2
[root@qfdeu ~]# sh first.sh
[root@qfdeu ~]# vim ping2.sh
#!/bin/bash
read -p "Input IP: " ip
ping -c2 $ip &>/dev/null
if [ $? = 0 ];then
echo "host $ip is ok"
else
echo "host $ip is fail"
fi
[root@qfdeu ~]# chmod a+x ping2.sh
[root@qfdeu ~]# ./ping.sh
6、引用变量
" "
弱引用
' '
强引用
[root@qfdeu ~]# school=1000phone
[root@qfdeu ~]# echo "${school} is good"
1000phone is good
[root@qfdeu ~]# echo '${school} is good'
${school} is good
(反引号)命令替换 等价于 $() 反引号中的Shell命令会被先执行
[root@qfdeu ~]# touch `date +%F`_file1.txt
[root@qfdeu ~]# touch $(date +%F)_file2.txt
[root@qfdeu ~]# disk_free3="df -Ph |grep '/$' |awk '{print $4}'" # 错误
[root@qfdeu ~]# disk_free4=$(df -Ph |grep '/$' |awk '{print $4}')
[root@qfdeu ~]# disk_free5=`df -Ph |grep '/$' |awk '{print $4}'`
7、变量运算
1、整数运算
方法一:expr
[root@qfdeu ~]# expr 1 + 2
[root@qfdeu ~]# expr $num1 + $num2 + - \* / %
方法二:$(())
[root@qfdeu ~]# echo $(($num1+$num2)) + - * / %
[root@qfdeu ~]# echo $((num1+num2))
[root@qfdeu ~]# echo $((5-3*2))
[root@qfdeu ~]# echo $(((5-3)*2))
[root@qfdeu ~]# echo $((2**3))
[root@qfdeu ~]# sum=$((1+2)); echo $sum
方法三:$[]
[root@qfdeu ~]# echo $[5+2] + - * / %
[root@qfdeu ~]# echo $[5**2]
方法四:let
[root@qfdeu ~]# let sum=2+3; echo $sum
[root@qfdeu ~]# let i++; echo $i
# let a+=2 //和a=a+2的效果一致
# echo $a
+= 加等
*= 乘等
-= 减等
/= 除等
%= 模等
2、小数运算
[root@qfdeu ~]# echo "2*4" |bc
[root@qfdeu ~]# echo "2^4" |bc
[root@qfdeu ~]# echo "scale=2;6/4" |bc
[root@qfdeu ~]# awk 'BEGIN{print 1/2}'
[root@qfdeu ~]# echo "print 5.0/2" |python
8、变量截取
1、匹配截取
[root@qfedu ~]# url=www.sina.com.cn
[root@qfedu ~]# echo ${#url} # 获取变量值的长度
15
[root@qfedu ~]# echo ${url} # 标准查看
www.sina.com.cn
[root@qfedu ~]# echo ${url#*.} # 从前往后,最短匹配
sina.com.cn
[root@qfedu ~]# echo ${url##*.} # 从前往后,最长匹配 贪婪匹配
cn
[root@qfedu ~]# url=www.sina.com.cn
[root@qfedu ~]# echo ${url}
www.sina.com.cn
[root@qfedu ~]# echo ${url%.*} # 从后往前,最短匹配
www.sina.com
[root@qfedu ~]# echo ${url%%.*} # 从后往前,最长匹配 贪婪匹配
www
[root@qfedu ~]# url=www.sina.com.cn
[root@qfedu ~]# echo ${url#a.}
www.sina.com.cn
[root@qfedu ~]# echo ${url#*sina.}
com.cn
[root@qfedu ~]# echo $HOSTNAME
qfedu.1000phone.com
[root@qfedu ~]# echo ${HOSTNAME%%.*}
qfedu
2、索引切片
[root@qfedu ~]# echo ${url:0:5}
[root@qfedu ~]# echo ${url:5:5}
[root@qfedu ~]# echo ${url:5}
3、字符替换
[root@qfedu ~]# url=www.sina.com.cn
[root@qfedu ~]# echo ${url/sina/baidu}
www.baidu.com.cn
[root@qfedu ~]# url=www.sina.com.cn
[root@qfedu ~]# echo ${url/n/N}
www.siNa.com.cn
[root@qfedu ~]# echo ${url//n/N} #贪婪匹配
www.siNa.com.cN
4、${变量名-新变量值}
[root@qfedu ~]# unset var1
[root@qfedu ~]# echo ${var1}
[root@qfedu ~]# echo ${var1-aaaaa}
aaaaa
[root@qfedu ~]# var2=111
[root@qfedu ~]# echo ${var2-bbbbb}
111
[root@qfedu ~]# var3=
[root@qfedu ~]# echo ${var3-ccccc}
变量没有被赋值:会使用“新的变量值“ 替代
变量有被赋值(包括空值): 不会被替代
[root@qfedu ~]# unset var1
[root@qfedu ~]# unset var2
[root@qfedu ~]# unset var3
[root@qfedu ~]# var2=
[root@qfedu ~]# var3=111
[root@qfedu ~]# echo ${var1:-aaaa}
aaaa
[root@qfedu ~]# echo ${var2:-aaaa}
aaaa
[root@qfedu ~]# echo ${var3:-aaaa}
111
5、${变量名:-新变量值}
变量没有被赋值(包括空值):都会使用“新的变量值“ 替代
变量有被赋值: 不会被替代
[root@qfedu ~]# echo ${var3+aaaa} 如果变量 var3 已定义,则输出默认值 "aaaa";否则不产生任何输出。
[root@qfedu ~]# echo ${var3:+aaaa} 如果变量 var3 已定义且不为空,则输出默认值 "aaaa";否则不产生任何输出。
[root@qfedu ~]# echo ${var3=aaaa} 如果变量 var3 未定义或为空,则将其赋值为 "aaaa",并输出赋值后的值
[root@qfedu ~]# echo ${var3:=aaaa} 如果变量 var3 未定义或为空,则将其赋值为 "aaaa",并输出赋值后的值;否则直接输出变量 var3 的值。
[root@qfedu ~]# echo ${var3?aaaa} 如果变量 var3 未定义或为空,则输出错误信息 "aaaa";否则不产生任何输出。
[root@qfedu ~]# echo ${var3:?aaaa} 如果变量 var3 未定义或为空,则输出错误信息 "aaaa",并终止脚本的执行;否则直接输出变量 var3 的值。
附 i++ 和 ++i
1、对变量值的影响
[root@qfedu ~]# i=1
[root@qfedu ~]# let i++
[root@qfedu ~]# echo $i
2
[root@qfedu ~]# j=1
[root@qfedu ~]# let ++j
[root@qfedu ~]# echo $j
2
2、对表达式的值的影响
[root@qfedu ~]# unset i
[root@qfedu ~]# unset j
[root@qfedu ~]#
[root@qfedu ~]# i=1
[root@qfedu ~]# j=1
[root@qfedu ~]#
[root@qfedu ~]# let x=i++ # 先赋值,再运算
[root@qfedu ~]# let y=++j # 先运算,再赋值
[root@qfedu ~]#
[root@qfedu ~]# echo $i
2
[root@qfedu ~]# echo $j
2
[root@qfedu ~]# echo $x
1
[root@qfedu ~]# echo $y
2
# i++ 先赋值,后运算 ++i 先运算再赋值 两者对变量的值没有影响,对表达式的值有影响