news 2026/9/16 4:35:14

Shell编程实例——shell变量(二)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Shell编程实例——shell变量(二)

shell变量

    • 14、设置默认值
    • 15、使用空值作为有效的默认值
    • 16、不只使用字符串常量作为默认值
    • 17、对不存在的参数输出错误消息
    • 18、修改部分字符串
    • 19、获得某个数的绝对值
    • 20、用bash实现basename
    • 21、用bash实现dirname
    • 22、选取CSV的替换值
    • 23、使用数组变量
    • 24、转换大小写
    • 25、转换为驼峰命名法

14、设置默认值

你的脚本依赖于某些常用(如 $USER)或业务特定的环境变量。要想构建一个稳健的 shell 脚本,就得确保这些变量都有合理的默认值。那么该如何确保呢?

首次引用 shell 变量时,如果该变量没有值,则使用赋值运算符为其赋值。

cd${HOME:=/tmp}

示例中所引用的$HOME会返回其当前值,除非它为空或者压根就没设置。对于后两种情况(为空或没有设置)​,返回 /tmp,该值还会被赋给$HOME,随后再引用$HOME的话,返回的就是这个新值。

我们来看一下实际应用。

$echo${HOME:=/tmp}/home/uid002 $unsetHOME# 这么做通常并非明智之举$echo${HOME:=/tmp}/tmp $echo$HOME/tmp $cd;pwd/tmp $

一旦删除该变量,它就不再具有任何值。然后,用 := 运算符作为变量引用的一部分时,$HOME 被替换成了新值(/tmp)​。接下来引用 $HOME 返回的都是这个新值。

记住,赋值运算符有一个重要的例外:不能对位置参数(如$1$*)赋值。在这种情况下,可以使用:-(如${1:-default})​,该表达式只返回值,但不进行赋值。

顺便说一下,注意${VAR:=value}${VAR:-value}在形式上的差异,也许可以帮助你记忆这两种让人抓狂的符号。:= 执行赋值操作,同时返回运算符右侧的值。:- 只做了前者一半的工作:返回值,但不赋值。因此,它的符号也只有等号的一半(一个横杠,而不是两个)​。

15、使用空值作为有效的默认值

你需要设置默认值,但希望空字符串也能作为有效值。你只想在变量不存在时才替换默认值。

${:=}运算符在两种情况下会使用新值:一种是指定的shell 变量不存在(或被明确删除)​;另一种是变量存在,但值为空,就像HOME=""HOME=$OTHER$OTHER没有值)这样。

shell 能够区分这两种情况,忽略冒号(:)表示只希望在变量不存在时才进行替换。如果写成不带冒号的${HOME=/tmp},那么赋值操作仅会在该变量不存在的情况下(从未设置或已明确删除)发生。

我们再用 $HOME 变量演示一次,但这次不使用冒号。

$echo${HOME=/tmp}# 不会被替换/home/uid002 $HOME=""# 通常并非明智之举$echo${HOME=/tmp}# 不会被替换$unsetHOME# 通常并非明智之举$echo${HOME=/tmp}# 会被替换/tmp $echo$HOME/tmp $

这个示例将 $HOME 设置为空字符串,虽然为空,但毕竟也是有效值,因此 = 运算符并不会替换 $HOME 的内容。但如果删除该变量,则会发生替换。要想允许出现空字符串,使用不带冒号的 = 即可。但大部分时候是使用 :=,因为无论你是否有意,空值基本上没什么用处。

16、不只使用字符串常量作为默认值

你不想只使用字符串常量作为变量默认值。

能出现在 shell 变量引用右侧的内容并不局限于字符串常量。例如:

cd${BASE:="$(pwd)"}

如上所示,用于替换的值并不一定非得是字符串常量。它可以是更为复杂的 shell 表达式的结果,其中包括在子shell 中运行的命令(如上述示例所示)​。如果 $BASE 不存在,那么 shell 会运行内建命令 pwd(以获得当前目录)​,并使用其返回的字符串。

那么,我们可以在该运算符(以及其他类似运算符)右侧做些什么?bash 手册页对于出现在运算符右侧的内容是这么表述的:​“……要经过波浪号扩展、参数扩展、命令替换以及算术扩展”​。

具体含义如下。

  • 参数扩展意味着可以使用其他变量,如${BASE:=${HOME}}
  • 波浪号扩展意味着可以使用~bob这样的表达式,它会扩展成用户 bob 的主目录。可以通过${BASE:=~uid17}将默认值设置为用户 uid17 的主目录,但注意不要给~uid17加引号,因为波浪号扩展不会在引号中执行。
  • 该示例用到的就是命令替换,它将命令的输出结果作为变量的值,其语法为$(cmds)
  • 算术扩展意味着可以用$(( ))语法执行整数算术运算。例如:
    echo${BASE:=/home/uid$((ID+1))}

17、对不存在的参数输出错误消息

这些能够设置默认值的便捷写法都挺酷,但有时你需要强制用户指定某个值,否则就无法继续往下进行。用户有可能会遗漏某个参数,因为他们确实不知道该怎样调用脚本。你希望能给用户点提示,省得他们自己瞎猜。相较于堆砌成堆的 if 语句,有没有更简洁的方法来检查各个参数?

引用参数时使用${:?}语法,如例所示。如果指定参数不存在或为空,那么 bash 会输出错误消息并退出。

#!/usr/bin/env bash# 实例文件:check_unset_parms#USAGE="usage: myscript scratchdir sourcefile conversion"FILEDIR=${1:?"Error. You must supply a scratch directory."}FILESRC=${2:?"Error. You must supply a source file."}CVTTYPE=${3:?"Error. ${USAGE}"}

如果执行脚本时没有指定足够的参数,则会出现下列结果。

$ ./myscript /tmp /dev/null ./myscript: line7:3: Error. usage: myscript scratchdir sourcefile conversion $

bash 会测试各个参数,如果参数不存在或为空,则输出错误信息并退出。

$3 所对应的错误消息中使用了另一个 shell 变量。你甚至可以在其中执行其他命令。

CVTTYPE=${3:?"Error. $USAGE. $(rm $SCRATCHFILE)"}

如果$3不存在,则错误消息中会包含短语 “Error.”、变量$USAGE 的值以及 rm $SCRATCHFILE 命令的输出。我们先说到这里。你可以让自己的 shell 脚本紧凑得吓人,没错,我们说的就是吓人(awfully)。最好还是多加些空白字符,让代码逻辑更加清晰可读。

if[-z"$3"]thenecho"Error.$USAGE"rm$SCRATCHFILEfi

另一方面的考虑:${:?}生成的错误信息包含 shell 脚本文件名和行号。例如,以下是示例产生的运行结果片段。

$ ./check_unset_parms ./check_unset_parms: line5:1: Error. You must supply a scratch directory. $ ./check_unset_parms somedir /tmp/check_unset_parms: line6:2: Error. You must supply asourcefile. $ ./check_unset_parms somedir somefile ./check_unset_parms: line7:3: Error. usage: myscript scratchdir sourcefile\conversion

因为无法控制错误消息的这部分内容,而且这看起来好像是 shell 脚本自身出现的错误,再加上可读性的问题,该技术在商业级的 shell 脚本中并不多见。​(不过确实有助于调试。​)

要想所有变量都具备这种行为,同时又不想逐个改动,可以使用 set -u 命令,​“在变量替换时,将不存在的变量视为一种错误”​。

$echo"$foo"➊ $set-u➋ $echo"$foo"bash: foo: unbound variable ➌ $echo$?# 退出码1$set+u ➍ $echo"$foo"➎ $echo$?# 退出码0$
  • ❶ 一开始是正常行为
  • ❷ 启用 nounset(-u)
  • ❸ 现在看到了错误信息和故障退出码
  • ❹ 再次关闭 nounset(-u)
  • ❺ 恢复正常行为

18、修改部分字符串

你想要重命名一批文件。这些文件的名称基本上正确,就是后缀不对。

使用 bash 的参数扩展特性来删除匹配模式的文本,如例所示。

#!/usr/bin/env bash# 实例文件: suffixer## 将文件结尾的.bad修改为.bashforFNin*.baddomv"${FN}""${FN%bad}bash"done

for 循环迭代当前目录中所有以 .bad 结尾的文件名。变量$FN 每次都会获得其中一个文件名。在该循环中,mv 命令负责重命名文件(将旧名称转变成新名称)​。为了避免文件名中包含空格,我们要将其放入引号。

重命名的关键之处是,所引用的F N 包含自动删除结尾字符串 b a d 的操作。 FN 包含自动删除结尾字符串 bad 的操作。FN包含自动删除结尾字符串bad的操作。{} 对该引用做了界定,以便与之相邻的字符串 bash 正好追加到已经过删除的文件名结尾。

以下是分解步骤。

NOBAD="${FN%bad}"NEWNAME="${NOBAD}bash"mv"${FN}""${NEWNAME}"

这样你就能从中看出删除无用后缀、创建新文件名、重命名文件的各个步骤。只要习惯了那些特殊运算符,将这些步骤合并成一行也不算糟糕。

因为我们不仅从变量中删除了子串,还将 bad 替换为bash,所以也可以将替换运算符 / 用于变量引用。类似于那些用 / 进行搜索替换的编辑器命令(vi 和 sed 中就有)​,我们也可以按以下方式这么写:

# 没有锚定好位置,可别这么做mv"${FN}""${FN/bad/bash}"

(和编辑器命令不一样的是,我们没有在结尾使用 /,右侧的花括号起到了相同的作用。​)

但这种写法有一个问题:没有锚定替换操作的位置,因此可以在变量中的任意位置执行。例如,名为subaddon.bad 的文件经过替换后就会变成subashdon.bad,这可不是我们想要的结果。如果将第一个 / 换成 //,则会替换变量中所有指定的内容,最后就成了 subashdon.bash,这也并非我们所愿。最好按以下方式这么写:

# 添加"."来"锚定"替换模式的位置。这种写法更好,但也不是万无一失mv"${FN}""${FN/.bad/.bash}"

解决方案中给出的 ${FN%bad}bash 已经锚定了位置,只删除字符串尾部的文本,在本例中,这正是我们想要的结果。

对变量中的字符串值执行各种操作的运算符有很多种。下表总结了这些运算符。



你可以挨个试试。用起来都非常方便。

19、获得某个数的绝对值

变量中的数值可能是负数,也可能是零或正数。你想得到它的大小(也就是绝对值)​,但 bash 似乎没有求绝对值的功能。

利用字符串操作。

${MYVAR#-}

这是一种简单的字符串操作。在本例中,# 从字符串起始位置开始搜索负号(-)​。如果找到,则将其删除;如果没有找到,就保留原值。不管是哪种情况,最后得到的都是不包含负号的绝对值。

也可以使用 if/then/else 按照数学方法来实现。

# 何必费事呢?if((MYVAR<0))thenletMYVAR=MYVAR*-1fi

但就像注释中所说,何必呢?字符串操作简洁有力。但考虑到可读性,也可以加上注释。

MYVAR=${MYVAR#-}# ABS(MYVAR)

20、用bash实现basename

basename 命令可以做到你想要的,但是能够在不调用外部命令的情况下得到相同的结果吗?bash 的字符串操作能不能做到?

是的,bash 能够从 shell 变量中剥离路径中的目录,只留下最后一部分(文件名)​。当你想按以下方式写时:

FILE=$(basename$FULLPATHTOFILE)

其实只需要写成:

FILE=${FULLPATHTOFILE##*/}

这两种写法的不同之处在于花括号。第一种写法使用的是括号(旧式写法是 ``)​,它会生成一个子 shell 来执行参数值为 $FULLPATHTOFILE 的命令 basename。第二种写法使用的花括号只是 shell 变量替换语法的一部分,并不会生成子 shell,也不会执行命令。它从字符串起始位置开始(因为 #)查找并删除匹配模式*/的最长子串(因为##)​。*匹配任意数量的字符,/ 仅代表其字面含义。对于路径 /usr/local/bin/mycmd,指定模式会匹配(并删除)/usr/local/bin/,将剩下的 mycmd 赋给变量 $FILE。

因为 basename 命令会忽略路径结尾的 /,所以 $(basename /usr/local/bin/) 会返回 bin,而我们用 bash 所实现的版本则返回空串(因为模式*/ 所能匹配的最长内容就是整个字符串)​。为了与basename 兼容,第一件事就是删除路径结尾的/。

真正的 basename 命令还可以将要删除的后缀作为第二个可接受的参数。bash 也可以做到,只是需要额外多加一步。所以,对于:

FILE=$(basename$MYIMAGEFILE .jpg)

更完整的替代方案是:

FILE=${MYIMAGEFILE%/}# 删除结尾的/FILE=${FILE##*/}# 删除结尾/之前的所有字符FILE=${FILE%.jpg}# 如果存在,删除后缀.jpg

21、用bash实现dirname

dirname 命令的作用和你预想的一样,但就像basename,它也是一个单独的可执行文件,需要在子shell 中运行。能不能用字符串操作来代替?

当然能。用字符串操作删除文件名(路径中的最后一部分)​,尽可能多地保留路径中的目录部分。

DIR=${MYPATHTOFILE%/*}

如果变量中保存的是 /usr/local/bin/mycmd,我们希望得到的是 /usr/local/bin,同时丢弃最后一部分(文件名)​。由于路径中的各个部分是由斜线分隔的,从右侧开始(因为 %)删除匹配模式“斜线之后跟着任意数量字符(/*)​”的最短子串(因为只有一个 %,而不是两个)即可。

这个示例演示了如何对 shell 变量进行字符串操作。其中并未给出完整的、可兼容的 dirname命令替代方案,尤其是针对一些路径名以斜线结尾的边缘情况。

22、选取CSV的替换值

你想制作一个由逗号分隔的值列表,但不希望开头或结尾处出现逗号。

如果在循环内部用 LIST=“L I S T , {LIST},LIST,{NEWVAL}” 构建该列表,那么第一次循环(此时 LIST 为空)过后会得到一个前导逗号。你可以对 LIST 进行特殊的初始化处理,以便它在进入循环前就先得到第一个值,但如果觉得这种方法不实用,或是为了避免重复代码(用于得到新值)​,你可以改用 bash 中的 ${:+} 语法。

LIST="${LIST}${LIST:+,}${NEWVAL}"

如果 {LIST} 为空或不存在,那么 $LIST 的两个表达式不会产生任何内容。这就意味着,第一次循环过后,LIST 中保存的只有 NEWVAL 的值。如果 LIST 不为空,则第二个表达式 ${LIST:+,} 会被替换为逗号,将先前的值与新值分隔开来。

下面的代码片段用于读取并构建 CSV 列表。

## 一次读取一个值,构建以逗号分隔的值列表#whilereadNEWVALdoLIST="${LIST}${LIST:+,}${NEWVAL}"doneecho$LIST

23、使用数组变量

到目前为止,你已经见识了不少使用变量的脚本,但是bash 能不能处理数组呢?

当然能。bash 有专门的一维数组语法。

如果编写脚本时已经知道具体的值,则初始化数组就很容易了。格式如下:

MYRA=(first second third home)

括号内列表的每个单词都对应着一个数组元素。你可以像下面这样引用各个元素:

echorunners on${MYRA[0]}and${MYRA[2]}

输出结果如下:

runners on first and third

如果只写$MYRA,那么只会得到第一个数组元素,相当于${MYRA[0]}

24、转换大小写

数码相机里全是一堆大写字母命名的文件,如IMG0001.JPG。你想将文件名改成小写,但又不愿意挨个重新输入。

bash 4.0 中的几个运算符可以在引用变量名时转换其大小写。如果变量 $FN 中包含一个需要转换成小写的文件名(字符串)​,那么F N , , 会返回全部是小写形式的字符串。与此类似, {FN,,} 会返回全部是小写形式的字符串。与此类似,FN,,会返回全部是小写形式的字符串。与此类似,{FN^^} 会返回全部是大写形式的字符串。甚至还有 ${FN~~},它可以切换大小写,将所有的小写字母转换成大写,大写字母转换成小写。

以下的 for 循环会将所有 .JPG 类型的文件名更改成小写字母。

forFNin*.JPGdomv"$FN""${FN,,}"done

或者写成单行脚本:

forFNin*.JPG;domv"$FN""${FN,,}";done

bash 4 或更高版本中还有另外一种方法:你可以将变量声明成始终为小写字母的类型。赋给该变量的任何文本都会转换成小写。要想在 for 循环中使用这种方法来重命名文件,使用简单的赋值即可,无须任何字符串操作。

declare-llcfn# 变量内容会转换成小写字母forFNin*.JPGdolcfn="$FN"mv"$FN""$lcfn"done

还有一些类似的变量声明,它们可以更改所有字母或首字母的大小写。以下是一个简单的演示程序,其中展示了这类声明的用法。

declare-uUP# 全部大写declare-ldn# 全部小写declare-cCa# 仅首字母大写whilereadTXTdoUP="${TXT}"dn="${TXT}"Ca="${TXT}"echo$TXT$UP$dn$Cadone

如果变量是通过 -c 声明的,即便字符串中包含多个单词,也只有首字母大写。你可以尝试运行并观察其如何工作。

25、转换为驼峰命名法

你希望字符串中每个单词的首字符都是大写,而不仅限于首字母。

结合使用数组与大小写转换运算符。

whilereadTXTdoRA=($TXT)# 一定得是($,不要写成$(echo${RA[@]^}done

$TXT 周围的括号令其被视为数组初始化。文本中分隔单词的空白字符划分了数组元素。[@] 语法一次性引用数组中的所有元素,^ 操作符将每个元素的首字符转换为大写。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 4:34:03

Intern-S1-Pro视觉模型:高效混合注意力机制解析与实践

1. Intern-S1-Pro模型概述Intern-S1-Pro是近期在计算机视觉领域引起广泛关注的新型视觉基础模型&#xff0c;由国内顶尖AI研究团队开发。作为Intern系列模型的最新升级版本&#xff0c;它在保持前代模型高效特性的同时&#xff0c;通过创新的网络架构设计和训练策略&#xff0c…

作者头像 李华
网站建设 2026/9/16 4:32:45

基于pytest+aiohttp的接口自动化测试框架设计与实践

做接口自动化时间长了&#xff0c;有一件事始终绕不过去&#xff1a;每来一个新接口&#xff0c;就要重复写一遍请求封装、构造参数、写断言、生成报告。团队从三个人扩到十几个人的时候&#xff0c;这个问题几乎成了压垮人的最后一根稻草——每个人写用例的风格都不一样&#…

作者头像 李华
网站建设 2026/9/16 4:32:38

告别Visio与draw.io:用Mermaid和在线白板重构绘图流程

我电脑里装了Visio七年&#xff0c;最后卸载了&#xff1b;draw.io也用了一段不算短的时间&#xff0c;装了桌面版&#xff0c;后来也一并删掉了。说实话&#xff0c;刚做这个决定的时候还有点舍不得——毕竟Visio当年算得上公司画图的标准工具&#xff0c;draw.io也是免费圈子…

作者头像 李华
网站建设 2026/9/16 4:32:31

Vibe Coding实战:用Trae Code和全局MD文档提升AI编程效率

Vibe Coding&#xff0c;这个词过去一年在开发者圈子里刷屏的频率越来越高。说白了&#xff0c;你不再是一行一行手敲代码&#xff0c;而是用自然语言描述你想要的功能&#xff0c;让 AI 帮你把代码写出来。很多人以为 Vibe Coding 就是“偷懒”&#xff0c;是“不会写代码的人…

作者头像 李华
网站建设 2026/9/16 4:32:24

Skywalking分布式链路追踪实战:从部署到调优的微服务排障指南

做了几年微服务&#xff0c;我最大的感受就是&#xff1a;排查问题的时间从“按分钟算”变成了“按小时算”。尤其是系统一旦拆分出十几个服务&#xff0c;一次用户请求背后可能串了七八个调用链&#xff0c;任何一个环节慢一拍&#xff0c;前端体感就是卡顿、超时。最痛苦的是…

作者头像 李华