hashub

hashub

Shell

超哥 入门

推荐阅读
https://effective-shell.com

一、基础#

shell 的作用是

  • 解释执行用户输入的命令或程序等
  • 用户输入一条命令,shell 就解释一条
  • 键盘输入命令,Linux 给与响应的方式,称之为交互式

shell 的作用_1
shell 的作用_1

shell 是一块包裹着系统核心的壳,处于操作系统的最外层,与用户直接对话,把用户的输入,解释给操作系统,然后处理操作系统的输出结果,输出到屏幕给与用户看到结果。

从我们登录 Linux,输入账号密码到进入 Linux 交互式界面,所有的操作,都是交给 shell 解释并执行

shell 的作用_2
image

1.1 什么是 shell 脚本#

当命令或者程序语句写在文件中,我们执行文件,读取其中的代码,这个程序文件就称之为 shell 脚本。

在 shell 脚本里定义多条 Linux 命令以及循环控制语句,然后将这些 Linux 命令一次性执行完毕,执行脚本文件的方式称之为,非交互式方式。

windows 中存在*.bat 批处理脚本

Linux 中常用*.sh 脚本文件

shell 脚本规则

在 Linux 系统中,shell 脚本或者称之为 (bash shell 程序)通常都是 vim 编辑,由 Linux 命令、bash shell 指令、逻辑控制语句 和 注释信息组成。

Shebang#

计算机程序中,shebang 指的是出现在文本文件的第一行前两个字符 #!

在 Unix 系统中,程序会分析 shebang 后面的内容,作为解释器的指令,例如

  • #! /bin/sh开头的文件,程序在执行的时候会调用 /bin/sh ,也就是 bash 解释器
  • #!/usr/bin/python开头的文件,代表指定 python 解释器去执行
  • #!/usr/bin/env 解释器名称,是一种在不同平台上都能正确找到解释器的办法

注意事项:

  • 如果脚本未指定 shebang ,脚本执行的时候,默认用当前 shell 去解释脚本,即$SHELL
  • 如果 shebang 指定了可执行的解释器,如/bin/bash /usr/bin/python ,脚本在执行时,文件名会作为参数传递给解释器
  • 如果#!指定的解释程序没有可执行权限,则会报错 "bad interpreter: Permission denied"
  • 如果#!指定的解释程序不是一个可执行文件,那么指定的解释程序会被忽略,转而交给当前的 SHELL 去执行这个脚本。
  • 如果#!指定的解释程序不存在,那么会报错 "bad interpreter: No such file or directory"
  • #!之后的解释程序,需要写其绝对路径 (如︰#!/bin/bash),它是不会自动到$PATH中寻找解释器的
  • 如果你使用bash test.sh这样的命令来执行脚本,那么#!这一行将会被忽略掉,解释器当然是用命令行中显式指定的 bash
#! /bin/bash

# Date: xxx
# Author: xxx
# Blog: xxx

shell 脚本语言很适合处理纯文本类型数据,且 Linux 的哲学思想就是一切皆文件,如日志、配置文件、文本、网页文件,大多数都是纯文本类型的,因此 shell 可以方便的进行文本处理,好比强大的 Linux 三剑客 (grep、sed、awk)

脚本语言#

shell 脚本语言属于一种 弱类型语言 无需声明变量类型,直接定义使用

强类型语言,必须先定义变量类型,确定是数字、字符串等,之后再赋予同类型的值

# Linux 默认 shell
[root@localhost ~]# echo $SHELL
/bin/bash

# Centos7 支持的shell
[root@localhost ~]# cat /etc/shells
/bin/sh
/bin/bash
/usr/bin/sh
/usr/bin/bash
/bin/tcsh
/bin/csh

默认的 sh 解释器

[root@localhost ~]# ll /usr/bin/sh
lrwxrwxrwx. 1 root root 4 Jul 18 19:54 /usr/bin/sh -> bash

1.2 bash#

历史命令

[root@localhost ~]# echo $HISTSIZE
1000

[root@localhost ~]# echo $HISTFILE
/root/.bash_history


history
-c 清除历史
-r $HISTFILE 恢复历史

[root@localhost ~]# history -c
[root@localhost ~]# history
    1  history
[root@localhost ~]# history -r ~/.bash_history


!历史ID	# 快速执行历史命令
!!		 # 执行上次的命令

bash 特性

  • 文件路径 tab 键补全
  • 命令补全
  • 快捷键ctrl + a, e, u, k, l
  • 通配符
  • 命令历史
  • 命令别名
  • 命令行展开

1.3 shell 变量#

变量定义与赋值,注意 == 变量与值之间不得有空格 ==

  • 变量类型,bash 默认把所有变量都认为是字符串
  • bash 变量是弱类型,无需事先声明类型,是将声明和赋值同时进行

变量替换 / 引用

name="This is test!"
echo ${name}
echi $name # 可以省略花括号

单引号变量,不识别特殊语法

双引号变量,能识别特殊符号

变量名规则

  • 名称定义要做到见名知意,切按照规则来,切不得引用保留关键字 (help 检查保留字)
  • 只能包含数字、字母、下划线
  • 不能以数字开头
  • 不能用标点符号
  • 变量名严格区分大小写

变量的作用域

  • 本地变量,只针对当前的 shell 进程
pstree 检查进程树

环境变量:也称为全局变量,针对当前 shell 以及其任意子进程,环境变量也分自定义、内置两种环境变量

局部变量:针对在 shell 函数 或是 shell 脚本 中定义

位置参数变量:用于 shell 脚本中传递的参数

特殊变量:shell 内置的特殊功效变量

  • $?
    • 0 : 成功
    • 1 - 255 : 错误码

自定义变量

  • 变量赋值:varName=value
  • 变量引用:${varName} , $varName
  • "" ,变量名会替换变量值

单引号变量,不识别特殊语法

双引号变量,能识别特殊符号

1. 每次调用 bash/sh 解释器执行脚本,都会开启一个子 shell,因此不保留当前的 shell 变量,通过 pstree 命 令检查进程树

2. 调用 source 或者点符号,在当前 shell 环境加载脚本,因此保留变量

# 1. 开启子 shell 的执行方式
cat make_vars.sh
name="test"

echo $name
aa

bash make_vars.sh

echo $name
aa

# 2. 不开启子 shell 的执行方式

`Linux命令`
反引号

echo dir=`ls`
echo $dir
xxxx  xxx 

shell 变量面试题

cat test.sh
user1=`whoami`

sh test.sh

echo $user1
B

A. 当前用户
B. 空

1.4 环境变量设置#

环境变量一般指的是用 export 内置命令导出的变量,用于定义 shell 的运行环境、保证 shell 命令的正确执行

shell 通过环境变量确定登录的用户名、PATH 路径、文件系统等各种应用

环境变量可以在命令行中临时创建,但是用户退出 shell 终端,变量即丢失,如要永久生效,需要修改环境变量配置文件

  • 用户个人配置文件~/.bash_profile ~/.bashrc远程登录用户特有文件
  • 全局配置文件/etc/profile/etc/bashrc ,且系统建议最好创建在/etc/profile.d/而非直接修改主文件,修改全局配置文件,影响所有登录系统的用户

检查系统环境变量的命令

  • set : 当前 shell 中的所有变量,包括全局变量、局部变量,包含函数
  • env : 只显示全局变量
  • declare : 输出所有的变量,如同 set
  • export : 显示和设置环境变量值

撤销环境变量

  • unset 变量名,删除变量或函数

设置只读变量

  • readonly ,只有 shell 结束,只读变量失效

系统保留环境变量关键字

bash 内嵌了诸多环境变量,用于定义 bash 的工作环境

export | awk -F '[ :=]' '{print $3}'

bash 多命令执行

ls /data/;cd /tmp/;cd /home;cd /data

环境变量初始化与加载顺序

ssh 登录 linux后,系统启动一个bash shell

/etc/profile : 全局环境变量文件,并从/etc/profile.d 目录的配置文件中搜集shell的设置

读取 /etc/profile.d 目录下的脚本

运行 $HOME/.bash_profile (用户环境变量文件)

运行 $HOME/.bashrc

运行 /etc/bashrc

1.5 特殊变量#

特殊参数变量

shell 的特殊变量,用在如脚本,函数传递参数使用,有如下特殊的,位置参数变量

$0	获取shell脚本文件名,以及脚本路径
$n	获取shell脚本的第n个参数,n 在 1~9 之间,如$1, $2$9 ,大于9则需要写,${10},参数空格隔开
$#	获取执行的shell脚本后面的参数总个数
$*	获取shell脚本所有参数,不加引号等同于 $@ 作用,加上引号 "$*" 作用是接收所有参数为单个字符串,"$1 $2...."
$@	不加引号,效果同上,加引号,是接收所有参数为独立字符串,如"$1” “$2” “$3” ..., 空格保留

for_variables.sh

#! /bin/bash

for var in "$*"
do
	echo "$var"
done

for var in "$@"
	echo "$var"
done


bash for_variables.sh test ha 1 2 3 4 

特殊状态变量

$?	上一次命令执行状态返回值,0正确,非0失败
$$	当前shell脚本的进程号
$!	上一次后台进程的PID
$_	再次之前执行的命令,最后一个参数

man bash  搜索 Special Parameters
echo
-n	不换行输出
-e	解析字符串中的特殊字符

\n 换行
\r 回车
\t 制表
\b 退格

printf
eval
执行多个命令
eval ls;cd /tmp
exec
不创建子进程,执行后续命令,执行完毕后,自动exit
expr
expr 函数名

1.6 shell 子串#

${变量}				返回变量值
${#变量}				返回变量长度,字符长度
${变量:Offset}		返回变量 Offset 数值之后的字符
${变量:Offset:length}	提取 Offset 之后的 length 限制的字符

# 删除
${变量#word}			从变量开头,删除最短匹配的 word 子串
${变量##word}			从变量开头,删除最长匹配的 word
${变量%word}			从变量结尾删除最短的 word
${变量%%word}			从变量结尾开始删除最长匹配的 word

# 替换
${变量/pattern/string}用 string 代替第一个匹配的 pattern
${变量//pattern/string}用 string 代替所有的 pattern


# #指定字符内容截取
a*c 匹配开头为a,中间任意个字符,结尾为c的字符串 (大小写敏感)
seq -s ":" 10
time for n in {1...10000};do char=`seq -s "testtt" 100`;echo ${#char} &>/dev/null;done

time for n in {1...10000};do char=`seq -s "testtt" 100`;echo ${char}|wc -L &>/dev/null;done

time for n in {1...10000};do char=`seq -s "testtt" 100`;expr length "${char}" &>/dev/null;done

time for n in {1...10000};do char=`seq -s "testtt" 100`;echo ${char}|awk "{print length($0)}" &>/dev/null;done

shell 编程,尽量使用 Linux 内置的命令,内置的操作,和内置的函数,效率最高

尽可能的减少管道符的操作

批量修改文件名

for file_name in `ls *.jpg`;do mv $file_name `echo ${file_name//_finished/}` done;

1.7 shell 扩展变量#

${parameter:-word}	如果parameter变量值为空,返回word字符串

${parameter:=word}	如果parameter变量为空,则word替代变量值,且返回其值

${parameter:?word}	如果parameter变量为空,word当作stderr输出,否则输出变量值。用于设置变量为空导致错误时,返回的错误信息

${parameter:+word}	如果parameter变量为空,什么都不做,否则word返回

stdout 1

stderr 2

应用:数据备份,删除过期数据

find xargs
# 删除 7 天以上过期数据
find path -name "" -type "文件类型" -mtime +7 | xargs rm -f

dir_path="/data/my_data"
find ${dir_path} -name '*.tar.gz' -type f -mtime +7 | xargs rm -f

find ${dir_path:=/data/my_data} -name '*.tar.gz' -type f -mtime +7 | xargs rm -f

1.8 父子 shell#

父子 shell_1
父子 shell_1

pstree

ps -ef
-e 列出所有进程的信息,如同 -A 选项
-f 显示 UID,PID,PPID

ps -ef --forest

父子 shell_2
父子 shell_2

创建进程列表 (创建子 shell)

# shell 进程列表
(cd ~; pwd; ls; cd /tmp; ls)

检测是否在子 shell 环境中

# 如果是0,就是在当前shell环境中执行的,否则就是开辟子shell去运行的
echo $BASH_SUBSHELL

(cd ~; echo $BASH_SUBSHELL)

子 shell 嵌套

(pwd; echo $BASH_SUBSHELL)

(pwd; (echo $BASH_SUBSHELL))

利用括号,开启 子 shell 的理念,在 shell 脚本开发中,经常会用 子 shell 进行 多进程 的处理提高程序并发执行效率

1.9 内置命令、外置命令#

内置命令:在系统启动时就加载入内存,常驻内存,执行效率更高,但是占用资源

外置命令:用户需要从硬盘中读取程序文件,再读入内存加载

type cd

type ls

外置命令 特点:一定会开启子进程执行

ps -f --forest

内置命令 是 shell 的一部分,不需要单独去某个文件,系统启动后,便被加载到内存中

# 查看所有内置命令
compgen -b

总结#

echo "username: ${USER}"
echo "UID: $UID"
echo "User home: " $HOME
${}		取出变量值
$()		在括号中执行命令,得到执行结果
``		同上
()		开启子 shell,执行
$vars	取出变量值

二、shell 开发#

2.1 数学运算#

Shell 算术运算符

算术运算符说明 / 含义
+、-加法(或正号)、减法(或负号)
*、/、%乘法、除法、取余(取模)
**幂运算
++、--自增和自减,可以放在变量的前面也可以放在变量的后面
!、&&、||逻辑非(取反)、逻辑与(and)、逻辑或(or)
<、<=、>、>=比较符号(小于、小于等于、大于、大于等于)
==、!=、=比较符号(相等、不相等;对于字符串,= 也可以表示相当于)
<<、>>向左移位、向右移位
~、|、 &、^按位取反、按位或、按位与、按位异或
=、+=、-=、*=、/=、%=赋值运算符,例如 a+=1 相当于 a=a+1,a-=1 相当于 a=a-1

Shell 中常用的六种数学计算方式

运算操作符 / 运算命令说明
(( ))用于整数运算,效率很高,推荐使用。语法层面
let用于整数运算,和 (()) 类似。
$[]用于整数运算,不如 (()) 灵活。
expr可用于整数运算,也可以处理字符串。比较麻烦,需要注意各种细节,不推荐使用。
bcLinux 下的一个计算器程序,可以处理整数和小数。Shell 本身只支持整数运算,想计算小数就得使用 bc 这个外部的计算器。
declare -i将变量定义为整数,然后再进行数学运算时就不会被当做字符串了。功能有限,仅支持最基本的数学运算(加减乘除和取余),不支持逻辑运算、自增自减等,所以在实际开发中很少使用。

2.1.1 双小括号#

$(())	# $ 符号必须
# .sh 脚本中可不写 echo
a=10
b=2
echo $(($a - $b))
echo $(($a > $b))

2.1.2 运算脚本#

#! /bin/bash

print_usage() {
	echo "请输入数字!!!"
	exit 1
}

read -p "请输入第一个数字: " firstnum
if [ -n "`echo` $firstnum | sed 's/[0-9]//g'" ]
  then
    print_usage
fi 


if [ "${operator}" != "+" ] && [ "${operator}" != "-" ] && [ "${operator}" != "*" ] && [ "${operator}" != "/" ]
  then
    echo "只能输入 +-*/"
    exit 2
fi


read -p "请输入第二个数字: " secondnum
if [ -n "`echo` $secondnum | sed 's/[0-9]//g'" ]
  then
    print_usage
fi

echo "${firstnum}${operator}${secondnum} 结果是:$((${firstnum}${operator}${secondnum}))"

2.1.3 检测 nginx 服务是否运行#

while true;
  do
    wget --timeout=${timeout} --tries=1 http://xxx.com/ -q -O /dev/null
    if [ $? -ne 0 ]
      then
        let fails+=1
      else
        let success+=1
     fi
     
     if [ $success -ge 1 ]
       then
         echo "正常"
         exit 0
     fi
     
     if [ ${fails} -ge 2 ];then
       echo "error"
       exit 2
     fi
done
-ne		不等于
-ge		大于等于

vim 定位另一个括号,shift + %

2.1.4 expr#

expr 1 + 2

# 模式匹配
expr aa.png ":" ".*"

expr aa.png ":" ".p"

# 判断 jpg 文件
expr bb.jpg ":" ".*\.jpg" # 6
expr bb.jpghhfff ":" ".*\.jpg" # 也能匹配成功 # 6

判断文件名后缀是否合法

#!/bin/bash

if expr "$1" ":" ".*\.jpg" &> /dev/null
  then
  	echo "This is a jpg!"
else
  echo "not a jpg!"
fi

找出长度不大于 2 的单词

#!/bin/bash

for str1 in This is a str, you know?
	do
		if [ `expr length str1` -le 2 ]
			then
				echo $str1
	done

2.1.5 bc, awk, 中括号计算#

bc 交互式 计算

计算 1~100 的和

echo {1..100} | tr " " "+" | bc

seq -s "+" 100 | bc

echo $((`seq -s "+" 100`)) # 效率最高

seq -s "+" 100 | xargs expr
echo "2.2 1.1" | awk '{print ($1 + $2)}'
num=5
res=$[num+4]
echo $res

res=$[num*4]
echo $res

2.2 shell 条件测试#

条件测试语法说明
test <测试表达式>利用 test 命令进行条件测试表达式的方法。test 命令和 <测试表达式> 之间至少有一个空格
[ <测试表达式> ]通过[](单中括号)进行条件测试表达式的方法,和 test 命令的用法相同,这是老男孩推荐的方法。[]的边界和内容之间至少有一个空格
[[ <测试表达式> ]]通过[](双中括号)进行条件测试表达式的方法,是比 test 和 [] 更新的语法格式。[[]] 的边界和内容之间至少有一个空格
((<测试表达式>))这是通过 (()) 进行条件测试表达式的方法,一般用于 if 语句里。(())两端不需要有空格

2.2.1 test 条件测试#

-e 判断该文件是否存在,(普通文件,目录),存在就为真(0),否则为假(1)
echo $?

结合 &&    ||  使用

文件类型

选 项作 用
-b filename判断文件是否存在,并且是否为块设备文件
-c filename判断文件是否存在,并且是否为字符设备文件
-d filename判断文件是否存在,并且是否为目录文件(常用)
-e filename判断文件是否存在(常用)
-f filename判断文件是否存在,井且是否为普通文件(常用)
-L filename判断文件是否存在,并且是否为符号链接文件
-p filename判断文件是否存在,并且是否为管道文件
-s filename判断文件是否存在,并且是否为非空
-S filename判断该文件是否存在,并且是否为套接字文件

文件权限判断

选 项作 用
-r filename判断文件是否存在,并且是否拥有读权限
-w filename判断文件是否存在,并且是否拥有写权限
-x filename判断文件是否存在,并且是否拥有执行权限
-u filename判断文件是否存在,并且是否拥有 SUID 权限
-g filename判断文件是否存在,并且是否拥有 SGID 权限
-k filename判断该文件是否存在,并且是否拥有 SBIT 权限

文件比较

选 项作 用
filename1 -nt filename2判断 filename1 的修改时间是否比 filename2 的新
filename -ot filename2判断 filename1 的修改时间是否比 filename2 的旧
filename1 -ef filename2判断 filename1 是否和 filename2 的 inode 号一致,可以理解为两个文件是否为同一个文件。这个判断用于判断硬链接是很好的方法
-z 字符串为空就为真,否则为假
-n 反过来的,字符串是有内容就为真,否则为假

2.2.2 [ ] 条件测试#

test 和 [] 的作用一样

[ ] 中前后有空格

条件测试中使用变量,必须使用双引号""

[ -f "${file1}" ]
[[ -f "${file1}" ]]

2.2.3 变量测试#

使用双引号

2.2.4 字符串比较测试#

常用字符串测试操作符说明
-n "字符串"若字符串的长度不为 0,则为真。n 可以理解为 no zero
-z "字符串"若字符串的长度为 0,则为真。z 可以理解为 zero 的缩写
"串 1" = "串 2"若字符串 1 等于字符串 2,则为真。可使用 "=="代替"="
"串 1" != "串 2"若字符串 1 不等于字符串 2,则为真。但不能用 !== 代替 "!="

变量要有双引号

= 两边要有空格

2.2.5 数值比较测试#

在 [] 和 test 中使用在 (()) 和 [[]] 中使用的比较符号说明
-eq== 或 =相等 (equeal)
-ne!=不等 (not equal)
-gt>大于 (greater than)
-lt<小于 (less than)
-ge>=大于等于 (greater than or equal)
-le<=大于小于 (less than or equal)
-a&&and
-o||or
!!

一般使用[]

[[]]也支持 -eq 等

2.2.6 逻辑运算#

read -p "pls input a char:" var1

[ "$var1" -eq "1" ] && {
	echo $var1
	exit 0
}

[ "$var2" = "2" ] && {
	echo $var1
	exit 0
}

[ "$var1" != "2" -a "$var1" != "1" ] && {
	echo "pls input 1 or 2!!"
	exit 1
}
# 正则表达式
[[ $num =~ [1-3] ]]

总结#

逻辑与 -a

逻辑或 -o

[[]] 无法使用 >=<= ,只能使用 -ge-le

测试表达式符号[]test[[]](())
边界为是否需要空格需要需要不需要需要
逻辑操作符!, -a, -o!, -a, -o!, &&, ||!, &&, ||
整数比较操作符-eq, -gt, -lt, -ge, -le-eq, -gt, -lt, -ge, -le 或 =, >, <,>=, <=-eq, -gt, -lt, -ge, -le 或 =, >, <,>=, <==, >, <, >=, <=
字符串比较操作符=, ==, !==, ==, !==, ==, !==, ==, !=
是否支持通配符匹配不支持不支持支持不支持

最常用 []

2.3 脚本开发#

开发内存检测脚本#

# 第二行,最后一个值
free -m | awk 'NR==2 {print $NF}'
#! /bin/bash
FreeMem=`free -m | awk 'NR==2 {print $NF}'`
CHARS="cruuent mem is $FreeMem"

if [ "$FreeMem" -lt "100" ]; then
	echo $CHARS|tee /tmp/message.txt
	# mail -s 标题,收件人 < 内容
	mail -s "`date +%F-%T`$CHARS" [email protected] < /tmp/message.txt
fi

mysql 监控脚本#

mysql 监控脚本流程
mysql 监控脚本流程

# 本地
netstat -tunlp | grep mysql | wc -l
ss -tunlp | grep mysql | wc -l
lsof -i tcp:3380 | wc -l
# 远程
nmap 127.0.0.1 -p 3380 | grep open | wc -l

# telnet
echo -e "\n" | telnet 127.0.0.1 3380 2>/dev/null | grep Connected | wc -l

Rsync 启停脚本开发#

2.4 函数#

function xx() {

}

function xx{

}

xx(){

}

美化相关

lsb_functions="/lib/lsb/init-functions"
if test -f $lsb_functions;then
	. $lsb_functions
else
	init_functions="/etc/init.d/functions"
	if test -f $init_functions;then
		. $init_functions
	fi
	log_success_msg() {
		echo " SUCCESS! $@"
	}
	log_failure_msg() {
		echo " ERROR! $@"
	}
fi


log_success_msg echo "test"

函数返回值#

shell 的函数只能返回 整数值,如果想让函数返回字符串,一般有两种方法:将返回值赋值给一个字符串变量、输出返回值,在函数调用处使用变量赋值。

https://www.cnblogs.com/duanxz/p/4661767.html

https://roc-wong.github.io/blog/2017/03/shell-%E5%87%BD%E6%95%B0%E8%BF%94%E5%9B%9E%E5%AD%97%E7%AC%A6%E4%B8%B2%E7%9A%84%E6%96%B9%E6%B3%95.html

1. 将返回值赋值给一个字符串变量

get_project_version(){
  VERSION=`grep version $1/pom.xml | head -n 1 | sed -E 's:</?version>::g' | sed -E 's/^\t*//' | sed -E 's/^ *//' | sed -E 's/\r$//'`
}

2. 输出返回值,在函数调用处为变量赋值

get_project_version(){
  echo `grep version $1/pom.xml | head -n 1 | sed -E 's:</?version>::g' | sed -E 's/^\t*//' | sed -E 's/^ *//' | sed -E 's/\r$//'`
}
version=`get_project_version $PROJECT_PATH`
加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。