在 Linux 中,有多种 Shell 程序可供选择,比如 dash、csh、zsh 等,默认的 Shell 可以在 /bin/sh
查看,在etc/passwd
中修改。
cat /etc/shells
echo $SHELL
echo $0
在 Unix/Linux 里,一个程序/命令只做好一件事;复杂的问题可以通过多个命令的组合来解决;形式最简单的 Shell 脚本就是一系列命令构成的可执行文件,并可以被其他脚本复用。编写风格良好易读的 Shell 脚本可以提高日常任务的自动化程度和准确性。
可以在任意文本编辑器中打开新文件来创建 Shell 脚本。
高级编辑器如 Vim 和 Emacs,在识别文件的后缀为 .sh 后,可以提供语法高亮、检查、补全等功能。
[root@localhost~]# vim demo.sh #新建一个脚本文件,并写入如下内容:
#!/bin/bash
echo "Hello World“
[root@localhost~]# sh demo.sh
Hello World
Shell 脚本只是静态的代码,若要输出结果,还需要解释器的参与。一般在脚本的第一行,指定执行此脚本的解释器。如果不指定解释器,脚本也能在默认的解释器中正常运行,但出于规范和安全的考虑,建议指定如下:
#!/bin/bash
#!/bin/csh
对于脚本文件,我们有两种执行方式:
sh script_name.sh
./script_name.sh
Linux 中一切皆文件,脚本/命令/程序都是一个文件,文件作为一个对象,具有权限的属性。在执行别人发送或从网上下载的脚本时可能会遇到权限问题,赋予执行权限可解决:
chmod +x script_name.sh
如果某个 Shell 脚本可执行,则可以通过在命令行中输入其名称调用.被成功调用的前提是,脚本所在路径包含在 $PATH 变量中
[root@localhost~]# echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin
[root@localhost~]# PATH=$PATH:/New/path
[root@localhost ~]# echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin:/New/path
#永久修改 $PATH 需要在 .bashrc 或 .bash_profile 中添加 export PATH=“$$PATH:/new/path”
有时候一些脚本执行时间较长,命令行界面会被占用,因此可以采取后台运行脚本:
./my_script.sh &
这种方法在退出 Shell 后,脚本进程会随之终止,为了保证脚本一直运行,可以采用:
nohup ./my_script.sh &
脚本的的标准输出和标准错误会重定向到 nohup.out
文件里
使用 jobs 命令可以查看后台中运行着的进程。
文本流存在于 Linux 的每一个进程中。Linux 的每个进程启动时,会打开三个文本流的端口:标准输入、标准输出、标准错误。这三个端口对应着一个程序的输入、输出和异常的抛出。
例如:
在 bash 中输入一串字符后,bash 进程中的标准输入端口捕获命令行中的输入,进行处理后从标准输出端口中传出,回显在屏幕上,如果处理过程中发生异常,则会通过标准错误端口,将异常回显在屏幕上。
某些情况下,我们需要保存程序的输出,此时就可以通过重定向,将程序的输出保存到文件中
ls > dir_log
ls >> dir_log
cat dir_log
查看保存的文件内容与输出重定向类似,输入重定向是把程序的标准输入进行重新定向。
输入重定向:
command < inputfile
wc -l < /dev/null
内联输入重定向:
command << maker
command << maker >file,同时将输入的内容存成文件
有时需要将一个命令的输出连到另一个命令的输入,如果用重定向实现会较复杂。
管道( | )就像现实中的水管一样,可以连接两个命令的输入和输出,甚至是串联多个命令
格式:command1 | command2 | command3
[root@localhost ~]# ls /bin/ | grep python | less
管道实际上是进程间通信(IPC)的一种方式
和其他编程语言一样,Shell 也有一些保留字(特殊字符),在编写脚本时需要注意。
#!/bin/bash
;ls a[!0-9]
表示a后面不是紧接一个数字的文件;任何语言都有变量这个要素。Shell 与其他强类型的编程语言如:C,Java 和 C++ 等有很大不同,Shell 中的变量是无类型的。通过一个变量,我们可以引用一块内存区域的值,变量名就是这块内存区域上贴的一个标签。
在 Linux Shell 中,变量主要有两大类:环境变量和用户定义变量。每种类型的变量依据作用域不同,又分为全局变量和局部变量
查看变量
printenv
查看全局变量set
查看某个特定进程中的所有变量,包括局部变量、全局变量以及用户定义变量对比 printenv 与 set 的区别:dif -yw <(printenv) <(set)
命令set是Shell中的一个内建命令,它能够显示当前Shell中的变量,已经用户自定义的变量,不管该变量有没有export。set命令允许你更改Shell选项的值并设置位置参数,或者显示Shell变量的名称和值。如果未提供任何选项或参数,则会设置显示所有Shell变量和函数的名称和值(按照当前语言环境排序),并且输出的格式可以重新用作设置或重置当前设置变量的输入
printenv和env在环境变量打印方面是类似的。但是在功能上,env主要用于设置环境变量并运行指定的命令命令,而printenv是为了打印环境变量。
set则是一个Shell的内建命令,与Shell有关,用于设置Shell的属性。
修改变量
.bash_profile
或 .bashrc
中添加 export 语句,永久修改变量Shell 变量命名规则:
定义变量的格式:
variable=value
variable='value'
variable="value"
help | grep
在以下示例中,如果不使用花括号,Bash 会将$FIRST_$LAST
解释为变量 $FIRST_ 后跟变量 $LAST,而不是由_ 字符分隔的变量 $FIRST 和 $LAST。
因此,在此情况下,必须使用花括号引用的形式才能使变量扩展正确运行。
算术扩展可用于执行简单的整数算术运算
语法:$[表达式]
如:
[root@localhost ~]# echo $[1+9]
10
[root@localhost ~]# echo $[8*8]
64
[root@localhost ~]# count=1;echo $[$[$count+3]*2]
8
定义24小时,60分钟,60秒变量,定义每天秒数变量并赋值,输出变量
[root@localhost ~]# SEC_PER_MIN=60
[root@localhost ~]# MIN_PER_HR=60
[root@localhost ~]# HR_PER_DAY=24
[root@localhost ~]# SEC_PER_DAY=$[$SEC_PER_MIN*$MIN_PER_HR*$HR_PER_DAY]
[root@localhost ~]# echo "There are $SEC_PER_DAY seconds in a day"
There are 86400 seconds in a day
i++ 要开辟一个变量来保存 i 的值 并返回,然后让 i 这个变量 的值 +1 。而 ++i 直接把 1 加到 i 这个 变量的空间中去,并返回这个空间 中的值, 没有开辟任何临时空间,性能更高。
在 Shell 脚本里除了顺序执行,还需要一些额外的逻辑控制流程。
和其他编程语言类似, Shell 中的结构化命令主要包括条件、循环两类
Shell 中的 if 和其他 if 判断的条件不太一样,需注意。
If-then 语句
语法:
if command
then
commands
fi
Bash Shell 会先执行 if 后面的语句,如果其退出状态码为 0,则会继续执行 then 部分的命令,否则会执行脚本中的下一个命令。
在涉及多条件判断时,可能会使用较为繁琐的 if-then-else 语句,通过 elif 语句频繁检测同一个变量的值,此时更适合使用 case 语句
语法:
case variable in
pattern1 | pattern2) commands1;;
pattern3) commands2;;
*) default commands;;
esac
Shell 脚本中常会遇到一些重复任务,相当于循环执行一组命令直到满足了某个特定条件
常见的循环语句有三种:for、while、until
循环控制符有两种:break、continue 用于控制循环流程的转向
#Shell风格语法
for var in list
do
commands
done
#例:
for i in {1..10}
do
printf "$i\n"
done
#c语言风格
for ((var assignment ; condition ; iteration process))
do
commands
done
#例:
for (( i = 1; i < 10 ; i++))
do
echo “Hello”
done
do和done之间的命令称为循环体,执行次数和list列表中常数或字符串的个数相同。for循环,首先将in后list列表的第一个常数或字符串赋值给循环变量,然后执行循环体,以此执行list,最后执行done命令后的命令序列。
[root@localhost~]# for HOST in host1 host2 host3; do echo $HOST; done
host1
host2
host3
[root@localhost ~]# for HOST in host{1..3}; do echo $HOST; done
host1
host2
host3
[root@localhost~]# for EVEN in $(seq 2 2 8); do echo "$EVEN"; done;
2
4
6
8
定义奇数累加器文件,并执行
参考脚本:
#!/bin/bash
sum=0
for i in {1..100..2}
do
let "sum+=i"
done
echo "sum=$sum"
展示目录下所有文件
参考脚本:
#!/bin/bash
for file in $(ls)
do
echo "file: $file“
done
也称为前测试循环语句,重复次数是利用一个条件来控制是否继续重复执行这个语句。为避免死循环,必须保证循环体中包含循环出口条件
#!/bin/bash
sum=0;i=1
while(( i <= 100 ))
do
let "sum+=i"
let "i += 2"
done
echo "sum=$sum"
与 while 循环类似,但是跳出循环的条件判断有所区别
#!/bin/bash
#奇数累加器
sum=0;i=1
until(( i > 100 ))
do
let "sum+=i"
let "i += 2"
done
echo "sum=$sum"
参考脚本:
#!/bin/bash
for (( i = 1; i <=9; i++ ))do
for (( j=1; j <= i; j++ ))do
let "temp = i * j"
echo -n "$i*$j=$temp "
done
printf “\n”
done
编写、使用或维护 Shell 脚本的管理员不可避免地会遇到脚本的错误
脚本上激活调试模式,请向脚本第一行中的命令解释器中添加 -x 选项
如此前乘法表,进行如下修改:
#!/bin/bash -x
bash 的调试模式将打印出脚本执行前由脚本执行的命令,已执行的所有 shell 扩展的结果都将显示在打印输出中,所有变量数据状态会实时打印,以供跟踪:
bash -x adder.sh
[root@localhost tmp]# ls /etc/hosts
/etc/hosts
[root@localhost tmp]# echo $?
0
----------------------------------------------------------------
[root@localhosttmp]# ls /etc/nofile
ls: cannot access /etc/nofile: No such file or directory
[root@localhost tmp]# echo $?
2
[root@localhosttmp]# cat hello.sh
#!/bin/bash
echo "Hello World"
exit 1
[root@localhosttmp]# ./hello.sh
Hello World
[root@localhost tmp]# echo $?
1
以下是要遵循的一些具体做法:
修改后的脚本:
#!/bin/bash
# 此脚本是读取关于kernel相关的软件包信息,并从RPM数据库中查询软件包的安装时间
PACKAGETYPE=kernel
PACKAGES=$(rpm -qa | grep $PACKAGETYPE)
# 循环处理信息
for PACKAGE in $PACKAGES; do
#查询每个软件包的安装时间截
INSTALLEPOCH=$(rpm -q --qf "%{INSTALLTIME}\n" $PACKAGE)
# 把时间截转换为普通的日期时间
INSTALLDATETIME=$(date -d @$INSTALLEPOCH)
# 打印信息
echo "$PACKAGE was installed on $INSTALLDATETIME"
done
场景描述:
某运营商要求对当前环境日志文件“error.log”进行长期监测,如发现关键词“danger”,则发送告警邮件
要求:
#!/bin/bash
# 日志检测脚本 test.sh
tail -f /var/log/error.log | while read danger;
do echo ‘mail’ >> /var/log/error.log;
sleep 1m;
done
#!/bin/bash
# 模拟报错脚本 addlog.sh
echo ‘danger’ >> /var/log/error.log
./test.sh &
./addlog.sh
cat /var/log/error.log