什么是 Shell?

Shell 这里指的是 Uinx 中 Shell 解释器,而不是某跨国公司品牌的 shell 名称,这才使得人搞混了。在常用的 Linux 系统中 Shell 是一种介于系统内核和用户之间的命令解释器程序,用户可以通过输入命令通过 Shell 解析器执行命令从而达到操作计算机内核作用。

如果经常使用类 UNIX 系统的话,如果掌握 Shell Script 会大大提高一下工作效率,常用的系统资源管理自动化操作,都可以使用编写 Shell 脚本来完成,而且好处是大多数操作系统默认都有自带的解释器。

每个操作系统的默认自带的解释器都有很多种,可以通过下面这个命令来查看支持的解释器列表:

$: cat /etc/shells
# List of acceptable shells for chpass(1).
# Ftpd will not allow users to connect who are not using
# one of these shells.

/bin/bash
/bin/csh
/bin/ksh
/bin/sh
/bin/tcsh
/bin/zsh #我默认使用的这个,功能强大于普通bash
/usr/local/bin/zsh

最常用的解释器也就是/bin/bash,下面可以编写一个简单的脚本,内容如下:

#!/bin/bash
echo "Hello Shell Script!" # 执行会打印这句话

变量的使用

在 Shell 脚本中也有变量的概念,也内置一些变量方便编写脚本的时候使用,例如$HOME$USER$PWD$SHELL... 在 Shell 脚本中的变量也简单,没有类型系统所有数据都字符串类型,无法参入运算,但是有区分只读静态变量,如下:

#!/bin/bash
variable = "这是一个普通的变量"
echo $variable
readonly immutable = 1024 # 只读不可变 zsh: read-only variable: immutable
unset variable # 取消设置的值
echo $variable

如果需要把当前脚本文件里面的变量导出给其他脚本使用的话,那就需要将变量导出为全局变量,使用关键字 export

#!/bin/bash
GLOBAL = "全局变量"
export GLOBAL

这样在其他的 Shell 环境变量就可以访问到这个变量了。

特殊变量 $n 这里的 n 表示数字,从 0 开始的可以是很多个,但是 1-9 可以使用 $n ,但是超过之后访问其值必须加上大括号 ${10} 这样才能访问到,一般这使用与脚本文件外部传入参数获取使用。

另外一个如果需要得到外部传入参数个数需要使用 $# 它返回就是参数个数。如果需要获取全部输入的参数直接使用 $@ ,它就获取到所有传入的参数。

#!/bin/bash

echo "args length: $#"
echo "$0 $1 $2"

args=("$@")
echo "first : ${args[0]}"
echo "second : ${args[1]}"

output:

$: sh shell-args.sh 1 2 3
args length: 3
shell-args.sh 1 2
first : 1
second : 2

还有一个比较特殊的 $? ,它可以检查上次执行命令是否出错,它会检查上次命令执行返回结果,如果结果为 0 则表示正常运行退出,反之则认为是程序非正常退出,当然退出错误码可以自定义,例如修改一个不可变变量:

#!/bin/bash
readonly immutable=1024
echo $? #0
immutable=2048
echo $? #1

运算符

上面介绍 Shell 脚本中变量的使用,但是变量的缺点就是没有类型系统,所有的变量都是字符串如果需要进行计算那么就不可能的。为了解决数值运算的问题,在 Shell 中运算使用的关键字 expr 命令,例如 expr n + x 加法计算。

expr 命令是一个手工命令行计数器,用于在 UNIX/LINUX 下求表达式变量的值,一般用于整数值也可用于字符串,运算的时候用空格隔开每个项,特殊字符需要转义的使用 \ ,对包含空格和其他特殊字符的字符串要用引号括起来。

#!/bin/bash
expr 10 + 10    #加法计算
expr 20 - 10    #减法计算
expr 10 \* 10   #乘法计算
expr 50 / 5     #除法计算
expr 8 % 7      #取余计算

特殊的字符串运算:

#!/bin/bash
expr length "text content"  # 计算长度 12
expr substr "this is text"  # 截取 is is
expr index "hello" e  #第一次出现的位置2

当然如果想纯做数值计算,使用 $[运算表达式] 如下:

#!/bin/bash
echo "10 + 10 = $[10 + 10]" # 10 + 10 = 20
expr `expr 2 + 3` \* 4 # 20

流程控制

shell 作为脚本语言同样也有条件语句,只要支持条件流程控制才能算是一门脚本语言,shell 脚本支持条件判断、流程控制、循环这些下面是一些例子。

IF 语句

条件判断表达式,条件语句需要放置在 [ 条件 ] 里面,条件相关的关键字有:

  • -ge是否大于或等于
  • -le是否小于或等于
  • -lt小于
  • -gt小于

检测是否有某种权限关键字:

  • -w是否有写权限
  • -r是否有读权限
  • -e文件夹或者文件是否存在
#!/bin/bash
x=$1
y=$2
if [ $x -le $y ]; then
    echo "x less than y" 
elif [ $x -gt $y ]; then
    echo "y grater than x"
else 
    echo "other result"
fi            

CASE 语句

熟悉其他编程语言的应该都使用过 Case 语句来匹配处理多个条件,在 shell 中也存在 case 语句,如下:

#!/bin/bash
language=$1

case $language in
    Java)
        echo "Java 语言";;
    Go)
        echo "Go 语言";;
    Rust)
        echo "Rust 语言";;
esac                

FOR 循环

for循环可以根据某种条件让程序重复执行块逻辑,下面是普通循环的例子:

#!/bin/bash
sum=0
for ((i=0;i<=100;i++))
do
    sum=$[$sum+$i]
done
echo $sum    

for loop in 1 2 3 4 5;do
    echo "number: $loop"
done    

WHILE循环

while循环也是一种循环的,在循环里面设置一个条件,直到该条件满足为止,如下:

#!/bin/bash
counter=0
while [ $counter -le 5 ]; do
    counter=`expr $counter + 1`
    echo $counter
done   

函数编程

shell脚本里面也有函数的概念,函数可以把一些重复执行逻辑块单独拿出来,通过传递参数到达不同结果,在shell脚本里面有内置函数,也可以自定义函数,本节将介绍内置函数和自定义函数的使用。

有时候编写的脚本需要接受外部的输入,这时我们就要失业内置的read函数了,read函数可以从控制台读入值,并且还能设置等待用户输入的超时时间,下面就是一个例子:

#!/bin/bash
read -t 8 -p "input your name:" NAME
echo "Hello $NAME~"

basename主要用于显示文件路径名不包括目录部分后的显示文件名,如果指定了后缀参数suffix那么返回就是文件名,如下:

basename /Users/ding/jmeter.log .log 

dirname从给定的文件路径,去掉文件部分返回文件路径部分,文件路径如下:

dirname /Users/ding/jmeter.log
/Users/ding

其实内置函数也就是一个一个内置命令,当然除了内置的一些函数以外,还可以在脚本里面自定义函数,但是函数必须定义在调用者前面,因为脚本是顺序执行的,如下是一个标准的函数定义:

#!/bin/bash
function sum() {
    read -t 5 -p "请输入加数:" x
    read -t 5 -p "请输入加数:" y
    return $[$x + $y]
}
sum
echo "和是:$?"

需要注意的是函数返回值,如果没有使用return关键字,那么返回值则使用最后一行作为返回值。

CUT命令

cut是内置的命令,主要用于在文件中负责剪切数据用的,cut可以在文件的每一行剪切字节、字符和字段并将这些字节、字符和字段输出。

例如下面通过ifconfig命令的内容,要把网卡的本地ip截取出来,内容如下:

$: ifconfig
lo0: flags=8049<UP,LOOPBACK,RUNNING,MULTICAST> mtu 16384
    options=1203<RXCSUM,TXCSUM,TXSTATUS,SW_TIMESTAMP>
    inet 127.0.0.1 netmask 0xff000000
    inet6 ::1 prefixlen 128
    inet6 fe80::1%lo0 prefixlen 64 scopeid 0x1
    nd6 options=201<PERFORMNUD,DAD>
gif0: flags=8010<POINTOPOINT,MULTICAST> mtu 1280
stf0: flags=0<> mtu 1280
XHC20: flags=0<> mtu 0
XHC0: flags=0<> mtu 0
en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
    options=400<CHANNEL_IO>
    ether 8c:85:90:c4:67:da
    inet6 fe80::ca2:121e:899c:3504%en0 prefixlen 64 secured scopeid 0x6
    inet 192.168.31.221 netmask 0xffffff00 broadcast 192.168.31.255
    nd6 options=201<PERFORMNUD,DAD>
    media: autoselect
    status: active
en1: flags=8963<UP,BROADCAST,SMART,RUNNING,PROMISC,SIMPLEX,MULTICAST> mtu 1500
    options=460<TSO4,TSO6,CHANNEL_IO>
    ether 82:87:9a:86:cc:01
    media: autoselect <full-duplex>
    status: inactive
en2: flags=8963<UP,BROADCAST,SMART,RUNNING,PROMISC,SIMPLEX,MULTICAST> mtu 1500
    options=460<TSO4,TSO6,CHANNEL_IO>
    ether 82:87:9a:86:cc:00
    media: autoselect <full-duplex>
    status: inactive
bridge0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
    options=63<RXCSUM,TXCSUM,TSO4,TSO6>
    ether 82:87:9a:86:cc:01
    Configuration:
        id 0:0:0:0:0:0 priority 0 hellotime 0 fwddelay 0
        maxage 0 holdcnt 0 proto stp maxaddr 100 timeout 1200
        root id 0:0:0:0:0:0 priority 0 ifcost 0 port 0
        ipfilter disabled flags 0x0
    member: en1 flags=3<LEARNING,DISCOVER>
            ifmaxaddr 0 port 7 priority 0 path cost 0
    member: en2 flags=3<LEARNING,DISCOVER>
            ifmaxaddr 0 port 8 priority 0 path cost 0
    nd6 options=201<PERFORMNUD,DAD>
    media: <unknown type>
    status: inactive

现在需要截取出来enoip地址:

ifconfig en0 | grep "inet " | cut -d " " -f 2
192.168.31.221

SED流式编辑器

sed是内置的一款流式编辑器,它可以把内容读取到内存里面缓冲区里,然后在对每行内容进行操作,然后把结果输出到标准输出,不会影响到原来的文件,如果需要转存可以使用重定向输出,常用的一些操作有:

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("HelloWorld!");
    }
}

现在有一块Java源代码文件,需要使用sed对该文件的一些操作,如下:

#替换HelloWorld为中文
$: sed -e 's/HelloWorld!/您好,世界!/' HelloWorld.java
public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("您好,世界!");
    }
}

好现在把源代码添加多一点方便,后面测试,现在内容如下:

[root@aliyun-centos7 developer]# cat /root/helloworld.java 
public class helloworld {
        public static void main(String[] agrs) {
                // 让程序输出双引号中的字符串
                System.out.println("Hello World!");
                System.out.println("Hello World!");
                System.out.println("Hello World!");
                System.out.println("Hello World!");
                System.out.println("Hello World!");
                System.out.println("Hello World!");
                System.out.println("Hello World!");
        }
}

下面是删除第三行,并且在第二行添加新的内容:

$: sed -e "3d" -e '2a\\t\t\// 修改了第3行注释' /root/helloworld.java
public class helloworld {
        public static void main(String[] agrs) {
                // 修改了第3行注释
                System.out.println("Hello World!");
        }
}
  • sed '5,9d' /root/helloworld.java删除文件的第九行然后把内容输出到标准输出上
  • sed '4a\\t\tSystem.out.println("您好!世界!");' /root/helloworld.java 在某一行向后插入内容
  • nl /root/helloworld.java | sed '5,9d' 列出文件行号,并且从第5行删到第9行!
[root@aliyun-centos7 developer]# nl /root/helloworld.java | sed '5,9d'
     1  public class helloworld {
     2          public static void main(String[] agrs) {
     3                  // 让程序输出双引号中的字符串
     4                  System.out.println("Hello World!");
    10                  System.out.println("Hello World!");
    11                  System.out.println("Hello World!");
    12          }
    13  }
  • 在第五行之前插入内容i前 a后
[root@aliyun-centos7 ~]# nl helloworld.java  | sed '5i\\t\t\tSystem.out.println("这是通过sed命令插入的内容!!");' | nl
     1       1  public class helloworld {
     2       2          public static void main(String[] agrs) {
     3       3                  // 让程序输出双引号中的字符串
     4       4                  System.out.println("Hello World!");
     5                          System.out.println("这是通过sed命令插入的内容!!");
     6       5                  System.out.println("Hello World!");
     7       6                  System.out.println("Hello World!");
     8       7                  System.out.println("Hello World!");
     9       8                  System.out.println("Hello World!");
    10       9                  System.out.println("Hello World!");
    11      10                  System.out.println("Hello World!");
    12      11                  System.out.println("Hello World!");
    13      12          }
    14      13  }
  • 修改内容c
[root@aliyun-centos7 ~]# nl helloworld.java  | sed '5c \\t\t\tSystem.out.println("通过sed命令修改的内容!!");' | nl
     1       1  public class helloworld {
     2       2          public static void main(String[] agrs) {
     3       3                  // 让程序输出双引号中的字符串
     4       4                  System.out.println("Hello World!");
     5                          System.out.println("通过sed命令修改的内容!!");
     6       6                  System.out.println("Hello World!");
     7       7                  System.out.println("Hello World!");
     8       8                  System.out.println("Hello World!");
     9       9                  System.out.println("Hello World!");
    10      10                  System.out.println("Hello World!");
    11      11                  System.out.println("Hello World!");
    12      12          }
    13      13  }

AWK 分析工具

一款强大的文本分析工具,把文本逐行读入,以空格为默认的方式分隔将其为切片,切开的部分进行分析处理,一般要配合正则表达式使用。

#获取Java版本号
$: java -version 2>&1 | sed '1!d' | sed -e 's/"//g' | awk '{print $3}'

大部分内置的函数和命令在使用的时候可以去查文档,没有必要记住那么多,不过了解 shell 脚本编写是有必要的!

便宜 VPS vultr
最后修改:2024 年 05 月 12 日
如果觉得我的文章对你有用,请随意赞赏 🌹 谢谢 !