脚本编程 类目

脚本语言(英语:Scripting language)是为了缩短传统的“编写、编译、链接、运行”(edit-compile-link-run)过程而创建的计算机编程语言。脚本语言是运维工程师的一大利器,常用的脚本语言有 Python、Shell、Perl 等。

Bash function 还能这么玩

今天看到一篇讲 Bash function 的有意思的文章,原文在这里

在 Bash 中一般我们这么定义一个函数:

function name () {
  ...
}

这是非常常见的写法,包括我自己在内,一直把他当做类似 Python、C 等语言一样的函数定义语法。实际上这里{ ... }并不代表函数体或者函数的作用域。它只是代表里面的内容是一组命令的集合。了解这点之后,接下来就有一些比较好玩的写法了。

比如下面的函数作用是测试文件是否存在,这里就没用大括号:

function fileExists () [[ -f $1 ]]

或者

function isEven () (( $1 % 2 == 0 ))

还有下面的用法:

function name () (
  ...
)

这里用小括号,当执行函数的时候,会 fork 一个子进程来执行里面的命令。子进程对环境的修改不会影响到外面的父进程,也就不需要保存现场或者恢复现场的操作了。比如设置一些参数:

function caseInsensitiveMatch () (
    shopt -s nocasematch
    ....
)

除了上面的写法,这个用法的前提是函数体仅包含一行命令,或复杂或简单,比如 while、for、if、case 等结构都是可以的:

function sleep1 () while :; do "$@"; sleep 1; done

CPython 源码中整数加法的实现

最近突然涌起兴趣去阅读 CPython 源码,网上也看了不少解析的文章,后来网上看到《Python源码剖析》评价不错,可惜现在已经绝版,只能从豆瓣阅读购买了一本电子书观摩 。

我从网上下载的是最新的 Python 2.7 源码,这本书配套的解说代码是 Python 2.5 的,这是一个遗憾,但是大体上相差不大,刚好昨天遇到一处。

昨天看到 Python int 实现的原理,这里不详细表述,有兴趣的可以去看看书。其中整数加法 (int_add) 的实现,虽然代码只有几行,但是其中隐藏的知识点还是非常多的,花了点时间回顾了一些基础知识,在这里也简单总结下。

以下是 2.5 里面加法的实现,也是书中提供的例子,这里直接引用过来作为参考对比,注释是作者加入的。

static PyObject* int_add(PyIntObject *v, PyIntObject *w)
{
    register long a, b, x;
    CONVERT_TO_LONG(v, a);
    CONVERT_TO_LONG(w, b);
    x = a + b;
    //[1] : 检查加法结果是否溢出
    if ((x^a) >= 0 || (x^b) >= 0)
        return PyInt_FromLong(x);
    return PyLong_Type.tp_as_number->nb_add((PyObject *)v, (PyObject *)w);
}

下面是 2.7 中的代码对比,大体都没有变化:

static PyObject *
int_add(PyIntObject *v, PyIntObject *w)
{
    register long a, b, x;
    CONVERT_TO_LONG(v, a);
    CONVERT_TO_LONG(w, b);
    /* casts in the line below avoid undefined behaviour on overflow */
    x = (long)((unsigned long)a + b);
    if ((x^a) >= 0 || (x^b) >= 0)
        return PyInt_FromLong(x);
    return PyLong_Type.tp_as_number->nb_add((PyObject *)v, (PyObject *)w);
}

在此之前,先简单介绍下上面的逻辑:
1)首先 int_add 函数是 Python 中 int 加法的实现函数,参数是两个 Python 整数对象,PyIntObject;
2)接着使用预先定义好的宏(不是重点,这里不具体展开),从整数对象中取出 value,这个value就是整数的值,类型是 long;
3)接下来做整数加法,判断是否溢出,如果没有发生溢出,则将新建一个整数对象,最后结果返回;
4)如果加法过程中发生溢出,则使用更长的类型(PyLong_Type)来做这个加法运算;

这个函数的精髓在与加法的处理,不是简单求和返回,可以看出 2.5 和 2.7代码的区别:

// 2.5
x = a + b;

// 2.7
x = (long)((unsigned long)a + b);

为什么 2.7 要搞得怎么复杂,又是转换成 unsigned long 最后又转换为 long,实际上原因是因为一个历史包袱,在C语言的定义中有符号数(signed)的加法溢出是 undefined behavior,所以这里先变成无符号数的加法,如果溢出就是简单做个截断(取模)。注意,无符号数和有符号数运算,有符号数会隐式转换成无符号数。

接下来我们看对溢出额判断,(x^a) >= 0 || (x^b) >= 0,为什么使用异或来判断。这里先梳理下,什么情况下会发生加法溢出:
1)如果两个不同符号的数字相加,不会发生溢出,比如 5 + (-128);
2)如果两个相同符号的数字相加,可能会发生溢出,比如正正相加溢出后变成负数,负负相加后变成整数;
这里实际上就是利用了这两点来作为判断依据,如果加法运算结果和原来的任意一个数字符号一致就没有溢出,使用异或来判断性能更好。关于溢出的判断还有其他方法,网上也有不少小伙伴提供了更多思路

这里还有一个隐含的点,在 Python 里整数对象是不可变的,这个要注意,相加之后是返回一个新的对象:

>>> a = 1
>>> id(a)
38821992L
>>> a += 1
>>> id(a)
38821968L

继续看书。

浅谈 Shell 脚本配置文件格式

开发过程中为了减少 hardcode,不可避免的需要提供配置文件给用户定制。对于高级编程语言来说,因为有丰富的第三方库,可供选择的配置文件格式有很多,比如 xml、jsno、ini、yaml 等等。

key=value 文本格式配置

而对于 linux shell,基本上很难使用前面提到的各种格式。所以在 unix 系统上,很多 shell 脚本的配置文件都是纯粹的 key=value 文本格式,例如绝大多数的开机服务启动脚本、网络配置文件等。

例子 1:ntp 配置文件

$ cat /etc/sysconfig/ntpd
# Drop root to id 'ntp:ntp' by default.
OPTIONS="-u ntp:ntp -p /var/run/ntpd.pid"

# Set to 'yes' to sync hw clock after successful ntpdate
SYNC_HWCLOCK=no

# Additional options for ntpdate
NTPDATE_OPTIONS=""

例子 2:网络配置文件

$ cat /etc/sysconfig/network
NETWORKING="yes"
HOSTNAME="xx.com"

而且,要注意得是,一般 key=value 的等号两边不应该有空格,因为大多数脚本都是直接 source 配置文件的(当然,也有部分脚本是会自己处理配置文件格式),使用起来很简单,基本上没有解析的操作:

$ cat /etc/init.d/network
if [ ! -f /etc/sysconfig/network ]; then
    exit 0
fi

. /etc/sysconfig/network

理所当然,这种格式无法满足更复杂的配置文件需求,比如 ini 格式的 section。那么,在 shell 中除了满世界去找一个解析库之外,能有什么方法可以实现呢?

扩展 key=value 文本格式配置

假设,我们管理着 n 个集群,每个集群配置项都是一样的,我们需要在 shell 脚本中,可以根据集群的名称来导入对应的配置。

查看全文

理解 IFS

Bash 里的 word splitting 是很基础的一个知识点,如果没有理解透彻,很多时候会犯下不少奇奇怪怪的错误(参见 Bash Pitfalls,或者本博客翻译 Bash Pitfalls: 编程易犯的错误(一))。

一个例子,现在我们现在要一次读入文件 onefile.txt 的内容并输出,假设文件的内容是这样的:

kodango -> ~/Workspace/coding/test
$ cat onefile.txt
hello world

当我们习惯性地使用 for 循环来解决这个问题时,你会发现输出的结果与预期大相径庭:

$ for i in $(<onefile .txt); do echo "$i"; done
hello
world

给我们的脑子也打开调试开关。首先 onefile.txt 的内容一次性地输出给 for 循环,我在中间用比较形象的 tag 来描述一个空白字符:

hello<blank>world<newline><blank><newline>

这个时候,word splitting 发生了,将以上字符串按照 IFS 分隔成 helloworld。IFS 是用来分隔命令中的每一个单词的,它可以有多个字符组成,每个字符都被视作分隔符。默认情况下,它的值为 <newline><tab><whitespace,这也是为什么默认都是按空格、回车等空白字符分隔的原因。

这样一来 hello 和 world 被分隔可以很好地解释,但是那又是为什么第二行的空行没有了呢?原来,当 IFS 包含空白字符时(比如回车、空格、制表符等),在任何需要分隔单词的场景下,位于字符串开头和结尾的空白字符会被删除,另外一点是,字符串中间的连续空白会被压缩成一个。

查看全文

Bash Pitfalls: 编程易犯的错误(四)

上一篇文章参见Bash Pitfalls: 编程易犯的错误(三)。这一篇翻译得不是非常满意,时间比较赶,请见谅,如果有问题可以在本文后方留言,大家一起深入探讨。

36. [ -n $foo ] or [ -z $foo ]

这个例子中,$foo 没有用引号引起来,当$foo包含空格或者$foo为空时都会出问题:

$ foo="some word" && [ -n $foo ] && echo yes
-bash: [: some: binary operator expected

$ foo="" && [ -n $foo ] && echo yes
yes

正确的写法是:

[ -n "$foo" ]
[ -z "$foo" ]
[ -n "$(some command with a "$file" in it)" ]

[[ -n $foo ]]
[[ -z $foo ]]

37. [[ -e "$broken_symlink" ]] returns 1 even though $broken_symlink exists

这里-e 选项是看文件是否存在,当紧跟的文件是一个软链接时,它不看软链接是否存在,而是看实际指向的文件是否存在。所以当软链接损坏时,即实际指向的文件被删除后,-e 的结果返回1。

查看全文

Bash Pitfalls: 编程易犯的错误(三)

上一篇文章参见Bash Pitfalls: 编程易犯的错误(二)

24. for arg in $*

和大多数 Shell 一样,Bash 支持依次读取单个命令行参数的语法。不过这并是$*或者$@,这两种写法都不正确,它们只能得到完整的参数列表,并非单独的一个个参数。

正确的语法是(没错要加上引号):

for arg in "$@"

# 或者更简单的写法
for arg

在脚本中遍历所有参数是一个再普遍不过的需求,所以 for arg 默认等价于 for arg in "$@"。$@使用双引号后就有特殊的魔力,每个参数展开后成为一个独立的单词。("$@"等价于"$1" "$2" "$3" ...)

下面是一个错误的例子:

for x in $*; do
   echo "parameter: '$x'"
done

执行的结果为:

$ ./myscript 'arg 1' arg2 arg3
parameter: 'arg'
parameter: '1'
parameter: 'arg2'
parameter: 'arg3'

正确的写法:

for x in "$@"; do
   echo "parameter: '$x'"
done

执行的结果为:

$ ./myscript 'arg 1' arg2 arg3
parameter: 'arg 1'
parameter: 'arg2'
parameter: 'arg3'

查看全文

Bash Pitfalls: 编程易犯的错误(二)

上一篇文章参见Bash Pitfalls: 编程易犯的错误(一)

13. cat file | sed s/foo/bar/ > file

你不应该在一个管道中,从一个文件读的同时,再往相同的文件里面写,这样的后果是未知的。

你可以为此创建一个临时文件,这种做法比较安全可靠:

# sed 's/foo/bar/g' file > tmpfile && mv tmpfile file

或者,如果你用得是 GNU Sed 4.x 以上的版本,可以使用-i 选项即时修改文件的内容:

# sed -i 's/foo/bar/g' file

14. echo $foo

这种看似无害的命令往往会给初学者千万极大的困扰,他们会怀疑是不是因为 $foo 变量的值是错误的。事实却是因为,$foo 变量在这里没有使用双引号,所以在解析的时候会进行单词拆分文件名展开,最终导致执行结果与预期大相径庭:

msg="Please enter a file name of the form *.zip"
echo $msg

这里整句话会被拆分成单词,然后其中的通配符会被展开,例如*.zip。当你的用户看到如下的结果时,他们会怎样想:

Please enter a file name of the form freenfss.zip lw35nfss.zip

查看全文

简洁的 Bash Programming 技巧(三)

这是简洁的 Bash Programming 技巧系列的第三篇文章,这一系列的文章专门介绍Bash编程中一些简洁的技巧,帮助大家提高平时 Bash 编程的效率。有兴趣的同学可以回顾下之前的两篇文章(一)续篇

1. 替换语法${parameter/pattern/string}的妙用

${parameter/pattern/string}将parameter中匹配pattern的部分替换成string,例如下面的例子将字符串中的e替换成x:

$ str="three"
$ echo "${str/e/x}"   # thrxe

如果pattern部分以/开头,表示替换parameter中所有匹配的内容,例如:

$ str="three"
$ echo "${str//e/x}"  # thrxx

如果pattern部分以#开头,表示仅当parameter开始处匹配pattern的时候替换,例如:

str="three"
$ echo "${str/#e/x}" # three
$ echo "${str/#t/x}" # xhree

与此对应地是,如果pattern部分以%开头,表示仅当parameter结尾处匹配pattern的时候替换,例如:

$ str="three"
$ echo "${str/%e/x}" # threx

如果string部分为空,匹配pattern的部分被删除(替换为空),例如:

$ str="three"
$ echo "${str/h/}"  # tree

查看全文