2
23
2012
8

迁移到64位 Arch Linux

曾经因为不了解,所以一开始选择了32位系统;后来内存大了,ArchLinux 的内核没有编译PAE支持,只能用 3GiB 多的内存,不爽。朋友又给了这篇教程,于是略作准备就开始动手了。

可是,但我真正动手时,那篇文章已经被另一篇英文教程取代了。当时忘记了 MediaWiki 会记录所有的编辑历史,所以只好将就着看了。结果,重启时 udev 没跑起来,说找不到 librt.so。我当然不会就此罢休,花费了一些时间,不仅拯救了系统,成功迁移到了 64 位,而且确定了 ArchWiki 上那篇教程的错误。

先说这个错误吧。现在已经修正了。方法 2: 从正在运行的系统一节,在安装 busybox 的时候,现在是一个红色的警告框,暗示着这里曾经发生的悲剧——

警告: 不要现在安装 lib32-glibc 软件包。在执行命令 ldconfig 后,当你安装 linux(内核)时,生成的镜像文件中,librt.so 等库文件会在 /usr/lib32 目录下,启动的时候二进制文件不会在此搜索库文件,导致启动失败。

当悲剧发生后,我有些紧张地拿出移动硬盘,先是进入移动硬盘上的 Arch,胡乱安装了几个 64 位软件包并重新生成 initramfs,结果连 init 也执行不了了。无奈我又启动64位内核的 PartedMagic chroot 进去查看。但是,chroot 失败了:

sh: cannot open shared object file

后来我才知道,因为我安装lib32-glibc/lib/ld-linux.so.2发生冲突,我覆盖了;后来卸载 32 位的 glibc 时,它被删除了。于是,动态链接的 32 位程序没有动态库加载器了。但其实还是有办法的,因为被误删的只是个软链接。

/usr/lib32/ld-linux.so.2 /bin/ls

老猫的提示下,手动指定ld-linux.so.2运行成功。我尝试把它链接过去,这个 32 位与 64 位库混合的系统开始有些可用了。接下来按 Wiki 里的指示操作就可以了。不过,我没有重装全部的库,而是只装了标明 i686 架构的库。这就是我之前那个用 Haskell 写的脚本的目的。不过还是出了点小问题——虽然我为防止程序运行不了而在开始之前把软件包列表生成了一份保存起来,但我忘了我的 HOME 是加密过的。PartedMagic 里没有 eCryptfs 工具,而 Arch 里的那个又因为库的原因运行不了,囧死了。。。幸运的是,我在迁移之前把 Dropbox 弄好同步了一遍,这个软件包列表也同步了。通过 Dropbox 网页界面下回来就好了。

弄好重启之后,整个事情还没完——我从 AUR 里编译安装的那些包还没重装呢。边重装边研究新的 64 位系统,发现 gcc 还是可以编译出 32 位程序的,只是要装gcc-multilib而已。现在可好了,既可以运行 64 位的程序,又可以运行 32 位的。库依赖少的 C 程序编译个 32 位的出来也没问题。

对了,最后说一句,vnstat 这厮的数据库格式竟然是平台相关的!换成 64 位后它就不断报错,直到我删除了以前的数据库。

Category: Linux | Tags: arch linux
2
17
2012
5

Haskell 实战:使用 Parsec 解析 lrc 歌词文件

既然来学 Haskell 了,Parsec 不应该错过。lrc 文件的格式大家应该都清楚。虽然说它用正则表达式解析很容易也很可靠,但是,我这不是练习么!

数据类型的定义

首先,我们想想歌词文件解析出来有些什么。主要数据当然是一条条带时间的歌词!除此之外,还会可选地有歌名啦歌手啦之类的东西。

先来定义一条歌词,也就是一个最高精确到百分之一秒的时间,和一个字符串。也就是:

data LrcLine = LrcLine {
  time :: Int,
  line :: String
} deriving (Eq, Show, Ord)

我们需要实现Ord类型类以便比较,因为 lrc 文件的歌词有一种紧凑的格式,在相同的歌词前有多个时间。这时,歌词就不是排好序的了。GHC 会自动推断出比较函数,也就是逐个域地进行比较。也可以手动定义其为Ord的实例:

-- import Data.Function (on)
instance Ord LrcLine where
  compare = compare `on` time

然后是整个歌词文件的信息:

data Lrc = Lrc {
  title :: Maybe String,
  artist :: Maybe String,
  album :: Maybe String,
  by :: Maybe String,
  metadata :: [(String, String)],
  lyrics :: [LrcLine]
}

因为可能会有未知的元信息,所以我们定义了一个metadata域来存储之。其类型为[(String, String)],以便使用lookup函数进行查询。

自顶向下设计解析器:顶层解析器

RWH的说明,似乎一般都不写解析器的类型签名。但既然是初学嘛,我还是写上好了——

lrcParser :: GenParser Char st Lrc

什么意思我还不太懂,不过最后那个Lrc很显然就是解析结果的类型啦。

我们的解析器先从歌词源文件中读取若干行的元信息,接下来读取所有的歌词数据,最后构造个 Lrc类型的数据。

lrcParser = do
  metadata <- many $ try lrcMeta
  ly <- concat <$> many lrcLine
  return Lrc {
    title = lookup "ti" metadata,
    artist = lookup "ar" metadata,
    album = lookup "al" metadata,
    by = lookup "by" metadata,
    metadata = metadata,
    lyrics = sort ly
  }

manytry都是 Parsec 里的函数。many接受一个类型为解析器的参数,在求值时它一直调用这个解析器,直到它不消耗输入为止。如果这个解析器消耗了输入却又没能成功,那么整个many解析器也就失败了。而try在消耗了任意数量的输入但没有最终成功时会把已消耗的输入退回去,结果是没有消耗输入。开个 GHCi 会话演示下:

>>> ghci
GHCi, version 7.0.3: http://www.haskell.org/ghc/  :? for help
Loading package ghc-prim ... linking ... done.
Loading package integer-gmp ... linking ... done.
Loading package base ... linking ... done.
Loading package ffi-1.0 ... linking ... done.
ghci> import Text.ParserCombinators.Parsec
ghci> let p = string "ab" :: GenParser Char st String
Loading package transformers-0.2.2.0 ... linking ... done.
Loading package mtl-2.0.1.0 ... linking ... done.
Loading package bytestring-0.9.1.10 ... linking ... done.
Loading package array-0.3.0.2 ... linking ... done.
Loading package containers-0.4.0.0 ... linking ... done.
Loading package deepseq-1.1.0.2 ... linking ... done.
Loading package text-0.11.0.5 ... linking ... done.
Loading package parsec-3.1.2 ... linking ... done.
ghci> parse p "<string>" "abc"
Right "ab"
ghci> parse p "<string>" "ac"
Left "<string>" (line 1, column 1):
unexpected "c"
expecting "ab"
ghci> parse p "<string>" "d"
Left "<string>" (line 1, column 1):
unexpected "d"
expecting "ab"
ghci> parse p "<string>" ""
Left "<string>" (line 1, column 1):
unexpected end of input
expecting "ab"
ghci> parse (many p) "<string>" "ababc"
Right ["ab","ab"]
ghci> parse (many p) "<string>" "ababa"
Left "<string>" (line 1, column 5):
unexpected end of input
expecting "ab"
ghci> parse (many $ try p) "<string>" "ababa"
Right ["ab","ab"]

所以,many $ try lrcMeta就是不断尝试解析歌词元信息,直到解析失败时停止。

接下来是对歌词数据的解析。因为一行可能有多个时间,我们把它存储成多条LrcLine,所以需要使用concat来连接下每次调用lrcLine返回的结果列表。

自顶向下设计解析器:余下的部分

lrcMeta很简单,一行文本,由中括号括起来,其中的键和值用冒号隔开:

lrcMeta :: GenParser Char st (String, String)
lrcMeta = do
  char '['
  key <- many $ noneOf ":"
  char ':'
  val <- many $ noneOf "]"
  char ']'
  eol
  return (key, val)

lrcLine差不多,不过涉及到时间的解析:

lrcLine :: GenParser Char st [LrcLine]
lrcLine = do
  times <- many1 lrcTime
  line <- many $ noneOf "\r\n"
  optional eol
  return $ map (\t -> LrcLine {
    time = t,
    line = line
  }) times

嗯?没看到对时间的解析?哦,它在这里:

lrcTime :: GenParser Char st Int
lrcTime = do
  char '['
  minutes <- readInt
  char ':'
  second <- readInt
  centisec <- option 0 $ char '.' >> readInt
  char ']'
  return $ 60 * 100 * minutes + 100 * second + centisec
  where readInt = read <$> many digit

好了,你可以编译下试试了。RWH说过了,Compile early, compile often。这样在你不小心出错时,强大的编译器能够及时提示你。

哦,下边是 import 列表:

import Data.Char (isDigit)
import Data.Functor ((<$>))
import Data.List (sort)
import Data.Maybe (isJust, fromJust)
import Text.ParserCombinators.Parsec

你试过了吗?发生了什么?

是的,我还有个「抄袭」RWH的换行符解析器没列出来。链接在文末给出了,大家自己去找吧 ;-)

什么?你没找到?好吧,那你加上这个,也可以编译的了。其实类型的语句早该写的。

eol :: GenParser Char st String
eol = undefined

这样就定义了eol函数,它被定义为一个匹配任意类型的「未定义」值。

最后加点工具函数

一个给把 offset 加到歌词数据里的,另一个则是给歌词在时间轴上偏移一定时间的。

lrcAddOffset :: Lrc -> Lrc
lrcAddOffset l = l { lyrics = ly', metadata = meta' }
  where ly = lyrics l
    meta = metadata l
    offset = lookup "offset" meta >>= parseInt
    ly' = case offset of
          Just t -> addTime (fromInteger t `div` 10) ly
          otherwise -> ly
    meta' = filter notOffset meta
    notOffset = (/= "offset") . fst

addTime :: Int -> [LrcLine] -> [LrcLine]
addTime t = map $ \l -> l { time = (t + time l) }

嗯,还是个parseInt用来把字符串转成整数,并且很好地处理异常。

parseInt :: String -> Maybe Integer
parseInt s = case reads s of
  [(int, "")] -> Just int
  otherwise   -> Nothing

完整代码

-- module Text.Lrc (
--   parseLrc,
--   addTime,
--   lrcAddOffset,
--   Lrc(..),
-- ) where
-- 为测试,这个被注释掉了

import Data.Char (isDigit)
import Data.Functor ((<$>))
import Data.List (sort)
import Data.Maybe (isJust, fromJust)
import Text.ParserCombinators.Parsec

data Lrc = Lrc {
  title :: Maybe String,
  artist :: Maybe String,
  album :: Maybe String,
  by :: Maybe String,
  metadata :: [(String, String)],
  lyrics :: [LrcLine]
}

data LrcLine = LrcLine {
  time :: Int,
  line :: String
} deriving (Eq, Show, Ord)

lrcParser :: GenParser Char st Lrc
lrcParser = do
  metadata <- many $ try lrcMeta
  ly <- concat <$> many lrcLine
  return Lrc {
    title = lookup "ti" metadata,
    artist = lookup "ar" metadata,
    album = lookup "al" metadata,
    by = lookup "by" metadata,
    metadata = metadata,
    lyrics = sort ly
  }

lrcMeta :: GenParser Char st (String, String)
lrcMeta = do
  char '['
  key <- many $ noneOf ":"
  char ':'
  val <- many $ noneOf "]"
  char ']'
  eol
  return (key, val)

lrcLine :: GenParser Char st [LrcLine]
lrcLine = do
  times <- many1 lrcTime
  line <- many $ noneOf "\r\n"
  optional eol
  return $ map (\t -> LrcLine {
    time = t,
    line = line
  }) times

lrcTime :: GenParser Char st Int
lrcTime = do
  char '['
  minutes <- readInt
  char ':'
  second <- readInt
  centisec <- option 0 $ char '.' >> readInt
  char ']'
  return $ 60 * 100 * minutes + 100 * second + centisec
  where readInt = read <$> many digit

eol :: GenParser Char st String
eol = try (string "\n\r")
  <|> try (string "\r\n")
  <|> string "\n"
  <|> string "\r"
  <?> "end of line"

lrcAddOffset :: Lrc -> Lrc
lrcAddOffset l = l { lyrics = ly', metadata = meta' }
  where ly = lyrics l
        meta = metadata l
        offset = lookup "offset" meta >>= parseInt
        ly' = case offset of
                   Just t -> addTime (fromInteger t `div` 10) ly
                   otherwise -> ly
        meta' = filter notOffset meta
        notOffset = (/= "offset") . fst

addTime :: Int -> [LrcLine] -> [LrcLine]
addTime t = map $ \l -> l { time = (t + time l) }

parseInt :: String -> Maybe Integer
parseInt s = case reads s of
  [(int, "")] -> Just int
  otherwise   -> Nothing

main = getContents >>= \lrcfile -> case parse lrcParser "<stdin>" lrcfile of
  Left err -> print err >> error "Failed."
  Right lrc -> mapM_ print $ lyrics $ lrcAddOffset lrc

参考链接

Category: Haskell | Tags: Haskell
1
28
2012
5

使用 eCryptfs 加密主目录

本文根据回忆记述在 Arch Linux 上为某一新用户建立使用 eCryptfs 加密的 $HOME 目录并使之在登录时自动解密挂载的过程。大量参考了 Unknown and partly hidden 的 eCryptfs and $HOME 一文。

依赖的软件包:ecryptfs-utils。

加密目录

# mkdir -p /home/.ecryptfs/user/private
# chmod 755 /home/.ecryptfs
# chmod -R 700 /home/.ecryptfs/user
# chown -R user:user /home/.ecryptfs/user
# ln -s /home/.ecryptfs/user/private /home/user/.private
# chmod 700 /home/user

注意:最后一步原文使用的是500权限,这里改成了700

第一次挂载加密目录:

# mount -t ecryptfs /home/user/.private /home/user

eCryptfs 会询问一些加密的选项,其中 Cypher(加密方法)和 Key byte 可自行选择:

Key type: passphrase
Passphrase: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Cypher: twofish
Key byte: 32
Plaintext passtrough: yes
Filename encryption: yes
Add signature to cache: yes

一定要记住密码,虽然可能并不怎么会用到。

mount命令的输出中找到这次挂载使用的参数,经过一些变更,把类似于以下的设置添加到/etc/fstab中:

/home/user/.private /home/user ecryptfs rw,user,noauto,exec,ecryptfs_sig=XYZ,ecryptfs_cipher=twofish,ecryptfs_key_bytes=32,ecryptfs_passthrough,ecryptfs_fnek_sig=XYZ,ecryptfs_unlink_sigs 0 0

注意:在登录挂载时,noexecnosuidnodev将会是默认选项。这里加上exec选项来覆盖掉noexec,这样加密的 $HOME 中才支持执行可执行文件。

挂载时生成了/root/.ecryptfs目录。我们先在里边保存些文件:

# touch /root/.ecryptfs/auto-mount
# ecryptfs-wrap-passphrase /root/.ecryptfs/wrapped-passphrase
Passphrase to wrap: [输入加密口令]
Wrapping passphrase: [输入用户的登录口令]

现在,使用用户的登录口令可以从文件/root/.ecryptfs/wrapped-passphrase中得到 eCryptfs 的加密口令。即使加密口令很强,如果登录口令弱的话,文件信息还是会泄漏的。所以,得选个强的登录口令,不然就不要玩登录时自动挂载加密 $HOME 了。

或者,你也可以玩点有趣的,把这个wrapped-passphrase文件放在 U 盘里,只留下一个指向 U 盘里的此文件的软链接。然后配置好 U 盘自动挂载,就做成了个简单的「U 盾」!

好了,现在卸载 $HOME:

# umount /home/user

自动挂载

先把 eCryptfs 的那个在/root下的目录弄回来:

# mv /root/.ecryptfs /home/.ecryptfs/user
# chown -R user:user /home/.ecryptfs/user/.ecryptfs
# ln -s /home/.ecryptfs/user/.ecryptfs /home/user/.ecryptfs

接下来,创建一个挂载用的脚本,暂时叫它/home/profile.sh吧。它将被写到用户登录时的自动执行脚本中,如~/.profile,或者~/.zprofile,如果你用 Zsh 的话。

if [ -r "$HOME/.ecryptfs/auto-mount" ]; then
  grep -qs "$HOME ecryptfs" /proc/mounts
  if [ $? -ne 0 ]; then
    mv $HOME/.Xauthority /tmp 2>/dev/null
    mount -i "$HOME"
    cd "$HOME"
    mv /tmp/.Xauthority $HOME 2>/dev/null
    (
      systemctl --user daemon-reload
      systemctl --user default
    ) &
  fi
fi

注意到这里加入了对~/.Xauthority文件的处理,不然从图形界面登录时,执行挂载命令后,会因授权文件不见了而失败。之前把 $HOME 的权限设置成700也是为了这个。单纯地允许写~/.Xauthority不行,因为 xauth 需要创建临时文件以防止此文件同时被多个进程修改。

现在,我们需要在用户登录时自动 unwrap 之前创建的那个wrapped-passphrase文件。在/etc/pam.d/login中添加几行(注意顺序):

#%PAM-1.0
#...
auth                required        pam_unix.so nullok
auth                required        pam_ecryptfs.so unwrap
#...
password            required        pam_ecryptfs.so
#password           required        pam_unix.so sha512 shadow use_authtok
#...

好了,我们先手动试试:

# su user
$ ecryptfs-insert-wrapped-passphrase-into-keyring /home/user/.ecryptfs/wrapped-passphrase
Passphrase: [输入用户密码]
$ mount -i /home/user

如果正确挂载的话,接下来就可以开始建设你的新 $HOME 了,比如把你以前的各种文件复制过去,等等。注意不要在加密的目录内进行 BT 下载哦。你可以建立个/home/.ecryptfs/user/public目录然后软链接到 $HOME 内来使用。

我同时还修改了/etc/pam.d/slim,似乎这样才能在使用 slim 登录时也有效。

呃,还没有结束呢。得把之前的/home/profile.sh文件弄进来。这里演示时只是创建了一个新的.profile文件。如果你已经有了此文件的话,一定不要将其加密,而要将其与此挂载脚本合并。它只能不加密,否则挂载后会出现两个.profile(一个加密了的,一个未加密、passthrough 来的),从而导致一些问题。

# umount /home/user
# chmod 600 /home/profile.sh
# chown user:user /home/profile.sh
# mv /home/profile.sh /home/.ecryptfs/user/private/.profile
# ln -s /home/.ecryptfs/user/private/.profile /home/user/.profile

好了,到此一切结束。

Category: Linux | Tags: linux 安全
1
10
2012
0

GM 脚本:MediaWiki 脚注 tooltip

MediaWiki 使用脚注插件后就多了脚注功能。可无奈这插件把网页当成纸质书了,脚注得点击跳转后才能看到内容。我不胜其烦,遂作此脚本。只对我自己的 wiki 和英文维基百科启用了,因为另一个常去的 MediaWiki 站点——中文维基百科有个导航Popup小工具更好用。我还是一如既往地没有使用 jQuery。

// ==UserScript==
// @name           MediaWiki 脚注 tip
// @namespace      http://lilydjwg.is-programmer.com/
// @include        http://localhost/wiki/*
// @include        https://en.wikipedia.org/wiki/*
// ==/UserScript==

var showTip = function(evt){
  var el = evt.target;
  var left = el.offsetLeft;
  var top = el.offsetTop;
  var tip = document.getElementById('gm-tip');
  //not el.href here; we need the original one
  var tipTextEl = document.getElementById(el.getAttribute('href').substring(1));
  tip.innerHTML = tipTextEl.textContent.substring(2);
  tip.style.top = (top+5) + 'px';
  tip.style.left = (left+25) + 'px';
  tip.style.display = 'block';
};

var hideTip = function(){
  var el = document.getElementById('gm-tip');
  if(el){
    el.style.display = "none";
  }
};

var cites = document.querySelectorAll('.reference > a');
// var cites = document.querySelectorAll('a[href^="#cite_note-"]');
for(var i=0, len=cites.length; i<len; i++){
  cites[i].addEventListener("mouseover", showTip, false);
  cites[i].addEventListener("mouseout", hideTip, false);
}

var setup = function(){
  el = document.createElement('div');
  el.setAttribute('id', 'gm-tip');
  el.style.display = 'none';
  el.style.position = 'absolute';
  el.style.zIndex = '100';
  el.style.border = '1px #1e90ff solid';
  el.style.backgroundColor = 'rgba(115, 201, 230, 0.75)';
  el.style.padding = '0.2em 0.5em';
  var parentEl = cites[0].offsetParent;
  parentEl.appendChild(el);
};
if(cites.length > 0){
  setup();
}
1
9
2012
5

一个 Python 调试函数

Python 有个code模块,可以在程序中开个 REPL 交互命令行,就像 Python 解释器的交互执行一样,调试时非常方便。为了偷懒,我又把它包装了下,写下了repl函数(on github):

def repl(local, histfile=None, banner=None):
  import readline
  import rlcompleter
  readline.parse_and_bind('tab: complete')
  if histfile is not None and os.path.exists(histfile):
    # avoid duplicate reading
    readline.clear_history()
    readline.set_history_length(10000)
    readline.read_history_file(histfile)
  import code
  readline.set_completer(rlcompleter.Completer(local).complete)
  code.interact(local=local, banner=banner)
  if histfile is not None:
    readline.write_history_file(histfile)

之所以要现在把这个函数拿出来,是因为我终于解决了一件让我郁闷很久的问题——补全。历史记录是早就弄好了的,可是补全却经常不给力,补不出东西来,只有少数时候比较正常。这个和 Python 解释器自己的 REPL 不一样。最近在开发 XMPP 群,经常要用到,于是终于去读了rlcompleter.py的代码。还好不长,很快就搞定了:默认使用的是__main__.__dict__这个里边的对象进行补全,而不是globals()。给readline重新设置下补全函数就好了:

readline.set_completer(rlcompleter.Completer(local).complete)
Category: python | Tags: python
1
7
2012
26

Haskell 实战:获取ArchLinux已安装的所有架构相关的软件包名

学而不用则惘。

任务内容

通过读取 pacman 数据库,获取本机已安装软件包中所有架构相关的软件包名。pacman 的数据库中,包描述文件位于/var/lib/pacman/local/*/desc,其中星号部分为软件包名加版本号。该文件中,%NAME%的下一行为软件包名,%ARCH%的下一行为架构,我这里是i686或者any。任务就是找出所有 i686 的软件包名。

任务解析

先写个纯函数,通过一块描述文本(Data.Text)判断这个包是否是架构相关的。类型声明为:

import qualified Data.Text as T
isArchDependent :: T.Text -> Bool

然后看看我们怎么才能办到这点。首先,用T.lines把这「块」文本解析成行的列表。然后我们来找为%ARCH%的这一行。怎么找呢,把前边的行丢掉好了:

(dropWhile (/= archstart)) . T.lines
  where archstart = T.pack "%ARCH%"

现在列表的第二项就是我们要的架构类别。先取两行,最后一行就是了:

last . (take 2) . (dropWhile (/= archstart)) . T.lines

然后做比较,得到最终的结果:

isArchDependent = (/= anyarch) . last . (take 2) . (dropWhile (/= archstart)) . T.lines
                  where archstart = T.pack "%ARCH%"
                        anyarch = T.pack "any"

知道一个包是不是我们要的了,但我们还不知道它的名字。此信息我可以肯定在第二行,就不慢慢 drop 了:

getPackageName :: T.Text -> T.Text
getPackageName = last . (take 2) . T.lines

再来个筛选函数,把将要显示的包描述信息找出来:

filterArchDependent :: [T.Text] -> [T.Text]
filterArchDependent = filter isArchDependent

接下来,是程序中「不纯」的部分。我们需要列出目录/var/lib/pacman/local下的所有目录,然后读取其中的desc文件。

getPackagePaths :: IO [FilePath]
getPackagePaths = (filter ((/= '.') . head)) `fmap` getDirectoryContents "."

getPackageDesc :: FilePath -> IO T.Text
getPackageDesc = TIO.readFile . (++ "/desc")

最后,把以上这些函数组合起来:

topDir = "/var/lib/pacman/local"

main = do
  setCurrentDirectory topDir
  getPackagePaths >>= mapM getPackageDesc >>= ((mapM TIO.putStrLn) . (map getPackageName) . filterArchDependent)

首先为了避免一大堆的路径拼接,进入topDir里边来。然后(main的第三行)写到:获取所有软件包的路径;对于每个路径,获取对应软件包的描述信息并处理;怎么处理呢?先过滤filterArchDependent,再逐个获取包名,最后把它打印出来。

代码

完整的代码如下:

import qualified Data.Text as T
import qualified Data.Text.IO as TIO
import System.Directory (getDirectoryContents, setCurrentDirectory)
import Control.Monad

isArchDependent :: T.Text -> Bool
isArchDependent = (/= anyarch) . last . (take 2) . (dropWhile (/= archstart)) . T.lines
                  where archstart = T.pack "%ARCH%"
                        anyarch = T.pack "any"

filterArchDependent :: [T.Text] -> [T.Text]
filterArchDependent = filter isArchDependent

getPackageName :: T.Text -> T.Text
getPackageName = last . (take 2) . T.lines

topDir = "/var/lib/pacman/local"

getPackagePaths :: IO [FilePath]
getPackagePaths = (filter ((/= '.') . head)) `fmap` getDirectoryContents "."

getPackageDesc :: FilePath -> IO T.Text
getPackageDesc = TIO.readFile . (++ "/desc")

main = do
  setCurrentDirectory topDir
  getPackagePaths >>= mapM getPackageDesc >>= ((mapM TIO.putStrLn) . (map getPackageName) . filterArchDependent)

性能分析

我使用这个 Perl 脚本来计时,跑 20 次取平均时间。Shell 算起算术来太麻烦了 :-(

#!/usr/bin/perl
 
use Time::HiRes qw(gettimeofday);
 
sub gettime {
  my ($sec, $usec) = gettimeofday;
  $sec * 1000_100 + $usec;
}
 
my $times = 20;
my $start = gettime;
for(my $var = 0; $var < $times; $var++){
  `$ARGV[0]`;
}
my $end = gettime;
printf "%lfus\n", ($end - $start) / $times;

作为对照的是个 Python 脚本:

#!/usr/bin/env python3

import os

topDir = "/var/lib/pacman/local"

def checkPackage(file):
  for l in open(file):
    l = l.rstrip()
    if l == '%NAME%':
      next = 'name'
    elif l == '%ARCH%':
      next = 'arch'
    else:
      if next == 'name':
        name = l
      elif next == 'arch':
        return name, l != 'any'
      next = ''

def main():
  for name in os.listdir(topDir):
    if name.startswith('.'):
      continue
    file = '%s/%s/desc' % (topDir, name)
    name, show = checkPackage(file)
    if show:
      print(name)

if __name__ == '__main__':
  main()

这两个脚本长度都差不多,但效率相差挺显著的:

>>> ~tmp/t.pl './packagestat > /dev/null'
86055.100000us
>>> ~tmp/t.pl './packagestat.py > /dev/null'
248090.450000us

花絮

最开始,我用的是Data.Text.LazyData.Text.Lazy.IO这个包里的 Lazy 文本类型,结果是——

>>> ./packagestat
packagestat: glpng-1.45-4/desc: openFile: resource exhausted (Too many open files)

评论

写完这两个脚本,我体会到了Real World Haskell里说的,Even with years of experience, we remain astonished and pleased by how often our Haskell programs simply work on the first try, once we fix those compilation errors. Haskell 程序基本上编译通过后就能正确运行——只是要先修正各种编译错误。Python 那个跑了几遍才得到正确的结果。不过我觉得,除了 GHC 的强大之外,编写逻辑简单、没有状态变量也是正确率高的重要原因之一。

疑问

如果我想同时统计这些软件包的总大小(包描述信息里有),怎么才能只读一遍这些文件就同时做到这两件事呢?

Category: Haskell | Tags: Haskell
1
3
2012
18

neocomplcache: 请尊重大小写!

今天给 Vim 加了个 CursorHold 事件。加完提交到 git 仓库中,发现写 Cur 时,neocomplcache 给出的补全是 Cursorhold,而不是代码中写的 CursorHold,得手动改过来,很郁闷。这已经不是我第一次遇到了,决定需求解决方案。

neocomplcache 关于大小写的设置变量有两个,g:neocomplcache_enable_ignore_caseg:neocomplcache_enable_smart_case。这两个我都没有设置啊。以为是 bug,上网搜索无果,遂看源码,最终发现这真是款自作聪明的插件——在输入文本而不是代码时,它将总是忽略大小写!

补丁如下:

commit aaed422a2dff182954813c16db242545389a0018
Author: 依云 <lilydjwg@gmail.com>
Date:   2012-01-03 23:01:43 +0800

    neocomplcache: never use 'is_text_mode'
    
    This always causes inconvenience for me.

diff --git a/autoload/neocomplcache.vim b/autoload/neocomplcache.vim
index 26a2d09..ac1e0f0 100644
--- a/autoload/neocomplcache.vim
+++ b/autoload/neocomplcache.vim
@@ -995,9 +995,6 @@ endfunction"}}}
 function! neocomplcache#is_eskk_enabled()"{{{
   return exists('*eskk#is_enabled') && eskk#is_enabled()
 endfunction"}}}
-function! neocomplcache#is_text_mode()"{{{
-  return s:is_text_mode || s:within_comment
-endfunction"}}}
 function! neocomplcache#is_win()"{{{
   return has('win32') || has('win64')
 endfunction"}}}
@@ -1135,9 +1132,7 @@ function! neocomplcache#get_complete_result(cur_text, ...)"{{{
       " Save options.
       let l:ignorecase_save = &ignorecase
 
-      if neocomplcache#is_text_mode()
-        let &ignorecase = 1
-      elseif g:neocomplcache_enable_smart_case && l:cur_keyword_str =~ '\u'
+      if g:neocomplcache_enable_smart_case && l:cur_keyword_str =~ '\u'
         let &ignorecase = 0
       else
         let &ignorecase = g:neocomplcache_enable_ignore_case
@@ -1289,26 +1284,6 @@ function! neocomplcache#integrate_completion(complete_result, is_sort)"{{{
     endfor
   endif"}}}
 
-  " Convert words.
-  if neocomplcache#is_text_mode()"{{{
-    if l:cur_keyword_str =~ '^\l\+$'
-      for l:keyword in l:complete_words
-        let l:keyword.word = tolower(l:keyword.word)
-        let l:keyword.abbr = tolower(l:keyword.abbr)
-      endfor
-    elseif l:cur_keyword_str =~ '^\u\+$'
-      for l:keyword in l:complete_words
-        let l:keyword.word = toupper(l:keyword.word)
-        let l:keyword.abbr = toupper(l:keyword.abbr)
-      endfor
-    elseif l:cur_keyword_str =~ '^\u\l\+$'
-      for l:keyword in l:complete_words
-        let l:keyword.word = toupper(l:keyword.word[0]).tolower(l:keyword.word[1:])
-        let l:keyword.abbr = toupper(l:keyword.abbr[0]).tolower(l:keyword.abbr[1:])
-      endfor
-    endif
-  endif"}}}
-
   if g:neocomplcache_max_keyword_width >= 0"{{{
     " Abbr check.
     let l:abbr_pattern = printf('%%.%ds..%%s', g:neocomplcache_max_keyword_width-15)
@@ -1671,9 +1646,7 @@ function! neocomplcache#complete_common_string()"{{{
   " Get cursor word.
   let [l:cur_keyword_pos, l:cur_keyword_str] = neocomplcache#match_word(s:get_cur_text())
 
-  if neocomplcache#is_text_mode()
-    let &ignorecase = 1
-  elseif g:neocomplcache_enable_smart_case && l:cur_keyword_str =~ '\u'
+  if g:neocomplcache_enable_smart_case && l:cur_keyword_str =~ '\u'
     let &ignorecase = 0
   else
     let &ignorecase = g:neocomplcache_enable_ignore_case
@@ -1771,9 +1744,7 @@ function! s:make_quick_match_list(list, cur_keyword_str)"{{{
   " Save options.
   let l:ignorecase_save = &ignorecase
 
-  if neocomplcache#is_text_mode()
-    let &ignorecase = 1
-  elseif g:neocomplcache_enable_smart_case && a:cur_keyword_str =~ '\u'
+  if g:neocomplcache_enable_smart_case && a:cur_keyword_str =~ '\u'
     let &ignorecase = 0
   else
     let &ignorecase = g:neocomplcache_enable_ignore_case
diff --git a/autoload/neocomplcache/sources/dictionary_complete.vim b/autoload/neocomplcache/sources/dictionary_complete.vim
index 97e7b12..dda34be 100644
--- a/autoload/neocomplcache/sources/dictionary_complete.vim
+++ b/autoload/neocomplcache/sources/dictionary_complete.vim
@@ -79,11 +79,7 @@ endfunction"}}}
 function! s:source.get_keyword_list(cur_keyword_str)"{{{
   let l:list = []
 
-  let l:filetype = neocomplcache#is_text_mode() ? 'text' : neocomplcache#get_context_filetype()
-  if neocomplcache#is_text_mode() && !has_key(s:dictionary_list, 'text')
-    " Caching.
-    call s:caching()
-  endif
+  let l:filetype = neocomplcache#get_context_filetype()
 
   for l:ft in neocomplcache#get_source_filetypes(l:filetype)
     call neocomplcache#cache#check_cache('dictionary_cache', l:ft, s:async_dictionary_list,
@@ -102,7 +98,7 @@ function! s:caching()"{{{
     return
   endif
 
-  let l:key = neocomplcache#is_text_mode() ? 'text' : neocomplcache#get_context_filetype()
+  let l:key = neocomplcache#get_context_filetype()
   for l:filetype in neocomplcache#get_source_filetypes(l:key)
     if !has_key(s:dictionary_list, l:filetype)
           \ && !has_key(s:async_dictionary_list, l:filetype)

2012年3月30日更新:针对 neocomplcache 6.2 的补丁在此

2013年6月2日更新:针对 neocomplcache 8.0 的补丁在这里或者这里。对应的 git 提交为 17dd164。

Category: Vim | Tags: vim
1
3
2012
60

为什么业界很少使用 Haskell?

这是 Stackoverflow 中一篇答案的粗略翻译,原文地址 http://stackoverflow.com/a/2302230/296473已失效

  1. 没有人听说过它。没有人会使用他们根本不知道的东西。

  2. 不够流行。人们认为最流行的语言就是最好的语言,因为如果它不好的话,它就不会流行。实际上这根本不成立。最流行的语言最流行,仅此而已。Haskell 不流行是因为它不流行。这就是 Haskell 里经常用到的「递归」。不管来自命令式编程世界的人们怎么说,递归在现实世界中非常常见。

  3. 它不一样。人们总是害怕新事物。

  4. 它很难。人们认为 Haskell 难学难用。这显然和第三点有关。Haskell 里充斥着一些高深晦涩的术语,如「单子就是自函子范畴中的独异点,有什么问题吗?」(译注:这句话真难译 :-( )。普通人可理解不了这个。

  5. 有风险。大多数公司不想第一个吃螃蟹。Haskell 的用户太少了,所以很少有用户愿意尝试它。(看吧,又是递归。)

  6. 招不到程序员。首先,按第二点,会 Haskell 的人很少。然后,大多数人相信第四点,所以找不到愿意学习的程序员。使用一门招不到程序员的编程语言风险太大了。(好吧,我们回到第五点了。)

  7. 库。这可能是最重要的一点,所以我多说一些。

    A. 质量。有很多库,可是质量参差不齐。大多数 Haskell 库(Hackage)是个人的业余项目,文档欠缺。有些不完整,有些已经不再能用,有些在特定情况下会出错。

    B. 多个不兼容的库。能够使用 Haskell 连接到数据库。但问题是,存在一堆这样的库,让人很难分辨出哪些是被支持的库,哪些在几年前就已经烂掉了。而且,在 Haskell 中连接数据库也不像开个 ODBC 连接那样简单。针对每种数据库,每个库都用不同的后端。在数据库支持的广泛性上 Haskell 做得不错,连新出现的 Mongo 或者 Cassandra 数据库都支持。开源可能没有给予 Haskell 以深度,但给予了其以广度。

    C. Windows。几乎所有重要的库(比如加密、二进制数据文件格式、网络协议、数据压缩、连接数据库等)是 C 语言库的包装。它们在 Windows 上编译不了。因为 Windows 是市场上最大的目标平台,这是个大问题

  8. 效率无法预测。由于对 Haskell 缺乏了解,很多人甚至都不知道这一点。很多人直接就认为「Haskell 效率低下」。这不对。事实是,很难预测一个 Haskell 程序的效率。微妙的、没有明显关联的不同有时可能导致效率的巨大差异。(译注:蝴蝶效应啊?)

  9. 正确性。大多数公司对正确性并不重视。它们不在意质量。它们只要尽可能迅速地把代码扔出去赚大把大把的钞票就好了。如果代码有 bug 的话,它们就向客户卖补丁。把代码写对没用;重要的是快速把代码写出来。Haskell 会用优美的解来回馈那些坐下来深入分析问题的人。大多数公司不喜欢这样;他们只要尽可能快地把产品搞出来,以后再修正它,如果还有以后的话。

的确有少数地方正确性很重要。这些地方基本上要么是级别甚高的安全系统,要么是金融系统。(译注:交集不为空?)就我所知,Haskell 在这些领域还是比较流行的。

最后说两点:

  • 我还记得不是太久前人们还叫嚷着「C++ 是给菜鸟的玩具!你应该用像 C 这样真正的编程语言。」现在再看看有多少大型 C++ 程序?

  • 人们总是在说 Lisp 是「下一个里程碑性语言」。他们说了多久?已经 40 年了?Lisp 比几乎所有主流编程语言都要老。现在看看有多少大型 Lisp 程序?

我不知道 Haskell 的命运终将如何。我觉得,Haskell 好的思想会被像 C# 或者 F#、OCaml 这样的杂交语言偷取。人们依旧不会使用 Haskell。它太不一样了。

不管怎么说,关于为什么业界不用 Haskell,见以上观点。它太罕见、太不流行、太奇特,库也不完善。大约就是这样。


后记:

也许,照耀大地的永远是在众恒星中普普通通的太阳,人们永远不会知道在宇宙的某个角落里曾经诞生过一颗绝美无比的小星星。这个世界是不完美的,否则如果它是完善的,缺少了不完美,它还完美吗?这个世界是不公平的,流星划过苍穹,带给多少人希望,而它自己却身殒,不留下一点痕迹。

Category: Haskell | Tags: Haskell 译作
12
28
2011
11

利用脚本提升 Wine QQ 登录体验

我从某处下载的QQ2010,其它都好,就是登录时焦点在密码框时,QQ就会崩溃。解决办法是使用QQ自带的软键盘输入密码。但在这个「半字母顺序」排列软键盘上找需要的需要实在费事。作为一名 Linuxer,我自然得想办法将其自动化。

很久之前就已经看到这个Xpresser软件,但可惜的是,它在Arch下跑不起来。但我从中学到了简单的图像匹配,再加上自己对 Xtest 的了解,解决方案呼之欲出。

本来是三个月前就打算写篇文章的,因各种原因迟迟未写。现在因为各种原因再次折腾这家伙,还是写出来分享一下吧。使用OpenCV做图像匹配部分我就不写了,有兴趣的自己去看 Xpresser 或者 winterpy 中的代码。

首先,介绍一下依赖。本脚本依赖众多的东西,其中我自己写的部分在 winterpy 里有,主要是 OpenCV 图像匹配,以及之前写过的 Xtest 调用使用 GDK 截图。最终,我利用它们写成了 xauto.py 库,功能还十分欠缺,但自动登录Wine QQ足够了,因为我做这些的主要目的就是这可恶的QQ。

#!/usr/bin/env python3
# vim:fileencoding=utf-8

import os
import sys
from xauto import XAuto, Image

QQNo = 'YourQQNo'
QQPwd = 'YourQQPassword'

def main():
  if os.fork() == 0:
    if os.fork() == 0:
      os.execlp('rwine', 'rwine')
    else:
      sys.exit()
  os.chdir(os.path.split(sys.argv[0])[0])

  rect = (20, 150, 500, 500)
  xa = XAuto()
  w, h = xa.screensize
  target_w, target_h = 500, 300
  w, h = w - target_w, h - target_h
  w, h = w // 2, h // 2
  center = (w, h, target_w, target_w)
  xa.default_rect = center

  xa.find_and_click('ok.png', repeat=10) or sys.exit('click 确定')
  xa.find_and_click('qq.png', repeat=10) or sys.exit('find qq no input')
  xa.wait(1)

  for k in QQNo:
    xa.key(k)

  xa.wait(0.4)
  pwd_pos = xa.find('input_pwd.png')
  xa.click(pwd_pos)
  caps = Image('caps.png')
  xa.wait(0.4)
  for ch in QQPwd:
    xa.find_and_click('%s.png' % ch) or sys.exit(2)
    xa.wait(0.1)
    xa.find_and_moveto(caps)
    xa.wait(0.1)
  xa.moveto(pwd_pos)
  xa.wait(0.4)
  xa.find_and_click('login.png')

if __name__ == '__main__':
  main()

几点说明:

  1. 执行以下命令禁止QQ记住用户信息,这样再次启动时焦点会在输入QQ号的地方而不是会导致崩溃的密码框。如果你使用我给的压缩包的话应该可以跳过。
    rm -rf Users/All\ Users
    mkdir Users/All\ Users
    chmod -w Users/All\ Users
    
  2. 需要 wine 1.3.32 或更低,以及 wine_gecko 1.3 或更低。新版本在调用 IE 的组件进行显示时会崩溃,这包括「消息管理器」、「查看聊天历史」、「聊天窗口」的侧栏等。
  3. 我执行的是自己包装过的具有隐私保护功能的「rwine」程序。不过也不是特别安全,QQ仍能够访问剪贴板、截图等。
  4. 密码当然是明文保存。你觉得有必要折腾的话可以自己修改。
  5. 程序中需要的图片自己截。应该很容易知道应该截哪里。这样也避免了字体不同导致图像匹配失败。
  6. 此版本的 QQ 可以在这里下载:115 网盘

另注:更简洁好用的 TM2009 没有 wine 成功,登录时弹出错误


2014年3月25日更新:TM2009 以及 TM2013 后来均 Wine 成功了,并且在输入密码时不会崩溃。详情见此文

Category: Linux | Tags: python QQ wine 腾讯
12
24
2011
64

危险的 Microsoft Word

在编辑 Word 文档时,记住 Microsoft Word 是危险的会很有用。

因为毕业设计的关系,需要使用 Word 文档。我用的是 Microsoft Word 2007,因为没有理由让我使用更旧的版本,而我也没有更新的版本。这才使用几天,我已经忍不住要来吐槽了。

几天前,寝室断电。于是乎,几百字不翼而飞。Word 有自动保存文档以供恢复,但默认是每十分钟才保存一次。这比我手动保存的频率还低。那次断电,Word 是唯一给我造成损失的软件。是的,我的火狐打开了若干标签页。但是没关系,除了最后几秒打开的标签页,其它时候火狐异常退出再打开时,所有的标签页都还在,只要重新载入就可以了。如果已经在某网页里输入了较多文字的话也没关系,Textarea Cache 会帮我保存它们的。至于自己最常用的 Vim,更是无需担心。Vim 默认每输入 200 个字符,或者停止操作 4 秒就会将修改写入交换文件。所以,我从来没有在 Vim 里在没有误操作的情况下丢失大量文字。

好吧,既然 Word 默认的 10 分钟太久了,那我改短点总行了吧?于是我把它改成了 1 分钟。结果,在我尝试在页脚插入页码的时候,我发现突然卡了一下,右下方出现了绿色的进度条。不过在我注意到它时它已经完成了。因为生疏,所以我又继续弄好了一会儿,然后,绿色的进度条又出现了。我看清楚了,它在保存文档。很慢,看样子是完整地保存了一份。Word 这次不仅仅是卡住了,而且——它崩溃了!哦不,在微软的字典中没有「崩溃」这个词。最多只能说是「异常退出」。当然这个词也不常用。微软最常用的是「程序遇到问题需要关闭」。就是这么一个框跳了出来,问我要不要发送报告。好吧,你发吧。点击「发送错误报告」,Word 继续卡在那里……等了会儿,我不耐烦地点了取消,它还没反应。最后终于反应过来,Word 告诉我有一大堆文档「已经修复」。这个我已经见怪不怪了,打开阅读的文档,我不记得做了任何修改,但 Word 总是喜欢问要不要保存。再看看那篇在自动保存中崩溃的文档,「已经修复」的版本格式已经乱掉了。我还是从自己手动保存的版本继续吧。

用 Vim 久了,以前在 Windows 下养成的随时保存文件的习惯渐渐地消失了。现在,重返 Windows,重返 Word,我得在使用过程中时刻提醒自己,要随时手动保存。自动保存并不可靠,甚至还可能是崩溃发生的根源。撒销什么的也不再可靠,要记得编辑一段时间去资源管理器里选中文档,Ctrl-CCtrl-V下,不然,当自己不小心把排版弄乱而 Word 又撒消不回来时就郁闷了。

2011年12月25日更新:Windows 也一样不可靠,竟然应用更新后置我打开的众多文档和程序于不顾,自动重启,完成后还不恢复打开的应用程序——

Windows 最近下载并安装了一个重要安全更新……此更新要求自动重新启动您的计算机!

Category: Windows | Tags: office windows 微软

Mastodon | Theme: Aeros 2.0 by TheBuckmaker.com