Shell Tips: Unix 时间到字面

我的工作需要天天跟报表数据打交道,在交换的文件中,一般时间的字段内容都是 Unix 时间。为了检查数据的正确性,不可避免地需要转换 Unix 时间到人类可读的字面时间。

下面想分享的是一个在 Shell 下转换 Unix 时间到字面的小方法。与前面几篇一样,这个小 shell 函数仍然可以放在 ~/.bashrc 中方便快捷使用。

# 转换 Unix 时间到本地时间字符串
function ctime()
{   
    date -d "UTC 1970-01-01 $1 secs"
}

使用方法很简单:

$ ctime 1234567890
Sat Feb 14 07:31:30 CST 2009

对 date 命令熟悉的同学会说,date 不是已经有直接转 Unix 时间的参数了吗?

$ date -d @1234567890
Sat Feb 14 07:31:30     2009

但是不好意思的是,小弟有时候用的 date 程序好老,不支持 @ 符号。

$ date --version
date (coreutils) 5.2.1
Written by David MacKenzie.

Copyright (C) 2004 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

PS: 写完这篇博文,我又想到了一个有趣的事情,既然很多 Linux 64 位版本的 time_t 已经是 long long 格式了,那么 date 命令有没有 year 2038 问题呢?

下面是 date (coreutils) 5.2.1 在 64 位服务器上的尝试结果:

$ date +%s -d "Tue Jan 19 11:14:07 CST 2038"
2147483647
$ date +%s -d "Tue Jan 19 11:14:08 CST 2038"
2147483648
$ date +%s -d "Tue Jan 19 11:14:09 CST 2999"
32473710849
$ ctime 2147483647
Tue Jan 19 11:14:07 CST 2038
$ ctime 2147483648
Sat Dec 14 04:51:44 LMT 1901
$ ctime 32473710849
Mon Mar 28 07:33:53 LMT 1910

看来字面时间和 Unix 时间之间互转存在着问题啊!但是用 Ubuntu 11.04 的 date (GNU coreutils) 8.5 尝试就不存在这个问题了。

Shell Tips: 用GNU Screen实现发送交互到所有会话

服务器冗余和分拆是互联网服务中经常用来缓解访问压力的手段,那么检查或者管理多台同构服务器也是互联网行业工程师们绕不开的操作。经常面临的问题是:如何高效地在多台服务器上执行相同的命令,进行批量系统操作或问题检查。

Windows 下的 ssh 客户端 XShellSecureCRT 都提供了类似的功能,当每个标签页都连接到一个服务器时,可以在命令窗口中发送交互到所有的标签页以实现同时操作多台服务器的目的。这招我还是从 OP 那里学来的,的确大大提高了生产力。

但这种方法也存在一些问题:

  1. 只适用于特定的 ssh 客户端。例如对 Linux 来说就有些不适用,不过据说 Konsole 也提供了类似功能,未验证。
  2. 每个标签页中,还是得一台一台地登陆上服务器,很难自动化。据说有的客户端支持编写脚本实现,但还要学习对应脚本语言,且灵活性有限。
  3. 无法一直保持持续的连接。特别是对有开发机的工程师,本来开发机是一直在线的,但由于客户端的限制,只能在本地电脑连接多服务器。当本地网络断开后,自然多服务器的连接也断开了。

为了解决这些问题,小弟想到了神器 GNU Screen。Screen 也是终端,难道无法做这件事吗?您还别说,在我费心劳力一上午之后,总算摸索出了用 Screen 解决上述问题的方法。下面两个可以放到 ~/.bashrc 中的函数,就是我心血的“结晶” :)

function screenssh ()
{
    local username=YOUR_USERNAME
    local password=YOUR_PASSWORD
    local server=''
    local timeout=3
    for server in $@; do
        screen -S $STY -X screen ssh $username@$server
    done
    sleep $timeout
    local cmd="screen -S $STY -X at ssh# stuff $'$password\n'"
    eval $cmd
}

function lets ()
{
    local cmd="screen -S $STY -X at ssh# stuff $'$1\n'"
    eval $cmd
}

Screen 的用法和技巧,在我之前的文章中也有提及,此处不再赘述。这里主要介绍一下上面两个函数的作用和用法:

screenssh 是在 screen 中自动登陆多台服务器的命令。这个 bash 函数接受服务器列表作为输入,执行后会在当前 screen 中为每个服务器打开一个 window,并使用提供的用户名和密码登陆这些服务器。这样当前 screen 中就会多出 N 个 window,分别对应登陆到 N 个服务器。在使用前,你要修改用户名、密码变量值为你需要的内容,而且该命令必须在 screen 中执行,在 screen 外执行是无效的。

执行完 screenssh 后,就可以祭出 lets 命令来在多个 window 中同时执行操作命令了。lets 接受一个字符串作为输入,执行后该字符串会作为命令发送到 N 个服务器对应的 N 个 windows 中执行。

看完以后令人困惑的地方可能是,我到底应该在哪里执行 screenssh 和 lets 这两个命令呢?下面用一个例子来更直白地阐述一下这两个命令的使用方法。

假设你需要在 3 台服务器:s1.solrex.org, s2.solrex.org, s3.solrex.org 上执行 grep FATAL ~/error_log 查看错误日志。那么你应当:

1. $ screen -S admin
# 首先创建一个 screen,这时候你有了 0 号 window;
2. $ screenssh s1.solrex.org s2.solrex.org s3.solrex.org
# 在 0 号 window 中执行 screenssh 命令,自动打开 3 个 window,连接到三个不同的服务器;
3. $ lets "grep FATAL ~/error_log"
# 在 0 号 window 中执行 lets,将命令自动分发到 3 台服务器上执行;
4. ctrl-a N 切换到不同的 window 查看命令的执行情况;
5. ctrl-a 0 切换到 0 号 window 执行下一条批量命令;

下面我们再回顾一下上文中提到的 3 个问题是否解决了:1. GNU Screen Linux 一般均自带,不存在专用客户端问题;2. screenssh 解决了自动化登陆多台服务器问题,且服务器列表作为参数,非常灵活且易定制;3. 开发机上运行的 screen 保证了客户端离线连接不断。

Shell Tips: cppath、scppath、mybackup

分享几个觉得有用的小 shell 函数。

1. scppath

在进行一些跨机器的操作时,每次 scp 总要手动去拼那个路径,首先从 PS1 拷贝粘贴用户名和主机名,然后再 pwd 拷贝粘贴当前目录,然后再 ls 拷贝粘贴要 scp 的文件名。好烦啊,所以就写了下面这个小函数来生成 scp 的文件路径,放到 ~/.bashrc 里。

function scppath()
{
    local _IFS=$IFS
    IFS=$(echo -en "\n\b")
    local _file
    for _file in $@; do
        echo "\"$USER@$HOSTNAME:$PWD/$_file\""
    done
    IFS=$_IFS
}

2. cppath

同样可以有 cppath。

function cppath()
{
    local _IFS=$IFS
    IFS=$(echo -en "\n\b")
    local _file
    for _file in $@; do
        echo "\"$PWD/$_file\""
    done
    IFS=$_IFS
}

3. mybackup

这个函数是偷懒备份用的。当写代码写到一半,不想或者不能 check in,但又想备份一下时,就用这个命令对文件或者目录进行自动的备份。

function mybackup()
{
    local _bak_dir=~/history
    local _path=''
    mkdir -p $_bak_dir
    local _IFS=$IFS
    IFS=$(echo -en "\n\b")
    for _path in $@; do
        if [ -f $_path ]; then
            cp $_path $_bak_dir/"$_path".`date +%Y-%m-%d.%H-%M-%S`
        elif [ -d $_path ]; then
            _path=`basename $_path`
            tar -cvf $_bak_dir/$_path.`date +%Y-%m-%d.%H-%M-%S`.tar $_path
        fi 
        echo "Backuped $_path to $_bak_dir."
    done
    IFS=$_IFS
}

在 shell 脚本里打日志

今天小弟在重构代码中的一个脚本模块,其中涉及到日志功能。上午花了点儿时间想出了个在 shell 打日志的技巧,觉得值得写一下。

希望要实现的效果是:实现一个 write_log 命令,给一条出错消息作为输入,write_log 记录日志时自动加上 时间戳、脚本文件名和行号。形如:

2010-12-17 19:13:44 [work.sh:24] FATAL: mkdir -p /x.

时间戳、脚本文件名都好获得,但是行号就没那么容易实现了。shell 中的 $LINENO 变量只能展开成当前行的行号,如果把 write_log 实现成函数的话,势必在函数中无法使用 $LINENO。

开始我想了好大一会儿,觉得 eval 能干这个事情。但是如果用 eval 的话,还不如直接把 $LINENO 传给 write_log 函数呢,与我的初衷不是太相符。我拉来同事讨论了一把,也没解决问题。正当我准备放弃了,计划每次传 $LINENO 参数时,忽然想起来,怎么把 alias 给忘了呢?

于是,write_log 的实现就是这个样子了:

function _write_log()
{
  if [ $# -eq 2 ]; then
    if [ -z $LOGFILE ]; then
      echo "$(date "+%Y-%m-%d %H:%M:%S") [$0:$1] $2"
    else
      echo "$(date "+%Y-%m-%d %H:%M:%S") [$0:$1] $2" >> $LOGFILE
    fi
  elif [ $# -eq 1 ]; then
    if [ -z $LOGFILE ]; then
      echo "$(date "+%Y-%m-%d %H:%M:%S") [$0] $1"
    else
      echo "$(date "+%Y-%m-%d %H:%M:%S") [$0] $1" >> $LOGFILE
    fi
  else
    return 1
  fi
}
alias write_log='_write_log $LINENO' # 这里必须使用单引号

存在的问题是:上面这段代码在 bash 里是不工作的,但是用 sh 可以——即使 sh 也是链接到 bash 的。问题出在 alias 上,可以把问题简化成这样,有一个脚本 a.sh:

$ cat a.sh
alias lss='ls -l'
lss /tmp

这个脚本用 /bin/sh 执行是这样的:

$ sh a.sh 
total 8
drwx------ 2 gdm gdm 4096 2010-12-17 19:34 orbit-gdm
drwx------ 2 gdm gdm 4096 2010-12-17 11:04 pulse-PKdhtXMmr18n

用 /bin/bash 执行是这样的:

$ bash a.sh 
a.sh: line 2: lss: command not found

把 bash 随便 link 成一个叫 sh 的链接文件,再执行是类似这样的:

$ ln -s /bin/bash ~/sh
$ ~/sh a.sh 
total 8
drwx------ 2 gdm gdm 4096 2010-12-17 19:34 orbit-gdm
drwx------ 2 gdm gdm 4096 2010-12-17 11:04 pulse-PKdhtXMmr18n

这个问题肯定是有原因的,我不愿意去翻 bash 源代码,也不知道哪里去找答案,所以我放弃了,直接在文件头加上

#!/bin/sh

如果哪位兄台知道这种“奇怪”现象的原因所在,请不吝赐教 :)

Shell Tips: GNU Screen 的一些小技巧

由于工作环境的问题,最近越来越感觉到 screen 命令的可贵,下面总结一点使用 screen 命令的小技巧。

最常用的参数组合:

screen -ls // 列出已有的 screen
screen -D -R // 进入指定的 screen 名,如果没有,则以该名称创建 screen

由于很常用,我把这两个命令取了个 alias:

alias sl='screen -ls'
alias sr='screen -D -R'

除了命令之外,还有快捷键 Ctrl+ac 创建 screen;Ctrl+aa 在两个 screen 之间相互切换;Ctrl+ad 从 screen 中 detach;Ctrl+a数字,跳转到数字指代的 screen。

在 screen 最下方显示状态栏,状态栏包括已经打开的 screen 标签列表,当前的 screen 和时间。其中在 screen 标签处显示该 screen 所处的目录名。显示 screen 所处的目录名这一点实现起来要困难一些,首先得修改 .bashrc,加入 screen term 对应的信息

case $TERM in
    screen*)
        # This is the escape sequence ESC k \w ESC
        # Use current dir as the title
        SCREENTITLE='\[\ek\W\e\\\]'
        PS1="${SCREENTITLE}${PS1}"
        ;;
    *)
        ;;
esac

然后 . 或者 source 一下,再修改 screen 的配置文件,添加状态栏,在 .screenrc 中添加:

caption always '%{=b cw}%-w%{=rb db}%>%n %t%{-}%+w%{-b}%< %{= kG}%-=%D %c%{-}'
shelltitle '$ |bash'

最终效果为:

GNU Screen 多标签状态栏

Google 拼音词库转 Vimim 词库脚本

我写了一个将 Google 拼音输入法词库转换为 Vimim 词库的脚本,贴在这里,希望对大家有用。

#!/bin/bash
iconv -f gbk -t utf-8 "$@" | sed -e 's/ //g;s/^M$//g' | awk 'NR==1 {a=$3; printf "%s %s",$3,$1; next; }{ if($3==a) printf " %s",$1;else printf "n%s %s",$3,$1; a=$3;}' | sort  -d

(注意:上面那个 ^M 在 vim 中的输入方法是 Ctrl+vm。)

使用方法:
1. 在 Google 拼音输入法“属性设置->词典”选项页,将 Google 输入法词库导出为 .dic 文件,例如 google.dic。
2. 将 google.dic 拷贝到 Linux 中,或者使用 Cygwin,进入到包含 google.dic 的目录。
3. 下载本邮件附件 google2vimim,给它增加可执行权限 chmod u+x google2vimim。
4. ./google2vimim google.dic > vimim.pinyin.txt,得到的 vimim.pinyin.txt 就是符合 Vimim 规范的词库。

PS: 是的,我忘记了 r 的作用,所以上面脚本可以完全替换为:

#!/bin/bash
iconv -f gbk -t utf-8 "$@" | sed -e 's/ //g;s/r$//g' | awk 'NR==1 {a=$3; printf "%s %s",$3,$1; next; }{ if($3==a) printf " %s",$1;else printf "n%s %s",$3,$1; a=$3;}' | sort  -d

脚本的最新版本下载地址可以是:http://share.solrex.org/scripts/google2vimim

用 Vim 对矩阵转置

前两天某个同学在科苑星空 BBS 上问到了一个有趣的问题:如何在 Vim 中对矩阵进行转置?

我当时想,转置不就是行列互换嘛,awk 可以取一列,那么拿出来每列然后打印成一行不就好了?类似于:

echo `awk '{printf "%s ",$1}' file`
echo `awk '{printf "%s ",$2}' file`
echo `awk '{printf "%s ",$3}' file`
...

本来觉得用 bash 写一个循环语句就可以了,但是怎么也尝试不出来如何替换那个 $1, $2, $3...就想没办法了只能搞 eval 了。但是我觉得这种事情 Unix 前辈们应该干过,所以就搜了一斧子,果然搜到了一个 AWK 程序,《Sed & Awk》 Ch13.9 Perform a Matrix Transposition:

#! /bin/sh
# Transpose a matrix: assumes all lines have same number of fields

exec awk '
NR == 1 {
    n = NF
    for (i = 1; i <= NF; i++)
        row[i] = $i
    next
}
{
    if (NF > n)
        n = NF
    for (i = 1; i <= NF; i++)
        row[i] = row[i] " " $i
}
END {
    for (i = 1; i <= n; i++)
        print row[i]
}' ${1+"$@"}

哈,除了觉得这样内存占用可能比较大之外,这可是一个相当不错的程序。

在 Vim 中调用就很简单了,将上面脚本保存成 transpose,加上可执行属性放在某个可执行路径下(比如 ~/bin),然后在 Vim 编辑矩阵文件时 :%!transpose 就可以了。

PS: 另外,在查证矩阵的“转置”应该写成“转秩”还是“转置”时,我在 Wikipedia 发现一个很有意思的东西:

矩阵用词
在中国大陆,横向称为“行”,纵向称为“列”。 在台湾,横向称为“列”,纵向称为“行”。

天那,要是用汉语和台湾同学讨论矩阵的话,该有多痛苦呀!

Poderosa 2009 特别版

自从讨厌了 Putty 黑黑的界面之后,在 Windows 下我一直使用 Poderosa 登录 ssh 主机。与 Putty 相比,Poderosa 的优点是支持标签和 Cygwin shell。 原生的 Cygwin shell 窗口太丑陋了,和 Linux 下的终端没办法比,相信经常在 Windows 下使用 Cygwin 的同志都会有同感。Poderosa 能使 Cygwin 的终端窗口获得与 Linux 终端类似的使用感受,这是我偏爱它的一个重要原因。

当然,国产的 Fterm 也支持登录 ssh 主机,使用起来也凑合,但是很多 ssh 的高级功能是不支持的。

我以前曾在这篇文章中推荐过 Poderosa,但是和很多开源软件一样,一旦遇到困难(比如主要开发人员流失),软件的升级就陷入了停滞。Poderosa 从 2006 年 11 月 22 日发布 4.10 版本之后就再也没有更新,虽然 SF Project 的 Activity 中一片对 BUG 的抱怨之声。

一直以来我对 Poderosa 最重要的不满是编码和按键问题。Poderosa 是日本人写的,所以在编码中只有ISO-8859-1、UTF-8 和日文支持,缺少对 GBK 中文编码的支持。那么在 Cygwin shell 中执行一些 Windows 原生命令比如 ipconfig 时,命令输出的中文就会是乱码;按键问题主要体现在登录到远程主机时一些按键不支持,比如 Home 键就无法正常使用。

虽然我很早之前就想自己添加进去这些特性,因为不懂 C# 语言,一直没有动手。昨天实在忍不住了,把 Poderosa 的源代码下载下来,准备学一下 C# 语言然后去修改它。

但是很不幸幸运的是,我看到 Poderosa 的 Activity 中 4 天前(09 年 1 月 2 日)增加了一篇 post,一个咱们的同胞xjzhang1979说:他改进了 Poderosa,我下载了一看,我想要的功能都有了,真开心。

xjzhang1979 将软件包上传到了一个网络文件共享网站,您可以点击这个链接下载:http://www.box.net/shared/7n7ps57jgn。为了避免该链接失效,我在我的共享网站做了一个备份,您也可以到这里去下载:http://share.solrex.org/ibuild/

PS: 后来搜索找到了作者的博客,关于此修改版介绍的原文在这

2009-03-29: 更新的 Poderosa 特别版在这里:http://share.solrex.org/ibuild/