Shell 难学?那是因为你没找到场景(二)

925 阅读6分钟

导读

在写完 # Shell 难学?那是因为你没找到场景(一) 后,反响不错。而且后续笔者又进一步完善了 git-workflow 的功能,让其脱离 nodejs 的依赖,正式成为所有语言通用的脚本工具。

这其中最关键的两个功能就是版本自动升级自动生成 CHANGELOG。完成这两个功能的过程中,笔者又提炼出了一些场景,来作为学习 shell 的载体。另外,还会顺带介绍一些不太常用但是很好玩的 git 命令。

正文

版本自动升级

git-workflow 的描述中可以看到,版本是用打 tag 的方式实现的。所以我们需要至少实现获取、更新、校验版本号的功能。具体如下:

获取远程最新版 tag

_tagVersion=$(git ls-remote --tags origin | awk "{print \$2}" | sort -t "." -k1,1n -k2,2n -k3,3n | awk "END{print}" | sed "s/.*\/\([0-9\.]*\).*/\1/g")

这个命令很长,我们一句句的解析。第一句 git ls-remote --tags origin 是列出远程的 tag 列表,效果如下:

3351d8e78b9d1cbacc9ec2cff468bb46c6c58c2e        refs/tags/1.1.0
cfa7224d274c4f0ace474267b2f16cb7bacbd9e5        refs/tags/1.1.1
76e49432ad7cbcac7e6bf920c5458ffca46d163b        refs/tags/1.1.1^{}
526a151cd2e558df416d9a65141812aad987ac36        refs/tags/1.2.0
6aeff0887811c998b77ff0308864ee312102c8ba        refs/tags/1.2.1
b3599b5c2f7d23ba70944db9b128b8a9bdc0daab        refs/tags/1.2.2

接下来 awk "{print \$2}" 的作用是获取第二列,也就是带版本号的这一列。

然后用 sort -t "." -k1,1n -k2,2n -k3,3n 进行排序,如果不这样排序,1.12.0 会排在 1.2.0 的前面,所以要用这个命令排一下,具体语法就不展开了,有兴趣的同学可以自己查阅一下。

再然后用 awk "END{print}" 获取最后一行数据,即 refs/tags/1.2.2

最后使用 sed "s/.*\/\([0-9\.]*\).*/\1/g" 利用正则匹配,获取最后的版本号,即 1.2.2

到这里,我们简单总结一下。

  • awk 命令主要用来处理多列文本,获取最后一行数据;
  • sort 命令用来排序;
  • sed 命令用来进行正则提取,但是其正则能力有限,可以配合 grep 的正则能力做更多事情。 我们只要知道大概的命令能够实现什么效果就可以了,具体怎么用,可以现用现查。

version 自动升级

获取到 version 后,接下来就是按照 semver 的规范来更新版本了。代码如下:

# SIDE EFFECTS
# Usage:
# _updateVersion 1.2.3 patch
# echo $_tagVersion # 1.2.4
_updateVersion() {
  ver=$1

  IFS=. read -r major minor patch <<EOF
$ver
EOF

  case "$2" in
  patch) tag="$major.$minor.$((patch + 1))" ;;
  major) tag="$((major + 1)).0.0" ;;
  *) tag="$major.$((minor + 1)).0" ;;
  esac

  _tagVersion=$tag
}

就像注释中展现的,该函数接收 2 个入参,第一个参数是基础版本号,第二个参数是升级模式(major、minor、patch)。

这个函数是参考了这个问答中的回答。其中 IFS 代表的是分隔符,这行代码大概的功能就是以 . 为分隔符,把分割的结果分别赋予 major、minor、patch 三个变量。case 的逻辑就不多展开了。

另外,值得一提的是,shell 函数的返回值只能是数字。所以要想改变 version 就不得不引入副作用,即在该函数内部修改全局变量,这实在是不得已的方法。所以笔者也特意在变量和方法前面加上了 _ 作为特别标识。

版本号校验功能会在后文提到。

自动生成 CHANGELOG

解决了 version 的问题,接下来是更复杂的 CHANGELOG。这里高度借鉴了 conventional-changelog,从符合 约定式提交 的 commit message 中提取信息,生成 CHANGELOG。细节先不展开了,可以简单的理解为要实现下面的转换:

commit b3599b5c2f7d23ba70944db9b128b8a9bdc0daab (HEAD -> develop, tag: 1.2.2, origin/develop)
Author: zhaolandelong <landelong.zhao@tusimple.ai>
Date:   Tue Feb 8 12:42:33 2022 +0800

    feat: add fileheader to ensure version

commit 6aeff0887811c998b77ff0308864ee312102c8ba
Author: zhaolandelong <landelong.zhao@tusimple.ai>
Date:   Tue Feb 8 10:49:06 2022 +0800

    fix: fix deploy tag

commit 1e868519053913704c5fe96f0c0582143ba9cda7
Author: zhaolandelong <landelong.zhao@tusimple.ai>
Date:   Tue Feb 8 10:45:32 2022 +0800

    feat: update README

转换为

## [1.2.2](https://github.com/zhaolandelong/git-workflow/compare/1.2.0...1.2.2) (2022-02-08)


### Bug Fixes

* fix deploy tag ([6aeff08](https://github.com/zhaolandelong/git-workflow/commits/6aeff0887811c998b77ff0308864ee312102c8ba))

### Features

* add fileheader to ensure version ([b3599b5](https://github.com/zhaolandelong/git-workflow/commits/b3599b5c2f7d23ba70944db9b128b8a9bdc0daab))
* update README ([1e86851](https://github.com/zhaolandelong/git-workflow/commits/1e868519053913704c5fe96f0c0582143ba9cda7))

这种可读性良好的 md 文本。

获取两个版本之间的 log 并格式化

先来个简单的,获取仓库的 url 前缀。

# 从 .git/config 文件查找 "url = xxx" 的内容,然后获取第三列。
gitOriginUrl=$(grep -E "^\s+url = .+\.git$" .git/config | awk "{print \$3}")
# 结果:https://github.com/zhaolandelong/git-workflow.git

# 截断最后 4 个字符
gitUrl=${gitOriginUrl:0:-4}
# 结果:https://github.com/zhaolandelong/git-workflow

然后就是如何获取到 message,hash 等内容了,这里介绍一下 git 的一些命令,不常用,但是很有趣。

logs=$(git log --pretty=format:"%s ([%h]($gitUrl/commits/%H))" $preCommit...$curCommit)

git log $preCommit...$curCommit 这个命令是获取两个 commit 之间的 log。注意,那三个点 ... 不是任何省略,就是固定语法,就是这样写的。这两个变量很灵活,可以是 commit,branch,tag。这样就可以应对不同的场景。比如正式发版时,会采用 tag;提测时会采用 HEAD 加 tag;甚至每次提 PR 时都可以用 HEAD 加 branch。这里就不展开了,有兴趣的同学可以自己试一试。

git log --pretty=format:"xxxx" 可以将 log 处理成你想要的格式,%x 表示占位符,比如 %s 表示内容,%h 表示短 hash,%H 表示长 hash 等,具体可以查看 git log 文档

经过上述命令生成的 $logs 长这样:

feat: add fileheader to ensure version ([b3599b5](https://github.com/zhaolandelong/git-workflow/commits/b3599b5c2f7d23ba70944db9b128b8a9bdc0daab))
fix: fix deploy tag ([6aeff08](https://github.com/zhaolandelong/git-workflow/commits/6aeff0887811c998b77ff0308864ee312102c8ba))
feat: update README ([1e86851](https://github.com/zhaolandelong/git-workflow/commits/1e868519053913704c5fe96f0c0582143ba9cda7))

已经跟想要的结果很像了,接下来就是按照 Fetures 和 Bug Fixes 分类。如果是其他高级语言还是很轻松的,用 shell 稍微麻烦一点,主要考验正则水平。

if [ $(echo "$logs" | grep -Ec "^fix(\(.+\))?: ") -gt 0 ]; then
  echo -e "\n### Bug fixes\n" >>CHANGELOG.md
  echo "$logs" | grep -E "^fix: " | sed "s/fix:/*/" >>CHANGELOG.md
  echo "$logs" | grep -E "^fix\(.+\): " | sort | sed "s/fix(\(.*\)):/* **\1:**/" >>CHANGELOG.md
fi

首先,利用 grep -Ec 来判断是否有 fix 开头的内容(注意还处理了 scope 的情况),并且计算出其数量。

然后,如果数量大于 0,则在 CHANGELOG.md 文件中写入 Bug Fixes 小标题。

最后,利用 grep + sed 分别处理不带 scope 和带 scope 的记录。注意,带 scope 的记录做了一下排序。处理后就是最终想要的效果。feat、perf、revert 同理,只需要替换 fix 即可。

获取某个 commit 的日期并格式化

这个需求初看起来好像不难。实际上还是有一些坑的,主要是 git 方面。

git show 1.2.2 -s --pretty=format:"%ad" --date=format:"%Y-%m-%d" | awk "END{print}"
# 2022-02-08

首先,git show xxx 是列出某个 commit 的具体信息,参数同样可以是 commit、tag、branch。这里有个坑,就是采用不同参数,打印的结果是不一样的,所以需要用 awk 获取一下最后一行内容,保证输出效果。至于会有什么不一样,这里就卖个关子,大家动手试一下印象会比较深刻。

然后,-s 可以过滤到 diff 信息,这是我们不需要的。光想方设法去掉这些大量的 diff 信息就花了笔者很多时间,主要是有一些惯性思维了,想用 shell 去解决,结果 git 自带这样的命令。所以说选择比努力更重要……

最后,--pretty=format 上文已经提到过了,%cd 代表提交日期。值得注意的是 --date=format:"xxx" 参数,是对日期进行格式化,比较好看懂,就不展开了。

检测分支和 tag 是否符合规范

git-workflow 已经说过,要切好功能分支并且打好版本 tag,脚本才能正常运行。锦上添花,我们再来实现一下校验功能。

检测远程分支是否存在并且红色警告

  # branch check
  if [ $(git branch -r | grep -v "HEAD" | grep -Ewc " origin/($deployBR|$releaseBR|$developBR)") -ne 3 ]; then
    echo -e "\n\033[0;31mPlease create [$deployBR, $releaseBR, $developBR] branches and push them to origin first.\033[0m\n"
    exit
  fi

首先,列出远程分支,过滤掉 HEAD 记录,用正则查找完全符合分支名的数据个数是否正确。注意 grep -w 是完全匹配整个单词的意思,可以省了我们在正则中加入 ^$

然后,如果发现数据不对,则进行红色打印,并退出。echo -e "\033[0;31m-----\033[0m",这个命令就是彩打,注意,-e 不能省,只需修改 31 这个数就可以改变颜色,具体参考:How to change the output color of echo in Linux

检测是否存在符合 semver 的远程 tag

  # tag check
  if [ $(git ls-remote --tags origin | awk '{print $2}' | grep -Ec "^refs/tags/[0-9]+\.[0-9]+\.[0-9]+(\^\{\})?$") -eq 0 ]; then
    echo -e "\n\033[0;31mPlease create a SEMVER tag and push it to origin first.\neg: 1.0.0, 0.0.0\033[0m\n"
    exit
  fi

有了上文的铺垫,这个命令就很好理解了。值得一提的是远程 tag 有可能会带有 ^{} 的结尾(具体什么意思笔者还没有完全搞懂,可以看下这个问答),所以正则里面会有一部分兼容表达式。这里就可以看出 grep 对于正则的支持是很充分的。

结语

这个纯 shell 全语言可用的版本出来之后,项目终于算告一段落了。整个过程前后大概持续了两周,实际上一周基本就可以了,主要是还隔了个春节假期。整个项目下来,笔者切实感觉自己 shellgit 的功力有了一定的提高。起码再出现类似的需求,心里更有谱了,也更具有判断力了,对此还是很欣喜的。

下一步就是在部门内部推这个脚本的落地,如果能够顺利推广到全部门,那么这个项目就有了更多价值。毕竟作为工程师,落地并解决实际问题才是我们的本职工作。最近发现了一篇汇总张一鸣博客的文章,终于又有新内容放到文章结尾了,与大家共勉。

有感:人生的差距就是在自我感觉良好中拉开的。 —— 张一鸣微博