一、需求背景
在程序猿的日常工作中,经常会遇到一些需要人工手动去操作或者需要重复去做的工作,而这基本上都会占用我们大量的时间,怎么办呢?那我们就可以使用脚本去替代我们去做这些事情,把更多的精力和时间花在更有价值的事情上。
我们常见的脚本语言有很多种,比如Perl、Python、Ruby、shell等等,这次要介绍的脚本就是以Bash为例的shell,让大家可以对shell有一个初步的认识。
二、认识shell
1、Hello,World
首先我们看一段代码
#!/bin/bash
echo "Hello World!";echo "time:" $(date)
echo "work directory: $(pwd)"
将以下代码加入到 test.sh 文件 ,并赋予权限 『chmod 777 test.sh』,最后执行 『./test.sh』 或 『sh test.sh』
显示如下:
Hello World!
time: 2021年 6月1日 星期二 15时00分56秒 CST
work directory: /Users/tuya/Desktop/sh
从上面的例子中,我们可以学到以下几个知识点:
● # 可以用来添加对代码的注释
● #! 用来说明shell的类型以及执行的路径,如果没有声明,则使用默认的shell类型
● echo 用来打印输出,默认添加换行符(也可以使用printf打印,不过需要自己加换行符)
● pwd 可以获取当前文件夹的路径
● $() 可以在内部执行指令
● 指令之间可以用分号或者换行符隔开
2、什么是shell
我们首先看一个图,了解一下计算机的运行状态:
可以发现,应用程序是在最外层,就跟鸡蛋的外壳一样,因为也被称呼为壳程序
shell是一种特殊的交互式工具,本质上是解释器,核心是命令提示符,允许输入文本命令,解释命令,并在内核中执行命令,返回结果
3、shell的版本历史
shell的版本有很多,早年的Unix时代,根据发展者的不同就有很多的版本,例如熟知的就有Bourne Shell (sh)、在 Sun 里头默认的 C Shell、 商业上常用的 K Shell, 还有 TCSH 等等,每一种 Shell 都各有其特点。
想知道当前系统有几种shell,我们可以通过 /etc/shells 文件可以查看:
$ cat /etc/shells
/bin/bash
/bin/csh
/bin/dash
/bin/ksh
/bin/sh
/bin/tcsh
/bin/zsh
那如何查看当前使用的 shell 类型呢?使用 $SHELL 变量就可以知道当前默认使用的 shell
$ echo $SHELL
/bin/zsh
我们发现在 Mac 上执行这个命令会发现多了一个 zsh ,也就是说 OS X 系统预装了 zsh,其实从 macOS Catalina 版开始,其默认 shell 从 bash 改为 zsh,目前github上有一个开源的框架『oh my zsh』,专门用于管理 zsh 配置,可以帮助我们提高开发效率。
三、Bash shell的优点
既然 /bin/bash 是 Linux 默认的 shell,那我们肯定首先是要了解它的功能,才能知道我们可以用它来做什么以及怎么用,这样对于了解其他类型的shell也是有帮助的,Bash shell主要有以下几个优点:
1、命令编修能力 - history
按上下键就可以找到前/后输入过的指令,默认的指令记忆功能可达1000个,可在终端通过输入history查询最近使用过的指令
2、命令与文件补全功能
●[Tab]接在第一串指令的第一个字的后面,则为命令补全
●[Tab]接在第一串指令的第二字的后面,则为文件补全
●X[Tab],可查询所有以X开头的指令
●若安装 bash-completion 软件,则使用[Tab]键后可进行选项参数的补齐功能
3、命令别名设置 - alias
可使用alias查询目前的命令别名有哪些,也可以直接输入 alias 命令来设置别名,例如:
alias lm='ls -al'
使用的时候我们直接使用 lm 就可以代替 ls -al 实现其具备的功能。
4、程序化脚本 - shell scripts
在Dos年代需将一堆命令写在一起的所谓的批处理文件,如今在Linux下使用shell scripts就可以实现
5、通配符 - wildcard
除了完整的字符串以外,bash还支持许多的通配符来帮助用户查询与命令下达。主要的通配符包含有, ?, [ ] 等等,例如想要知道 /usr/bin 底下有多少以 X 为开头的文件,就使用:『 ls -l /usr/bin/X 』查询
四、Shell的变量功能
简单来说,变量就是以一组文字或符号等,来取代一些配置或者是一串保留的数据!
变量在设置时,需要符合一定的规则:
● 变量与变量内容以『 = 』来连结,『 = 』两边不能有空格
正确:name=tuya
错误:name = tuya
●变量名称只能是英文字母与数字,开头不能是数字
正确:name=tuya
错误:2name=tuya
●变量内容若有空格可使用双引号或者单引号将变量内容结合起来
○双引号的特殊字符 $ 等,可以保有原有本性
○单引号的特殊字符 $ 等,则为一般字符
name=tuya
双引号:var="my name is $name" <== 输出:my name is tuya
单引号:var='my name is $name' <== 输出:my name is $name
●可用跳脱字符 『 \ 』将特殊字符(如$、'、\)等变成一般字符
var=my\ name\ is\ tuya <== 输出:my name is tuya
●在一串命令中,还需要借由其他的命令提供的信息,可以使用反单引号『 命令
』或者『 $(命令) 』
files=$(ls -al) <== 输出当前目录的所有文件夹及文件
●若该变量为扩增变量内容时,则可用"{变量}累加内容
path=Desktop
var=${path}:/home/bin <== 输出:Desktop:/home/bin
●若该变量需要在其他子程序运行,则需要 export 来使变量变成环境变量,使用方式『export var』
name=tuya
bash <== 进入到所谓的子程序
echo $name <== 子程序:输出一下
输出: <== 并没有内容
exit <== 退出子程序
export name <== 使变量变成环境变量
bash <== 进入到所谓的子程序
echo $name <== 子程序:再次输出一下
输出:tuya <== 出现结果
exit <== 退出子程序
●通常大写字符为系统默认变量,自行配置变量可以使用小写字符,方便判断
●取消变量的方法为使用 unset,例如『unset name』
●如果需要查看环境变量,可以使用命令 env 查看环境变量,set 还可以查看自定义变量
子程序的概念:简单来说,就是在当前的这个shell下去激活另外一个shell,新的那个shell就是子程序,一般情况下,父程序的自定义变量是无法在子程序里面使用的,但是通过 export 将变量变成环境变量之后,就能够在子程序里面使用父程序的自定义变量了。
五、数据流重导向
我们在运行命令的时候,可能会从文件读取数据,经过处理之后,再将结果输出到屏幕上。但是输出的内容不管是正确的还是错误的,都一股脑儿的都显示在屏幕上,就会很难区分,这就需要用到数据流重导向的功能,这里就涉及到三个概念,分别是标准输入(stdin)、标准输出(stdout)以及标准错误输出(stderr)。
名称 | 简称 | 代码 | 符号 |
---|---|---|---|
标准输入 | stdin | 0 | < 或者 << |
标准输出 | stdout | 1 | > 或者 >> |
标准错误输出 | stderr | 2 | 2> 或者 2>> |
●standard input
符号 < 的作用是读取文件内容,符号 << 代表的是结束的输入字符,表示当输入指定的字符串后,就停止输入,例如:
$ cat > catfile < ~/.bash_profile <== 将bash_profile的内容读取到catfile文件
$ cat catfile <== catfile的内容和bash_profile的一模一样
$ cat > catfile << 'end' <== 表示当输入 end 的时候就退出输入模式
one
two
end <== 这里将直接退出输入模式,不需要再使用 ctrl + d 退出了
●standard output 和 standard error output
符号 > 和 >> 的主要区别在于 > 会覆盖之前的内容,而 >> 会在之前的基础上进行累加
ls -al >list_right <==list_right文件未创建会自动创建,并会覆盖之前的内容
ls -al >>list_right <==list_right文件内容累加
find / -name .bashrc >list_right 2>list_error <==指定不同的输出结果至不同的文件,屏幕无任何信息展示
我们也可以同时将正确和错误的输出信息写入到同一个文件,但是需要用到特殊写法:
find / -name .bashrc >list 2>list <==错误
find / -name .bashrc >list 2>&1 <==正确
find / -name .bashrc &>list <==正确
●/dev/null 垃圾桶黑洞装置
如果需要将错误的输出结果忽略掉,既不想输出到屏幕上也不想要输出到文件上,这时候黑洞装置就派上用场了,它可以吃掉任何导向这个装置的信息,例如:
find / -name test.sh >list_right 2>/dev/null
综上所述,数据流重导向的优点还是有很多,最重要的是可以方便我们筛选信息,可以很快的找到需要的内容,这对于程序开发提供了极大的便利性。
六、管线命令(pipe)
管线命令就像是流水线上的工作,由一道道工序组成,下一道工序的来源产自上一道工序,而每一道工序只需完成其中的一部分工作内容,最终完成一个看起来很复杂的任务。
先来一张图来直观感受下管线命令是如何运行的
可以看到,管线命令(符号是『 | 』)仅能处理由前面一个命令传来的正确信息,也就是 standard output 的信息,但是对于 stdandard error output 没有直接处理的能力。
管道命令有很多,有撷取命令(cut,grep)、排序命令(sort, wc, uniq)、双向重导向(tee)、字符切换命令( tr, col, join, paste, expand)、分割命令(split)、参数代换(xargs)等等,具体使用可自行查阅相关资料,下面简单举一个例子如何使用撷取命令。
简单来说,撷取命令(cut, grep)就是将一段信息进行处理后得到我们想要的数据。
顾名思义,cut 的作用主要是通过裁剪信息来获取指定数据。
$ echo $PATH
/Library/Frameworks/Python.framework/Versions/3.9/bin:/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin:/Users/linbo/.rvm/bin
$echo $PATH | cut -d ':' -f 1 <== $PATH作为数据源,然后使用『 : 』作为分隔符,获取第一个值
/Library/Frameworks/Python.framework/Versions/3.9/bin
再来看看 grep 命令,它的作用区别于 cut,cut 只是截取一部分信息内容,而 grep 是获取含有指定内容的那一条信息。
$ last | grep 'root' <== 在显示的登录信息列表中,只要含有 root 就输出
$ last | grep -v 'root' <== 相反,没有 root 就输出
$ last | grep 'root' | cut -d ' ' -f1 <== 只要有 root 就输出,并且只取第一列
七、shell在项目中的应用
1、xcodebuild
我们可以通过xcodebuild写一个自动打包的脚本,其中主要的流程包括打包和导出ipa,关于xcodebuild的相关命令,可以通过 man xcodebuild 的方式查看详细用法。
1.1、构建APP
●构建Xcode项目,在项目目录下运行 xcodebuild 以下相关命令
xcodebuild [-project name.xcodeproj]
[[-target targetname] ... | -alltargets]
[-configuration configurationname]
[-sdk [sdkfullpath | sdkname]] [action ...]
[buildsetting=value ...] [-userdefault=value ...]
如果是单个的target和单个的schema,就可以不指定任何参数,直接运行 xcodebuild 即可,默认生成relaase版本真机模式的app
●如果要构建workspace,必须要指定 -workspace 和 -scheme 参数
xcodebuild -workspace name.xcworkspace -scheme schemename
[[-destination destinationspecifier] ...]
[-destination-timeout value]
[-configuration configurationname]
[-sdk [sdkfullpath | sdkname]] [action ...]
[buildsetting=value ...] [-userdefault=value ...]
1.2、导出ipa文件
archive完需要导出ipa文件,使用的命令是 -exportArchive ,需要指定打包的路径,即刚才打包出来的 *.xcarchive 文件,则命令行可以这么写:
xcodebuild -exportArchive -archivePath EXPORT_ARCHIVE_PATH
-exportPath EXPORT_IPA_PATH -exportOptionsPlist ExportOptionsPlistPath
-allowProvisioningUpdates
了解完xcodebuild相关命令之后,我们就可以用shell去创建一个自动打包上传的脚本,从而可以节省大量的时间和重复的工作。
2、Check Pods Manifest.lock
我们在使用cocopods的时候,可能会遇到这么一个问题:
error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.
这个是由于我们的 Manifest.lock 与 Podfile.lock 产生了差异造成的。我们可以打开xcode项目,找到 Target -> Build Phase -> Check Pods Manifest.lock,发现这里其实就是一个 shell script,它自动帮我们做了文件差异的比较。
diff "${PODS_PODFILE_DIR_PATH}/Podfile.lock" "${PODS_ROOT}/Manifest.lock" > /dev/null
if [ $? != 0 ] ; then
# print error to STDERR
echo "error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation." >&2
exit 1
fi
如果我们学习了上面的一些基础知识,就很容易看懂这段脚本内容,如果需要在后面添加一些自定义的需求,比如我可以不输出错误信息,自动执行 pod install 的操作等等都是可以的。
除了上面介绍的几种应用shell的场景外,我们还可以用 shell 做 oclint、codesign以及项目中硬编码和资源检测等等,让我们的项目更加规范、更加稳定。
八、小结
shell 在我们的日常工作中其实是很常见的,如果能借助 shell 去做一些节省工作流程或者化繁为简的任务,是很有意义的一件事。对于每个程序员来说,基本都或多或少接触过 shell,可能目前只是简单的满足日常工作,如果能够深入的去了解 shell 这门语言,其实是可以帮助我们发掘更多值得去做的事情。另外关于此篇本章只是简单的介绍一些概念及基本操作,关于 shell 后面还有更多的功能值得探索和学习,用尽量少的代码,做尽量多的事情。
参考资料
鸟哥的linux私房菜