脚本编程 类目

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

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 脚本中,可以根据集群的名称来导入对应的配置。

查看全文

Shell 一键安装命令

现在是懒人的天下,为了迎合用户的需求,很多开源软件或者包提供的安装步骤都非常简单,大家应该看到不少类似一键安装的命令。下面是几个典型的例子:

# homebrew 安装
$ ruby -e "$(curl -fsSL https://raw.github.com/Homebrew/homebrew/go/install)"

# nvm 安装, 两种方法
$ curl https://raw.githubusercontent.com/creationix/nvm/v0.8.0/install.sh | sh
$ wget -qO- https://raw.githubusercontent.com/creationix/nvm/v0.8.0/install.sh | sh

# rvm 安装
$ \curl -sSL https://get.rvm.io | bash -s stable

简单粗暴,CMD + C 再加 CMD + V,随手一个 Enter,就搞定了。

那么这上面的原理是什么样子的呢?其实很简单。

首先通过 curl 或者 wget 将安装脚本下载下来,将内容输出到标准输出。这一步对应上面的 curl -ssL 或者 wget -qO-,一定要注意将错误或者异常输出过滤掉,保证标准输出的内容就是脚本的内容。

然后通过管道传递给 shell,shell 在没有指定脚本文件的时候,支持从标准输入读取脚本内容并解释执行。这样将"下载 - 保存 - 安装"这几步操作合到一个命令中完成。

对于 rvm 的安装又有点特殊,安装脚本需要指定执行参数,bash -s stable-s 之后的部分就是透传给安装脚本的参数,翻译下可以理解的形式是:

$ \curl -sSL https://get.rvm.io > /tmp/rvm_install.sh
$ bash /tmp/rvm_install.sh stable
$ rm -f /tmp/rvm_install.sh

PS: \curl 的用法,我在 终端下肉眼看不见的东西 曾经提到过。

不过,建议执行类似一键安装的命令之前,一定要先大致看下安装脚本,避免里面有不安全的代码。

理解 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 One-Liners Explained 译文(五)

这是 Bash One-Liners Explained 系列的第五篇文章。在这一部分,我会教你如何快速在 Bash 命令行中使用 Emacs 风格的键盘导航快捷键。

0. 行编辑模式介绍

Bash 使用 GNU readline 库来提供行编辑特性。readline 库同时支持 Emacs 风格和 Vi 风格的快捷键绑定,也支持用户去做自定义绑定。默认情况下,readline 会使用 Emacs 风格的键绑定,不过你可以很方便的切换到 Vi 风格,或者自定义设置。

执行set -o emacs命令切换到 Emacs 风格,set -o vi则会切换到 Vi 风格。

除此之外,你仍可以通过~/.inputrc或者bind命令来自定义快捷键绑定。例如,bind '"\C-f": "ls\n"'CTRL+F绑定为执行ls命令。你可以通过查阅 Bash 手册中的 readline 一节来更多地了解 readline 的快捷键绑定语法。

1. 移动光标到行首

CTRL + a

2. 移动光标到行尾

CTRL + e

3. 光标往后(向左)移动一个单词

ESC + b 或者 ALT + b

4. 光标往前(向右)移动一个单词

ESC + f 或者 ALT + f

5. 删除上一个单词

CTRL + w

查看全文

Bash One-Liners Explained 译文(四)

这是 Bash One-Liners Explained 系列的第四篇文章。在这一篇里,我会给大家介绍 Bash 命令行历史功相关的内容。我会选择用最合适的 Bash 方法,各种常见的语法和技巧,向各位阐明如何用 Bash 内置的命令和 Bash 编程语言来完成各式各样的任务。

1. 清除命令行历史

$ rm ~/.bash_history

Bash 将历史执行的命令都保存在文件.bash_history中,该文件位于你的家目录下。为了清除命令行历史,只要把这个文件删除即可。

注意,当你执行完退出后,最后一个rm ~/.bash_history命令依然会被记录下来。如果你想隐藏清除的操作命令,请看下一条。

2. 当前会话下停止记录命令行历史

$ unset HISTFILE

环境变量HISTFILE指向命令行执行历史保存的目标文件路径,如果你重置了该变量,Bash 就不会保存历史。

另外一种方法是将它指向/dev/null

$ HISTFILE=/dev/null

3. 不要记录当前执行的命令

很简单,只要在命令之前加空格就行:

$  command

注意,以上正常工作的前提是,HISTIGNORE变量被正确的设置,它的值是冒号分隔的匹配表达式列表,如果一个命令匹配其中的任意一个表达式则不会被保存到记录中。

查看全文

Bash One-Liners Explained 译文(三)

最近工作,生活中有一堆事情要处理,博客好久没更新了,请见谅。

拖了好久才翻译好这篇文章,这篇翻译下来好累,主要是原文废话太多了,后来还是决定尽量省略部分内容,建议有时间的还是去看下原文。这一篇太长了,先翻译部分,我会继续不断的更新。

这是 Bash One-Liners Explained 系列的第三篇文章。在这一篇里,我会给大家介绍重定向相关的内容。我会选择用最合适的 Bash 方法,各种常见的语法和技巧,向各位阐明如何用 Bash 内置的命令和 Bash 编程语言来完成各式各样的任务。

重定向其实是通过操作文件描述符来完成的,这样会更容易理解。当 Bash 启动时,会自动创建三个标准的文件描述符,它们分别是 stdin(标准输入,文件描述符为0),stdout(标准输出,文件描述符为1)和 stderr(标准错误输出,文件描述符为2)。你也可以创建更多的文件描述符,例如3,4,5等等,或者关闭它们,又或者拷贝它们。你可以从对应的文件中读取或者写入内容。

文件描述符指向某个文件(除非它们被关闭)。通常情况下,Bash 启动的三个文件描述符 —— stdin,stdout 和 stderr 都是指向你的终端,从终端输入中读取内容,并且把标准输出和标准错误都送到终端上。

查看全文

Bash One-Liners Explained 译文(二)

这是 Bash One-Liners Explained 系列的第二篇文章。在这一篇里,我会给你们介绍如何用 Bash 来完成各种各样的字符串操作。我会选择用最合适的 Bash 方法,各种常见的语法和技巧,向各位阐明如何用 Bash 内置的命令和 Bash 编程语言来完成各式各样的任务。

1. 生成从 a 到 z 的字母表

$ echo {a..z}

这一行命令用到了括号展开(Brace expansion)功能,它可以用于生成任意的字符串。{x..y} 是一个序列表达式,其中 x 和 y 都是单个字符,这个表达式展开后包含 x 与 y 之间的所有字符。

运行上面的命令会生成从 a 到 z 的所有字母:

$ echo {a..z}
a b c d e f g h i j k l m n o p q r s t u v w x y z

2. 生成从 a 到 z 的字母表,字母之间不包含空格

$ printf "%c" {a..z}

这是一个 99.99% 的人都不知道的非常棒的技巧。如果你在printf命令之后指定一个列表,最终它会循环依次打印每个元素,直到完成为止。

在这一行命令中,printf 的格式为"%c",代表一个字符(character),后面的参数是从 a 到 z 的字符列表,字符之间以空格分隔。所以,当printf执行时,它依次输出每个字符直到所有字符全被处理完成为止。

下面是执行的结果:

abcdefghijklmnopqrstuvwxyz

输出的结果最后不包含换行符,因为printf的输出格式是"%c",其中并没有包含\n。如果你想输出完整的一行,可以简单地在字符列表后面增加一个$'\n'

$ printf "%c" {a..z} $'\n'

查看全文

1 2 3 5