2

前端学习 linux —— shell 编程 - 彭加李

 1 year ago
source link: https://www.cnblogs.com/pengjiali/p/16398736.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

前端学习 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 6月  17 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 6月  17 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 6月  17 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
2022年 06月 17日 星期五 16:52:57 CST
test11@pjl:~/tmp$ vim demo.sh

test11@pjl:~/tmp$ sh demo.sh
date1=2022年 06月 17日 星期五 16:54:02 CST
date2=2022年 06月 17日 星期五 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

语法有点怪,先看示例:

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

请看示例,如果传参是 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 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
1 到 100 的和: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
1 到 100 的和: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: =: 没有那个文件或目录
1 到 100 的和: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 可以是小数。
                如果 TIMEOUT 是 0,那么仅当在指定的文件描述符上输入有效的时候,
                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

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK