原文地址:https://juejin.cn/post/7016509974306095134
作者:恒无际涯

背景

最近团队中需要编写一个 shell 脚本,用于前端增量模块打包使用,主要就是为了解决每次打全量包耗费的时间较长,能够根据 git 提交判断哪些模块需要进行打包。为了实现这一个需求,那咱们作为前端切图仔就不得不去学习一些常用的 shell 命令了。

前言

学习一门语言基本的大致思路是相通的,就跟我们学习 javascript 一样,要知道 shell 都会有哪些东西,比如:变量、参数传递、运算符、流程控制、函数、文件包含等等。接下来我会根据这次学习到的知识进行一一分享。

变量

在 shell 中也需要像咱们写 javascript 一样进行变量的定义,那么在 shell 的变量定义中都有哪些规则呢?我这里列了一下有如下几点:

  • 命名只能使用英文字母、数字和下划线,首字符不能是以数字开头的
  • 中间不能有空格,可以使用下划线 _
  • 不能使用标点符号
  • 不能使用 bash 里的保留字(可以使用 help 命令查看保留字)

举个栗子:

  1. #符合规则变量
  2. SHELL
  3. A_B_C_D
  4. _abcd
  5. abc123
  6. #无效的变量命名
  7. ?var=123
  8. var*name=hwjy

在说完变量的定义后,就要聊该如何使用这些变量了,在变量的使用过程中一般会有两种写法,分别是 ${SHELL} 和 $SHELL 这两种,乍看之下这两种写法无非就是一个有花括号一个没有,那么这个花括号在这其中起的作用主要是什么呢?带着问题,咱们来看看下面这个栗子:

  1. for lang in Ada Coffe Action Java; do
  2. echo "I am a $langScript engineer"
  3. done

从这个栗子中大家应该不难发现,此时如果 lang 变量没有加上花括号就会被程序识别为 langScript 是一个变量,这样子执行出来的结果就跟我们的预期不符合了,所以为了避免这种情况的发生,我们在使用变量的时候需要加上花括号,为的就是能够更好的帮助解释器识别变量的边界,这也是一个良好的编码习惯。

参数传递

在我们执行 Shell 脚本的时候都需要将一些初始化的参数传递到 Shell 脚本中,就比如我这次写的脚本就必须要传入 git 分支名,让程序知道我是打哪个分支的包,以及需要上传到平台中的哪个版本中,举个栗子:
我们先编写一个简单的 Shell 脚本

  1. #!/bin/bash
  2. # test.sh
  3. echo "第一个参数为$0";
  4. echo "第二个参数为$1";
  5. echo "第三个参数为$2";

然后给脚本设置可执行权限并执行脚本

  1. chmod +x test.sh
  2. ./test.sh hello word
  3. #执行结果
  4. 第一个参数为./test.sh
  5. 第二个参数为hello
  6. 第三个参数为word

chmod +x以及chmod u+x

  • chmod +x chmod +x就是赋予用户文件的执行权限
  • chmod u+x chmod u+x代表设置某个用户获得执行文件的权限。

可能就会有同学不理解,为什么我明明只传了两个参数进去,但实际上获取的是三个参数呢?原因在于 $0 默认为执行的文件名并且它是包含文件路径的。 接下来我们来了解一下还有其他哪些常用处理参数的特殊字符吧,我这里列了一张表,可以参考看:

参数处理字符 说明
#$ 传递到脚本的参数个数
$* 以一个单字符串显示所有向脚本传递的参数。如 “∗”用「”」括起来的情况、以”*” 用「”」括起来的情况、以 “∗”用「”」括起来的情况、以”1 2…2 … 2…n” 的形式输出所有参数。
$$ 脚本运行的当前进程 ID 号
$! 后台运行的最后一个进程的 ID 号
$@ 与 ∗相同,但是使用时加引号,并在引号中返回每个参数。如”* 相同,但是使用时加引号,并在引号中返回每个参数。如 “∗相同,但是使用时加引号,并在引号中返回每个参数。如”@” 用 「”」 括起来的情况、以 “1””1” “1””2” … “$n” 的形式输出所有参数。
$- 显示 Shell 使用的当前选项,与 set 命令功能相同。
$? 显示最后命令的退出状态。0表示没有错误,其他任何值表明有错误。

运算符

无论在什么语言中都少不了一些运算,那么在 Shell 中它又包含了哪些运算符呢?在进行 Shell 脚本的编写过程中,发现常用的运算符有关系运算符、布尔运算符、字符串运算符、算数运算符,除此之外还有文件测试运算符。
这里主要跟大家聊一聊在使用运算符时需要注意的一些事情,举个栗子:
栗子一:

  1. #!/bin/bash
  2. value=`expr 1 + 1`
  3. echo "两数之和为:${value}"
  4. # 这里需要注意的地方在于表达式和运算符之间要有空格,并且完整的表达式要被 ` ` 包含在其中。

栗子二:

  1. #error
  2. [${a} == ${b}]
  3. #success
  4. [ ${a} == ${b} ]
  5. # 这里需要注意的是在进行条件表达式判断时,务必要将表达式放在方括号之间,并且要有空格,否则会报错。

if else流程控制

在一个 Shell 脚本中经常能遇到流程控制,就比如这次脚本的编写中,就需要判断当前提交的代码是否有改动到哪个对应的 App 并将其收集起来,最后生成一个打包队列,将各个 App 打成一个完整的前端包。
在 Shell 中的流程控制跟以往我们写 JavaScript 有些许的不同,它的不同之处在于 Shell 的流程控制是不能为空的,意思就是如果 else 没有语句执行就不要写这个 else 流程。
常见的流程控制语法如下:

  1. #!/bin/bash
  2. # if语句
  3. if condition
  4. then
  5. command
  6. fi
  7. # if else语句
  8. if condition
  9. then
  10. commandIf
  11. else
  12. commandElse
  13. fi
  14. # if else-if else语句
  15. if conditionIf
  16. then
  17. commandIf
  18. elif conditionElif
  19. then
  20. commandElif
  21. else
  22. commandElse
  23. fi
  24. # for循环语句
  25. for lang in js html css
  26. do
  27. echo ${lang}
  28. done
  29. # while循环语句
  30. while condition
  31. do
  32. command
  33. done

函数

在脚本的编写中,常常会涉及到将公共的方法封装成一个函数,那么在 Shell 脚本中,函数的定义又是怎样的呢? 这里我们先看一下函数的一个定义格式:

  1. #!/bin/bash
  2. [ function ] funname [()]
  3. {
  4. action;
  5. [return int;]
  6. }

在这里我们可以发现,其实 Shell 的函数定义与我们平时写的 javascript 函数差别不会特别大,基本上我们在写 javascript 函数该有的东西,在 Shell 中也会有。
因此在函数的定义上咱们就不多去赘述了,既然在函数的定义上我们能找到一些共性,那么在函数的调用上应该也是差不多的。这里我们简单的定义一个函数以及调用它,就可直接明了的理解了。

  1. #!/bin/bash
  2. function fun(){
  3. echo $1$2
  4. }
  5. fun hello world

这里就很好的展示了函数的一个传参使用方式,但是这里需要注意的一点是,函数的参数获取不是无限制的一直 $1 $2 … $100 这样子获取参数,在这里它会有一个限制,就是当获取的参数大于 10 时,就需要改变一下写法,由 $10 变为 ${10} ,否则无法获取到对应的参数。

文件包含

在 Shell 中的文件包含我们该怎么去理解呢?我们可以想象为我们写 Vue 时,经常写的 Mixins 一样,将一些常用的公共代码、函数方法作为一个独立的文件封装起来。在这次的脚本编写中,依赖的安装、打包编译、上传至管理平台这些操作都属于公共的操作,所以在这里使用文件包含恰恰符合使用场景。
Shell 的文件包含语法格式如下:

  1. #!/bin/bash
  2. . path/filename # 注意点号(.)和路径中间有一个空格
  3. 或者
  4. source path/filename

这样通过文件包含的形式促使我们脚本的灵活性,可以随时随地去使用到相关的函数方法,是一个不错的实践方式。

其他命令

  • echo字符串输出 ```shell

    !/bin/bash

显示普通字符串

echo “hello world”

显示转义字符

echo “\”hello world\”” # 此时结果显示为”hello world”

显示变量

content=”hello world” echo “He said ${content}”

显示换行

echo -e “hello \n” # -e开启转义,即\n可以换行 echo “world”

显示结果定向至文件

echo “hello world > test”

  1. - exit [code]
  2. 以退出码为 code 退出当前进程
  3. - rm [options] name...
  4. ```shell
  5. #!/bin/bash
  6. # rm参数有如下几个
  7. # -i 删除文件时进行交互式询问
  8. # -f 强制删除
  9. # -r 递归删除列出的所有目录及其子目录
  10. # -v 显示处理过程
  11. # 一般我们常用的就是 -rf
  12. rm -rf /* # 顾名思义就是删除根目录下的所有目录文件,俗称删库跑路
  • cp [options] source dest ```shell

    !/bin/bash

cp参数有如下几个

-a 此选项通常在复制目录时使用,它保留链接、文件属性,并复制目录下的所有内容。其作用等于 dpR 参数组合

-d 复制时保留链接。这里所说的链接相当于 Windows 系统中的快捷方式。

-f 覆盖已经存在的目标文件而不给出提示。

-r 若给出的源文件是一个目录文件,此时将复制该目录下所有的子目录和文件。

一般我们常用的就是 -r

cp -r /opt/ /newopt/

  1. - cd
  2. ```shell
  3. #!/bin/bash
  4. # 跳到 /home/aaa/
  5. cd /home/aaa
  6. # 跳到自己的 home 目录
  7. cd ~
  8. # 跳到当前目录的上上两层目录
  9. cd ../..
  • ls ```shell

    !/bin/bash

列出当前目录的所有目录

ls /

列出当前目录下所有名称是以 test 开头的文件

ls -ltr test*

将 /home 目录下面的所有目录包含文件详细信息列出

ls -lR /home ```

总结

在这次 Shell 脚本的编写中又加固了相关的 Shell 知识。对于不常写 Shell 脚本命令的同学来说,我的个人体会是,抓住语言中的互通性,共性,就能够很快的上手一门新的语言,其实无论是语言也好,前端框架也好,都要有这样的思维,才不会面对完全没写过的东西感到如此陌生,以致于无从下手。永远相信条条大路通罗马