shell

学习收获

1、能看到代码实现的原理
2、能根据业务需求写脚本
3、能根据脚本执⾏问题优化脚本代码
4、具体代码的debug和问题解决  

⼀、简介

1.1 Shell 环境

Shell 编程跟 java、 php 编程⼀样,只要有⼀个能编写代码的⽂本编辑器和⼀个能解释执⾏的脚本解释器就可以了。

Shell 的解释器种类众多,常⻅的有:

  • sh - 即 Bourne Shell。 sh 是 Unix 标准默认的 shell。
  • bash - 即 Bourne Again Shell。 bash 是 Linux 标准默认的 shell。
  • fish - 智能和⽤户友好的命令⾏ shell。
  • xiki - 使 shell 控制台更友好,更强⼤。
  • zsh - 功能强⼤的 shell 与脚本语⾔。

1.1.1 指定脚本解释器

在 shell 脚本,#!告诉系统其后路径所指定的程序即是解释此脚本⽂件的 Shell 解释器。 #! 被称作shebang(也称为 Hashbang )

所以,你应该会在 shell 中,⻅到诸如以下的注释:

  • 指定 sh 解释器

    #!/bin/sh
    
  • 指定 bash 解释器

    #!/bin/bash
    

注意

上⾯的指定解释器的⽅式是⽐较常⻅的,但有时候,你可能也会看到下⾯的⽅式:

#!/usr/bin/env bash

这样做的好处是,系统会⾃动在 PATH 环境变量中查找你指定的程序(本例中的 bash )。相⽐第⼀种写法,你应该尽量⽤这种写法,因为程序的路径是不确定的。这样写还有⼀个好处,操作系统的 PATH 变量有可能被配置为指向程序的另⼀个版本。⽐如,安装完新版本的 bash ,我们可能将其路径添加到 PATH 中,来“隐藏”⽼版本。如果直接⽤#!/bin/bash,那么系统会选择⽼版本的 bash 来执⾏脚本,如果⽤ #!/usr/bin/env bash ,则会使⽤新版本。

1.1.2 模式

shell 有交互和⾮交互两种模式。

交互模式

简单来说,你可以将 shell 的交互模式理解为执⾏命令⾏。

看到形如下⾯的东⻄,说明 shell 处于交互模式下:

user@host:~$

接着,便可以输⼊⼀系列 Linux 命令,⽐如 ls grep cdmkdirrm 等等。

⾮交互模式

简单来说,你可以将 shell 的⾮交互模式理解为执⾏ shell 脚本。

在⾮交互模式下, shell 从⽂件或者管道中读取命令并执⾏。
当 shell 解释器执⾏完⽂件中的最后⼀个命令, shell 进程终⽌,并回到⽗进程。

可以使⽤下⾯的命令让 shell 以⾮交互模式运⾏:

sh /root/shell//script.sh
bash /root/shell/script.sh
source/root/shell/script.sh
/root/shell/script.sh

上⾯的例⼦中, script.sh 是⼀个包含 shell 解释器可以识别并执⾏的命令的普通⽂本⽂件, sh 和 bash 是 shell解释器程序。你可以使⽤任何喜欢的编辑器创建 script.sh (vim, nano, Sublime Text, Atom 等等)。

其中, source /shell/script.sh 和 ./shell/script.sh 是等价的。

除此之外,你还可以通过 chmod 命令给⽂件添加可执⾏的权限,来直接执⾏脚本⽂件:

chmod +x /root/shell/script.sh #使脚本具有执⾏权限
/root/shell/script.sh

这种⽅式要求脚本⽂件的第⼀⾏必须指明运⾏该脚本的程序,⽐如:

『示例源码』

#!/usr/bin/env bash
echo "Hello, world!"

上⾯的例⼦中,我们使⽤了⼀个很有⽤的命令 echo 来输出字符串到屏幕上。

⼆、基本语法

2.1 解释器

# 以下两种⽅式都可以指定 shell 解释器为 bash,第⼆种⽅式更好
#!/bin/bash
#!/usr/bin/env bash

2.2 注释

  • 单⾏注释 - 以 # 开头,到⾏尾结束。
  • 多⾏注释 - 以 :<<EOF 开头,到EOF结束

『示例源码』

#--------------------------------------------
# shell 注释示例
# author: yuan
#--------------------------------------------

# echo '这是单⾏注释'

########## 这是分割线 ##########

:<<EOF
echo '这是多⾏注释'
echo '这是多⾏注释'
echo '这是多⾏注释'
EOF

echo "learn shell script annotation "

2.3 echo

输出普通字符串:

echo "hello, world"
# Output: hello, world
echo "hello, \"yuan\""
# Output: hello, "yuan"

输出含变量的字符串:

name=wing
echo "hello, \"${name}\""
# Output: hello, "yuan"

输出含换⾏符的字符串:

# 输出含换⾏符的字符串
echo "YES\nNO"
# Output: YES\nNO

echo -e "YES\nNO" # -e 开启转义
# Output:
# YES
# NO

输出含不换⾏符的字符串:

echo "YES"
echo "NO"
# Output:
# YES
# NO

echo -e "YES\c" # -e 开启转义 \c 不换⾏
echo "NO"
# Output:
# YESNO

输出重定向⾄⽂件

echo "test" > test.txt

输出执⾏结果

echo `pwd`
# Output:(当前⽬录路径)

2.4 printf

printf ⽤于格式化输出字符串。

默认, printf 不会像 echo ⼀样⾃动添加换⾏符,如果需要换⾏可以⼿动添加 \n 。

printf 的转义符

序列 说明
\a 警告字符,通常为 ASCII 的 BEL 字符
\b 后退
\c 抑制(不显示)输出结果中任何结尾的换⾏字符(只在%b 格式指示符控制下的参数字符串中有 效),⽽且,任何留在参数⾥的字符、任何接下来的参数以及任何留在格式字符串中的字符,都被忽略
\f 换⻚(formfeed)
\n 换⾏
\r 回⻋(Carriage return)
\t ⽔平制表符
\v 垂直制表符
\\ ⼀个字⾯上的反斜杠字符
\ddd 表示 1 到 3 位数⼋进制值的字符。仅在格式字符串中有效
\0ddd 表示 1 到 3 位的⼋进制值字符

**『示例源码』 **

# 单引号
printf '%d %s\n' 1 "abc"
# Output:1 abc

# 双引号
printf "%d %s\n" 1 "abc"
# Output:1 abc

# ⽆引号
printf %s abcdef
# Output: abcdef(并不会换⾏)

# 格式只指定了⼀个参数,但多出的参数仍然会按照该格式输出
printf "%s\n" abc def
# Output:
# abc
# def

printf "%s %s %s\n" a b c d e f g h i j
# Output:
# a b c
# d e f
# g h i
# j

# 如果没有参数,那么 %s ⽤ NULL 代替, %d ⽤ 0 代替
printf "%s and %d \n"
# Output:
# and 0

# 格式化输出
printf "%-10s %-8s %-4s\n" 姓名 性别 体重kg
printf "%-10s %-8s %-4.2f\n" 郭靖 男 66.1234
printf "%-10s %-8s %-4.2f\n" 杨过 男 48.6543
printf "%-10s %-8s %-4.2f\n" 郭芙 ⼥ 47.9876
# Output:
# 姓名 性别 体重kg
# 郭靖 男 66.12
# 杨过 男 48.65
# 郭芙 ⼥ 47.99

三、变量

跟许多程序设计语⾔⼀样,你可以在 bash 中创建变量。

Bash 中没有数据类型, bash 中的变量可以保存⼀个数字、⼀个字符、⼀个字符串等等。同时⽆需提前声明变量,给变量赋值会直接创建变量。

3.1 变量命名原则

  • 命名只能使⽤英⽂字⺟,数字和下划线,⾸个字符不能以数字开头。
  • 中间不能有空格,可以使⽤下划线(_)。
  • 不能使⽤标点符号。
  • 不能使⽤ bash ⾥的关键字(可⽤ help 命令查看保留关键字)。

3.2 声明变量

访问变量的语法形式为:${var}$var

变量名外⾯的花括号是可选的,加不加都⾏,加花括号是为了帮助解释器识别变量的边界,所以推荐加花括号。

word="hello"
echo ${word}
echo $word
# Output: hello

3.3 只读变量

使⽤ readonly 命令可以将变量定义为只读变量,只读变量的值不能被改变。

rword="hello"
echo ${rword}
readonly rword
# rword="bye" # 如果放开注释,执⾏时会报错

3.4 删除变量

使⽤ unset 命令可以删除变量。变量被删除后不能再次使⽤。 unset 命令不能删除只读变量。

dword="hello" # 声明变量
echo ${dword} # 输出变量值
# Output: hello

unset dword # 删除变量
echo ${dword}
# Output: (空)

3.5 变量作用域

  • 局部变量 - 局部变量是仅在某个脚本内部有效的变量。它们不能被其他的程序和脚本访问。
  • 环境变量 - 环境变量是对当前 shell 会话内所有的程序或脚本都可见的变量。创建它们跟创建局部变量类似,但使⽤的是 export 关键字, shell 脚本也可以定义环境变量。

常见的系统环境变量:

变量 描述
$HOME 当前⽤户的⽤户⽬录
$SHELL 默认 Shell
$LANG 默认语⾔
$PATH ⽤分号分隔的⽬录列表, shell 会到这些⽬录中查找命令
$HOSTNAME 主机名
$PWD 当前⼯作⽬录
$RANDOM 0 到 32767 之间的整数
$UID 数值类型,当前⽤户的⽤户 ID
$PS1 主要系统输⼊提示符
$PS2 次要系统输⼊提示符

这⾥ 有⼀张更全⾯的 Bash 环境变量列表。

注意系统环境变量加载顺序

执⾏顺序: /etc/profile -> ~/.bash_profile -> ~/.bashrc -> /etc/bashrc -> ~/.bash_logout

3.6 位置变量

位置变量指的是函数或脚本后跟的第n 个参数。
$1-$n,需要注意的是从第 10 个开始要⽤花括号调⽤,例如:

#!/bin/bash
echo "1: $1"
echo "2: $2"
echo "3: $3"
echo "$*"
echo "$@"

[root@localhost shell]# ./test.sh a b c d
1: a
2: b
3: c
a b c d
a b c d

#!/bin/bash
echo "1: $1"
shift
echo "2: $2"
shift
echo "3: $3"

# bash /test01.sh a b c 1: e
1: a
2: c
3: e

[root@localhost shell]# ./test01.sh a a b b c 
1: a
2: b
3: c

每执⾏⼀次 shift 命令,位置变量个数就会减⼀,⽽变量值则提前⼀位。 shift n,可设置向前移动n 位。

3.7 特殊变量 √

$0 脚本自身名字
$? 返回上⼀条命令是否执⾏成功, 0 为执⾏成功,⾮ 0 则为执⾏失败
$# 位置参数总数
$* 所有的位置参数被看做⼀个字符串 √
$@ 每个位置参数被看做独⽴的字符串 √
$$ 当前进程 PID
$! 上⼀条运⾏后台进程的 PID
#!/bin/bash
#$0 返回脚本名称
echo $0

# $?: 上个命令的退出状态, 0 表示成功,⾮零值表示失败
echo "Executing ls command"
ls
echo "Exit status of ls command: $?"

# $#: 命令⾏参数的个数
if [ $# -gt 0 ]; then
echo "Number of input arguments: $#"
echo "Input arguments: $@"
else
echo "No input arguments provided"
fi

# $*: 所有命令⾏参数作为单个字符串
echo "Outputting all input arguments as single string: $*"

# $@: 所有命令⾏参数作为单独的字符串
echo "Outputting all input arguments as separate strings:"
for arg in "$@"; do
echo "$arg"
done

# $$: 当前进程的 PID
echo "PID of current process: $$"

# $!: 最后⼀次运⾏的后台进程的 PID
echo "Starting sleep command in background"
sleep 5 &
echo "PID of sleep command: $!"

执⾏上述脚本的结果类似于以下输出:

Executing ls command
file1.txt
file2.txt
Exit status of ls command: 0

Number of input arguments: 3
Input arguments: arg1 arg2 arg3

Outputting all input arguments as single string: arg1 arg2 arg3
Outputting all input arguments as separate strings:
arg1
arg2
arg3

PID of current process: 1234

Starting sleep command in background
PID of sleep command: 5678

注意事项 $* $@区别

################### 系统变量 ###################
echo "UID:$UID"
echo LOGNAME:$LOGNAME
echo User:$USER
echo HOME:$HOME
echo PATH:$PATH
echo HOSTNAME:$HOSTNAME
echo SHELL:$SHELL
echo LANG:$LANG

################### ⾃定义变量 ###################
days=10
user="admin"
echo "$user logged in $days days age"
days=5
user="root"
echo "$user logged in $days days age"
# Output:
# admin logged in 10 days age
# root logged in 5 days age

################### 从变量读取列表 ###################
colors="Red Yellow Blue"
colors=$colors" White Black"
for color in $colors
do
echo " $color"
done
# Output:
[root@localhost shell]# bash test03.sh 
 Red
 Yellow
 Blue
 White
 Black

四、字符串

img

4.1 单引号和双引号

shell 字符串可以⽤单引号'',也可以⽤双引号“ ”,也可以不⽤引号。

  • 单引号的特点
    • 单引号⾥不识别变量
    • 单引号⾥不能出现单独的单引号(使⽤转义符也不⾏),但可成对出现,作为字符串拼接使⽤。
  • 双引号的特点
    • 双引号⾥识别变量
    • 双引号⾥可以出现转义字符
#!/bin/bash

name='yuan'
echo 'Hello, $name'
# Output: Hello, $name

name='yuan'
echo "Hello, $name"
# Output: Hello, yuan

echo 'I'\''m a programmer'
# Output: I'm a programmer

echo "I'm a programmer"
# Output: I'm a programmer

echo "some text" # Output: some text
echo 'some text' # Output: some text

echo "the date is $(date)"
# Output: the date is Wed May 12 11:47:02 CST 2021
echo 'the date is $(date)'
# Output: the date is $(date)

在上述脚本中,展示了单引号和双引号之间的区别,主要包括以下⼏点:

  • 使⽤单引号时,其中的变量不会被展开。例如,在示例1中,由于使⽤了单引号,因此 $name 不会被展开,输出的结果就是 "Hello, $name"
  • 使⽤双引号时,其中的变量会被展开。例如,在示例2中,由于使⽤了双引号,因此 $name 会被展开为
  • John,输出的结果就是"Hello, John"
  • 使⽤单引号时,需要通过反斜杠来转义其中的特殊字符,例如示例3。
  • 使⽤双引号时,⼀些特殊字符不需要转义,例如单引号和反斜杠,例如示例4。
  • 使⽤双引号时,可以保留空格和制表符,例如示例5。
  • 使⽤单引号时,可以保留其中的所有字符,包括其中的特殊字符和变量名,例如示例6。

综上所述,单引号和双引号之间存在明显的区别,需要根据具体情况和需求来选择使⽤哪种引号。

4.2 拼接字符串

# 使⽤单引号拼接
name1='white'
str1='hello, '${name1}''
str2='hello, ${name1}'
echo ${str1}_${str2}
# Output:
# hello, white_hello, ${name1}

# 使⽤双引号拼接
name2="black"
str3="hello, "${name2}""
str4="hello, ${name2}"
echo ${str3}_${str4}
# Output:
# hello, black_hello, black

4.3 获取字符串长度

text="12345"
echo ${#text}
# Output:
# 5

4.4 截取子字符串

text="12345"
echo ${text:2:2}
# Output:
# 34

从第 2 个字符开始,截取 2 个字符

4.5 从指定字符(子字符串)开始截取 √

1.使用# 号截取右边字符

使⽤ # 号可以截取指定字符(或者⼦字符串)右边的所有字符,具体格式如下:

${string#*chars}
其中, string 表示要截取的字符, chars 是指定的字符(或者⼦字符串), * 是通配符的⼀种,表示任意⻓度的字符串。*chars连起来使⽤的意思是:忽略左边的所有字符,直到遇见chars(chars 不会被截取)。

请看下⾯的例⼦:

url="http://www.yuan.com/index.html"
echo ${url#*:}

结果为//www.yuan.com/index.html

以下写法也可以得到同样的结果:

echo ${url#*p:}
echo ${url#*ttp:}

如果不需要忽略 chars 左边的字符,那么也可以不写 * ,例如:

url="http://www.yuan.com/index.html"
echo ${url#http://}

结果为 www.yuan.com/index.html
注意,以上写法遇到第⼀个匹配的字符(⼦字符串)就结束了。
例如:

url="http://www.yuan.com/index.html"
echo ${url#*/}

结果为 /www.yuan.com/index.html 。 url 字符串中有三个 / ,输出结果表明, Shell 遇到第⼀个 / 就匹配结束了。
如果希望直到最后⼀个指定字符(⼦字符串)再匹配结束,那么可以使⽤ ## ,具体格式为:

${string##*chars}

请看下⾯的例⼦:

#!/bin/bash
url="http://www.yuan.com/index.html"
echo ${url#*/}
#从左往右第一个/,截取其右边的字符
#结果为 /www.yuan.com/index.html

echo ${url##*/}
#结果为 index.html

str="---aa+++aa@@@"
echo ${str#*aa}
#结果为 +++aa@@@

echo ${str##*aa}
#从左往右匹配到最后一个字符aa,截取其右边的字符,结果为 @@@

2.使用 % 截取左边字符

使⽤ % 号可以截取指定字符(或者⼦字符串)左边的所有字符,具体格式如下:

${string%chars*}

请注意 * 的位置,因为要截取 chars 左边的字符,⽽忽略 chars 右边的字符,所以 * 应该位于 chars 的右侧。其他⽅⾯ %# 的⽤法相同,这⾥不再赘述,仅举例说明:

#!/bin/bash

url="http://www.yuan.com/index.html"
echo ${url%/*}
#从右往左第一个/,截取其左边的字符
#结果为 http://www.yuan.com

echo ${url%%/*}
#从右往左匹配到最后一个字符/,截取其左边的字符,结果为 http:

str="---aa+++aa@@@"
echo ${str%aa*}
#结果为 ---aa+++

echo ${str%%aa*}
#结果为 ---

汇总

对以上 8 种格式做⼀个汇总,请看下表:

格式 说明
$ 从 string 字符串的左边第 start 个字符开始,向右截取 length 个字符。
$ 从 string 字符串的左边第 start 个字符开始截取,直到最后。
$ 从 string 字符串的右边第 start 个字符开始,向右截取 length 个字符。
$ 从 string 字符串的右边第 start 个字符开始截取,直到最后。
$ 从 string 字符串第⼀次出现 *chars 的位置开始,截取 *chars 右边的所有字符。
$ 从 string 字符串最后⼀次出现 *chars 的位置开始,截取 *chars 右边的所有字 符。
$ 从 string 字符串第⼀次出现 *chars 的位置开始,截取 *chars 左边的所有字符。
$ 从 string 字符串最后⼀次出现 *chars 的位置开始,截取 *chars 左边的所有字 符。

4.6 查找子字符串

#!/usr/bin/env bash
# expr 是计算命令,index 会返回索引ll第一次

text="hello"
echo `expr index "${text}" ll`

# Execute: ./str-demo5.sh
# Output:
# 3

查找 ll ⼦字符在 hello 字符串中的起始位置。

这个命令调⽤ expr 命令来查找字符串 ${text} 中⼦串 "ll" 的第⼀次出现位置。 "${text}" ⽤双引号括起来,表示将变量 $text 的值作为⼀个单独的参数传递给 index 函数。注意,为了避免在$text的值中包含空格或其他特殊字符时出现错误,我们使⽤了双引号将 ${text} 包起来。
最后,我们使⽤ echo 命令将 index 函数的输出打印到屏幕上。为了调⽤ index 函数并将它的输出作为参数传递给 echo 命令,我们使⽤反引号将整个表达式括起来,这会告诉 shell 执⾏该命令并将其输出返回给 echo 命令。

在这种情况下, index 函数将返回 3 ,因为 "ll" 第⼀次出现在 ${text} 中的第三个位置(从1开始计数)。因此,命令的输出为 3 。
这个命令的作⽤是在给定的字符串 ${text} 中查找⼦串 "ll" 第⼀次出现的位置(从1开始)。

『示例源码』

################### 判断字符串中是否包含⼦字符串 ################### 用的较多 √
str="hello feature/"
result=$(echo "${str}" | grep "feature/")
if [[ "$result" != "" ]]; then
echo "feature/ 是 ${str} 的⼦字符串"
else
echo "feature/ 不是 ${str} 的⼦字符串"
fi

################### 截取关键字右边内容 ###################
full_branch="feature/1.0.0"
branch=`echo ${full_branch#feature/}`
echo "branch is ${branch}"
# Output:
# branch is 1.0.0

################### 截取关键字左边内容 ###################
full_version="0.0.1-SNAPSHOT"
version=`echo ${full_version%-SNAPSHOT}`
echo "version is ${version}"
# Output:
# version is 0.0.1

################### 字符串分割成数组 ###################
str="0.0.0.1"
OLD_IFS="$IFS"
IFS="."
array=( ${str} )
IFS="$OLD_IFS"
size=${#array[*]} 
lastIndex=`expr ${size} - 1`
echo "数组⻓度: ${size}"
echo "最后⼀个数组元素: ${array[${lastIndex}]}"
for item in ${array[@]}  #遍历数组
do
echo "$item"
done
# Output:
# 4
# 1
# 0
# 0
# 0
# 1

################### 判断字符串是否为空 ###################
#-n 判断⻓度是否⾮零
#-z 判断⻓度是否为零
str=testing
str2=''
if [[ -n "$str" ]]
then
echo "The string $str is not empty"
else
echo "The string $str is empty"
fi
if [[ -n "$str2" ]]
then
echo "The string $str2 is not empty"
else
echo "The string $str2 is empty"
fi
# Output:
# The string testing is not empty
# The string is empty

################### 字符串⽐较 ###################
str=hello
str2=world
if [[ $str = "hello" ]]; then
echo "str equals hello"
else
echo "str not equals hello"
fi
if [[ $str2 = "hello" ]]; then
echo "str2 equals hello"
else
echo "str2 not equals hello"
fi
# Output:
# str equals hello
# str2 not equals hello

五、数组

bash 只⽀持⼀维数组。

数组下标从 0 开始,下标可以是整数或算术表达式,其值应⼤于或等于 0。

5.1 创建数组

shell 语⾔中⽀持关联数组。关联数组是⼀种基于键值对(key-value pairs)的数据结构,它允许你使⽤字符串或数字作为索引来查找和访问数组元素。在 shell 中,关联数组可以使⽤ declare -A 命令或者在数组赋值时使⽤+= 符号来定义

下⾯是⼀个简单的例⼦,展示了如何定义和使⽤关联数组:

# 声明关联数组,并为其指定元素
declare -A fruits
fruits=([apple]="red" [banana]="yellow" [kiwi]="green")

# 访问关联数组元素
echo "Apple is ${fruits[apple]} in color"
# Output:Apple is red in color
echo "Banana is ${fruits[banana]} in color"
# Output:Banana is yellow in color
echo "Kiwi is ${fruits[kiwi]} in color"
# Output:Kiwi is green in color

echo
# 循环输出所有键值对
for key in "${!fruits[@]}"
do
echo "index: $key value: ${fruits[$key]}"
done
# Output:
# index: kiwi value: green
# index: apple value: red
# index: banana value: yellow


上⾯的代码创建了⼀个名为 fruits 的关联数组,并且通过指定元素的⽅式为其赋值。然后使⽤${fruits[apple]} 等⽅式访问元素值,它们的输出结果即为对应的颜⾊。最后⼀个循环⽤于输出所有键值对,加了!使⽤ ${!fruits[@]} 来获得所有的键名,再使⽤ ${fruits[$key]} 来获得对应的值。

# 创建数组的不同⽅式
nums=([2]=2 [0]=0 [1]=1)
colors=(red yellow "dark blue")

5.2 访问数组元素

  • 访问数组的单个元素:
# 访问下标为1的元素
echo ${nums[1]}
# Output: 1

  • 访问数组的所有元素:
echo ${colors[*]}
# Output: red yellow dark blue

echo ${colors[@]}
# Output: red yellow dark blue

上⾯两⾏有很重要(也很微妙)的区别:

为了将数组中每个元素单独⼀⾏输出,我们⽤ printf 命令:

printf "+ %s\n" ${colors[*]}
# Output:
# + red
# + yellow
# + dark
# + blue

为什么 darkblue 各占了⼀⾏?尝试⽤引号包起来:

printf "+ %s\n" "${colors[*]}"
# Output:
# + red yellow dark blue

现在所有的元素都在⼀⾏输出 —— 这不是我们想要的!让我们试试 ${colors[@]}

printf "+ %s\n" "${colors[@]}"
# Output:
# + red
# + yellow
# + dark blue

在引号内, ${colors[@]} 将数组中的每个元素扩展为⼀个单独的参数;数组元素中的空格得以保留。

  • 访问数组的部分元素:
 echo ${nums[@]:0:2}
 # Output:
 # 0 1

在上⾯的例⼦中, ${array[@]} 扩展为整个数组, :0:2 取出了数组中从 0 开始,⻓度为 2 的元素。

5.3 访问数组长度

echo ${#nums[*]}
# Output:
# 3

5.4 向数组中添加元素

向数组中添加元素也⾮常简单:

colors=(white "${colors[@]}" green black)
echo ${colors[@]}
# Output:
# white red yellow dark blue green black

上⾯的例⼦中, ${colors[@]} 扩展为整个数组,并被置换到复合赋值语句中,接着,对数组 colors 的赋值覆盖了它原来的值。

5.5 从数组中删除元素

unset 命令来从数组中删除⼀个元素:

unset nums[0]
echo ${nums[@]}
# Output:
# 1 2

六、运算符

6.1 算术运算符

下表列出了常⽤的算术运算符,假定变量 x 为 10,变量 y 为 20:

运算符 说明 举例
+ 加法 expr $x + $y 结果为 30。
- 减法 expr $x - $y 结果为 -10。
* 乘法 expr $x * $y 结果为 200。
/ 除法 expr $y / $x 结果为 2。
% 取余 expr $y % $x 结果为 0。
= 赋值 x=$y 将把变量 y 的值赋给 x。
== 相等 ⽤于⽐较两个字符串,相同则返回 true。 [ $x == $y ] 返回 false。
!= 不相等 ⽤于⽐较两个数字,不相同则返回 true。 [ $x != $y ] 返回 true。

注意: 条件表达式要放在⽅括号之间,并且要有空格,例如: [$x==$y] 是错误的,必须写成 [ $x == $y ]
『示例源码』

x=10
y=20

echo "x=${x}, y=${y}"

val=`expr ${x} + ${y}`
echo "${x} + ${y} = $val"

val=`expr ${x} - ${y}`
echo "${x} - ${y} = $val"

val=`expr ${x} \* ${y}`
echo "${x} * ${y} = $val"

val=`expr ${y} / ${x}`
echo "${y} / ${x} = $val"

val=`expr ${y} % ${x}`
echo "${y} % ${x} = $val"

if [[ ${x} == ${y} ]]
then
echo "${x} = ${y}"
fi

if [[ ${x} != ${y} ]]
then
echo "${x} != ${y}"
fi

# Output:
# x=10, y=20
# 10 + 20 = 30
# 10 - 20 = -10
# 10 * 20 = 200
# 20 / 10 = 2
# 20 % 10 = 0
# 10 != 20

6.2 关系运算符 √

关系运算符只⽀持数字 ,不⽀持字符串,除⾮字符串的值是数字。

下表列出了常⽤的关系运算符,假定变量 a 为 10,变量b 为 20:

运算符 说明 举例
-eq 检测两个数是否相等,相等返回 true。 [ $a -eq $b ] 返回 false。
-ne 检测两个数是否相等,不相等返回 true。 [ $a -ne $b ] 返回 true。
-gt 检测左边的数是否⼤于右边的,如果是,则返回 true。 [ $a -gt $b ] 返回 false。
-lt 检测左边的数是否⼩于右边的,如果是,则返回 true。 [ $a -lt $b ] 返回 true。
-ge 检测左边的数是否⼤于等于右边的,如果是,则返回 true。 [ $a -ge $b ] 返回 false。
-le 检测左边的数是否⼩于等于右边的,如果是,则返回 true。 [ $a -le $b ] 返回 true。

『示例源码』

x=10
y=20

echo "x=${x}, y=${y}"

if [[ ${x} -eq ${y} ]]; then
echo "${x} -eq ${y} : x 等于 y"
else
echo "${x} -eq ${y}: x 不等于 y"
fi

if [[ ${x} -ne ${y} ]]; then
echo "${x} -ne ${y}: x 不等于 y"
else
echo "${x} -ne ${y}: x 等于 y"
fi

if [[ ${x} -gt ${y} ]]; then
echo "${x} -gt ${y}: x ⼤于 y"
else
echo "${x} -gt ${y}: x 不⼤于 y"
fi

if [[ ${x} -lt ${y} ]]; then
echo "${x} -lt ${y}: x ⼩于 y"
else
echo "${x} -lt ${y}: x 不⼩于 y"
fi

if [[ ${x} -ge ${y} ]]; then
echo "${x} -ge ${y}: x ⼤于或等于 y"
else
echo "${x} -ge ${y}: x ⼩于 y"
fi

if [[ ${x} -le ${y} ]]; then
echo "${x} -le ${y}: x ⼩于或等于 y"
else
echo "${x} -le ${y}: x ⼤于 y"
fi

# Output:
# x=10, y=20
# 10 -eq 20: x 不等于 y
# 10 -ne 20: x 不等于 y
# 10 -gt 20: x 不⼤于 y
# 10 -lt 20: x ⼩于 y
# 10 -ge 20: x ⼩于 y
# 10 -le 20: x ⼩于或等于 y

6.3 布尔运算符

下表列出了常⽤的布尔运算符,假定变量 a为 10,变量 b 为 20:

运算符 说明 举例
! ⾮运算,表达式为 true 则返回 false,否则返回 true。 [ ! false ] 返回 true。
-o 或运算,有⼀个表达式为 true 则返回 true。 [ $a -lt 20 -o $b -gt 100 ] 返回 true。
-a 与运算,两个表达式都为 true 才返回 true。 [ $a -lt 20 -a $b -gt 100 ] 返回 false。

『示例源码』

x=10
y=20


echo "x=${x}, y=${y}"
if [[ ${x} != ${y} ]]; then
echo "${x} != ${y} : x 不等于 y"
else
echo "${x} != ${y}: x 等于 y"
fi

if [[ ${x} -lt 100 && ${y} -gt 15 ]]; then
echo "${x} ⼩于 100 且 ${y} ⼤于 15 : 返回 true"
else
echo "${x} ⼩于 100 且 ${y} ⼤于 15 : 返回 false"
fi

if [[ ${x} -lt 100 || ${y} -gt 100 ]]; then
echo "${x} ⼩于 100 或 ${y} ⼤于 100 : 返回 true"
else
echo "${x} ⼩于 100 或 ${y} ⼤于 100 : 返回 false"
fi

if [[ ${x} -lt 5 || ${y} -gt 100 ]]; then
echo "${x} ⼩于 5 或 ${y} ⼤于 100 : 返回 true"
else
echo "${x} ⼩于 5 或 ${y} ⼤于 100 : 返回 false"
fi

# Output:
# x=10, y=20
# 10 != 20 : x 不等于 y
# 10 ⼩于 100 且 20 ⼤于 15 : 返回 true
# 10 ⼩于 100 或 20 ⼤于 100 : 返回 true
# 10 ⼩于 5 或 20 ⼤于 100 : 返回 false

6.4 逻辑运算符

以下介绍 Shell 的逻辑运算符,假定变量 x 为 10,变量 y 为 20:

运算符 说明 举例
&& 逻辑的 AND [[ ${x} -lt 100 && ${y} -gt 100 ]] 返回 false
|| 逻辑的 OR [[ ${x} -lt 100 || ${y} -gt 100 ]] 返回 true

『示例源码』

x=10
y=20


echo "x=${x}, y=${y}"
if [[ ${x} -lt 100 && ${y} -gt 100 ]]
then
echo "${x} -lt 100 && ${y} -gt 100 返回 true"
else
echo "${x} -lt 100 && ${y} -gt 100 返回 false"
fi

if [[ ${x} -lt 100 || ${y} -gt 100 ]]
then
echo "${x} -lt 100 || ${y} -gt 100 返回 true"
else
echo "${x} -lt 100 || ${y} -gt 100 返回 false"
fi

# Output:
# x=10, y=20
# 10 -lt 100 && 20 -gt 100 返回 false
# 10 -lt 100 || 20 -gt 100 返回 true

6.5 字符串运算符 √

下表列出了常⽤的字符串运算符,假定变量 a 为 “abc”,变量 b 为 “efg”:

运算符 说明 举例
== 检测两个字符串是否相等,相等返回 true。 [ $a == $b ] 返回 false。
!= 检测两个字符串是否相等,不相等返回 true。 [ $a != $b ] 返回 true。
-z 检测字符串⻓度是否为 0,为 0 返回 true。 [ -z $a ] 返回 false。
-n 检测字符串⻓度是否为 0,不为 0 返回 true。 [ -n $a ] 返回 true。
str 检测字符串是否为空,不为空返回 true。 [ $a ] 返回 true。

『示例源码』

x="abc"
y="xyz"

echo "x=${x}, y=${y}"

if [[ ${x} == ${y} ]]; then
echo "${x} = ${y} : x 等于 y"
else
echo "${x} = ${y}: x 不等于 y"
fi

if [[ ${x} != ${y} ]]; then
echo "${x} != ${y} : x 不等于 y"
else
echo "${x} != ${y}: x 等于 y"
fi

if [[ -z ${x} ]]; then
echo "-z ${x} : 字符串⻓度为 0"
else
echo "-z ${x} : 字符串⻓度不为 0"
fi

if [[ -n "${x}" ]]; then
echo "-n ${x} : 字符串⻓度不为 0"
else
echo "-n ${x} : 字符串⻓度为 0"
fi

if [[ ${x} ]]; then
echo "${x} : 字符串不为空"
else
echo "${x} : 字符串为空"
fi

# Output:
# x=abc, y=xyz
# abc = xyz: x 不等于 y
# abc != xyz : x 不等于 y
# -z abc : 字符串⻓度不为 0
# -n abc : 字符串⻓度不为 0
# abc : 字符串不为空

6.6 文件测试运算符 √

⽂件测试运算符⽤于检测 Unix ⽂件的各种属性。
属性检测描述如下:

操作符 说明 举例
-b file 检测⽂件是否是块设备⽂件,如果是,则返回 true。 [ -b $file ] 返回 false。
-c file 检测⽂件是否是字符设备⽂件,如果是,则返回 true。 [ -c $file ] 返回 false。
-d file 检测⽂件是否是⽬录,如果是,则返回 true。 √ [ -d $file ] 返回 false。
-f file 检测⽂件是否是普通⽂件(既不是⽬录,也不是设备⽂件),如果 是,则返回 true。 √ [ -f $file ] 返回 true。
-g file 检测⽂件是否设置了 SGID 位,如果是,则返回 true。 [ -g $file ] 返回 false。
-k file 检测⽂件是否设置了粘着位(Sticky Bit),如果是,则返回 true。 [ -k $file ] 返回 false。
-p file 检测⽂件是否是有名管道,如果是,则返回 true。 [ -p $file ] 返回 false。
-u file 检测⽂件是否设置了 SUID 位,如果是,则返回 true。 [ -u $file ] 返回 false。
-r file 检测⽂件是否可读,如果是,则返回 true。 √ [ -r $file ] 返回 true。
-w file 检测⽂件是否可写,如果是,则返回 true。 √ [ -w $file ] 返回 true。
-x file 检测⽂件是否可执⾏,如果是,则返回 true。 √ [ -x $file ] 返回 true。
-s file 检测⽂件是否为空(⽂件⼤⼩是否⼤于 0),不为空返回 true。 [ -s $file ] 返回 true。
-e file 检测⽂件(包括⽬录)是否存在,如果是,则返回 true。 √ [ -e $file ] 返回 true。
-L file 检测⽂件(包括⽬录)是否为符号链接(软链接),如果是,则返回 true。 √ [ -L $file ] 返回 true。

『示例源码』

file="/etc/hosts"

if [[ -r ${file} ]]; then
echo "${file} ⽂件可读"
else
echo "${file} ⽂件不可读"
fi

if [[ -w ${file} ]]; then
echo "${file} ⽂件可写"
else
echo "${file} ⽂件不可写"
fi

if [[ -x ${file} ]]; then
echo "${file} ⽂件可执⾏"
else
echo "${file} ⽂件不可执⾏"
fi

if [[ -f ${file} ]]; then
echo "${file} ⽂件为普通⽂件"
else
echo "${file} ⽂件为特殊⽂件"
fi

if [[ -d ${file} ]]; then
echo "${file} ⽂件是个⽬录"
else
echo "${file} ⽂件不是个⽬录"
fi

if [[ -s ${file} ]]; then
echo "${file} ⽂件不为空"
else
echo "${file} ⽂件为空"
fi

if [[ -e ${file} ]]; then
echo "${file} ⽂件存在"
else
echo "${file} ⽂件不存在"
fi

# Output:(根据⽂件的实际情况,输出结果可能不同)
# /etc/hosts ⽂件可读
# /etc/hosts ⽂件可写
# /etc/hosts ⽂件不可执⾏
# /etc/hosts ⽂件为普通⽂件
# /etc/hosts ⽂件不是个⽬录
# /etc/hosts ⽂件不为空
# /etc/hosts ⽂件存在

七、控制语句

7.1 条件语句

跟其它程序设计语⾔⼀样, Bash 中的条件语句让我们可以决定⼀个操作是否被执⾏。结果取决于⼀个包在 [[ ]]
⾥的表达式。

[[ ]] ( sh 中是 [ ] )包起来的表达式被称作 检测命令基元。这些表达式帮助我们检测⼀个条件的结果。这⾥可以找到有关bash 中单双中括号区别的答案。

在 Bash Shell 中,⽅括号 ("[]") 和双⽅括号 ("[[]]") ⽤于条件测试和模式匹配。⽅括号和双⽅括号在⼤多数情况下是等效的,但是双⽅括号提供了更多的功能和可读性。

具体来说,下⾯是它们的区别:

  1. 双⽅括号⽀持⾼级字符串操作,⽐如 =~ 正则匹配和 == 不区分⼤⼩写的字符串⽐较,⽽⽅括号不⽀持这些
    操作。
  2. 双⽅括号中的变量不需要使⽤双引号引起来,即使变量值中包含空格或其他特殊字符也可以正确⽐较。但⽅括
    号中的变量需要使⽤双引号引起来,否则可能会导致错误的⽐较结果。
  3. 双⽅括号⽀持多个条件的逻辑与和逻辑或,⽐如 [[ $name == "John" && $age -eq 20 ]] ,⽽⽅括号则
    需要使⽤ -a 和 -o 选项实现这个功能。
  4. 双⽅括号中的参数扩展(Parameter expansion)和命令替换(Command substitution)需要使⽤转义字符
    " \ ",⽽⽅括号则可以直接使⽤。
  5. 双⽅括号中的模式匹配,默认情况下不进⾏⽂件名扩展,⽽⽅括号默认进⾏⽂件名扩展。
  6. 双⽅括号的错误处理更友好,可以使⽤ set -o errexit 命令打开错误检查功能(如果任何⼀个⼦命令返回
    ⾮零退出码,则整个条件测试就会失败),⽽⽅括号不⽀持这个功能。

双⽅括号提供了更多的功能和更直观的语法,所以⼀般建议在 Bash Shell 中使⽤双⽅括号进⾏条件测试。

共有两个不同的条件表达式: ifcase

if

(1) if 语句
if 在使⽤上跟其它语⾔相同。如果中括号⾥的表达式为真,那么thenfi之间的代码会被执⾏。fi标志着条
件代码块的结束。

# 写成⼀⾏
if [[ 1 -eq 1 ]]; then
echo "1 -eq 1 result is: true";
fi
# Output: 1 -eq 1 result is: true

# 写成多⾏
if [[ "abc" -eq "abc" ]]
then
echo ""abc" -eq "abc" result is: true"
fi
# Output: abc -eq abc result is: true

(2) if else 语句

同样,我们可以使⽤ if..else 语句,例如:

if [[ 2 -ne 1 ]]; then
echo "true"
else
echo "false"
fi
# Output: true

(3) if elif else 语句
有些时候, if..else 不能满⾜我们的要求。别忘了 if..elif..else ,使⽤起来也很⽅便。

『示例源码』

x=10
y=20
if [[ ${x} > ${y} ]]; then
echo "${x} > ${y}"
elif [[ ${x} < ${y} ]]; then
echo "${x} < ${y}"
else
echo "${x} = ${y}"
fi
# Output: 10 < 20

case

如果你需要⾯对很多情况,分别要采取不同的措施,那么使⽤ case 会⽐嵌套的 if 更有⽤。使⽤ case 来解决复杂
的条件判断,看起来像下⾯这样:
『示例源码』

x=10
y=20
oper=$1
case ${oper} in
"+")
val=`expr ${x} + ${y}`
echo "${x} + ${y} = ${val}"
;;
"-")
val=`expr ${x} - ${y}`
echo "${x} - ${y} = ${val}"
;;
"*")
val=`expr ${x} \* ${y}`
echo "${x} * ${y} = ${val}"
;;
"/")
val=`expr ${x} / ${y}`
echo "${x} / ${y} = ${val}"
;;
*)
echo "Unknown oper!"
;;
esac

每种情况都是匹配了某个模式的表达式。 | ⽤来分割多个模式, ) ⽤来结束⼀个模式序列。第⼀个匹配上的模式对应的命令将会被执⾏。 * 代表任何不匹配以上给定模式的模式。命令块⼉之间要⽤ ;; 分隔。

# sh cal.sh '+'
10 + 20 = 30
# sh cal.sh '-'
10 - 20 = -10
# sh cal.sh '/'
10 / 20 = 0
# sh cal.sh '%'
Unknown oper!

7.2 循环语句

循环其实不⾜为奇。跟其它程序设计语⾔⼀样, bash 中的循环也是只要控制条件为真就⼀直迭代执⾏的代码块。
Bash 中有四种循环: forwhileuntilselect

for 循环

for 与它在 C 语⾔中循环⾮常像。看起来是这样:

for arg in elem1 elem2 ... elemN
do
	### 语句
done

在每次循环的过程中, arg 依次被赋值为从 elem1 elemN 。这些值还可以是通配符或者⼤括号扩展。
当然,我们还可以把 for 循环写在⼀⾏,但这要求do之前要有⼀个分号,就像下⾯这样:

for i in {1..5}; do echo $i; done

还有,如果你觉得 for..in..do 对你来说有点奇怪,那么你也可以像 C 语⾔那样使⽤ for ,⽐如:

for (( i = 0; i < 10; i++ )); do
	echo $i
done

当我们想对⼀个⽬录下的所有⽂件做同样的操作时, for 就很⽅便了。举个例⼦,如果我们想把所有的 .sh ⽂件移
动到 script ⽂件夹中,并给它们可执⾏权限,我们的脚本可以这样写:

『示例源码』

DIR=/home/yuan
for FILE in ${DIR}/*.sh; do
/bcp "$FILE" "${DIR}/scripts"
done

# 将 /home/yuan ⽬录下所有 sh ⽂件拷⻉到 /home/yuan/scripts

#cp 加绝对路径 或者 前面 \cp 去覆盖

while 循环

while 循环检测⼀个条件,只要这个条件为 真,就执⾏⼀段命令。被检测的条件跟 if..then 中使⽤的基元并⽆⼆异。因此⼀个 while 循环看起来会是这样:

while [[ condition ]]
do
	### 语句
done

for 循环⼀样,如果我们把 do 和被检测的条件写到⼀⾏,那么必须要在do之前加⼀个分号。

『示例源码』

### 0到9之间每个数的平⽅
x=0
while [[ ${x} -lt 10 ]]; do
	echo $((x * x))
	x=$((x + 1))
done
# Output:
# 0
# 1
# 4
# 9
# 16
# 25
# 36
# 49
# 64
# 81

⽆限循环

while [ 1 ]; do
	#监控某个服务
	ps aux |egrep sshd >/dev/null
	#服务挂掉了
	if [[ $? -ne 0 ]];then
		system start sshd
	fi
	echo "ssh service up"
	sleep 30
done

until 循环

until 循环跟 while 循环正好相反。它跟 while ⼀样也需要检测⼀个测试条件,但不同的是,只要该条件为 假 就⼀直执⾏循环:

『示例源码』

x=0
until [[ ${x} -ge 5 ]]; do
	echo ${x}
	x=`expr ${x} + 1`
done
# Output:
# 0
# 1
# 2
# 3
# 4

select 循环

select 循环帮助我们组织⼀个⽤户菜单。它的语法⼏乎跟for循环⼀致:

select answer in elem1 elem2 ... elemN
do
	### 语句
done

select 会打印`` elem1..elemN 以及它们的序列号到屏幕上,之后会提示⽤户输⼊。通常看到的是 $? PS3 变量)。⽤户的选择结果会被保存到 answer 中。如果 answer 是⼀个在 1..N 之间的数字,那么 语句 会被执⾏,紧接着会进⾏下⼀次迭代 —— 如果不想这样的话我们可以使⽤ break` 语句。

『示例源码』

#!/usr/bin/env bash

PS3="Choose the package manager: "
select ITEM in bower npm gem pip
do
echo -n "Enter the package name: " && read PACKAGE
case ${ITEM} in
bower)
echo "bower install ${PACKAGE}" ;;
npm)
echo "npm install ${PACKAGE}" ;;
gem)
echo "gem install ${PACKAGE}" ;;
pip)
echo " pip install ${PACKAGE}" ;;
esac
break # 避免⽆限循环
done

这个例⼦,先询问⽤户他想使⽤什么包管理器。接着,⼜询问了想安装什么包,最后执⾏安装操作。
运⾏这个脚本,会得到如下输出:

$ ./select.sh
1) bower
2) npm
3) gem
4) pip
Choose the package manager: 2
Enter the package name: gitbook-cli

breakcontinue

如果想提前结束⼀个循环或跳过某次循环执⾏,可以使⽤ shell 的 breakcontinue 语句来实现。它们可以在任何循环中使⽤。

break 语句⽤来提前结束当前循环。
continue 语句⽤来跳过某次迭代。

『示例源码』

# 查找 10 以内第⼀个能整除 2 和 3 的正整数
i=1
while [[ ${i} -lt 10 ]]; do
if [[ $((i % 3)) -eq 0 ]] && [[ $((i % 2)) -eq 0 ]]; then
echo ${i}
break;
fi
i=`expr ${i} + 1`
done
# Output: 6

『示例源码』

# 打印10以内的奇数
for (( i = 0; i < 10; i ++ )); do
if [[ $((i % 2)) -eq 0 ]]; then
continue;
fi
echo ${i}
done
# Output:
# 1
# 3
# 5
# 7
# 9

⼋、函数

bash 函数定义语法如下:

[ function ] funname [()] {
	action;
	[return int;]
}

说明:

  1. 函数定义时,function关键字可有可⽆。
  2. 函数返回值 - return 返回函数返回值,返回值类型只能为整数(0-255)。如果不加 return 语句, shell
    默认将以最后⼀条命令的运⾏结果,作为函数返回值。
  3. 函数返回值在调⽤该函数后通过 $? 来获得。
  4. 所有函数在使⽤前必须定义。这意味着必须将函数放在脚本开始部分,直⾄ shell 解释器⾸次发现它
    时,才可以使⽤。调⽤函数仅使⽤其函数名即可。

『示例源码』

#!/usr/bin/env bash

calc(){
PS3="choose the oper: "

select oper in + - \* / # ⽣成操作符选择菜单
do
echo -n "enter first num: " && read x # 读取输⼊参数
echo -n "enter second num: " && read y # 读取输⼊参数
exec
case ${oper} in
"+")
return $((${x} + ${y}))
;;
"-")
return $((${x} - ${y}))
;;
"*")
return $((${x} * ${y}))
;;
"/")
return $((${x} / ${y}))
;;
*)
echo "${oper} is not support!"
return 0
;;
esac
break
done
}
calc
echo "the result is: $?" # $? 获取 calc 函数返回值

执⾏结果:

$ ./function-demo.sh
1) +
2) -
3) *
4) /
choose the oper: 3
enter first num: 10
enter second num: 10
the result is: 100

尝试将上⾯的select修改成while循环实现 菜单可以通过cat 实现

#!/bin/bash

echo "Welcome to My Menu"
echo "------------------"
cat << EndOfMenu
1. Option 1
2. Option 2
3. Option 3
4. Quit
EndOfMenu

while true; do
read -p "Please select an option: " option
case $option in
1) echo "You selected Option 1";;
2) echo "You selected Option 2";;
3) echo "You selected Option 3";;
4) echo "Quitting..."; exit;;
*) echo "Invalid option. Please try again.";;
esac
done

8.1 函数中如何使用位置参数 √

位置参数是在调⽤⼀个函数并传给它参数时创建的变量。

位置参数变量表:

变量 描述
$0 脚本名称
$1 … $9 第 1 个到第 9 个参数列表
${10} … $ 第 10 个到 N 个参数列表
$* or $@ 除了 $0 外的所有位置参数
$# | 不包括 $0 在内的位置参数的个数
$FUNCNAME 函数名称(仅在函数内部有值)

『示例源码』

#!/usr/bin/env bash
# -n 参数长度不为0返回
x=0
if [[ -n $1 ]]; then
echo "第⼀个参数为: $1"
x=$1
else
echo "第⼀个参数为空"
fi

y=0
if [[ -n $2 ]]; then
echo "第⼆个参数为: $2"
y=$2
else
echo "第⼆个参数为空"
fi

paramsFunction(){
echo "函数第⼀个⼊参: $1"
echo "函数第⼆个⼊参: $2"
}
paramsFunction ${x} ${y}

执⾏结果:

$ ./function-demo2.sh
第⼀个参数为空
第⼆个参数为空
函数第⼀个⼊参: 0
函数第⼆个⼊参: 0

$ ./function-demo2.sh 10 20
第⼀个参数为: 10
第⼆个参数为: 20
函数第⼀个⼊参: 10
函数第⼆个⼊参: 20

执⾏ ./variable-demo4.sh hello world ,然后在脚本中通过 $1$2 … 读取第 1 个参数、第 2 个参
数。。。

8.2 函数处理参数

另外,还有⼏个特殊字符⽤来处理参数:

参数处理 说明
$# 返回参数个数
$* 返回所有参数
$$ 脚本运⾏的当前进程 ID 号
$! 后台运⾏的最后⼀个进程的 ID 号
$@ 返回所有参数
$- 返回 Shell 使⽤的当前选项,与 set 命令功能相同。
$? 函数返回值

『示例源码』

runner() {
return 0
}

name=wing
paramsFunction(){
echo "函数第⼀个⼊参: $1"
echo "函数第⼆个⼊参: $2"
echo "传递到脚本的参数个数: $#"
echo "所有参数: "
printf "+ %s\n" "$*"
echo "脚本运⾏的当前进程 ID 号: $$"
echo "后台运⾏的最后⼀个进程的 ID 号: $!"
echo "所有参数: "
printf "+ %s\n" "$@"
echo "Shell 使⽤的当前选项: $-"
runner
echo "runner 函数的返回值: $?"
}
paramsFunction 1 "abc" "hello, \"yuan\""

# Output:
# 函数第⼀个⼊参: 1
# 函数第⼆个⼊参: abc
# 传递到脚本的参数个数: 3
# 所有参数:
# + 1 abc hello, "yuan"
# 脚本运⾏的当前进程 ID 号: 26400
# 后台运⾏的最后⼀个进程的 ID 号:
# 所有参数:
# + 1
# + abc
# + hello, "yuan"
# Shell 使⽤的当前选项: hB
# runner 函数的返回值: 0


# ps:himBH 每个字母都代表了一个 shell 选项,具体如下
# h - hashall:bash 的 hash 功能,可以实现让某些 command 和 具体路径 绑定在一起。      
# i - interactive-comments:配置在交互 shell 模式下,是否允许注释。	 
# m - monitor:配置是否打开控制 Job control 功能。        	 
# B - braceexpand:关于括号使用的flag,打开后可以快捷地实现某些效果    	 
# H-  history:是否允許用 “感叹号 !+ history number ” 来执行历史命令      


九、 Shell 扩展

扩展 发⽣在⼀⾏命令被分成⼀个个的 记号(tokens) 之后。换⾔之,扩展是⼀种执⾏数学运算的机制,还可以⽤
来保存命令的执⾏结果,等等。

感兴趣的话可以阅读关于 shell 扩展的更多细节

大括号扩展

⼤括号扩展让⽣成任意的字符串成为可能。它跟 ⽂件名扩展 很类似,举个例⼦:

echo beg{i,a,u}n ### begin began begun

[root@localhost shell]# echo beg{i,a,u}n | xargs -n1
begin
began
begun

⼤括号扩展还可以⽤来创建⼀个可被循环迭代的区间。

echo {0..5} ### 0 1 2 3 4 5
echo {00..8..2} ### 00 02 04 06 08

命令置换

命令置换允许我们对⼀个命令求值,并将其值置换到另⼀个命令或者变量赋值表达式中。当⼀个命令被 或 $()
围时,命令置换将会执⾏。举个例⼦:

now=`date +%T`
### or
now=$(date +%T)

echo $now ### 19:08:26

算数扩展

在 bash 中,执⾏算数运算是⾮常⽅便的。算数表达式必须包在 $(( )) 中。算数扩展的格式为 :

result=$(( ((10 + 5*3) - 7) / 2 ))
echo $result ### 9

在算数表达式中,使⽤变量⽆需带上 $ 前缀:

x=4
y=7
echo $(( x + y )) ### 11
echo $(( ++x + y++ )) ### 12
echo $(( x + y )) ### 13

十、流和重定向

Bash 有很强⼤的⼯具来处理程序之间的协同⼯作。使⽤流,我们能将⼀个程序的输出发送到另⼀个程序或⽂件,
因此,我们能⽅便地记录⽇志或做⼀些其它我们想做的事。

管道给了我们创建传送带的机会,控制程序的执⾏成为可能。

学习如何使⽤这些强⼤的、⾼级的⼯具是⾮常⾮常重要的。

10.1 输入、输出流

Bash 接收输⼊,并以字符序列或 字符流 的形式产⽣输出。这些流能被重定向到⽂件或另⼀个流中。

有三个⽂件描述符:

代码 描述符 描述
0 stdin 标准输⼊
1 stdout 标准输出
2 stderr 标准错误输出

10.2 重定向

重定向让我们可以控制⼀个命令的输⼊来⾃哪⾥,输出结果到什么地⽅。这些运算符在控制流的重定向时会被⽤到:

Operator Description
> 重定向输出
&> 重定向输出和错误输出
&>> 以附加的形式重定向输出和错误输出
< 重定向输⼊
<< Here ⽂档 语法
<<< Here 字符串

以下是⼀些使⽤重定向的例⼦:

### ls的结果将会被覆盖写到list.txt中
ls -l > list.txt

### 将输出追加附加到list.txt中
ls -a >> list.txt

### 所有的错误信息会被写到errors.txt中
grep da * 2> errors.txt

### 从errors.txt中读取输⼊
less < errors.txt

10.3 /dev/null ⽂件 √

如果希望执⾏某个命令,但⼜不希望在屏幕上显示输出结果,那么可以将输出重定向到 /dev/null:

$ command > /dev/null

/dev/null 是⼀个特殊的⽂件,写⼊到它的内容都会被丢弃;如果尝试从该⽂件读取内容,那么什么也读不到。但
是 /dev/null ⽂件⾮常有⽤,将命令的输出重定向到它,会起到”禁⽌输出”的效果。

如果希望屏蔽 stdout 和 stderr,可以这样写:

$ command > /dev/null 2>&1

十一、 Debug

shell 提供了⽤于 debug 脚本的⼯具。一般 set -x

如果想采⽤ debug 模式运⾏某脚本,可以在其中使⽤⼀个特殊的选项:

#!/bin/bash options

options 是⼀些可以改变 shell ⾏为的选项。下表是⼀些可能对你有⽤的选项:

Short Name Description
-f noglob 禁⽌⽂件名展开(globbing)
-i interactive 让脚本以 交互 模式运⾏
-n noexec 读取命令,但不执⾏(语法检查)
-t 执⾏完第⼀条命令后退出
-v verbose 在执⾏每条命令前,向 stderr 输出该命令
-x xtrace 在执⾏每条命令前,向 stderr 输出该命令以及该命令的扩展参数 √

举个例⼦,如果我们在脚本中指定了 -x 例如:

#!/bin/bash -x

for (( i = 0; i < 3; i++ )); do
	echo $i
done

这会向 stdout 打印出变量的值和⼀些其它有⽤的信息:

$ ./debug.sh
+ (( i = 0 ))
+ (( i < 3 ))
+ echo 0
0
+ (( i++ ))
+ (( i < 3 ))
+ echo 1
1
+ (( i++ ))
+ (( i < 3 ))
+ echo 2
2
+ (( i++ ))
+ (( i < 3 ))

有时我们值需要 debug 脚本的⼀部分。这种情况下,使⽤ set 命令会很⽅便。这个命令可以启⽤或禁⽤选项。使⽤ - 启⽤选项, + 禁⽤选项:

『示例源码』

# 开启 debug
set -x
for (( i = 0; i < 3; i++ )); do
printf ${i}
done
# 关闭 debug
set +x
# Output:
# + (( i = 0 ))
# + (( i < 3 ))
# + printf 0
# 0+ (( i++ ))
# + (( i < 3 ))
# + printf 1
# 1+ (( i++ ))
# + (( i < 3 ))
# + printf 2
# 2+ (( i++ ))
# + (( i < 3 ))
# + set +x

for i in {1..5}; do printf ${i}; done
printf "\n"
# Output: 12345

11.1 Shell脚本常用方法总结 √

#!/bin/bash
#Name: by yuan

#设置下载地址
URL='https://cdn.mysql.com//Downloads/MySQL-5.6/mysql-5.6.38.tar.gz'

#设置安装⽬录
PREFIX=/usr/local/$(basename $(echo " ${URL%.tar.gz}"))
# 截取左边字符串为 https://cdn.mysql.com//Downloads/MySQL-5.6/mysql-5.6.38
# basename 命令后为 mysql-5.6.38

#设置数据⽬录
DATADIR=/usr/local/$(basename ${URL%.tar.gz})/data

#提示⻛格
启动: Starting sshd:
停⽌: Stopping sshd:
成功: SUCESS
失败: FAILED

#主程序
PROG=/bin/cp

#进程号
PID="./$PROG.pid"

#家⽬录
HOME=/home/mysql

#取得当前⽬录的绝对路径
$(dirname `readlink -f $0`)

经过试验,这种⽅法最稳妥,因为它能追踪软连接到真实的⽬录,就算⾃身被软连接也不影响使⽤。

#检测root权限
[ $(id -u) != "0" ] && echo "Error: You must be root to run this script" && exit
# id -u 当前用户的uid ,不为0则无权限退出 

#检测是否已经安装过
[ -f $PREFIX/bin/mysqld_safe ] && echo "Error:MY-SQL Has been installed" && exit

#安装EPEL源
if hostnamectl &>/dev/null; then
[[ -f /etc/yum.repos.d/epel.repo ]] || curl -o /etc/yum.repos.d/epel.repo
http://mirrors.aliyun.com/repo/epel-7.repo
else
[[ -f /etc/yum.repos.d/epel.repo ]] || curl -o /etc/yum.repos.d/epel.repo
http://mirrors.aliyun.com/repo/epel-6.repo
fi

#下载函数
#调⽤: download <tar.gz的⽂件链接>,调⽤结果: 1:下载并解压⽂件到当前⽬录, 2:得到⽂件名称
#功能:检测URL是否正确、本地⽂件存在不再重复下载,本地⽬录存在不再解压

function download() {
# 先判断文件是否存在(-f 是否是普通文件)
if [[ -f ${1##*/} ]] ;then
# -d 是否是目录
FILE=${1##*/} ; [[ -d ${FILE%.tar.gz} ]] || tar vxf ${1##*/}
else
[[ $1 != *.tar.gz ]] && echo 'URL Check Failed' && exit
curl -o ${1##*/} $1 && tar vxf ${1##*/}
fi
}

# 编译函数
function Construct() {
...
}

# 下载源码并进⼊⽬录
download $URL && cd ${FILE%.tar.gz} || exit 1
# exit 0 代表正常运行程序并退出程序
# exit 1 代表非正常运行导致退出程序

#编译安装
Construct && make && make install || exit 1

#显示成功的样式
function SUCESS() {
echo -e "$*\033[59G[\033[32;1m OK \033[0m]"
}

# 显示失败的样式
function FAILED() {
echo -e "$*\033[59G[\033[1;31m FAILED \033[0m]"
}

#在当前⽤户添加⼀个定时任务
#作⽤:添加⼀个定时任务
#优点:免交互,⽆需root权限,防重复添加
NOTE='#logs clean'
TASK='00 02 * * * /bin/bash /app/yunwei/logs_clean.sh >/dev/null 2>/dev/null'
crontab -l | fgrep -q "${NOTE}" || echo -e "`crontab -l`\n${NOTE}" | crontab -
crontab -l | fgrep -q "${TASK}" || echo -e "`crontab -l`\n${TASK}" | crontab -

#启动脚本菜单
case "$1" in
start)
starting ;;
stop)
stopping ;;
restart)
stopping && sleep 5 ; starting ;;
status)
statusing ;;
*)
echo "Usage: $0 {start|stop|restart|status}" ;;
esac

11.2 日志封装函数 √

#⽇志函数封装

function wirelog() {
level=$1 # ⽇志级别
message=$2 # ⽇志信息
timestamp=$(date +"%Y-%m-%d %T") # 时间戳

# 根据⽇志级别选择输出颜⾊
case $level in
"error")
color='\033[0;31m'
;;
"warning")
color='\033[0;33m'
;;
"info")
color='\033[0;36m'
;;
*)
color='\033[0m' # 默认颜⾊
;;
esac

# 格式化⽇志输出
echo -e "$timestamp | $color$level$color | $message\033[0m"
}
echo \033[0;36m'"info" '\033[0;36m'
# 使⽤示例
wirelog 'info' 'This is an info message!'
wirelog 'warning' 'This is a warning message!'
wirelog 'error' 'This is an error'

11.3 获取随机字符串或数字

⽅法 1:
# echo $RANDOM |md5sum |cut -c 1-8 #471b94f2
⽅法 2:
# openssl rand -base64 4 #vg3BEg==
⽅法 3:
# cat /proc/sys/kernel/random/uuid |cut -c 1-8 ed9e032c

获取随机 8 位数字

⽅法 1:
# echo $RANDOM |cksum |cut -c 1-8 #23648321
⽅法 2:
# openssl rand -base64 4 |cksum |cut -c 1-8 38571131
⽅法 3:
# date +%N |cut -c 1-8 69024815

十二、 Shell 脚本编写实战 √

12.1 软件安装

nginx安装脚本

#!/bin/bash
#Description: nginx install script from yuan
#Release: 1.0
#Auther: yuan
#Email:
#OS: Centos 7

cat <<EOF
#Description: nginx install script
#!/bin/bash
#Description: nginx install script from yuan
#Release: 1.0
#Auther: yuan
#Email:
#OS: Centos 7
EOF
#Description: nginx install script

#nginx源码包下载路径
nginx_pkg_url=http://nginx.org/download/nginx-1.17.8.tar.gz

#nginx安装路径,安装路径为$nginx_install_doc/nginx
nginx_install_doc=/usr/local

#nginx服务管理⽤户
nginx_manage_user=www

#统计本机CPU核数
cpu_count=`grep -c "flags" /proc/cpuinfo`

check () {

#安装nginx需要管理员权限
[ $UID -ne 0 ] && echo "need to be root so that" && exit 1

#安装前的依赖包解决
#wget 命令
#gcc 编译命令
#pcre-devel URL重写功能
#zlib-devel 压缩⽀持
#make 编译命令

if ! (yum -y install wget gcc pcre-devel zlib-devel make &>/dev/null);then
echo "yum install soft package fail"
exit 1
fi

if ! (egrep "^www" /etc/passwd &>/dev/null);then
useradd -s /sbin/nologin -r -M www
fi

}

nginx_install () {
#1、下载软件包
if wget $nginx_pkg_url &>/dev/null;then
#2、解压软件包
echo $nginx_pkg_url|awk -F "/" '{print $5}'|xargs tar xf
nginx_source_doc=`echo $nginx_pkg_url|awk -F "/" '{print $5}'|cut -d "." -f 1-3`
#3、进⼊软件包
if [ ! -d $nginx_source_doc ];then
echo "unzip `echo $nginx_pkg_url|awk -F "/" '{print $5}'` fail"
exit 1
fi

cd $nginx_source_doc

#4、 configure nginx
./configure --prefix=$nginx_install_doc/nginx --user=$nginx_manage_user --group=$nginx_manage_user 1>/dev/null
[ $? -ne 0 ]&&echo "nginx configure fail"&&exit 1

#5、 make nginx
make -j $cpu_count 1>/dev/null
[ $? -ne 0 ]&&echo "nginx make fail"&&exit 1

#6、 install nginx
make install 1>/dev/null
[ $? -ne 0 ]&&echo "nginx install fail"&&exit 1||echo "`clear`nginx install
success..."

#7、 delete nginx soft package
#cd ..
#rm -rf ${nginx_source_doc}*

else
echo "$nginx_pkg_url download fail"
exit 1
fi

}

#####callable function
check
nginx_install

# sh nginx_install.sh
#Description: nginx install script
#Release: 1.0
#Auther: yuan
#Email:
#OS: Centos 7
nginx install success...

12.2 检查服务状态

#!/bin/bash
#Description:
#Author: yuan
#Created Time:
#监控⼀个服务端⼝

#main

temp_file=`mktemp port_status.XXX`
# mktemp 创建临时文件的命令

#1、判断依赖命令telnet是否存在
[ ! -x /usr/bin/telnet ]&& echo "telnet: not found command"&& exit 1

#2、测试端⼝ $1 IP $2 port
( telnet $1 $2 <<EOF
quit
EOF
) &>$temp_file

#3、分析⽂件中的内容,判断结果
if egrep "\^]" $temp_file &>/dev/null;then

#4、打印结果
echo "$1 $2 is open"
else
echo "$1 $2 is close"
fi

#5、删除临时⽂件
rm -f $temp_file

# sh check_service.sh 127.0.0.1 5522
127.0.0.1 5522 is open

[root@localhost shellfile]# sh check_service.sh www.baidu.com 80
www.baidu.com 80 is open

()是Shell中的⼦shell语法。⼦shell是⼀个独⽴的Shell环境,可以在当前Shell进程中运⾏,并且可以在其中执
⾏命令和操作,⽽不会影响⽗Shell进程中的环境和变量。在这个例⼦中, ()中的命令将在⼦shell中运⾏,并
且这个⼦shell的输出将被重定向到指定的⽂件中。

监控⽅法:

  1. 通过systemctl status service 服务启动状态
  2. lsof 查看端⼝是否存在
  3. 查看进程是否存在
  4. 测试端⼝是否有响应 推荐telnet 协议

12.3 检查主机存活状态

脚本思路

1、通过ICMP协议的ping命令ping⽬标主机

⽹络延迟,假报警如何解决?
3次

2、分析ping结果

3、给出结论

全部ping结果为假,报宕机
全部ping成功,报正常
否则报警告

#!/bin/bash
#
#Author: yuan
#Created Time:
#Release:
#Description:

#1、 ping ⽬标主机三次,并接收每次的状态值,ping成功返回1,不成功返回0
for ((i=1;i<4;i++));do
#测试代码
if ping -c1 $1 &>/dev/null;then
#分析结果
export ping_count"$i"=1
else
export ping_count"$i"=0
fi
#时间间隔
sleep 0.3
done

#3、分析结果
# 3次ping失败报警
#
if [ $ping_count1 -eq $ping_count2 ] && [ $ping_count2 -eq $ping_count3 ] && [ $ping_count1 -eq 0 ];then
echo "$1 宕机"
elif [ $ping_count1 -eq $ping_count2 ] && [ $ping_count2 -eq $ping_count3 ] && [ $ping_count1 -eq 1 ];then
echo "$1 正常"
else
echo "warn: $1 ⽹络延迟"
fi

#4、释放变量
unset ping_count1
unset ping_count2
unset ping_count3

[root@localhost shellfile]# sh ping.sh www.baidu.com
www.baidu.com 正常

12.4 监控 CPU、内存和硬盘利用率

cpu使⽤率⼤于80告警

#!/bin/bash

# 获取当前的 CPU 使⽤率,并以整数形式输出
usage=$(top -bn1 | grep "Cpu(s)" | awk '{print $2 + $4}' | cut -d'.' -f1)
# 批处理显示一次更新,打印"Cpu(s)"这行 用户态+内核态 的变量,保留整数

# 判断 CPU 使⽤率是否⼤于 80%
if [ $usage -gt 80 ]; then
# 发送告警信息
echo "CPU usage is over 80%!"
# 在这⾥可以添加发送告警信息的代码
else
echo "CPU usage is normal."
fi

# sh cpu_usage.sh
CPU usage is normal. value:4

内存使⽤率监控

#!/bin/bash
#
#Author: yuan
#Created Time:
#Release:
#Description:内存使⽤率计算脚本

#1、通过free命令结合数据处理获得对应数据
#1.1、获得内存总量
memory_total=`free -m|grep -i "mem"|tr -s " "|cut -d " " -f2`
# tr -s 把连续重复的字符以单独一个字符表示
swap_total=`free -m|grep -i "swap"|tr -s " "|cut -d " " -f2`
#1.2、获得内存使⽤的量
memory_use=`free -m|grep -i "mem"|tr -s " "|cut -d " " -f3`
swap_use=`free -m|grep -i "swap"|tr -s " "|cut -d " " -f3`
#1.3、 buff/cache
buff_cache=`free -m|grep -i "mem"|tr -s " "|cut -d " " -f6`

#2、计算输出
#运算的时候是否需要⼩数点 浮点运算,要考虑使⽤的命令 (难点 重点)
#echo "内存使⽤率: $((memory_use*100/memory_totle))%"
#难点:浮点运算中,同优先级的情况下,⼤数除以⼩数 尽可能保证精确
echo "内存使⽤率: `echo "$memory_use*100/$memory_total"|bc`%,buff&cache:$buff_cache MB"
echo "Swap使⽤率: `echo "$swap_use*100/$swap_total"|bc`%"
# bc算数使用工具

# sh mem_usage.sh
内存使⽤率: 76%,buff&cache:3144 MB
Swap使⽤率: 26%

磁盘监控

#!/bin/bash
#
#Author: yuan
#Created Time:
#Release:
#Description:

#指定for条件的分割符为回⻋,⼀⾏⼀个条件
IFS=$'\n'

#1、遍历符合条件的每⼀⾏磁盘数据
for i in `df -Th|egrep -v "(tmpfs|sr0)"|tail -n +2|tr -s " "`;do
size=`echo $i|cut -d " " -f6|tr -d "%"`
name=`echo $i|cut -d " " -f1`

#2、判断每⾏中的磁盘使⽤率并输出结果
if [ $size -ge 95 ];then
#3、输出⽇志并关机,不许在写⼊数据
logger "ERROR:$name use is $size.halt"
#halt -p
elif [ $size -ge 90 ];then
echo -e "\033[31m $name use is ${size}%\033[0m"
elif [ $size -ge 80 ];then

echo -e "\033[33m $name use is ${size}%\033[0m"
else
echo -e "\033[32m$name use is ${size}%\033[0m"
fi
done

# sh disk_usage.sh
/dev/mapper/vg0-root use is 40%
/dev/mapper/vg0-usr use is 43%
/dev/sda1 use is 38%
/dev/mapper/vg0-opt use is 4%
/dev/mapper/vg0-app use is 64%
/dev/mapper/vg0-var use is 61%
overlay use is 64%
overlay use is 64%
overlay use is 64%
overlay use is 64%
overlay use is 64%

12.5 监控接口

监控服务的接⼝是否正常

#!/bin/bash
#
#Author: yuan
#Created Time:
#Release:
#Description: URL监控脚本
#监控阈值可以是: 状态码、⻚⾯字符串、⻚⾯所有内容
#本例以状态码为例

#variables
init_url_status=200
temp_file=`mktemp /tmp/check_url.XXX`

#help
if [ -z "$1" ]||[ "$1" == "--help" ];then
echo "$0 url"
echo "--help: 打印该帮助"
fi

#如果⽤户没有传参则退出
[ $# -lt 1 ]&&exit 1
#main

#1、使⽤curl命令访问⼀次URL
#1.1 判断脚本依赖命令是否存在
[ ! -x /usr/bin/curl ]&&echo "curl: not found command"&&exit 1

#1.2 访问⼀次URL
curl -I $1 &> $temp_file

#2、从输出中截取状态码
url_status=`grep "HTTP/1.1" $temp_file|cut -d " " -f2`

#2.1如果取值失败直接报错(测试发现当⽆法访问URL时会在第三步中报⽐较错误,所以这⾥取不到值就不往下⾛了)
[ -z "$url_status" ]&&echo -e "\033[31mstatus:NULL\033[0m"&&exit 1

#3、判断状态码是否和预设的⼀致
#3.1 ⼀致 输出绿⾊字体 "status:200"
#3.2 不⼀致 输出红⾊字体 "status:XXX"

if [ $init_url_status -eq $url_status ];then
echo -e "\033[32mstatus:$url_status\033[0m"
else
echo -e "\033[31mstatus:$url_status\033[0m"
fi

#4、删除临时⽂件
rm -f $temp_file

[root@localhost shellfile]# sh check.sh www.baidu.com
status:200

12.6 应用程序的启动脚本

go程序启动脚本

#!/bin/bash
#程序名称
PROG=goweb
PID=${PROG:+$PROG.pid}

#⽬录名改变,脚本被链接,仍然可以准确进⼊脚本所在⽬录
cd $(dirname $(readlink -f $0))

sucess() {
echo -e "$*\033[59G[\033[1;32m OK \033[0m]"
}
failed() {
echo -e "$*\033[59G[\033[1;31m FAILED \033[0m]"
}

#函数成功: PID不存在 || 进程不存在
#函数失败: PID⽂件不可写 || 进程已经存在
startup() { #
#函数成功:进程存在
#函数失败:进程不存在
start_process() {
nohup ./${PROG} & echo $! >${PID}
if sleep 2 ; kill -0 `<${PID}` 2>/dev/null ;then
sucess "Starting ${PROG}:"
else
failed "Starting ${PROG}:" ; exit 1
fi
}
if [[ -f ${PID} ]] ;then
if [[ -w ${PID} ]] ;then
if [[ -L /proc/`<${PID}`/exe ]] 2>/dev/null ;then
echo "${PROG} Already Running"
else
start_process
fi
else
echo 'PID File: Permission denied' ; exit 1
fi
else
start_process
fi
}

#函数成功: PID⽂件可写 && 进程存在 && 进程属于⾃⼰
#函数失败: PID⽂件不存在 || 进程不存在 || 进程不属于⾃⼰
shutdown() {
#函数成功:进程不存在
#函数失败:进程依然存在
stop_process() {
kill -15 `cat ${PID} 2>/dev/null`
if sleep 2 ; kill -0 `cat ${PID} 2>/dev/null` 2>/dev/null ;then
failed "Stopping ${PROG}:" ; exit 1
else
sucess "Stopping ${PROG}:"
rm -f ${PID}
fi
}
if [[ -f ${PID} ]] ;then
if [[ -w ${PID} ]] ;then
if [[ -L /proc/`<${PID}`/exe ]] 2>/dev/null ;then
if [[ -w /proc/`<${PID}`/mem ]] ;then
stop_process
else
echo "Process: Permission denied"
fi
else
echo 'Process: No such Process' ; rm -f ${PID}
fi
else
echo 'PID File: Permission denied' ; exit 1
fi
else
echo 'PID File: No such PID file'
fi
}

status() {
ps aux | awk '$11~'/${PROG}/
#netstat -tnlp 2>/dev/null | grep ${PROG} | column -t
if [[ -f ${PID} ]] ;then
if [[ -w ${PID} ]] ;then
if [[ -L /proc/`<${PID}`/exe ]] 2>/dev/null ;then
if [[ -w /proc/`<${PID}`/mem ]] ;then
#stop_process
netstat -nlp 2>/dev/null | grep `cat ${PID} 2>/dev/null` | column -t
else
echo "Process: Permission denied"
fi
else
echo 'Process: No such Process' ; rm -f ${PID}
fi
else
echo 'PID File: Permission denied' ; exit 1
fi
else
echo 'PID File: No such PID file'
fi
}

case "$1" in
start)
startup ;;
stop)
shutdown ;;
restart)
shutdown && sleep 5 && startup ;;
status)
status ;;
*)
echo "Usage: $0 {start|stop|restart|status}" ;;
esac

java启动脚本

#!/bin/bash
#这⾥可替换为你⾃⼰的执⾏程序,其他代码⽆需更改
APP_NAME=hello-0.0.1-SNAPSHOT.jar

#APP_DIR=./
APP_DIR=`pwd`
echo "当前路径"
echo $APP_DIR

#使⽤说明,⽤来提示输⼊参数
usage() {
echo "Usage: sh console.sh [start|stop|restart|status]"
exit 1
} 
#检查程序是否在运⾏
is_exist(){
pid=`ps -ef|grep $APP_NAME|grep -v grep|awk '{print $2}' `

echo "ps number is: ${pid}"
#如果不存在返回1,存在返回0
if [ -z "${pid}" ]; then
return 1
else
return 0
fi
} 

#启动⽅法
start(){
is_exist
if [ $? -eq "0" ]; then
echo "${APP_NAME} is already running. pid=${pid} ."
else
nohup java -jar $APP_DIR/$APP_NAME > $APP_DIR/log.out 2>&1 &
#nohup java -jar $APP_DIR/$APP_NAME
echo "${APP_NAME} start success"
fi
}

#
停⽌⽅法
stop(){
is_exist
if [ $? -eq "0" ]; then
kill -9 $pid
else
echo "${APP_NAME} is not running"
fi
}

#输出运⾏状态
status(){
is_exist
if [ $? -eq "0" ]; then
echo "${APP_NAME} is running. Pid is ${pid}"
else
echo "${APP_NAME} is NOT running."
fi
}

#重启
restart(){
stop
start
}

#根据输⼊参数,选择执⾏对应⽅法,不输⼊则执⾏使⽤说明
case "$1" in
"start")
start
;;
"stop")
stop
;;
"status")
status
;;
"restart")
restart
;;
*)
usage
;;
esac

#脚本编写完成后,可以使⽤命令sh xxx.sh执⾏,也可以使⽤./xxx.sh执⾏。
#sh xxx.sh与./xxx.sh区别
#sh xxx.sh 是不需要有执⾏权限
#./xxx.sh 是需要有执⾏权限的,可以通过 chmod +x xxx.sh 赋予权限。

# sh start.sh start
# 当前路径
/root/shell/java-hello
ps number is:
hello-0.0.1-SNAPSHOT.jar start success

12.7 mysql备份脚本

每天全量备份,逻辑备份,热备份。已用场景:数据量小于20G

!/bin/bash

# 定义备份⽂件名
DATE=`date +%Y%m%d%H%M%S`
FILENAME="backup_$DATE.sql"

# 定义MySQL连接参数
MYSQL_HOST="localhost"
MYSQL_PORT="3306"
MYSQL_USER="root"
MYSQL_PASSWORD="password"
MYSQL_DATABASE="mydatabase"

# 执⾏备份命令
mysqldump -h $MYSQL_HOST -P $MYSQL_PORT -u $MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DATABASE > $FILENAME

# 打印备份结果
if [ $? -eq 0 ]; then
echo "Database backup successful! Backup file: $FILENAME"
else
echo "Database backup failed!"
fi

十三、服务管理进程管理⼯具

Supervisor

推荐所有的模块服务启动使⽤Supervisor 进程管理⼯具或者systemd 进⾏管理

Supervisor进程管理 - 少年Dev - 博客园 (cnblogs.com)

十四、 shell可以做到什么

服务启动脚本

⾃动化部署脚本

数据库备份脚本

业务监控脚本

安全防护脚本

⼯具封装

等等

posted on 2023-07-20 15:50  暗狱碳水  阅读(15)  评论(0编辑  收藏  举报