shell shift与{}_如何编写可靠shell

1a2ca1c9bea8de7f78c9722df1b36324.png

前言

作为互联网从业者,经常需要和Linux打交道,当然不可避免的也要写一些shell,无论是进行CI/CD流水线搭建、数据处理、抑或是进行系统管理,随处可见大量shell脚本的影子。shell有一个很大的特点,也不知道该称之为优点还是缺点,就是它的语法相当灵活,100个人中就有100种写法或是代码风格,究竟该怎么写,可能会逼死强迫症。

为什么是“可靠shell”呢?大家可能都有过shell语言在某些情况下造成的“灾难”,也许是因为我们使用了非预期的变量、产生了非预期的返回,却没有及时判断处理导致了不可预知的问题。

于是花了些功夫查阅资料,给团队做了一次分享,始有此文。在这里,笔者无视那些众多的shell解释器,就以Linux标准的bash为例,斗胆整理了了一份关于shell的部分编写建议作为自己写shell时的参考指南,还请各位读者抱着批判性思维来审阅。

开头指定bash

指定bash的方式有很多,不过建议大家使用下面两种中的一种:

dad674b3225fe2be83028eb19dc0b38b.png

有几点说明:

1、运行./a.sh时,当没有指定shebang时,就会默认用$SHELL指定的解释器,否则就会用shebang指定的解释器

2、#!/bin/bash 的方式限制了代码注入的可能,在某些情况下更安全

3、#!/usr/bin/env bash 的方式通过添加env中间层,使得可以在$PATH中搜索bash,提供灵活性、适应性

用双引号包围变量

考虑如下代码段:

f4b4aff799bb0e5d9e18b1bc876a942d.png

运行会报错,因为等号前后字符串个数不一致。正确的做法是如下代码:

a4377ff78a70fc107d202f6c822aa773.png

要小心命令行参数中的空格。如果变量要放到if语句中,最好用双引号包围,其他情况下,包围变量也是一个不错的实践。当然,在双引号中继续用{}大括号包围变量,比如"${filename}" ,也是推荐的写法。

全部代码进函数

建议除公共部分外,所有的代码都封装进函数,即使只有一个函数,也定义一个main函数。

17206b51db1e12246787520fd566ada6.png

最常见到的不规范的写法就是大家的shell全都不在函数中,一条条命令顺序执行(笔者以前也经常这么写)。

定义函数有几种方式:

369d7629244642e5a4291f8deeafe8f6.png

建议使用标准写法(第一种)。Shell函数在定义时不能指明参数,只在调用时可以传递参数,且传递什么则接收什么。上面的"$@"则是接收命令行参数的写法。

使用readonly定义常量

ed577c544079c38c2e9b786b1e7916a9.png

使用readonly修饰的变量定义会变成只读变量,无法在脚本中被修改,更加安全。

关注变量作用域

b2b116042bdee5ebac940dd994a469d5.png

1、Shell中默认变量作用域为全局(无论定义在外层还是函数中)

2、强烈建议定义变量时用local、readonly修饰(定义在函数内),有充足理由时可以使用declare(如需定义整型变量)

3、如果必须定义全局变量,则建议全局变量大写

警惕未被初始化的变量

0a12f995066f5c3e0596beab4dd57c02.png

如果运行上面的脚本,参数为空的话,你的根目录的data目录就被删掉了。可以使用nounset标志来防止这种意外情况的发生:

59d0b5323eabf9e79d5972e62176feab.png

1、set –o nounset的另一种表达方式:set -u

2、当使用了未初始化的变量时,设置set -o nounset,可以让程序强制退出

当然,上面例子只是为说明问题,脚本可不能这么写,太危险。

让代码执行可追踪

不多说,使用set -o xtrace可达到该目的,将每行的执行命令输出。也可以简写为set -x。常用于调试场景,也可以在执行shell时使用sh -x 的方式调试脚本。

防止错误滚雪球

假如这么写shell(假设a.txt不存在):

bea78db082a5c31bb7e464b98280d5b1.png

那么仍然会尝试删除a.txt。

如果我们想判断下上一步的执行结果再决定下一步行动,常用的做法可能是这样:

87a5b09bffc5bf791ef531da531980b6.png

我们通过$?的值来判断上一步的状态,进而决定是继续删除还是直接退出。

还有另外一种方式也可以达到目的:

a59d222797c80af1608d31067f88ae6a.png

1、set -o errexit的另一种表达方式:set -e

2、使用set -o errexit,一但有任何一个语句返回非0值,则退出bash,从而尽早捕获错误

3、此时无法使用$?获取命令执行状态,因为bash无法获得任何非0返回值

4、如果需要让程序即使出错也继续执行,可以在可能出错的语句追加" || true"

学会查路径

强烈建议在脚本开头定义基础目录,如下:

2a91c580a60ae79d93c096d53cd365e0.png

1、基于当前脚本执行路径,指定其他路径;

2、在每个脚本前设置当前工作区、脚本名、工程根目录的只读变量是一个好习惯

3、让脚本在任何目录下都可以正常执行(脚本中所有位置全部使用决定路径,尽量少使用相对路径;)

巧用shift

93d82dfdc0c703a86fd5881dc3ce8d9e.png

上面的func2使用了shift,使得所有命令行参数都可以通过$1读取。再举个更实用的例子:

4f7c5e532d47967484ebc7e218e3c7a3.png

假设该文件命名为test.sh,我们运行时使用:sh test.sh --file a.txt --module module_a 的方式,程序就可以精确获取到每个参数,用于后续的逻辑处理。

封装一些常用指令

假如我们经常需要检查命令执行状态,就可以封装一个函数:

cf68cce64199d00659493500d179ab34.png

以后在需要的地方调用该函数即可。

提供help信息

不多说,脚本最好提供一个help函数,当用户输入参数异常时能够及时给出反馈。

切换目录的几种方式

假如我们需要临时在某个路径下执行一些指令,为了不改变主程序的执行路径,可以有几种方式:

b843bf429d650255fe983781e4e66f21.png

巧用trap信号

4c115cc5785f01207466977f3de80a00.png

trap func EXIT允许在脚本结束时调用函数,用它注册清理函数。

让脚本可以单独运行任意一个函数

c0978d82a147a76375226d1d21d23b16.png

如上编写的脚本(假设test.sh),我们在运行的时候,可以使用sh test.sh --eval start 来单独运行start方法。

一些额外的小tip

1、在条件判断时,尽量使用双中括号"[["而非单中括号"["。单中括号是一个Linux命令,每次使用都会fork一个子进程。双中括号是shell关键字,更加强大,可完全替代单中括号

2、判断时,有个小技巧:[[ "z${var}" = "z" ]] 加入任意前导字符(此处是z)可以防止var变量为空时脚本报错

3、利用/dev/null过滤不需要的输出信息:$ command > /dev/null 2>&1

4、变量可以习惯性使用{}包围,以防意外情况,且用双引号包围是个好习惯,如 "${var}"

5、把then,do等和if、while或者for写在同一行,不换行

6、一行太长时使用 进行换行,换行原则是整齐美观

7、禁止直接操作$1、$2等参数,除非这些变量只用一次

8、整数运算使用$(()),如 echo $((3+4)) ;小数运算使用bc计算器,如 echo "scale=2; 5/3" |bc

9、尽量使用$()将命令结果赋值给变量,而非使用反引号

10、尽量使用绝对路径,不易出错

11、shell脚本main函数接收参数时,尽量使用main "$@" 的形式。以下是各种形式传参的结果:

……

7fe93265bbfa9989ec7406a22a732733.png

你的脚本可以这样开始

7e6fdad1694fb62691a483a5d0cb688f.png

后记

shell作为一门很灵活的语言,编写的规范性也越发显得重要。可靠性作为软件质量模型中的第二个特性,很多时候都体现在代码层面。文章稍长,为了加深印象,使用了较多代码段,提到的建议也只是冰山一角,建议感兴趣的读者有机会自己写代码尝试下,也欢迎提出宝贵意见。


关于360技术:360技术是360技术团队打造的技术分享公众号,每天推送技术干货内容,更多技术信息欢迎关注“360技术”微信公众号

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 在 Shell 编程中,`shift` 是一个命令行参数处理命令,用于将命令行中的参数向左移动一个位置。它的作用是将第一个命令行参数 `$1` 赋值给 `$0`,将第二个命令行参数 `$2` 赋值给 `$1`,以此类推,同时 `$#`(表示命令行参数的个数)减 1。 例如,如果在脚本中使用了 `shift` 命令,那么脚本中的 `$2` 变量将变为 `$1`,`$3` 变为 `$2`,以此类推。下面是一个示例: ``` #!/bin/bash echo "The first argument is: $1" shift echo "The first argument after shift is: $1" ``` 如果你在执行这个脚本时传入两个参数,那么输出将如下所示: ``` $ ./script.sh arg1 arg2 The first argument is: arg1 The first argument after shift is: arg2 ``` 在这个示例中,`shift` 命令将第一个参数向左移动一位,并将第二个参数作为新的第一个参数进行处理。 ### 回答2: shell中的shift命令是用来将参数列表进行移动的。在脚本中,我们经常需要处理多个参数,使用shift可以将参数列表向左移动一位,即丢弃第一个参数,第二个参数变为第一个参数,依此类推。 shift命令的语法格式为:shift [n],其中n是一个可选的参数,表示要移动的位数,默认情况下为1。 一般情况下,shift命令配合循环语句使用,用来处理不定数量的参数。比如,我们可以使用shift和while循环来依次处理所有的参数。 下面是一个简单的例子,演示了shift命令的使用: ```shell #!/bin/bash while [ $# -gt 0 ] do echo "当前参数列表:$@" echo "当前处理参数:$1" shift done ``` 在上述例子中,我们首先通过$#判断是否还有参数需要处理,然后使用$@打印当前的参数列表,使用$1打印当前处理的参数。接着使用shift命令将参数列表向左移动一位,进入下一轮循环。 假设我们运行该脚本并传入三个参数,如下所示: ```shell $ ./script.sh foo bar baz ``` 输出结果将会是: ```shell 当前参数列表:foo bar baz 当前处理参数:foo 当前参数列表:bar baz 当前处理参数:bar 当前参数列表:baz 当前处理参数:baz ``` 可以看到,随着每一次循环,参数列表会逐渐减少,直到没有参数需要处理为止。 总结起来,shift命令是一个非常有用的工具,可以方便地处理多个参数。通过移动参数列表,我们可以依次对每个参数进行操作,从而实现更加灵活和复杂的脚本编写。 ### 回答3: shell中的shift命令是用来向左移动命令行参数的位置的。通常情况下,在命令行输入一条命令时,命令本身是第一个参数($0),紧接着是其他的参数。而shift命令可以将参数的位置向左移动一个位置,原来的第二个参数变成了第一个参数,以此类推。 当使用shift命令时,我们可以指定要向左移动的位置数。例如,shift 2将使得当前的第三个参数变成了第一个参数,第四个参数变成了第二个参数。 shift命令通常用在需要处理多个命令行参数的脚本中。通过不断地使用shift命令,我们可以逐个地处理命令行参数,而不需要依靠特定的参数位置。 一种常见的用法是通过循环来处理所有的命令行参数。在每次循环中,我们可以使用$1来引用当前的第一个参数,并使用shift命令将其向左移动一个位置,接着循环处理下一个参数,直到所有的参数都被处理完毕。 shift命令还可以用来判断命令行参数的个数。例如,在一个脚本中使用shift命令后,通过使用$#来获取当前命令行参数的个数,可以判断是否还有参数需要处理。 总之,shift命令是一个在shell脚本中非常有用的命令,它可以通过改变命令行参数的位置来简化命令行参数的处理。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值