(一)Shell 编程概述
1.1 shell 简述
Shell 编程和 JavaScript 非常相似, Shell 和 JavaScript 都是弱类型语言, 同时也都是解释型语言解释型语言需要解释器, JavaScript 的解释器是浏览器, Shell 脚本的解释器时 bash, 是一个 shell 一个命令行用户接口
1.2 bash 简述
bash 在执行或者解释脚本的时候, 此 bash 非彼 bash 用户登录进来的时候就用一个 bash 通过敲一个命令来解释脚本的时候, 是在当前 bash 中打开一个新的 bash, 这两个 bash 是父子关系其实在空行敲回车也是从当前 bash 打开新的 bash
在命令行中输入 $ps -aux |grep bash 可以看见当前只有一个 bash 这个 bash 就是系统和用户进行交互的一个用户接口
在命令行中输入命令 bash 回车这就是打开了一个新的 bash 再次输入 ps -aux |grep bash 可以看见当前有两个 bash 因为敲一个命令就是一个程序的入口, 这个命令就被启动, 这个程序就是 bash 这两个 bash 就是父子关系这种有层次结构的 bash 可以使用 exit 退出
(二)变量
2.1 变量的类型
shell 脚本的变量实际上也是 bash 的变量, 共有四种类型: 环境变量本地变量 (或称局部变量, 但略有差别) 位置变量特殊变量环境变量, 作用在当前 bash 和所有子 bash, 与本地变量的区别在于作用域不同; 本地变量(局部变量, 当前代码段), 作用在当前 bash, 所有子 bash 都不能用; 位置变量和特殊变量, 是 bash 内置的用来保存某些特殊数据的变量, 也不存在作用域的问题(也叫系统变量)
2.2 环境变量
环境变量,$export 变量名 = 值, 其作用域为当前的 shell 和其子 shell
注意: 脚本在执行时都会启动一个子 shell 进程, 命令行中启动的脚本会继承当前 shell 环境变量; 系统自动启动脚本(非命令行启动), 则需要自我定义环境变量
2.3 本地变量(局部变量)
本地变量是只属于某一个 bash 的变量例如,$var_name = 值, 其作用域是整个 bash 进程
局部变量类似, 例如 $local var_name = 值, 其作用域为当前代码段
2.4 位置变量
位置变量是指用于脚本执行的参数,$1 表示第一个参数, 以此类推 $1,$2
2.5 特殊变量
特殊变量是每一个 bash 进程中特有的一些变量, 无需声明总共就几个,$? 和 $#j 较为常用
2.5.1 $?
$? 表示上一个命令的执行状态返回值事实上, 任何一个程序执行的结果只有两类, 即程序有两类返回值:
第一类是命令的执行结果例如输入 $ls 显示的目录下的所有目录和文件就是执行结果, 再如输入 $id -u root 看见的 0 就是执行结果
第二类是命令的执行状态任何命令的执行状态都被 $? 这个变量存储起来 $?: 0 表示正确, 1-255 表示错误例如使用 $echo $? 查看上一个命令的执行状态, 打印上一个命令的执行状态 0 表示正确, 非 0 表示错误注意不能查看上上个命令的执行状态, 因为被覆盖了
- 2.5.2 $#
- $# 表示传递到脚本的参数个数
- 2.5.3 $*
$* 表示传递到脚本的参数, 与位置变量不同, 此选项参数可超过 9 个
2.5.4 $$
$$ 表示脚本运行时当前进程的 ID 号, 常用作临时变量的后缀
2.5.5 $!
$! 表示后台运行的 (&) 最后一个进程的 ID 号
2.5.6 $@
$@ 与 $# 相同, 使用时加引号, 并在引号中返回参数个数
2.5.7 $-
$- 表示上一个命令的最后一个参数
2.6 变量的声明撤销查看与引用
2.6.1 声明变量
环境变量的声明必须加 export; 本地变量和局部变量的声明不需写 export, 直接写变量名后面接值即可
2.6.2 撤销变量
使用 unset 变量名, 撤销变量
2.6.3 查看变量
查看 shell 中变量: set
查看 shell 中的环境变量: printenv 或 env
2.6.4 引用变量
a) 引用变量
引用变量格式为:${变量名}, 一般可以省略 {} 只有特殊情况下不能省略, 具体效果见下图:
b) 单引号: 强引用
单引号: 强引用, 不作变量替换, 引用字符串常量(单引号的内容都是字符串)
c) 双引号: 弱引用
双引号: 弱引用, 做变量替换
d) 反引号: 命令替换
反引号:`` 命令替换当字符串表示一个命令, 需要执行时, 则要用反引号
(三)输出重定向
3.1 命令执行结果保存在一个文件中
3.1.1 >覆盖重定向
$ls >/path/file 不会重定向错误结果
3.1.2 >> 追加重定向
$cat file1 file2 >> file3 // 将 file1 和 file2 的文件内容追加重定向到 file3 后面
3.1.3 2> 错误覆盖重定向
程序执行出错的结果放到文件中
3.1.4 2>>错误追加重定向
程序执行出错的结果放到文件中
3.1.5 &> 全部覆盖重定向
无论命令执行对错, 都会覆盖重定向到文件
3.1.6 &>> 全部追加重定向
无论命令执行对错, 都会追加重定向到文件
3.2 命令执行结果直接丢弃
/dev/null 文件, dev 是设备, null 是一个设备文件, 称之为数据黑洞, 所有数据放到这里都无法恢复
$ls >> /dev/null
(四) 脚本
通过组织命令及变量来完成具有某种业务逻辑的功能称之为脚本
4.1 简单脚本案例
4.1.1 案例一(添加用户)
a) 业务描述
添加 6 个用户, 每个用户的密码同用户名, 不显示添加密码的信息, 并给显示添加用户成功信息
b) 编写脚本
首先, 创建脚本
- mkdir /opt/shell
- cd /opt/shell
- vim test1.sh
其次, 编写脚本主体
- #!/bin/bash# 脚本的第一行一定是这个脚本的声明, 这里声明的是脚本的解释器
- # 用户可以有参数传过来, 也可以直接定义一个变量
- U=user1 #声明变量, 本地变量的声明直接变量名 = 值, 无需 export 这里选择单引号, 因为 user1 是个字
符串, 只需强引就行, 双引号也行
- useradd $U #引用变量
- # 设置密码和用户名相同, 但是设置完之后不显示 passwd 的执行结果但是这个 passwd 会在控制台出现信息
, 所以需要使用管道传入数据, 并且重定向
- echo "$U" | passwd --stdin $U &>/dev/null #前一个命令的输出传给后一个命令的输入
- echo "success." #打印成功信息
以上是添加 1 个给定名称的用户, 可以稍作修改, 变为参数传入的方式动态添加用户:
- cp test1.sh test2.sh
- vim test2.sh
- #!/bin/bash
- #
- useradd $1 #这里是使用传递参数的方式, 1 代表 1 个参数, 实际上 $1 是特殊变量 $* 的特例
- echo "$1" | passwd --stdin $1 &>/dev/null
- echo "Add user $1 success."
c) 执行脚本
这个两个脚本当前没有执行权限有两种方法使该脚本有执行权限: 一是添加执行权限; 二是, 使用命令 sh /path / 脚本名(sh 是一个执行脚本的命令)
脚本 1 的执行:
$sh test1.sh
脚本 2 的执行:
$sh test2.sh username
查看是否添加成功:
$cat /etc/passwd
d) 查看脚本执行程度
$bash -x /opt/shell/test2.sh
4.1.2 案例二(删除用户)
a) 业务描述
写一个脚本, 完成以下任务: 第一, 使用一个变量保存一个用户名; 第二, 删除此变量中的用户, 且一并删除其家目录; 第三, 显示用户删除成功信息
b) 编写脚本
首先, 创建脚本
vim /opt/shell/test3.sh
其次, 编写脚本主体
- #!/bin/bash
- U=$* #使用一个变量存储用户名
- userdel $* #删除这个用户
- rm -rf /home/$* #删除该用户的家目录
- echo "delete user and $*home success." #打印成功信息
c) 执行脚本
- sh test3.sh username #注意一定要有参数(要删除的用户), 不然会删除整个 / home 目录
- cat /etc/passwd #查看用户是否删除成功
- ls /home #查看用户的家目录是否删除成功
4.2 条件判断
条件判断是布尔类型, 而 shell 是弱类型的语言, 也即没有类型, 所以表达式只有真或假
4.2.1 条件表达式
条件表达式有两种形式: 一是[ expression ], 注意中括号和表达式之间一定要加空格隔开; 二是, test expression,test + 空格 + 表达式
4.2.2 整数的比较
=:-eq 等于
!=:-ne 不等于
>:-gt 大于
<:-lt 小于
>=:-ge 大于或等于
<=:-le 小于或等于
4.2.3 逻辑运算
a) 命令的逻辑关系
在 linux 中, 命令执行状态: 0 为真, 其他为假
b) 逻辑与或非
可以使用离散数学的逻辑与或非来进行逻辑判断
逻辑与:&& 当第一个条件为假时, 第二条件不用再判断, 最终结果已经有; 当第一个条件为真时, 第二条件必须得判断
逻辑或:||
逻辑非:!
c) 案例(命令逻辑关系的条件判断)
业务描述: 添加用户前先判断是否存在, 如果存在就打印该用户已存在, 并且退出
编写脚本:
- vim /opt/shell/test4.sh
- #!/bin/bash
- #
- id $1 &>/dev/null && echo "User $1 exist" && exit 3 #使用命令的执行状态作为逻辑判断 id $1 如果用户 $1 存在, 则为 0(真), 需要继续判断后面的, 所以执行 echo, 并且退出脚本, 不再
创建该用户退出, 3 是非 0, 当前脚本执行不成功
- # 上一行可以使用逻辑或的代替, 见下两行
- #!id $1 &>/dev/null || echo "User $1 exist" #如果用户存在 id $1 则为 0, 前面加了! 就是非 0, 就为假前面为假, 对于逻辑或来说需要继续判断后面的, 所以执行 echo 打印用户存在
- #id $1 &>/dev/null && exit 3 #用户存在就退出
- useradd $1 #添加用户
- id $1 &>/dev/null && echo "$1" | passwd --stdin $1 &>/dev/null #如果用户创建成功了>, 就执行后面的 echo passwd 添加密码
- echo "Add user $1 success."
执行脚本:
sh test4.sh user1
4.2.4 if 条件判断
a) 语法结构
if 条件表达式的语法结构为:
if [ 条件 ]; then
语句
elif [ 条件 ]; then
语句
else
语句
fi
b) if 的逻辑与或
-a : 并且
-o : 或者
例如:
if [ $# -gt 1 -a $# -lt 3 -o $# -eq 2 ] ; then
如果, 上一个命令传入的参数个数大于 1, 并且小于 3, 或者等于 2&& 是离散数学中的逻辑与,-a 是一个逻辑运算符, 并且的意思, 不同于 &&
c) 案例(if 语句)
业务描述: 给定一个用户, 如果他的 UID 为 0 则显示为管理员, 否则显示其他普通用户
编写脚本:
- vim /opt/shell/test5.sh
- #!/bin/bash
- USER_ID=`id -u $1`&> /dev/null || exit 3 #注意这里需要用反引号, 这里也不能将变量名取为 UID, 会和环境变量冲突, 这样声明的变量是只读的
- if [ $USER_ID -eq 0 ] ; then
- echo "admin."
- else
- echo "other."
- fi
执行脚本:
sh test5.sh root
4.3 算术运算符
4.3.1 算术运算符的类型
a) let 算术运算表达式
let C=$A + $B
b) $[算术表达式]
- C = $[$A+$B]
- c) $((算术表达式))
- C=$(($A+$B))
d) expr 算术表达式
注意: 表达式中各操作数及运算符之间要有空格而且要使用命令引用
C=`expr $A + $B`
4.3.2 案例(算术运算符的使用)
a) 业务描述
给定一个用户, 获取其密码警告期限, 然后判断用户密码使用期限是否已经小于警告期限, 如果小于, 则是显示 WARN, 否则显示密码还有多少天到期
密码警告期限: 距离警告的天数; 密码使用期限: 密码有效期减去使用的时间
b) 编写脚本
首先, 需要知道:
$date +%s 可以获得今天的秒数
$cat /etc/shadow 可以获得密码使用时间
cat /etc/shadow | grep root
观察上图, 这一行数据是以冒号隔开的其中的 17021 是指, 创建密码的那天距离 1970 年 1 月 1 日的天数; 0 表示密码有效期最短 0 天; 99999 表示密码最多 99999 天有效; 7 表示有效期还剩 7 天时警告
然后, 编辑脚本如下:
- vim /opt/shell/test6.sh
- #!/bin/bash
- #
- UN=$1 #根据传入的数据作为变量 UN 的值
- C_D=`grep "^$UN" /etc/shadow | awk -F: '{print $3}' ` #grep "^$UN" /etc/shadow 是从 shadow 中找到到以 $UN 这个用户名开头的, print $3 是密码的创建时间, 这里使用 - F 来指定: 分隔符加反引号是一个命令
- M_D=`grep "^$UN" /etc/shadow | awk -F: '{print $5}' ` #'{print $5}'是找到最长有效期
- W_D=`grep "^$UN" /etc/shadow | awk -F: '{print $6}'` #'{print $6}'是警告时间
- NOW_D=$[`date +%s`/86400] #`date +%s` 获得当前系统时间 (秒), 使用数学运算符[] 相当于数>学的括号, 除以一天的秒数
- U_D=$[$NOW_D-$C_D] #算出了使用的天数
- L_D=$[$M_D-$U_D] #算出剩余的天数
- if [ $L_D -le $W_D ];then #判断注意空格, 剩余时间小于等于警告时间输出 warn.
- echo "warn."
- else
- echo "left day is $L_D" #其他情况输出剩余天数
- fi
c) 执行脚本
sh test6.sh root
4.4 文件与字符串测试
4.4.1 文件与字符串测试语法
a) 文件测试
文件测试需要中括号 [ ]
-e FILE: 测试文件是否存在
-f FILE: 测试文件是否为普通文件
-d FILE: 测试文件是否为目录
-r 当前用户有没有读的权限
-w 当前用户有没有写的权限
-x 当前用户有没有执行的权限
b) 字符串测试
== 等号两端需要空格
!=
-n string : 判断字符串是否为空
-s string : 判断字符串是否不空
4.4.2 案例(文件与字符串测试)
a) 业务描述
指定一个用户名, 判断此用户的用户名和它的基本组, 组名是否相同
b) 编写脚本
- vim /opt/shell/test7.sh
- #!/bin/bash
- #
- if !id $1 &>/dev/null ; then
- echo "No such user."
- exit 12
- fi
- if [ $1 == `id -n -g $1` ] ;then #`id -n -g $1` 是指输入的用户的组的名称(-g 是组 id)
- echo "相同"
- else
- echo "不相同"
- fi
c) 执行脚本
sh test7.sh root
4.5 循环语句
4.5.1 for 循环
a) for 循环的语法结构
for 变量 in 列表; do
语句
done
b) for 循环中的列表
for 循环中的列表尤为重要, 列表可以是以空格隔开的一组数, 可以理解为数组; 也可以动态生成列表
静态数组列表
以下是个静态列表的实例:
for I in 1 2 3 4 5 ; do
语句
done
动态生成列表
动态生成列表有三种方式, 分别为:
第一,{1..100}中间两个点, 1-10 可以成{1..10}
第二, seq [起始数] [跨度数] 结束数 seq 相当于 oracle 中 sequence, 递增的序列,[起始数]带有中括号意为可以不给, 默认为 1;[跨度数]带有中括号也可以不给, 默认值为 1; 结束数必须给, 不然会造成死循环
第三, ls /PATH/ 文件列表这个实际上是通过 for 循环递归目录下的文件
c) for 循环案例一
业务描述:
将那些可以登录的用户查询出来, 并且将用户的帐号信息提取出来, 后放入 / tmp/test.txt 文件中, 并在行首给定行号
编写脚本:
- vim /opt/shell/test8.sh
- #!/bin/bash
- #
- J=1
- count=`wc -l /etc/passwd | cut -d'' -f1` #wc -l /etc/passwd 可以获得 passwd 文件的行数; cut -d' ' -f1 中 - d 是指定切割符,-f1 是取第一个
- #count=`wc -l /etc/passwd | awk -F ""'{print $1}'` #也可以使用 awk
- for I in `seq $count`;do #这里使用 seq 生成列表, 以 1 开始, 跨度为 1, 到 $count 结束
- u_shell=`head -$I /etc/passwd | tail -1 | cut -d: -f7` #head 取前 I 行, 然后管道到 tail -1 拿到最后一行, 实际上就是第 I 行, 然后管道到 cut 按: 切割, 取第七个(shell)
- u_n=`head -$I /etc/passwd | tail -1 | cut -d: -f1` #取用户
- if [ $u_shell == '/bin/bash' ];then #判断字符串是否相等, 相等表示可以登录可以登录>就将 / tmp/users.txt
- echo "$J $u_n" >> /tmp/users.txt #输出时加上编号, 这里不能使用 I, 因为 I 是行号, 业务要求是新的序号, 需要连续的
- J=$[$J+1] #相当于 J++
- fi
- done
执行脚本:
sh test8.sh root
d) for 循环案例二
业务描述:
计算 100 以内所有能被 3 整除的整数的和
编写脚本:
- vim test9.sh
- #!/bin/bash
- #
- sum=0
- for I in {1..100};do
- S=$[$I%3] #对 3 取模(求余数)
- if [ $S -eq 0 ];then #数字比较只能用 - eq, 不能使用等号
- sum=$[$sum+$I]
- fi
- done
- echo "$sum"
执行脚本:
sh test9.sh
e) for 循环案例三
业务描述:
传给脚本一个参数: 目录, 输出该目录中文件最大的, 文件名和文件大小
编写脚本:
- vim test10.sh
- #!/bin/bash
- #
- # 首先判断参数是否正确
- if [ -f $1 ];then #-f 判断传过来的目录是否是文件
- echo "Arg is error."
- exit 2
- fi
- if [ -d $1 ];then #-d 是判断 $1 是目录
- c=`du -a $1 | sort -nr | wc -l` #du -a $1 是列出 $1 目录下的所有文件和目录; sort -nr 是按数值降序排列; wc -l 获取行数但是这样把目录页选出来了
- for I in `seq $c`;do
- f_size=`du -a $1 | sort -nr | head -$I | tail -1 | awk '{print $1}'` #取到第 I 行, 拿到文件大小
- f_path=`du -a $1 | sort -nr | head -$I | tail -1 | awk '{print $2}'` #取到第 I 行, 拿到文件路径
- if [ -f $f_path ];then #判 $f_path 是否是文件, 是文件就直接输出, 并退出循环
- echo -e "the biggest file is $f_path \t $f_size"
- break
- fi
- done
- fi
执行脚本:
sh test10.sh /path/
4.5.2 while 循环
a) 格式一
while 条件; do
语句
[break]
done
b) 格式二(死循环)
while true
do
语句
done
c) 格式三(死循环)
while :
do
语句
done
d) 格式四(死循环)
while [ 1 ]
do
语句
done
e) 格式五(死循环)
while [ 0 ]
do
语句
done
f) while 循环案例
业务描述:
使用 echo 输出 10 个随机数
编写脚本:
- vim /opt/shell/test11.sh
- #!/bin/bash
- #
- I=1
- while [ $I -le 10 ] ; do
- echo -n "$RANDOM"
- I=$[$I+1]
- done
执行脚本:
sh test11.sh
来源: https://www.cnblogs.com/yangp/p/8511321.html