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

Shell编程实例——shell变量(二) shell变量14、设置默认值15、使用空值作为有效的默认值16、不只使用字符串常量作为默认值17、对不存在的参数输出错误消息18、修改部分字符串19、获得某个数的绝对值20、用bash实现basename21、用bash实现dirname22、选取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$((ID1))}17、对不存在的参数输出错误消息这些能够设置默认值的便捷写法都挺酷但有时你需要强制用户指定某个值否则就无法继续往下进行。用户有可能会遗漏某个参数因为他们确实不知道该怎样调用脚本。你希望能给用户点提示省得他们自己瞎猜。相较于堆砌成堆的 if 语句有没有更简洁的方法来检查各个参数引用参数时使用${:?}语法如例所示。如果指定参数不存在或为空那么 bash 会输出错误消息并退出。#!/usr/bin/env bash# 实例文件check_unset_parms#USAGEusage: myscript scratchdir sourcefile conversionFILEDIR${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]thenechoError.$USAGErm$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$foobash: foo: unbound variable ➌ $echo$?# 退出码1$setu ➍ $echo$foo➎ $echo$?# 退出码0$❶ 一开始是正常行为❷ 启用 nounset-u❸ 现在看到了错误信息和故障退出码❹ 再次关闭 nounset-u❺ 恢复正常行为18、修改部分字符串你想要重命名一批文件。这些文件的名称基本上正确就是后缀不对。使用 bash 的参数扩展特性来删除匹配模式的文本如例所示。#!/usr/bin/env bash# 实例文件: suffixer## 将文件结尾的.bad修改为.bashforFNin*.baddomv${FN}${FN%bad}bashdonefor 循环迭代当前目录中所有以 .bad 结尾的文件名。变量$FN 每次都会获得其中一个文件名。在该循环中mv 命令负责重命名文件将旧名称转变成新名称​。为了避免文件名中包含空格我们要将其放入引号。重命名的关键之处是所引用的F N 包含自动删除结尾字符串 b a d 的操作。 FN 包含自动删除结尾字符串 bad 的操作。FN包含自动删除结尾字符串bad的操作。{} 对该引用做了界定以便与之相邻的字符串 bash 正好追加到已经过删除的文件名结尾。以下是分解步骤。NOBAD${FN%bad}NEWNAME${NOBAD}bashmv${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((MYVAR0))thenletMYVARMYVAR*-1fi但就像注释中所说何必呢字符串操作简洁有力。但考虑到可读性也可以加上注释。MYVAR${MYVAR#-}# ABS(MYVAR)20、用bash实现basenamebasename 命令可以做到你想要的但是能够在不调用外部命令的情况下得到相同的结果吗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}# 如果存在删除后缀.jpg21、用bash实现dirnamedirname 命令的作用和你预想的一样但就像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$LIST23、使用数组变量到目前为止你已经见识了不少使用变量的脚本但是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,,};donebash 4 或更高版本中还有另外一种方法你可以将变量声明成始终为小写字母的类型。赋给该变量的任何文本都会转换成小写。要想在 for 循环中使用这种方法来重命名文件使用简单的赋值即可无须任何字符串操作。declare-llcfn# 变量内容会转换成小写字母forFNin*.JPGdolcfn$FNmv$FN$lcfndone还有一些类似的变量声明它们可以更改所有字母或首字母的大小写。以下是一个简单的演示程序其中展示了这类声明的用法。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 周围的括号令其被视为数组初始化。文本中分隔单词的空白字符划分了数组元素。[] 语法一次性引用数组中的所有元素^ 操作符将每个元素的首字符转换为大写。