最近终于放假了,当然修仙了一些时日。日子不能太水,最近再研究mc服务器的时候,觉得手动启动mc服务器实在太麻烦了,所以像写出来一个系统服务脚本来启动minecraft服务器,但是又没有shell编程的基础,我花了一天的时间研究了一下,现在把笔记誊写上来!

1. 认识shell

1.1 什么是shell

Shell是LINUX内核和用户之间的解释器程序,解释器有很多种,通常是指bash,负责向内核翻译以及传达用户/程序指令。Shell 既是一种命令语言,又是一种程序设计语言

image-20210713143128383

shell的使用方式:

  1. 交互指令模式 :人工输入命令,执行效率低
  2. 非交互指令模式:使用脚本,在后台执行,需要程序员编写

我们可以选择的解释器有很多,我们通常默认的就是bash,使用cat /etc/shells可以查看电脑上有哪几种shell解释器:

image-20210713145813428

我这里使用的是 zsh,可以说是个性话的bash解释器。如果我们向切换解释器只需要在,命令行中输入对应的解释器即可!输入 exit就可以退出当前shell解释器,我们登录linux系统后,进入的界面就是shell解释器,我们使用pstree查看进程树

image-20210713150519584

我采用的是 ssh链接到服务器,由于默认的解释器是zsh ,所以一进去系统就是zsh的交互界面。如果我们此时输入 exit 退出当前解释器,那么就会自动终端我们的ssh连接。

1.2 什么是shell脚本

标准脚本的构成:

  1. 声明解释器,一般我们声明bash解释器,兼容最好,一般机器上的默认解释器

    #!/bin/bash

  2. 注释 脚本介绍

  3. 具体代码 ,其实就是我们的shell命令,系统会一行一行的执行

    #! /bin/bash
    
    echo hello world
    echo shell脚本
    

如何执行shell脚本

  1. 输入绝对或者相对路径 执行 ,需要脚本具有可执行权限
  2. 输入 bash [脚本名/路径] ,重新开辟一个bash解释器,执行脚本,并且执行完毕后自动退出开辟的shell脚本。无需可执行权限
  3. 输入 source 脚本名/路径 ,在当前解释器里执行脚本,执行完毕解释器状态会改变,也无需可执行权限
#! /bin/bash

cd ~
mkdir bash

我们输入编写一个测试脚本来验证一下后面两条:

使用bash执行

image-20210713152707775

使用 source执行:

image-20210713152901335

退出shell脚本使用 exit命令即可

2. shell 语法

2.1 变量

我们可以使用set查看所有定于的变量

变量的类型有如下几种:

  1. 自定义变量
  2. 环境变量
  3. 位置变量
  4. 预定义变量

自定义变量以及使用

有了变量我们写的程序更加灵活了。 我们可以直接在 bash里直接设定变量,然后也可以读取它,例如:

image-20210713183832595

我们在自定义变量的时候,可能要求和其他编程语言语法有些不同,但是大致语法一致!

  • 命名只能使用英文字母,数字和下划线,首个字符不能以数字开头。
  • 中间不能有空格,可以使用下划线(_)。
  • 不能使用标点符号。
  • 不能使用bash里的关键字(可用help命令查看保留关键字)。
  • 变量名不加美元符号 $

定义变量的时候,我们通常直接显式赋值! 如

a=10
b= "10"
echo $a
echo $b

变量名外面的花括号是可选的,加不加都行,加花括号是为了帮助解释器识别变量的边界

除了显式地直接赋值,还可以用语句给变量赋值,如:

for file in `ls /etc`
或
for file in $(ls /etc)

使用变量,想到大家已经猜到了 这里和php的做法一致: $变量名的形式取出来,但是有如下情况,我们向变量名外面的花括号是可选的,加不加都行,加花括号是为了帮助解释器识别变量的边界,例如:

a=5
echo 我是第$a个程序员

image-20210713184955188

解释器无法智能的理解变量名是 a,而去把变量名理解成了 a个程序员,这里少了什么?少了边界! 如果我们使用{}括起来,完美解决了:

echo 我是第${a}个程序员

image-20210713185512982

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

a=10
readonly a
a=11#错误 zsh: read-only variable: a

image-20210713190019880

删除变量 :unset ,变量被删除后不能再次使用。unset 命令不能删除只读变量。例如 unset a

环境变量

所有的程序,包括shell启动的程序,都能访问环境变量,有些程序需要环境变量来保证其正常运行。必要的时候shell脚本也可以定义环境变量。使用env 命令就可以输出所有环境变量!

bash有两个基本的系统级配置文件:/etc/bashrc/etc/profile。这些配置文件包含了两组不同的变量:shell变量和环境变量。shell变量是局部的,而环境变量是全局的。环境变量是通过shell命令来设置。设置好的环境变量又可以被所以当前用户的程序使用。

常用的环境变量如下:USER UID HOME PATH SHELL PS1 PS2 TMOUT

这些都是大写命令组成,我们shell可以直接使用它,我们简单介绍一下 PS1 PS2 TMOUT

  • PS1

    第一级Shell命令提示符,root用户是#,普通用户是$

  • PS2

    第二级命令提示符,默认是“>” ,什么是 二级命令提示符 ? 我们是输入一大串命令的时候 ,可以使用 \连接符换到下一行输入,此时,bash的提示符就是二级提示符:image-20210713192935149

  • TMOUT

    ​ 用户和系统交互过程的超时值。

    ​ 系统与用户进行交互时,系统提示让用户进行输入,但用户迟迟没有输入,时间超过TMOUT设定的值后,shell将会因超时而终止执行。

  • PATH , 这里和windows上的 path是一致的,只要我们把可执行文件添加到PATH中去 ,我们输入文件名就可以执行对应的可执行文件。每一个冒号都是一个路径,这些搜索路径都是一些可以找到可执行程序的目录列表。当我们输入一个指令时,shell会先检查命令是否是内部命令,不是的话会再检查这个命令是否是一个应用程序。然后shell会试着从搜索路径,即PATH中寻找这些应用程序。如果shell在这些路径目录里没有找到可执行文件。则会报错。若找到,shell内部命令或应用程序将被分解为系统调用并传给Linux内核。image-20210713193532371

    那么我们如果添加 PATH呢?使用export指令,将a.out的路径添加到搜索路径当中,命令:export PATH=$PATH:路径 (PATH中路径是通过冒号“:”进行分隔的,把新的路径加在最后就OK)

位置变量 与 预定义变量

和bat脚本一样,也有位置变量,bat中读取%1 %2 %...可以读取执行命令传入的第一个参数和第二个参数 等,shell这里也是这样的,$1 $2 可以取出对应的变量。

常用的预定义变量有很多 ,如 $ # * @ ? 0 ! 我们直接使用shell脚本展示一下

#! /bin/bash
echo 位置变量和预定义变量
echo 1:$1
echo 2:$2
echo '$':$$
echo '#':$#
echo *:$*
echo @:$@
echo ?:$?
echo !:$!

image-20210713200359979

部分预定义变量含义如下:

  1. # $#变量是命令行参数或位置参数的数量
  2. ?,$? 变量是最近一次执行的命令或shell脚本的出口状态,0表示成功 非零表示错误
  3. $ ,$$ 变量是shell脚本里面的进程ID。Shell脚本经常使用 $$ 变量组织临时文件名,确保文件名的唯一性。
  4. !,&! 变量的值是最近运行的一个后台进程的PID ,我们执行命令的时候使用 &,可以让程序后台执行,使用&!获取它的进程号
  5. * ,$* 变量的值表示所有的位置参数,其值是所有位置参数的值
  6. @,$@ 变量的值类似于$*,表示所有的位置参数,其值也是所有位置参数的值。

2.2三种引号

shell脚本有三种引号我们要注意

  1. “” 界定范围

  2. ‘’ 界定范围并且屏蔽特殊符号!

  3.  `` //反撇号
    

    image-20210713202424140

反撇号的作用就是,当包含的是指令的时候,会将该指令的执行结果返回出来,我们称之为命令替换,与之作用相同的是 $()

image-20210713202824934

2.3全局变量和局部变量

我们通常使用 a=10 ,声明的是局部变量,只能在当前bash中使用,如果我们新开一个bash, a变量就未定义了!

有局部就有全局,我们可以将一个定义完成的变量声明为全局变量 使用 export 变量名 ,也可以边定义边 声明为全局 export a=10 ,如果想取消它的全局身份,我们需要输入 export -n a 这样a转换成为局部变量了。

2.4回显控制

和C语言一样,我们需要scanf()类似功能。shell中我们常用 read 命令

read可以带有-a, -d, -e, -n, -p, -r, -t, 和 -s八个选项。会使用 -p ,我感觉就够了

#! /bin/bash
read -p "请输入变量 n" n
echo 输入变量为$n

image-20210713204122450

当然如果我们输入的变量如果是命令是 密码等 这样 私密的数据,我们可以使用 stty -echo 屏蔽键盘输入bash的内容 却不屏蔽系统输出 ,再次输入stty echo就会恢复了。

当然如果我们想把系统的回显也屏蔽呢? 我们可以把 该命令的标准错误和 标准输出都重定向到 /dev/null中,如下

ls &> /dev/null

image-20210714092259895

2.5变量运算

expr

常有运算符 + - * / %,不必多言

我们使用 expr命令来运算表达式并且输出:例如

#! /bin/bash
echo + :
expr 1 + 5
echo / :
expr 1 / 2
echo \* :
expr 5 \* 6
echo % :
expr 6 % 4
echo 使用 echo '$[]'
echo $[5*6]

image-20210713210642003

使用 echo $[表达式] 也可以达到类似的效果,使用 epre ,值得注意的是 * 是特殊符号,需要转义

let

我们可以使用let 可以修改 变量的本身 并且不显示结果,例如 如果我们这样使用

image-20210713211448824

image-20210713211909374

如果使用let ,如下:

image-20210713211539406

当然让我们也可使用 let a++ 来自增变量 -- += -= ... 等C语言出现过的一元赋值运算符都是一个道理。

很可惜的是 shell脚本无法直接使用小数计算

使用bc 完成小数以及复杂的运算

我们可以使用bc命令以及 echo管道配合| 来计算小数

例如:

#! /bin/bash
echo "scale=3;10/3" | bc

image-20210713214034413

2.6流程控制

shell条件表达式

一门编程语言常具这几种条件控制语句:

  1. if
  2. case
  3. for
  4. while

条件式我们需要 使用 [ ]包括起来,注意表达式与 []两侧有空格隔离,并且条件运算符之间也需要空格隔离 ,这是为了防止识别成一个变量,关系运算符有 (列举常用的)

  1. = 相等 ,常用字符类数据对比
  2. != 不相等 ,常用字符类数据对比
  3. -z 判断字符串长度是否为0
  4. -eq 等于,这个以及下面的常用于数值类型数据对比
  5. -ne 不等于
  6. -gt 大于
  7. -ge 大于等于
  8. -lt 小于
  9. -le 小于等于
  10. ! 取非

shell中还有争对文件和目录中进行判断的

  1. -e 判断文件或者目录是否存在
  2. -f 判断 是否为文件
  3. -d判断是否为目录
  4. -r 判断是否可读(root用户 无效)
  5. -w 判断是否可写(root用户 无效)
  6. -x 判断是否可执行

条件式不仅仅有关系运算符,还有逻辑运算符 && || 含义无需多少了, ,和C语言一样 这里也存在逻辑短路的特性!使用逻辑短路可以完成一些条件判断

[ $USER != root ] || echo root

image-20210713221144699

如果我们要判断一个变量是否为空 我们需要这样操作: [ -Z "$变量" ] , -Z判断字串长度是否为0

image-20210713223257790

当然我们可以采用这样的书写方式 [[ -z $g ]] && echo 空, 规则如下: [ -z "$g" ] 单对中括号变量必须要加双引号

[[ -z $g ]] 双对括号,变量不用加双引号.

当然还有存在一个符号 ; 这里提一下 多个指令可以;隔开,; 可以看作没有逻辑功能的 运算符,可以利用它将多个命令写在一行内。

条件语句

if 单分支语句

虽然逻辑短路可以实现条件判断,但是可读性非常差,所以 条件语句我们一般都会使用 if语句,如果你是压缩代码狂,那么逻辑短路就非常适合你。if语法如下

# 语法一
if 条件表达式 ; then
	命令序列
fi//表示if语句块结束

#语法 2
if 条件表达式 
then
	命令序列
fi//表示if语句块结束

#例如
read -p "请输入用户名" u
if [ -z $u ] ; then
	echo "请输入用户名"
	#...处理
	exit
fi

由于 shell没有像C语言用{}来标记语句块,shell的做法是 采用 fi 倒序的if来标记if语句的结束 。语法一和语法二 的区别不大,主要是是否使用 ; 来将命令放在一行

if多分支语句

语法如下:

# 双分支
if 条件语句 ; then
	#命令序列
else
	#命令序列
if

# 多分支
if 条件语句 ; then
	#命令序列
elif 条件语句 ; then
	#命令序列
elif 条件语句 ; then
	#命令语句
#... 
else
 #命令序列
fi

case语句

这个case语法和Pascal有点类似

case 值 in
模式1)
   命令序列 
    ;;
模式2)
	命令序列
    ;;
*)
	默认情况
;;
esac

case 工作方式如上所示,取值后面必须为单词 in,每一模式必须以右括号结束。取值可以为变量或常数,匹配发现取值符合某一模式后,其间所有命令开始执行直至 ;;

取值将检测匹配的每一个模式。一旦模式匹配,则执行完匹配模式相应命令后不再继续其他模式。如果无一匹配模式,使用星号 * 捕获该值,再执行后面的命令。

循环

和C语言一样,我们可以使用 break,continue 来进行循环控制,语法一致!

for 语句语法

for 变量名 in 值列表
do
	命令序列
done 

#例如
for i in {1..10}
do
	echo &i
dono

for i in a b c
do
#这样也会循环三次
	echo abc
dono

#如果你想使用 变量来控制 循环测试 请使用 seq命令
for i in `seq 10`
do
# 条件语句
done 

seq命令可以输出连续的数字,或者输出固定间隔的数字,或者输出指定格式的数字。

while 语句

# 条件语句为真 循环
while 条件语句
do
# 任务序列
done

# 另外一种形式 永远执行命令序列,无限循环
while :
do
	#命令序列
done

until 循环

# 条件为假执行 
until 条件语句
do
    #命令序列
done

3.shell 函数

基础语法

我们可以使用 alias 来给一些命令起别名,比如 我们.bashrc中就定义着许多变量的别名,我们当然也可以手动更改。

shell中的函数,我感觉理解成加强版的alias不错,函数是为了给一堆命令序列起别名,使用函数和使用命令的语法是一致的。例如:

#! /bin/bash
fun()
{
    echo 函数的使用和命令一样
    for i in $*
    do
        echo $i
    done
}

fun c1 c2 c3

image-20210714100246366

由于shell是逐行执行的,我们需要把函数的 定义放在 执行的前面

#! /bin/bash
fun 1 2 3
function fun()
{
    echo 函数的使用和命令一样
    for i in $*
    do
        echo $i
    done
}

fun c1 c2 c3

fun 1 2 3会执行错误!

当然shell函数完整声明方式为:

function  funname ()

{

    #命令序列
		#返回值 必须是整数 0-255
    return int;

}
  • 可以带function fun() 定义,也可以直接fun() 定义,不带任何参数。
  • 参数返回,可以显示加:return 返回,如果不加,将以最后一条命令运行结果,作为返回值。 return后跟数值n(0-255)

函数的传参和取返回值

函数是为了给一堆命令序列起别名,使用函数和使用命令的语法是一致的,所以传参使用 位置变量即可 ,取返回值使用 $? 取返回值。这里就说明了 返回值为啥是 0-255的原因了

练习 -- 检查当前网段有多少主机和主机ip

#! /bin/bash
myping()
{
    ping -c1 -W1 $1 &> /dev/null
    [ $? -eq 0 ] && echo "$1 is open"
}
# 获取主机ip
a=` ip addr | grep 'state UP' -A2 | tail -n1 | awk '{print $2}' | cut -f1 -d '/' `
echo $a
b=`echo ${a%.*}`
echo "测试网段为 ${b}.x"

#ping 
for i in `seq 254`
do
    myping ${b}.${i} &
done

wait

wait 命令的作用是等待所有的 后台程序结束后 才结束 wait配合 & 后台执行 相当于 C语言的fork语句

我们可以写一个fork炸弹 :

image-20210714110108145

#! /bin/bash
fun()
{
    fun | fun &
}
fun

shell 字串处理

使用shell的过程中,我们常常需要对串进行拼接获取是截取 ,我们在这里讨论shell如何处理字串

字串的截取

语法为 : ${变量名:起始位置:长度},字串的位置 从0开始计数

image-20210714133212315

从0开始截取可以省略0.

字串替换

语法为:${变量名/old/new}

image-20210714133842007

这里默认替换的位置是首次出现的位置,也就是只会替换一次

使用 ${变量名//old/new} 就可以将所有的old替换成 new

image-20210714134233445

字串删除

准备工作:首先使用 给str 赋值

str=`head -1 /etc/password`
root:x:0:0:root:/root:/usr/bin/zsh

这里我们会使用两种符号:

  1. #从左往右处理

  2. %从右手往左处理

  3. 最短匹配删除(从左到右) 格式:${变量名#*关键词} , 删除从左侧第1个字符到最近的关键词 ,*是通配符 ,匹配任意字串

    root:x:0:0:root:/root:/usr/bin/zsh
    ╭─root@localhost /etc
    ╰─# echo ${str#*root}
    :x:0:0:root:/root:/usr/bin/zsh
    

    我们发现这里匹配到第一个root删除了,我们如果想匹配到第三个root呢,再去删除前面的字符呢?

  4. 最长匹配删除(从左到右) 格式:${变量名##*关键词}

    ╭─root@localhost /etc
    ╰─# echo ${str##*root}
    :/usr/bin/zsh
    

那么反过来 ${变量名%%*关键词}${变量名%*关键词}的左右就不必多言了 %是从由右手往左匹配

努力成长的程序员