NoCome

 找回密码
 立即注册

QQ登录

只需一步,快速开始

查看: 48904|回复: 1

Bash 技巧

[复制链接]

0

主题

0

帖子

1

RMB

NoCome(No.3)

Rank: 7Rank: 7Rank: 7

积分
-548
发表于 2019-12-15 22:20:44 | 显示全部楼层 |阅读模式
英文原文:BASH TIPS & TRICKS: GOOD AND NOT-SO-GOOD BASH PRACTICES

                                                                                来源:OSCHINA,中国最大的开源技术社区                        


Bash不是最佳的程序员友好之工具。它需要小心谨慎,底层知识并且不允许出现任何错误(你知道你不能输入foo = 42,对吧?)。 另一方面,bash是无处不在的(即使在Windows 10上),它非常容易移植且功能强大,实际上它是自动化任务时最实用的选择。幸运的是,遵循一套简单的规则可以使你免于其诸多雷区。



1. Shebang

目前有数个可用的shebang,你可以用其引用你想执行代码的解释器。他们中的一些是:

  • #!/usr/bin/env bash
  • #!/bin/bash
  • #!/bin/sh
  • #!/bin/sh –
我们都知道shebang仅仅是一个指向shell解释器的路径(绝对火相对于当前目录),但哪一个更受欢迎呢?
长话短说 –为了可移植性,你应该使用#!/usr/bin/env bash。这是因为POSIX并没有标准化路径名,因此不同的基于UNIX的系统可能会将bash放到不同的位置。你不能完全假定——例如——/bin/bash是必然存在的 (一些BSD系统将bash可执行文件放到/usr/local/bin/bash中)。




Env实用程序可以帮助我们规避这种限制:#!/usr/bin/env bash将是代码在执行时使用PATH路径下找到的第一个解释器。尽管这不是最完美的解决方案 (那么如果同样的问题也适用于/usr/bin/env呢? 幸运的是,据我所知每个UNIX OS将env放置到同一个位置), 这是我们能做到的最好的方案。
然而,我意识到这也有个例外:对于系统启动脚本,既然/bin/sh是系统标准命令行解释器,使用之。
更多信息请查阅这篇这篇文章。



2. 始终使用引号

这是你应该遵循的最简单以及最好的建议,以避免诸多可能的陷阱。错误的shell引用是让bash程序员头痛的最常见原因。不幸的是,它并不像重要那么容易。
目前有很多不错的文章完全涵盖了这一特定主题。我没有更多要说的,但向你推荐这篇以及t这篇文章。
值得记住的是:你通常应该使用双引号。



3.变量的使用
$foo 是bash中引用变量的经典方法,但是bash2.0版本(通过echo $BASH_VERSION查看)给我们提供了新标记方法——变量扩展。这种方法是通过在变量标识符的两边使用大括号来做标记的,比如${foo}。为什么说这是一种好的实践呢?因为它给我们带来了一些新的特性:
数组元素的扩展:${array[42]}
参数的扩展,例如${filename%.*} (删除了文件的扩展名),, ${foo// } (删除了空格), ${BASH_VERSION%%.*}(获取bash的大版本号)
变量名的拼接:${dirname}/${filename}
将字符串拼接到变量的后面: ${HOME}/.bashrc
通过位置参数来访问参数变量(脚本的输入参数),例如 $9
支持子字符串的访问:${foo:1:5}
间接引用:${!foo}  将会展开成一个由名称为foo,且存储在其中的值来间接表示的值(bar=42; foo="bar"; echo "${!foo}" 将会打印42)
大小写修改:${foo^}会将foo的首字母修改为大写字母,,(单独一个逗号会将其转换成小写字母)双重形式的这种方法 (^^ 和 ,,) 会将所有的字母进行转换。



在大多数常见情况下,使用变量扩展形式使我们没有优于经典扩展形式的地方,但为了保持代码一致性,在所有地方使用它可以被认为是一种好的做法。在阅读更多相关信息。
你还需要了解关于bash中的变量的是,默认情况下,所有变量都是全局变量。这可能导致诸如浅拷贝、覆盖或歧义引用等问题。local运算符限制了变量的范围,防止它们泄漏到全局命名空间中。请记住 - 将所有函数的变量设置为local变量。



4.观察脚本的运行目录
你经常会在bash脚本中和其他的文件进行交互。因此,你必须十分小心使用相对路径。默认情况下,当前的工作路径是由脚本所在的父shell环境下所得到的。
  1. $ pwd/home/jakub$ cat test/test#!/usr/bin/env bashecho "$(pwd)"$ ./test/test/home/jakub
复制代码
当pwd和脚本所在的路径不一致的时候,会存在一些问题。这个时候,不能够简单的通过./脚本名称的方式运行脚本,因为它不会指向你的脚本旁边的文件。为了更加简单的将脚本作用到特定路径的文件,并且避免不小心引用到其他的系统文件,你应该考虑使用这个方便的单行指令将子shell工作目录更改为bash脚本的所在的目录:
  1. cd "$(cd "$(dirname "${BASH_SOURCE[0]}")" > /dev/null && pwd)" || return$ pwd/home/jakub$ cat test/test#!/usr/bin/env bashcd "$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" || returnecho "$(pwd)"$ ./test/test/home/jakub/test
复制代码
看起来是不是更加自然?



5. 你真的不需要 ls

在 bash 脚本中使用 ls 的方法几乎总是有缺陷的,我甚至无法记起这样做的一个理由。为了解释其原因,我们来看看两个常见的例子:
  1. for file in $(ls *.txt)
复制代码
当任意文件名包含空格时,分词将破坏此 for 循环。更重要的是 —— 如果文件名包含 glob 字符(也称为通配符,如*、?、[、]),它将被识别为 glob 模式并由 shell 扩展,但这可能不是你想要的。另一个问题是 POSIX 允许路径名包含除 \0 之外的任何字符(包括 |,/ 甚至换行符)。这使得在处理 ls 输出时无法确定第一个路径名的结束位置以及第二个路径名的起始位置。
  1. for file in "$(ls *.txt)"
复制代码
将 ls 包含在双引号内将导致其输出被视为单个词 —— 而不是期望的文件列表。
如何以正确的方式遍历文件列表呢?有两种可行策略:
  1. for file in ./*.txt
复制代码
这会使用上述的 bash globbing 功能。记得是双引用"${file}"!
  1. find . -type f -name '*.txt' -exec ...
复制代码
这个可能是最好的解决方案。 Find 工具允许你使用基于正则表达式的搜索(-regex),递归并具有许多你可能觉得有用的内置功能。这里有一个不错的简介。
  1. find . -type f -name '*.txt' -print0 | xargs -0 ...
复制代码
另一种替代 find 的用法是使用 xargs。它既不简单也不简短,但 xargs 的优势在于它支持并行管道执行。更多有关此差异的信息阅读此文
总而言之,永远不要尝试解析 ls 命令的输出。它根本没有被设计用于解析,你无法让其正常工作。点此阅读更多。



6. 期待意外

通常忘记检查在bash脚本中执行命令的非零状态代码。很容易想象当我们的cd命令在文件操作之前静默失败时会发生什么(因为例如“没有这样的文件或目录”)。
  1. #!/usr/bin/env bashcd "${some_directory}"rm -rf ./*
复制代码
上面的一个例子会很好的工作,但只有在没有出错的情况下。目的是删除some_directory/目录的内容,但最终可能会在完全不同的目录位置执行rm -rf ./*。
cd“$ {some_directory}”&& rm -rf ./* 和 cd“$ {some_directory}”|| return是最简单的自描述解决方案。在这两种情况下,如果cd返回非零,则不会执行删除。值得指出的是,此代码仍然容易受到常见编程错误的影响 - 拼写错误。
执行cd“$ {some_dierctory}”&& rm -rf ./*将最终删除您可能要保留的文件(只要没有拼写错误的some_dierctory变量声明)。 “$ {some_dierctory}”将扩展为“”,这是完全有效的cd参数,将我们带到主目录。不过不用担心,这不是故事的结局。



Bash编程有一些值得注意的对于程序员友好的开关:

  • set -o nounset 可以设置bash将引用未被初始化的变量视为错误,这一特性可以避免我们犯下拼写等低级错误。
  • set -o errexit 可以设置bash脚本在语句的返回值为非0值的时候立即退出。虽然使用errexit可以帮助我们有效的检验程序的错误,但要正确的使用errexit却需要一些技巧。一些命令故意返回非0的值来产生告警,并且程序员确切的知道应该如何去处理特定命令返回的错误值。参考这里了解更多。
  • set -o pipefail 可以改变使用管道时的默认行为。默认情况下,bash会将管道前面的命令返回的状态码作为管道后面的命令的输入,这意味着false| true返回0(管道符前面的状态是非0,会立马执行管道符后的命令)。这样的结果有可能不是你所期望的,因为这种情况下会忽略管道符前面的命令的结果。此时需要使用pipefail命令了,通过set -o pipefail的设置,可以设置管道的退出码为最右边的返回非零的命令(或者在所有的指令的执行成功的情况下设置退出码为0)。
当然,错误问题的处理不仅仅适用于上面提到的cd命令,你的bash脚本应该考虑各种情况下可能出现的问题,比如路径名称中的空格,文件的缺失,目录未创建,或者是错误的使用了原本不存在的命令等(例如,就像你所了解的,并不是所有运行你的bash脚本的linux操作系统都预装了awk命令)






本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接。
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
点评回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

关闭

NoCome推荐上一条 /2 下一条

小黑屋|手机版|Archiver|NoCome ( 粤ICP备19043184号 )

GMT+8, 2021-7-24 02:00 , Processed in 0.081929 second(s), 29 queries .

Powered by Discuz! X3.4

© 2001-2017 Comsenz Inc.

快速回复 返回顶部 返回列表