ci_cd相关学习

看起来有好多要学要做

Posted by 大狗 on October 11, 2021

ci_cd相关学习

1 CI/CD使用原理相关

2 Bazel编译部署相关

2.1 part1 bazel入门

文章链接为:https://zhuanlan.zhihu.com/p/262171925

为何需要自动构建系统?

  • 代码无需人工干涉即可构建、测试并发布生产
  • 提交代码到code review的时候可以直接测试
  • 底层库可以对整个代码库测试代码修改,确保修改是安全的
  • 程序员可以进行大规模的代码修改

如果没有构建系统呢?可以想见华耀的时候:

  • 漫长的编译时间
  • 不能单独编译并替换单个文件
  • 没办法共享其它语言的模块和代码(比方说php和python)
  • 大量的脚本文件,不断地修修补补

因此我们需要一个moder build system。

但是我们需要解决哪些问题呢?

  • 如何解决脚本无法保证完成任务的问题?
  • 如何并发?避免出现记录文件的冲突
  • 如何实现增量构建?
  • 维护和调试脚本的难度

为了解决这些问题,google(bazel)的解决方法是

我们需要从程序员手里拿走一些能力,把这些能力交换到系统本身,并且重新定义系统角色不再是执行任务(running tasks),而是生成制品(artifacts)。这就是Google对于Blaze和Bazel的做法,我们下面就会讲到。

2.2 part2 了解bazel

文章链接为:https://zhuanlan.zhihu.com/p/262497747

与其让程序员自己定义任务,不如让系统定义一组任务,程序员受限的情况下进行配置。

一个构建系统最重要的功能是去构建代码。程序员仍然需要告诉系统他们想去build什么东西,但是“如何构建“则是系统自己的事情。

这就是Blaze以及其它基于artifact的构建系统(如Bazel, Pants, 及Buck)所采用的方案。和基于task任务的系统一样,我们仍然需要buildfile,但是这些buildfile的内容就很不一样了。相对于之前使用图灵完备的脚本语言来实现各种命令(an imperative set of commands),以描述如何生成结果的方式,Blaze的buildfile是一种声明式的形式(declarative manifest)来描述一组制品(artifacts)如何去构建,他们的相互依赖,以及一组受限的选项来定义如何构建。当程序员运行blaze命令行时,他们会定义一组要构建的目标(what),然后blaze负责配置、运行和调度编译步骤(how)。由于现在构建系统完全控制了运行时所用的工具,它才能确保运行期间的高效和正确。

但是有个问题,即任务的定义还是来自程序员,只不过程序员只关心成品,还是有点迷惑bazel的

2.2.1 走进Bazel

先看一个bazel的语法:

java_binary(
name = "MyBinary",
srcs = ["MyBinary.java"], 
deps = [":mylib", ],
)
java_library(
name = "mylib",
srcs = ["MyLibrary.java", "MyHelper.java"],
visibility = ["//java/com/example/myproduct:__subpackages__"],
deps = [
"//java/com/example/common", "//java/com/example/myproduct/otherlib", "@com_google_common_guava_guava//jar",
], )

在Bazel中,BUILD文件定义了targets。上面的两个targets分别是java_binary和java_library. 每个target都对应着bazel能够创建的一种artifact.

binary targets生成能够直接运行的二进制文件,library targets生成能够被其它binary或者library所使用的内容。每个target都有一个name(定义它在命令行和其它target中应该如何被引用),srcs(定义必须被编译的相关源文件以生成对应的target制品),以及deps(定义前置必须先构建或链接的依赖)。

依赖关系可以限制在当前package以内(e.g. MyBinary依赖于:mylib),也可以是在同一个源代码层级中的不同package(e.g. mylib依赖于//java/com/example/common),或者源代码层级之外的第三方artifact(e.g. mylib依赖于”@com_google_common_grava_grava//jar”). 每个源代码层级(source hierarchy)都被称为一个workspace,并由根目录下的一个WORKSPACE文件来标示。

2.2.2 其它bazel解决的问题和技巧

  • 对工具的依赖(比方说开发authsdk时依赖boost & gcc的版本),解决方式是将工具当成依赖,即Tools as dependencies

  • 拓展构建系统,Bazel允许通过自定义规则(custom rules)来扩展所支持的target类型。要定义一个Bazel的rule,开发者首先要定义rule需要的input(以BUILD文件中传递的参数形式)和该rule所生成的output。开发者还要定义该rule所要生成的actions. 每个action同样也要声明input和output,运行一个特定可执行文件或在文件中写入特定字符串,并能够通过input/output连接到其它的action. 这也意味着在Bazel里面,action是最底层的可编辑单元(lowest-level composable unit)–只要一个action只使用它所声明的input/output,它就能做任何它想做的事情,而Bazel则会负责对action进行规划安排并在合适的时候缓存其执行结果。

  • 环境隔离,使用沙盒来实现action后果的隔离

  • 可确定的外部依赖,更新一个依赖应该是一个有意识的行为,但这个行为应该由一个中心控制来一次完成,而不是由单独的程序员或者系统自动完成。这是因为我们希望构建本身是可确定的(deterministic),这意味着如果你check out上周的一个commit,你应该看到所有的依赖都和上周时一样,而不会仍然是今天的版本。

    Bazel和其它构建系统通过workspace范围内的清单(manifest)来处理这个问题,这个清单列出了所有外部依赖的加密hash值(cryptographic hash)。这个hash值是一个相当简洁的方式来唯一标示外部依赖文件,而无需将文件本身提交到source control. 只要workspace中有任何新的外部依赖被引用时,该依赖的hash值就会加入到清单中,有可能是自动的,也可能是手动。当Bazel执行一个构建的时候,它会去检查缓存中的依赖(dependency)的hash值,和清单中的hash值做比较,如果不同的话则再去重新下载。

2.2.3 分布式构建

  • 远程缓存,多个developer workstations,都共享一个指向公用缓存服务,bazel本身就支持保证共享artifact和其输入是相同的
  • 远程执行,
  • Google的分布式构建(Distributed Builds at Google
  • 时间、规模与取舍(Time, Scale, Trade-Offs)

2.2.4 处理模块与依赖

  • 使用精调的模块和1:1:1规则(Using Fine-Grained Modules and the 1:1:1 Rule),需要决定一个独立模块到底要包含多少功能。对于Bazel而言,一个“模块”则是用target来定义的一个可构建的单位,例如java_library或go_library.
  • 最小化模块可见度(Minimizing Module Visibility)
  • 内部依赖(INTERNAL DEPENDENCIES),内部依赖主要是A=>B=>C,本来A包含B&C,但是某天B不在包含C那么,A就找不到C了,但这个问题并不是依赖本身的问题,是开发者对依赖脚本的编写导致的。因此最终Google在bazel中严格传递依赖模式:在这个模式中,Blaze如果监测到一个target试图引用一个没有直接依赖的符号(symbol),就直接报错,同时提供一个脚本命令来直接插入该依赖。对Google内部推广这种形式并重构几百万个targets,让他们显式列出自己的依赖项是一个需要多年时间的投入,但是很值得。现在google的构建快了很多,因为不必要的依赖项都移除了,同时开发人员也不再担心会错误移除依赖项。

2.2.5 外部依赖

外部依赖,和内部依赖不同,外部依赖存在版本。因此外部依赖采用了三个规则:

  • Bazel要求所有依赖项都手动定义。即便在中等规模项目中,手工定义版本号所带来的额外工作,相对于所带来的稳定性而言也是非常值得的;

  • 不同的library版本通常用不同的artifacts来表示,所以理论上讲,一个构建系统中没有理由对一个相同的外部依赖采用不同版本。尽管如此,每个target实际上都能选择它自己想要的依赖项。Google则发现这会导致很多实践问题,所以我们强制了严格的单一版本规则(One-version rule),要求所有内部代码库的第三方依赖都是用同样的版本。(实际上这个问题就可以解决当时遇见到的authsdk使用多个log4cplus问题:clogv2用了1.2.2的log4cplus,而我们用的默认log4cplus,冲突了,实际上我们是避免宝石状依赖);

  • 使用外部依赖缓存构建结果

  • 外部依赖的安全性和可靠性,依赖第三方源的artifacts本质上是有风险的。例如存在可用性风险,如果第三方源因为某些原因挂掉,而你的整个构建就可能会因为无法下载外部依赖项而停止。这也可能有安全风险,如果第三方系统被人攻击,攻击者可能会把你用到的artifact用他们自己的东西替换,从而在你的构建中注入他们的代码。

    这两个问题都可以通过对artifacts做镜像来解决,这样你就可以控制并阻止你的系统访问第三方仓库如Maven Central. 然而代价就是需要投入时间和资源去维护镜像,所以是否采用这种方式取决于你的项目规模。安全问题也可以被解决,只需要要求每个第三方artifact都在代码仓库中定义它的hash值,这样如果artifact被污染,构建就会直接终止。

    另一个可选方案是把你项目的依赖项完全独立出去(原文有外包/出租的意思,vendor)。当一个项目独立之后,他会把这些依赖项和源代码一起check in到项目的source control中,用源代码或二进制都可以。这就以为和所有的外部依赖都变成了内部依赖。实际上,Google内部就采用了这种方式,把引用到的每个第三方库都提交到一个叫third_party的目录。然而,Google用这种方式也是因为Google自己的source control系统是设计来处理巨大的monorepo的形式,所以这种方式未必适合其它公司。

2.3 part3 bazel简单demo

原文链接为:https://zhuanlan.zhihu.com/p/263600968

以https://github.com/bazelbuild/examples/ 为例,参照里面的文件为例子

希望同时编译两个binary,两者不是同级的,两种解决方法:

#一种是把第二个binary变为第一个binary的依赖,加入的src里面
#另一种是filegroup,或者deps里面,但是可能会导致默认的文件位置错误

3 gitlab的CI & CD

关于环境相关:

相关文档:

  • https://zhuanlan.zhihu.com/p/105157319

  • 单元测试pipeline部分教学:https://nick-chen.medium.com/gitlab-ci-%E5%85%A5%E9%96%80%E7%AD%86%E8%A8%98-%E5%96%AE%E5%85%83%E6%B8%AC%E8%A9%A6%E7%AF%87-156455e2ad9f

gitlab-pipeline:可以由一个或多个Stage 组成,即是一次CI/CD 的所有执行阶段,且会依序执行Stage。如果执行过程中只要有一个Job 错误了,预设后续的Stage 都会被略过(skip),一次pipeline其实相当于一次任务构建,里面可以包含多个流程,如安装依赖、运行测试、编译代码、部署测试服务器、部署生产服务器等。

gitlab-stage:可以包含一个或多个Job,代表一个执行阶段,Stage表示一个构建阶段,我们可以在一个Pipeline中定义多个Stage,这些Stage会有以下特点:

gitlab-job:是整个Gitlab-CI 里面最小的执行单位,job表示构建工作,即某个Stage里面执行的工作内容。我们可以在同一个Stage里面定义多个Job,相同的stage里面job并行执行,只有所有job成功才会成功

gitlab runner:实际上是一个docker 的image,在Gitlab CI/CD 中每一个工作(Job) 都是在此运行的,优点是可以确保每次执行的环境都是干净的,且可以让多个工作同时进行,比如执行多种测试项目就可以启用不同的runner 同步进行。

gitlab-ci:会在专案资料夹中侦测到Gitlab-ci.yml 这个设定档的时候触发,主要是负责协调在各种不同情境时Runner 该启动是否该启用及先后顺序,比如流程是先测试后部署,就会限制是测试成功后才能部属,不成功则中断。

简单总结:实际上是gitlab-ci/cd集成bazel+k8s进行runner test的环境,所以首先需要关注gitlab-ci/cd的基础命令。

简单来说我们使用gitlab ci/cd实现先编译,然后打tag(打tag的目的是标记本地镜像,并将之归为某一个仓库),再进行部署的操作。那么如何打tag的

3.1 .gitlab-ci.yml 文件

关键词详细参考文档为:https://docs.gitlab.com/ee/ci/yaml/

yml文件中主要是设定自动化部署的任务Job和脚本内。我们可以看到yml是一种文档方式的存储记录,以利用结构化优势。

yml文件首先通过stage区分阶段,一个阶段完成做下个阶段的内容。另一方面,stage内部再利用job来区分阶段。

简单举例,

image: lorisleiva/laravel-docker:latest     //表示镜像或者docker的东西

stages:             //阶段,必须完成每个阶段再执行下一个阶段
  - testing
  - deployment

unit_test:
  stage: testing   //表明属于哪个阶段,单元测试阶段
  script:
    - composer install --prefer-dist --no-ansi --no-interaction --no-progress --no-scripts
    - ./vendor/bin/phpunit --testsuit Unit
  
production_deploy:     //job,下面的内容用于指定阶段,变量,内容等东西
  stage: deployment    //部署阶段
  tags:                //tag用于说明哪些runner可以执行job,从而可以挑选机器来进行相关的job,方便选择runner
    - server1          
    - server2
  variables:           //执行脚本时内部的变量内容
    HEROKU_PROJECT_NAME: $HEROKU_PRODUCTION_PROJECT_NAME
    HEROKU_API_KEY: $HEROKU_PRODUCTION_API_KEY
  before_script:       //每个job执行关键script之前执行的内容
    - apk add ruby ruby-dev ruby-irb ruby-rake ruby-io-console ruby-bigdecimal ruby-json ruby-bundler yarn ruby-rdoc >> /dev/null
    - apk update
    - gem install dpl >> /dev/null
  script:              //具体执行脚本内容,每个job的关键步骤
    - dpl --provider=heroku --app=$HEROKU_PRODUCTION_PROJECT_NAME --api-key=$HEROKU_API_KEY
  only:                //设定只能在哪些分支进行部署
    - master

再看一个例子,实际上我需要做的工作就是将deploy流程自动化,添加灰度发布。换言之,build,打包,发布融入代码当中

image:
  name: golang:1.10.3-stretch
  entrypoint: ["/bin/sh", "-c"]

# 为了能够使用go get,需要将代码放在 $GOPATH 中,比如你的 gitlab 域名是 mydomain.com,你的代码仓库是 repos/projectname,默认的 GOPATH 是 /go,然后你就需要将你的代码放置到 GOPATH 下面,/go/src/mydomain.com/repos/projectname,用一个软链接指过来就可以了
before_script:
  - mkdir -p "/go/src/git.qikqiak.com/${CI_PROJECT_NAMESPACE}"
  - ln -sf "${CI_PROJECT_DIR}" "/go/src/git.qikqiak.com/${CI_PROJECT_PATH}"
  - cd "/go/src/git.qikqiak.com/${CI_PROJECT_PATH}/"

stages:
  - test
  - build
  - release
  - review
  - deploy

test:
  stage: test
  script:
    - make test

test2:
  stage: test
  script:
    - sleep 3
    - echo "We did it! Something else runs in parallel!"

compile:
  stage: build
  script:
    # 添加所有的依赖,或者使用 glide/govendor/...
    - make build
  artifacts:
    paths:
      - app

image_build:
  stage: release
  image: docker:latest
  variables:
    DOCKER_DRIVER: overlay
    DOCKER_HOST: tcp://localhost:2375
  services:
    - name: docker:17.03-dind
      command: ["--insecure-registry=registry.qikqiak.com"]
  script:
    - docker info
    - docker login -u "${CI_REGISTRY_USER}" -p "${CI_REGISTRY_PASSWORD}" registry.qikqiak.com
    - docker build -t "${CI_REGISTRY_IMAGE}:latest" .
    - docker tag "${CI_REGISTRY_IMAGE}:latest" "${CI_REGISTRY_IMAGE}:${CI_COMMIT_REF_NAME}"
    - test ! -z "${CI_COMMIT_TAG}" && docker push "${CI_REGISTRY_IMAGE}:latest"
    - docker push "${CI_REGISTRY_IMAGE}:${CI_COMMIT_REF_NAME}"

deploy_review:
  image: cnych/kubectl  #用于镜像,具体含义为
  stage: review
  only:
    - branches
  except:
    - tags
  # 环境,用于指定具体部署的环境,我们的希望部署到哪个环境,就往哪个name发出数据
  environment:
    # 环境名字,想往哪个环境进行部署就需要对这个name进行修改
    name: dev
    # a URL, which determines the deployment URL。但是说实话我没明白这个url是干啥的?
    url: https://dev-gitlab-k8s-demo.qikqiak.com
    on_stop: stop_review
  script:
    - kubectl version
    - cd manifests/
    - sed -i "s/__CI_ENVIRONMENT_SLUG__/${CI_ENVIRONMENT_SLUG}/" deployment.yaml ingress.yaml service.yaml
    - sed -i "s/__VERSION__/${CI_COMMIT_REF_NAME}/" deployment.yaml ingress.yaml service.yaml
    - |
      if kubectl apply -f deployment.yaml | grep -q unchanged; then
          echo "=> Patching deployment to force image update."
          kubectl patch -f deployment.yaml -p "{\"spec\":{\"template\":{\"metadata\":{\"annotations\":{\"ci-last-updated\":\"$(date +'%s')\"}}}}}"
      else
          echo "=> Deployment apply has changed the object, no need to force image update."
      fi
    - kubectl apply -f service.yaml || true
    - kubectl apply -f ingress.yaml
    - kubectl rollout status -f deployment.yaml
    - kubectl get all,ing -l ref=${CI_ENVIRONMENT_SLUG}

stop_review:
  image: cnych/kubectl
  stage: review
  variables:
    GIT_STRATEGY: none
  when: manual
  only:
    - branches
  except:
    - master
    - tags
  environment:
    name: dev
    action: stop
  script:
    - kubectl version
    - kubectl delete ing -l ref=${CI_ENVIRONMENT_SLUG}
    - kubectl delete all -l ref=${CI_ENVIRONMENT_SLUG}

deploy_live:
  image: cnych/kubectl
  stage: deploy
  environment:
    name: live
    url: https://live-gitlab-k8s-demo.qikqiak.com
  only:
    - tags
  when: manual
  script:
    - kubectl version
    - cd manifests/
    - sed -i "s/__CI_ENVIRONMENT_SLUG__/${CI_ENVIRONMENT_SLUG}/" deployment.yaml ingress.yaml service.yaml
    - sed -i "s/__VERSION__/${CI_COMMIT_REF_NAME}/" deployment.yaml ingress.yaml service.yaml
    - kubectl apply -f deployment.yaml
    - kubectl apply -f service.yaml
    - kubectl apply -f ingress.yaml
    - kubectl rollout status -f deployment.yaml
    - kubectl get all,ing -l ref=${CI_ENVIRONMENT_SLUG}

我们的pipeline触发的条件很大,

  only:
    refs:
      - merge_requests   //只有merge request试用
    variables:
      - $CI_MERGE_REQUEST_LABELS =~ /ci::pipeline_passed/
  interruptible: true

  only:
    refs:
      - merge_requests

  rules:
    - !reference [.mr_rule_templates, skip_rule]
    - !reference [.mr_rule_templates, bazel_rule]
    - !reference [.mr_rule_templates, default_rule]

4 学习kubernetes

参考的书籍是《每天5分钟玩转kubernetes》,使用minikube学习,命令为https://minikube.sigs.k8s.io/docs/start/

基础概念:

  • cluster:集群
  • master:调度的关键,决定调度给哪个node
  • node:执行任务的单位,负责监控并汇报容器状态
  • pod:最小工作单元,每个pod包含一个或多个容器。POD的容器会作为一个整体被master调度到一个node上运行。
  • Controller:
  • Service:
  • Namespace:

4.1 k8s架构

master节点:

  • API Server:提供各种http/https/restful管理方式,前端接口,通过api server进行管理
  • scheduler:决定将pod放在哪个nod运行
  • controller manager:资源管理
  • etcd:保存资源的信息方便检索
  • pod网络:方便pod之间通信

nod节点:

  • kubelet:node的agent,scheduler获得在某个node上运行pod后,会将pod的具体配置信息(image,volume)发送给该节点的kubelet,然后该节点创建和运行容器,进行工作
  • kube-proxy:部署完了pod,外界实际是访问服务。kube-proxy负责实现请求转发
  • pod网络:pod之间通信使用

4.2 运行应用

4.2.1 deployment

4.2.1.1 手动部署

手动部署的方式:创建deployment之后会创建replicast再创建pod,简单描述过程就是:从deployment ==> replicast ==> pod。这里要注意k8s会按照replicaset的数量创建副本,如果某个node挂了,上面的pod的状态就变为unknown,然后会在别的node上创建足够数量的pod。等故障node恢复以后,不会恢复这个node上的pod,而是删除这个node上的pod

具体的命令参看下面:

#该命令用于发起一个部署
kubectl run nginx-deployment --image=nginx:1.7.9 --replicase=2
#该命令用于获得部署状态
kubectl get deployment nginx-deployment
#该命令用于获得部署的详细状况
kubectl describe deployment nginx-deployment
#获得replicaset & 获得replicaset详细信息
kubectl get replicast
kubectl describe replicast

#然后再获得pod信息,和描述pod
kubectl get pod
kubectl describe pod

    - scripts/build_sim_server_v2.sh
    # replace image name
    - sed -i "s#qcraft-docker.qcraft.ai/qcraft/offboard/dashboard/sim_server:live#qcraft-docker.qcraft.ai/qcraft/offboard/dashboard/sim_server:${CI_COMMIT_SHA}#g" "production/k8s/offboard/dashboard/sim_server/base/deployment.yaml"
    # replace namespace name
    - sed -i "s#production:${KUBE_NAMESPACE}#g"
    - kubectl apply -k production/k8s/offboard/dashboard/sim_server/cn_edge
  dependencies: []
4.2.1.2 文件部署

再来看通过文件方式部署,看下部署到哪里

4.2.1.3 label控制部署到哪里

如果向控制具体部署到哪里,可以通过给node添加标签,然后再修改部署文件里面的nodeSelector添加disktype:ssd即可实现部署到特定机器上:

#添加一个标签,
kubectl label node k8s-node1 disktype=ssd

4.2.2 daemon set

和deployment不同,daemon set在每个node上只能有一个副本

#####

4.2.3 Job

容器分为两种,一种是服务类容器,另一种是工作类容器,其中服务类容器需要一直运行,而工作类容器只需要运行一次

4.3 service

####

5 Docker相关内容的简单学习

Docker是什么?

6 SHELL遇到的一些小问题和教程

6.1 SED替换问题

今天在做一个很简单的事情,利用commit hash作为tag,获取commit hash很简单git rev-parse HEAD,然后使用sed命令替换镜像名称,然后遇到一个问题就是sed替换的原始名称当中包含”/”,这个会导致sed报错

sed: -e expression #1, char 34: unknown option to `s'

如何解决呢?有一个很简单的解决方法,因为sed的分隔符并不是固定的,它是sed -s后面的第一个字符当成分隔符,也就是说如果把命令改成

sed -i "s#$FROM:live#$TO:${TAG}#g" "$FILE_NAME"

就会以#为分隔符,而不需要自己进行地址里面”/”的转义了。

6.2 SHELL脚本当中变量的部分含义

缺省值(:-)

${var:-string} 若变量var为空或者未定义,则用在命令行中用string来替换${var:-string} 否则变量var不为空时,则用变量var的值来替换${var:-string}

$ COMPANY=
$ printf "%s\n" "${COMPANY:-Unknown Company}"
Unknown Company
$ echo $COMPANY

变量的实际值保持不变。

指定缺省值(:=)

如果变量后面跟着冒号和等号,则给空变量指定一个缺省值。

$ printf "%s\n" "${COMPANY:=Nightlight Inc.}"
Nightlight Inc.
$ printf "%s\n" "${COMPANY}”
Nightlight Inc.

变量的实际值已经改变了。 比较${var:-string}和${var:=string} 后者发现$var为空时,把string赋值给了var 后者是一种赋值默认值的常见做法

变量是否存在检查(:?)

替换规则:若变量var不为空,则用变量var的值来替换${var:?string} 若变量var为空,则把string输出到标准错误中,并从脚本中退出。 可利用此特性来检查是否设置了变量的值

根据变量是否存在,显示不同的信息。信息不是必选的。

printf "Company is %s\n" "${COMPANY:?Error: Company has notbeen defined—aborting}"

覆盖缺省值(:+)

${var:+string} 规则和${var:-string},${var:=string}的完全相反 即只有当var不是空的时候才替换成string,若var为空时则不替换或者说是替换成变量var的值,即空值

$ COMPANY="Nightlight Inc."
$ printf "%s\n" "${COMPANY:+Company has been overridden}"
Company has been overridden

替换部分字符串(:n)

如果变量后面跟着一个冒号和数字,则返回该数字开始的一个子字符串,如果后面还跟着一个冒号和数字。则第一个数字表示开始的字符,后面数字表示字符的长度。

$ printf "%s\n" "${COMPANY:5}"
light Inc.
$ printf "%s\n" "${COMPANY:5:5}"
light

根据模板删除字串(%,#,%%,##) #删除左边,%删除右边 如果变量后面跟着井号,则返回匹配模板被删除后的字串。一个井号为最小可能性的匹配,两个井号为自大可能性的匹配。表达式返回模板右边的字符。

$ COMPANY="Nightlight Inc."
$ printf "%s\n" "${COMPANY#Ni*}"
ghtlight Inc.
$ printf "%s\n" "${COMPANY##Ni*}"

$ printf "%s\n" "${COMPANY##*t}"
Inc.
$ printf "%s\n" "${COMPANY#*t}"
light Inc.

#使用百分号,表达式返回模板左边的字符
$ printf "%s\n" "${COMPANY%t*}"
Nightligh
$ printf "%s\n" "${COMPANY%%t*}"
Nigh

案例: 获取文件名和后缀名

$ f=file.tar.gz
$ echo ${f##*.}
gz
$ echo ${f%%.*}
file

#假设我们定义了一个变量为:
file=/dir1/dir2/dir3/my.file.txt

#可以用${ }分别替换得到不同的值:
${file#*/}:删掉第一个 / 及其左边的字符串:dir1/dir2/dir3/my.file.txt
${file##*/}:删掉最后一个 /  及其左边的字符串:my.file.txt
${file#*.}:删掉第一个 .  及其左边的字符串:file.txt
${file##*.}:删掉最后一个 .  及其左边的字符串:txt
${file%/*}:删掉最后一个  /  及其右边的字符串:/dir1/dir2/dir3
${file%%/*}:删掉第一个 /  及其右边的字符串:(空值)
${file%.*}:删掉最后一个  .  及其右边的字符串:/dir1/dir2/dir3/my.file
${file%%.*}:删掉第一个  .   及其右边的字符串:/dir1/dir2/dir3/my

记忆的方法为: #是 去掉左边(键盘上#在 $ 的左边) %是去掉右边(键盘上% 在$ 的右边) 单一符号是最小匹配;两个符号是最大匹配

使用模板进行子字符串的替换(//)

如果变量后只有一个斜杠,则两个斜杠中间的字符串是要被替换的字符串,而第二个斜杠后面的字符串是要替换的字符串。如果变量后面跟着两个斜杠,则所有出现在两个斜杠中间的字符都要被替换为最后一个斜杠后面的字符。

$ printf "%s\n" "${COMPANY/Inc./Incorporated}"
Nightlight Incorporated
$ printf "You are the I in %s\n" "${COMPANY//i/I}"
You are the I in NIghtlIght Inc.

如果模板以#号开始,则匹配以模板开始的字符,如果模板以%号结尾(在centos7上测试不生效),则匹配以模板结尾的字符。

$ COMPANY="NightLight Night Lighting Inc."
$ printf "%s\n" "$COMPANY"
NightLight Night Lighting Inc.
$ printf "%s" "${COMPANY//Night/NIGHT}"
NIGHTLight NIGHT Lighting Inc.
$ printf "%s" "${COMPANY//#Night/NIGHT}"
NIGHTLight Night Lighting Inc.

如果没有指定新的值,则匹配的字符会被删除。

$ COMPANY="Nightlight Inc."
$ printf "%s\n" "${COMPANY/light}"
Night Inc.

也可以使用范围符号。例如:删除所有字符串中的标点符号,使用范围[:punct:]。

$ printf "%s" "${COMPANY//[[:punct:]]}"
Nightlight Inc

使用号或@符号替换变量会替换shell脚本中所有的参数,同样,在数组中使用号或@符号也会替换数组中的所有元素

结尾

唉,尴尬

狗头的赞赏码.jpg