• 前端学习 linux —— shell 编程


    前端学习 linux - shell 编程

    shell 原意是“外壳”,与 kernel(内核)相对应,比喻内核外的一层,是用户和内核沟通的桥梁。shell 有很多种,国内通常使用 bash

    第一个 shell 脚本

    创建 hello-world.sh 文件,内容如下:

    test11@pjl:~/tmp$ vim hello-world.sh
    
    #!/bin/bash
    echo 'hello world'
    

    第一行指定 shell 的类型:

    test11@pjl:~/tmp$ echo $SHELL
    /bin/bash
    

    Tip:通常约定以 sh 结尾。提前剧透:

    test11@pjl:~/tmp$ sh hello-world.xx
    hello world
    

    执行 sh 文件,提示权限不够:

    test11@pjl:~/tmp$ ./hello-world.sh
    -bash: ./hello-world.sh: 权限不够
    
    test11@pjl:~/tmp$ ll
    -rw-rw-r-- 1 test11 test11   31 617 16:18 hello-world.sh
    

    增加可执行权限:

    test11@pjl:~/tmp$ chmod u+x hello-world.sh
    
    
    test11@pjl:~/tmp$ ll
    # hello-world.sh 变绿了
    -rwxrw-r-- 1 test11 test11   31 617 16:18 hello-world.sh*
    
    

    使用相对路径方式再次执行即可:

    test11@pjl:~/tmp$ ./hello-world.sh
    hello world
    

    也可以使用绝对路径执行:

    test11@pjl:~/tmp$ /home/test11/tmp/hello-world.sh
    hello world
    

    通过 sh xx.sh 无需可执行权限也可以执行。

    Tip:下文还会使用 bash xx.sh 的执行方式。

    首先删除可执行权限:

    test11@pjl:~/tmp$ chmod u-x hello-world.sh
    test11@pjl:~/tmp$ ll
    总用量 20
    -rw-rw-r-- 1 test11 test11   31 617 16:18 hello-world.sh
    
    test11@pjl:~/tmp$ sh hello-world.sh
    hello world
    

    shell 注释

    • 单行注释:# 内容

    • 多行注释:

    :<<!
    内容1
    内容2
    ...
    内容N
    !
    

    变量

    系统变量

    例如 $SHELL 就是系统变量:

    test11@pjl:~/tmp$ echo $SHELL
    /bin/bash
    

    可以通过 set 查看系统变量。例如过滤 SHELL 系统变量:

    test11@pjl:~/tmp$ set |more |grep SHELL
    SHELL=/bin/bash
    SHELLOPTS=braceexpand:emacs:hashall:histexpand:history:interactive-comments:monitor
    

    自定义变量

    定义变量age并输出:

    test11@pjl:~/tmp$ vim demo.sh
    
    test11@pjl:~/tmp$ sh demo.sh
    age=18
    age=18
    

    内容如下:

    test11@pjl:~/tmp$ cat demo.sh
    #!/bin/bash
    age=18
    echo age=$age
    echo "age=$age"
    

    :1. 定义变量不要在等号前后加空格;2. 使用变量要使用 $;3. 最后两行输出效果相同

    # `age=18` 改为 `age= 18`
    test11@pjl:~/tmp$ sh demo.sh
    demo.sh: 2: 18: not found
    age=
    age=
    

    使用 unset 可以销毁变量。请看示例:

    test11@pjl:~/tmp$ vim demo.sh
    test11@pjl:~/tmp$ sh demo.sh
    age=18
    age=
    
    # 脚本内容
    test11@pjl:~/tmp$ cat demo.sh
    #!/bin/bash
    age=18
    echo age=$age
    unset age
    echo age=$age
    

    :销毁变量 age 后再使用该变量,没有报错。

    通过 readonly 定义静态变量,不能 unset。请看示例:

    test11@pjl:~/tmp$ vim demo.sh
    test11@pjl:~/tmp$ sh demo.sh
    age=18
    demo.sh: 4: unset: age: is read only
    test11@pjl:~/tmp$ cat demo.sh
    #!/bin/bash
    readonly age=18
    echo age=$age
    unset age
    

    变量定义规则:

    • 字母数字下划线,不能以数字开头
    • 等号两侧不能有空格
    • 变量名习惯大写

    可以将命令运行结果赋予变量。请看示例:

    命令 date

    test11@pjl:~/tmp$ date
    20220617日 星期五 16:52:57 CST
    
    test11@pjl:~/tmp$ vim demo.sh
    
    test11@pjl:~/tmp$ sh demo.sh
    date1=20220617日 星期五 16:54:02 CST
    date2=20220617日 星期五 16:54:02 CST
    
    test11@pjl:~/tmp$ cat demo.sh
    #!/bin/bash
    date1=`date`
    date2=$(date)
    echo date1=$date1
    echo date2=$date2
    

    环境变量

    比如我在多个 .sh 文件中需要使用一个公共的变量,这时就可以使用环境变量,或称之为全局变量

    环境变量通过 export=value 定义在 /etc/profile 文件中。请看第 30 行:

    test11@pjl:~$ cat -n /etc/profile
         1  # /etc/profile: system-wide .profile file for the Bourne shell (sh(1))
         2  # and Bourne compatible shells (bash(1), ksh(1), ash(1), ...).
         3
         4  if [ "${PS1-}" ]; then
         5    if [ "${BASH-}" ] && [ "$BASH" != "/bin/sh" ]; then
         6      # The file bash.bashrc already sets the default PS1.
         7      # PS1='\h:\w\$ '
         8      if [ -f /etc/bash.bashrc ]; then
         9        . /etc/bash.bashrc
        10      fi
        11    else
        12      if [ "`id -u`" -eq 0 ]; then
        13        PS1='# '
        14      else
        15        PS1='$ '
        16      fi
        17    fi
        18  fi
        19
        20  if [ -d /etc/profile.d ]; then
        21    for i in /etc/profile.d/*.sh; do
        22      if [ -r $i ]; then
        23        . $i
        24      fi
        25    done
        26    unset i
        27  fi
        28
        29
        30  export ANDROID_HOME=/home/pjl/software/android-studio-2021.1.1.22-linux/android-studio/bin
        31  export PATH=$PATH:$ANDROID_HOME
    

    这里定义了一个环境变量 ANDROID_HOME,将其输出看一下:

    test11@pjl:~$ echo $ANDROID_HOME
    /home/pjl/software/android-studio-2021.1.1.22-linux/android-studio/bin
    

    现在我们定义一个环境变量 EVN-VAR-TEST=pjl

    # 查看文件最后两行
    root@pjl:/home/test11# tail -2 /etc/profile
    export PATH=$PATH:$ANDROID_HOME
    export EVN_VAR_TEST=pjl
    

    新的环境变量需要执行 source 才能立即生效。请看实例:

    # 新的环境变量未生效
    root@pjl:/home/test11# echo $EVN_VAR_TEST
    
    # 修改后的配置信息立即生效
    root@pjl:/home/test11# source /etc/profile
    
    # 新的环境变量已生效
    root@pjl:/home/test11# echo $EVN_VAR_TEST
    pjl
    

    尝试在 demo.sh 中使用新增环境变量。请看示例:

    test11@pjl:~/tmp$ cat demo.sh
    #!/bin/bash
    :<<!
    date1=`date`
    date2=$(date)
    echo date1=$date1
    echo date2=$date2
    !
    echo env_var_test=$EVN_VAR_TEST
    

    运行 demo.sh,发现变量为空,让配置立即生效即可:

    test11@pjl:~/tmp$ sh demo.sh
    env_var_test=
    test11@pjl:~/tmp$ echo $EVN_VAR_TEST
    
    test11@pjl:~/tmp$ source /etc/profile
    
    test11@pjl:~/tmp$ echo $EVN_VAR_TEST
    pjl
    test11@pjl:~/tmp$ sh demo.sh
    env_var_test=pjl
    

    :笔者以 root 用户新增环境变量,并让配置生效,接着切换到 test11 用户,需要再次让配置生效。

    位置参数变量

    请先看示例:

    test11@pjl:~/tmp$ sh demo.sh 100 200
    demo.sh 100 200
    100 200
    100 200
    2
    
    test11@pjl:~/tmp$ cat demo.sh
    #!/bin/bash
    echo $0 $1 $2
    echo $*
    echo $@
    echo $#
    

    语法介绍:

    • $0 - 命令本身
    • $1 - 第一个参数。第10个参数需要写成 ${10}
    • $* - 命令行中所有参数。所有参数看做一个整体
    • $@ - 命令行中所有参数。把每个参数区分对待
    • $# - 参数个数

    预定义变量

    shell设计者预先定义变量,可以在 shell 脚本中直接使用。

    Tip:用得不多,仅做了解

    语法介绍:
    $$ - 当前进程的进程号
    $! - 后台运行的最后一个进程的进程号
    $? - 最后一次执行的命名的返回状态。0 表示执行成功。

    请看示例:

    test11@pjl:~/tmp$ sh demo.sh
    29174
    
    test11@pjl:~/tmp$ cat demo.sh
    #!/bin/bash
    echo $$
    

    运算符

    请看示例:

    test11@pjl:~/tmp$ ./demo.sh 1 8
    v1=18
    v2=18
    v3=18
    v4=9
    
    test11@pjl:~/tmp$ cat -n demo.sh
         1  #!/bin/bash
         2  v1=$(((1+8)*2))
         3  echo v1=$v1
         4  # 推荐
         5  v2=$[(1+8)*2]
         6  echo v2=$v2
         7
         8  tmp=`expr 1 + 8`
         9  v3=`expr $tmp \* 2`
        10  echo v3=$v3
        11
        12  v4=$[$1+$2]
        13  echo v4=$v4
    

    语法介绍:

    • 有三种运算的方式:$((运算符))$[运算式]expr a + b。推荐使用 $[]
    • expr 运算符需要有空格,例如 expr 1+8 就没有空格,而且乘号需要加一个转义符 \*

    Syntax error: "(" unexpected

    test11@pjl:~/tmp$ sh demo2.sh
    demo2.sh: 2: Syntax error: "(" unexpected
    test11@pjl:~/tmp$ cat -n demo2.sh
         1  #!/bin/bash
         2  v1=$[(1+8)*2]
         3  echo v1=$v1
    

    据网友介绍:ubuntu 上 sh 命令默认是指向 dash,而不是 bash。dash 比 bash 更轻量,只支持基本的 shell 功能,有些语法不识别。可以直接用 bash a.sh,或者./a.sh 来执行脚本。

    改为 bash./ 的方式执行,确实可以。请看示例:

    test11@pjl:~/tmp$ bash demo2.sh
    v1=18
    

    if

    语法有点怪,先看示例:

    test11@pjl:~/tmp$ sh demo2.sh
    abc 等于 abc
    100 大于等于 99
    存在 /home/test11/tmp/demo2.sh
    
    test11@pjl:~/tmp$ cat -n demo2.sh
         1  #!/bin/bash
         2  if [ 'abc'='abc' ]
         3  then
         4          echo 'abc 等于 abc'
         5  fi
         6
         7  if [ 100 -ge 99 ]
         8  then
         9          echo '100 大于等于 99'
        10  fi
        11
        12  if [ -f /home/test11/tmp/demo2.sh ]
        13  then
        14          echo '存在 /home/test11/tmp/demo2.sh'
        15  fi
    

    语法介绍:

    • if 判断使用 [ 条件 ] 语法,[] 前后要有空格
    • 字符串比较用 =。非空返回 true
    • 数字比较:-lt 小于、-le 小于等于、-eq 等于、-gt 大于、-ge 大于等于、-ne 不等于
    • 文件权限进行判断:-r 有读的权限、-w 有写的权限、-e 有执行的权限
    • 文件类型进行判断:-f 文件存在且是一个常规文件、-e 文件存在、-d 文件存在并是一个目录

    [] 前后要有空格,否则会报错。请看示例:

    test11@pjl:~/tmp$ sh demo2.sh
    demo2.sh: 2: []: not found
    test11@pjl:~/tmp$ cat demo2.sh
    #!/bin/bash
    if []
    then
            echo '空字符'
    fi
    

    增加一个空格,由于空字符是假值,所以不会有输出:

    test11@pjl:~/tmp$ sh demo2.sh
    test11@pjl:~/tmp$ cat demo2.sh
    #!/bin/bash
    if [ ]
    then
            echo '空字符'
    fi
    

    elseif

    请看示例:

    test11@pjl:~/tmp$ sh demo2.sh dog
    狗,100块
    test11@pjl:~/tmp$ sh demo2.sh cat
    猫,102
    test11@pjl:~/tmp$ cat demo2.sh
    #!/bin/bash
    # echo 参数1=$1
    if [ $1 = "dog" ]
    then
            echo '狗,100块'
    elif [ $1 = "cat" ]
    then
            echo '猫,102块'
    fi
    

    类似前端的 if...elseif...elseif

    case

    请看示例,如果传参是 dog ,输出 '狗':

    test11@pjl:~/tmp$ sh demo2.sh dog
    狗
    test11@pjl:~/tmp$ sh demo2.sh cat
    猫
    test11@pjl:~/tmp$ sh demo2.sh xx
    其他动物
    test11@pjl:~/tmp$ cat demo2.sh
    #!/bin/bash
    case $1 in
    
    "dog")
    echo '狗'
    ;;
    
    "cat")
    echo '猫'
    ;;
    
    *)
    echo '其他动物'
    ;;
    esac
    

    语法介绍:

    case $变量名 in
    
    "值1")
    变量的值等于“值1”,执行程序1
    ;;
    
    "值2")
    变量的值等于“值2”,执行程序2
    ;;
    
    *)
    都不满足,执行
    ;;
    
    esac
    

    循环

    for

    // 具体的几个值
    for i in v1 v2 v3 ...
    do
         程序
    done
    

    以下示例演示了 $@$* 的区别:

    
    test11@pjl:~/tmp$ sh demo2.sh a b c
    num1=a
    num1=b
    num1=c
    num2=a b c
    
    test11@pjl:~/tmp$ cat demo2.sh
    #!/bin/bash
    
    for i in "$@"
    do
            echo num1=$i
    done
    
    for i in "$*"
    do
            echo  num2=$i
    done
    

    你有几个参数,$@ 就把你当做几个;$* 只会把你当做一个整体;

    语法二
    for (( 初始值;循环控制条件;变量变化))
    do
         程序
    done
    

    请看示例:

    
    test11@pjl:~/tmp$ bash demo2.sh 100
    1100 的和:5050
    
    test11@pjl:~/tmp$ cat -n demo2.sh
         1  #!/bin/bash
         2
         3  sum=0
         4  for(( i=1; i <= $1; i++))
         5  do
         6          sum=$[$sum+$i]
         7  done
         8  echo 1 到 $1 的和:$sum
    

    :第6行不要写成 $sum=$[$sum+$i]

    while

    请看示例:

    test11@pjl:~/tmp$ bash demo2.sh 100
    1100 的和:5050
    
    test11@pjl:~/tmp$ cat -n demo2.sh
         1  #!/bin/bash
         2
         3  sum=0
         4  i=1
         5
         6  # while  [ $i <= $1 ]
         7  while  [ $i -le $1 ]
         8  do
         9          sum=$[$sum+$i]
        10          i=$[$i+1]
        11  done
        12
        13  echo 1 到 $1 的和:$sum
    

    假如把第 6 行放开,报错如下:

    test11@pjl:~/tmp$ bash demo2.sh 100
    demo2.sh: 行 6: =: 没有那个文件或目录
    1100 的和:0
    

    语法介绍:

    while [ 条件判断 ]
    do
         程序
    done
    

    Tipwhile[ 之间有空格;条件判断] 有空格。例如删除一个空格就会报错 while[ $i -le $1 ]

    test11@pjl:~/tmp$ bash demo2.sh 100
    demo2.sh: 行 6: while[ 1 -le 100 ]:未找到命令
    demo2.sh: 行 7: 未预期的符号“do”附近有语法错误
    demo2.sh: 行 7: `do'
    

    read 获取控制台输入

    test11@pjl:~/tmp$ sh demo2.sh
    请输入你个名字:
    

    程序会阻塞,当你输入后会继续执行:

    test11@pjl:~/tmp$ sh demo2.sh
    请输入你个名字:pjl
    name=pjl
    

    通过 -t 参数能指定等待时间(秒),例如 5 秒内如果没有输入,程序会继续执行:

    test11@pjl:~/tmp$ bash demo2.sh
    请输入你个名字:name=
    test11@pjl:~/tmp$ cat demo2.sh
    #!/bin/bash
    read -t 5 -p 请输入你个名字: name
    

    语法:read 选项 参数

    test11@pjl:~/tmp$ read -h
    -bash: read: -h:无效选项
    read:用法: read [-ers] [-a 数组] [-d 分隔符] [-i 缓冲区文字] [-n 读取字符数] [-N 读取字符数] [-p 提示符] [-t 超时] [-u 文件描述符] [名称 ...]
    test11@pjl:~/tmp$ read --help
    read: read [-ers] [-a 数组] [-d 分隔符] [-i 缓冲区文字] [-n 读取字符数] [-N 读取字符数] [-p 提示符] [-t 超时] [-u 文件描述符] [名称 ...]
        从标准输入读取一行并将其分为不同的域。
    
        从标准输入读取单独的一行,或者如果使用了 -u 选项,从文件描述符 FD 中读取。
        该行被分割成域,如同词语分割一样,并且第一个词被赋值给第一个 NAME 变量,第二
        个词被赋值给第二个 NAME 变量,如此继续,直到剩下所有的词被赋值给最后一个 NAME
        变量。只有 $IFS 变量中的字符被认作是词语分隔符。
    
        如果没有提供 NAME 变量,则读取的行被存放在 REPLY 变量中。
    
        选项:
          -a array  将词语赋值给 ARRAY 数组变量的序列下标成员,从零开始
          -d delim  持续读取直到读入 DELIM 变量中的第一个字符,而不是换行符
          -e        使用 Readline 获取行
          -i text   使用 TEXT 文本作为 Readline 的初始文字
          -n nchars 读取 nchars 个字符之后返回,而不是等到读取换行符。
                    但是分隔符仍然有效,如果遇到分隔符之前读取了不足 nchars 个字符。
          -N nchars 在准确读取了 nchars 个字符之后返回,除非遇到文件结束符或者读超时,
                    任何的分隔符都被忽略
          -p prompt 在尝试读取之前输出 PROMPT 提示符并且不带
                    换行符
          -r        不允许反斜杠转义任何字符
          -s        不回显终端的任何输入
          -t timeout        如果在 TIMEOUT 秒内没有读取一个完整的行则超时并且返回失败。
                    TMOUT 变量的值是默认的超时时间。TIMEOUT 可以是小数。
                    如果 TIMEOUT0,那么仅当在指定的文件描述符上输入有效的时候,
                    read 才返回成功;否则它将立刻返回而不尝试读取任何数据。
                    如果超过了超时时间,则返回状态码大于 128
          -u fd     从文件描述符 FD 中读取,而不是标准输入
    
        退出状态:
        返回码为零,除非遇到了文件结束符、读超时(且返回码不大于128)、
        出现了变量赋值错误或者无效的文件描述符作为参数传递给了 -u 选项。
    

    函数

    请看示例:

    test11@pjl:~/tmp$ bash demo2.sh
    sum=300
    
    test11@pjl:~/tmp$ cat demo2.sh
    #!/bin/bash
    # 定义函数
    function sum() {
            # 第一个参数为 $1
            sum=$[$1+$2]
            echo sum=$sum
    }
    
    # 执行函数
    sum 100 200
    

    语法介绍:

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

    系统函数

    shell 中也有系统函数。我们介绍两个抛砖引玉一下:

    • basename,返回文件名
    • dirname,返回路径
    test11@pjl:~/tmp$ basename /a/b/c/a.txt
    a.txt
    test11@pjl:~/tmp$ basename /a/b/c/a.txt .txt
    a
    test11@pjl:~/tmp$ dirname /a/b/c/a.txt
    /a/b/c
    

    shell 综合练习

    需求:每天凌晨 3 点备份数据库。

    实现如下:

    假如 test.txt 就是我们备份完成的数据库:

    root@pjl:/home/test11/tmp# ls
    backup_database.sh  test.txt
    

    执行三次写好的备份数据库的脚本:

    root@pjl:/home/test11/tmp# bash backup_database.sh
    DATETIME=2022-06-21_200903
    2022-06-21_200903/
    2022-06-21_200903/2022-06-21_200903.txt.gz
    root@pjl:/home/test11/tmp# bash backup_database.sh
    DATETIME=2022-06-21_200904
    2022-06-21_200904/
    2022-06-21_200904/2022-06-21_200904.txt.gz
    root@pjl:/home/test11/tmp# bash backup_database.sh
    DATETIME=2022-06-21_200905
    2022-06-21_200905/
    2022-06-21_200905/2022-06-21_200905.txt.gz
    

    我们需要将数据备份到 /data/backup/db/ 目录中,现已生成3个备份:

    root@pjl:/home/test11/tmp# ls /data/backup/db/
    2022-06-21_200903.tar.gz  2022-06-21_200904.tar.gz  2022-06-21_200905.tar.gz
    

    最后看一下备份脚本内容:

    root@pjl:/home/test11/tmp# cat -n backup_database.sh
         1  #!/bin/bash
         2
         3  # 将数据备份到这 db 目录
         4  BACKDIR=/data/backup/db
         5
         6  # 当前时间
         7  # 输出:DATETIME=2022-06-21_110318
         8  DATETIME=$(date +%Y-%m-%d_%H%M%S)
         9  echo DATETIME=$DATETIME
        10
        11  # 创建备份目录。如果不存在,则创建
        12  [ ! -d "${BACKDIR}/${DATETIME}" ] && mkdir -p "${BACKDIR}/${DATETIME}"
        13
        14  # 备份数据。读取 text.txt 传给 gzip 压缩,在重定向到指定目录
        15  cat test.txt | gzip > ${BACKDIR}/${DATETIME}/$DATETIME.txt.gz
        16
        17  # 将文件处理成 tar.gz
        18  cd ${BACKDIR}
        19  tar -zcvf $DATETIME.tar.gz ${DATETIME}
        20
        21  # 删除对应的备份目录
        22  rm -rf ${BACKDIR}/${DATETIME}
    

    Tip${}通常用于划定变量名的边界,例如:

    test11@pjl:~$ a=1
    test11@pjl:~$ aa=2
    test11@pjl:~$ echo $aa
    2
    test11@pjl:~$ echo ${a}a
    1a
    test11@pjl:~$ echo "${a}a"
    1a
    
  • 相关阅读:
    使用Redis将单机登录改为分布式登录
    2023年7月婴幼儿辅食市场数据分析(京东商品数据)
    JavaWeb开发之——DDL操作数据库(06)
    [C++]Leetcode17电话号码的字母组合
    2022 CCPC 华为云计算挑战赛 A:95计费法
    史上最强,Jmeter性能测试-性能场景设计实例(详全)
    C++类和对象-多态->案例1计算器类、案例2制作饮品、案例3电脑组装需求分析和电脑组装具体实现
    excel中公式结合实际的数据提取出公式计算的分支
    【正点原子STM32连载】 第三十一章 ADC实验 摘自【正点原子】MiniPro STM32H750 开发指南_V1.1
    【Django】开发日报_2.2_Day:ORM-增删改查
  • 原文地址:https://www.cnblogs.com/pengjiali/p/16398736.html