初识shell

Shell定义

  1. 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.文件描述符

文件描述符文件名类型硬件
0stdin标准输入文件键盘
1stdout标准正确输出文件显示器
2stderr标准错误输出文件显示器

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
	登录的时候不需要输入用户
  1. /etc/profile
    • 全局(公有)配置,不管是哪个用户,登录时都会读取该文件。
  2. /etc/bashrc
    • 它也是全局(公有)的
    • bash 执行时,不管是何种方式,都会读取此文件
  3. ~/.profile
    • 若 bash 是以 login 方式执行时,读取 ~/.bash_profile,若它不存在,则读取 ~/.bash_login,若前两者不存在,读取 ~/.profile。

    • 图形模式登录时,此文件将被读取,即使存在 ~/.bash_profile 和 ~/.bash_login。

  4. ~/.bash_login
    • 若 bash 是以 login 方式执行时,读取 ~/.bash_profile,若它不存在,则读取 ~/.bash_login,若前两者不存在,读取 ~/.profile。
  5. ~/.bash_profile
    • 只有 bash 是以 login 形式执行时,才会读取此文件。通常该配置文件还会配置成去读取 ~/.bashrc。
  6. ~/.bashrc
    • 当 bash 是以 non-login 形式执行时,读取此文件。若是以 login 形式执行,则不会读取此文件。
  7. ~/.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、环境变量

设置永久生效的方法:放在四个用户登录脚本内部

定义环境变量:

  1. 方法一 export back_dir2=/home/backup
  2. 方法二 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 先运算再赋值   两者对变量的值没有影响,对表达式的值有影响