3
14
2017
18

我的 zsh 提示符

这是我用了多年的 zsh 提示符。

My zsh prompt

右提示符比较简单,先说。

首先,这个右提示符是 zsh 才支持的,不是 hack 左提示符来的哦。

我的右提示符显示的是(提示符打印出来时的)时间。在有后台任务时,会在左边以黄色显示出后台任务的数量,增加些许后台默默工作的进程的存在感啦。

截图中可以看到,只有最后一行才显示了右提示符(以至于我截图都得 hack 一下)。我使用了setopt transient_rprompt,这样 zsh 会清掉旧的右提示符,就不会影响复制了。以前每次复制时都带上一堆空格然后几个时间,折行之后根本没法看,后来才发现体贴的 zsh 已经有这么个选择了。

另外,在输入命令到右提示符时,右提示符会自动消失,以免和命令混淆。都说了很体贴的哦~

左边,是一个两行的提示符。之所以做成两行,是为了保持命令的起始位置不会因为提示符的长度变化而变化,每次输入新命令的时候,光标都在同一列,易读好找。我就不明白,那些坚持 bash 默认提示符的人是怎么坚持下来的,用着用着不知道自己光标去哪里了……对了,zsh 在输出提示符时,会保证它从终端最左边那一列开始输出。如果上一行不完整,zsh 会打印一个反色的「%」来表示(截图里 ^C 那里就有一个)。

蓝色「>>> 」是学 Python 的,但是使用了蓝色以免和 Python 混淆。如果是 root 用户,则显示红色的「### 」以警示。这个比较刺眼,所以就尽量不用 root 跑 shell 啦。

第一行开头是命令序号,就是历史记录里有多少条命令。每执行一条命令它就会加一,空行或者 Ctrl-C 放弃的不算。其实没什么用的样子。

然后是一个用于标识不同机器的名字。比如这里 lilywork 表示我正在我的工作机上。我家里那个系统里不会显示这个。这个信息可以通过ZSH_PS_HOST变量来设置,比如一般可以设置成$(hostname)。GitLab 之前的提示符里大概没有这个吧。

再就是最后一条命令的状态码($?)。如果命令成功就不显示,否则显示一个红色的数字,以提示上条命令出错了。所以说了嘛,我没法理解坚持使用 bash 及其默认提示符的人……

然后是缩短过的当前目录。~tmp是我的临时目录,有名字(hash -d tmp=....)的。但是它不会缩短中间路径的名字,反正我在它下边写命令,不用担心路径太长。不过我不建议深入探索 nodejs 的模块树,显示好几行的路径并不好看的。

最后一项又是可选的,git 当前分支。这个功能是我自己写的,不是 zsh 自带的那个,是异步显示的哦~忙着干活呢,不能在这种小事上浪费时间、中断思绪嘛。并且还可以通过设置来排除一些目录,比如访问特别慢的远程目录,比如已经死掉很久的 Wuala。

显示的信息不多,也一点都不华丽,但十分有用呢。

介绍完毕,提示符的定义我这里就不写啦。代码都在这里:https://github.com/lilydjwg/dotzsh

Category: shell | Tags: zsh linux
11
7
2014
4

使用 GraphViz 给 alembic 绘制历史关系图

alembic 这个升级/降级的工具,看上去挺好的,编写好一系列版本脚本之后,能够自动地把数据库给升级或者降级到指定版本。它也使用类似 git 的一串十六进制数来表示各个版本,也支持分支,不过呢,比 git 的易用性差太远了。

我今天有个需求,给一些列添加外键。因为懒得单独新写一些脚本,所以我直接改了相关脚本,手动去数据库执行了 SQL。本以为这样子就好了,后来发现新添加外键所引用的表的创建顺序不对,应该在所有引用到它的表之前创建才对。

可是 alembic 没有 git rebase -i 命令啊,不能简单地调整各种版本的顺序。我尝试着手工编辑了一下,结果弄出来两个 head,一个 branchpoint,但是我就是没能看出来是哪里分叉了……于是想到把各个版本的关系给画出来。这种图 GraphViz 最适合了,而简单地解析 alembic history 的输出,用 awk 就好了:

#!/usr/bin/awk -f

BEGIN {
  print "digraph alembic {";
  shape = "box";
}

/^Rev:/ {
  switch($3) {
    case "(branchpoint)":
      shape = "hexagon";
      break;
    case "(head)":
      shape = "ellipse";
      break;
    default:
      shape = "box";
  }
}

/^Path:/ {
  finding_title = 1;
}

/^    \S/ && finding_title {
  sub(/^\s+|\s+$/, "");
  title = $0;
  finding_title = 0;
}

/^    Revision ID:/ {
  rev = $NF;
}

/^    Revises:/ {
  printf("  r%s -> { r%s };\n", rev, $NF);
  printf("  r%s[label=\"%s: %s\",shape=%s];\n", rev, rev, title, shape);
}

END {
  print "}";
}

head(以及第一个之前的 None 版本)会使用椭圆,分叉点(alembic 说的)会使用六边形,而其它版本是矩形的。这样就可以很方便地看出来是哪里分叉啦:

alembic history | alembic_graph | dot -Txlib

结果发现,我的数据库版本们根本就没有分叉嘛……没办法 revert 回去,把关系图导出 SVG 然后放 Inkscape 里边画边改,总算是把顺序给调整对了=w=

Category: shell | Tags: python 数据库 graphviz awk
2
19
2014
3

zsh 异步生成提示符

为什么要异步?当然是因为慢了。比如 Arch 核心仓库 git 版挺大的,第一次进去时显示个 git 分支名要等好一会儿。今天在 zsh-users 列表中看到 Bart Schaefer 给出了一个使用 coprocess 的解决方案,眼前一亮,立即照葫芦画瓢给自己的 zsh 用上了。以下是整个提示符设置部分的代码:

if [[ -n $commands[git] ]]; then
  _nogit_dir=()
  for p in $nogit_dir; do
    [[ -d $p ]] && _nogit_dir+=$(realpath $p)
  done
  unset p

  typeset -g _current_branch= vcs_info_fd=
  zmodload zsh/zselect 2>/dev/null

  _vcs_update_info () {
    eval $(read -rE -u$1)
    zle -F $1
    exec {1}>&-
    zle reset-prompt
  }

  _set_current_branch () {
    cwd=$(pwd -P)
    for p in $_nogit_dir; do
      if [[ $cwd == $p* ]]; then
        return
      fi
    done

    setopt localoptions no_monitor
    coproc {
      _br=$(git branch --no-color 2>/dev/null)
      if [[ $? -eq 0 ]]; then
        _current_branch=$(echo $_br|awk '{if($1 == "*"){print "%{\x1b[33m%} (" substr($0, 3) ")"}}')
      fi
      # always gives something for reading, or _vcs_update_info won't be
      # called, fd not closed
      typeset -p _current_branch
    }
    disown %{\ _br
    exec {vcs_info_fd}<&p
    # wait 0.1 seconds before showing up to avoid unnecessary double update
    # precmd functions are called *after* prompt is expanded, and we can't call
    # zle reset-prompt outside zle, so turn to zselect
    zselect -r -t 10 $vcs_info_fd 2>/dev/null
    zle -F $vcs_info_fd _vcs_update_info
  }

  typeset -gaU precmd_functions
  precmd_functions+=_set_current_branch
  setopt PROMPT_SUBST
fi

[[ -n $ZSH_PS_HOST && $ZSH_PS_HOST != \(*\)\  ]] && ZSH_PS_HOST="($ZSH_PS_HOST) "

E=$'\x1b'
PS1="%{${E}[2m%}%h $ZSH_PS_HOST%(?..%{${E}[1;31m%}%?%{${E}[0m%} )%{${E}[32m%}%~\$_current_branch
%(!.%{${E}[0;31m%}###.%{${E}[1;34m%}>>>)%{${E}[0m%} "

比较坑的是使用chpwd_functions的话只能在目录改变时显示一次,再随便执行个什么命令分支提示就没了。又想到目录不改变的时候分支也可以变化(切换分支了嘛),所以使用precmd_functions,每次显示提示符前(单纯的重绘除外)都执行一次。另外,为了避免每次显示提示符时都明显地分为两步干扰视线,所以在那个_set_current_branch函数里等了 0.1 秒,超时才会不管分支名显示先继续了。

2014年2月24日更新:注意,直到 zsh 5.0.5(就是当前最新版本)有个 bug,在显示提示符之后、用户输入之前,上述代码会经常出现「忙等待」的情况浪费 CPU。这里有个补丁可以修复。

Category: shell | Tags: linux Git zsh
11
14
2013
2

zsh 按 shell 参数移动

很早以前,我就想,在命令比较长的时候,M-fM-b按单词移动太慢了,特别是遇到长的 URL 或者文件名的时候。用鼠标吧,选择文本又比较麻烦了。所以很希望按 shell 参数来移动的功能,甚至尝试自己写过,但是因为对 zsh 了解太少,终究移动不正常。

昨天夜读 zsh 手册时才发现,原来,我曾见过这个功能的背影。

文档 26.6.1 节(「User Contributions」->「ZLE Functions」->「Widgets」)第一个,讲的是「bash-style word functions」。之前我也在哪里看到过,但是不知道其实这家伙支持好几种风格。使用以下配置就可以把 ZLE 里原来的「单词」概念变成 shell 解析出来的参数了:

autoload -Uz select-word-style
select-word-style shell

但是,我不想替换掉默认的,而是使用另外的键来这样子移动。研究了下代码,最终弄出来了:

# move by shell word {{{2
zsh-word-movement () {
  # see select-word-style for more
  local -a word_functions
  local f

  word_functions=(backward-kill-word backward-word
    capitalize-word down-case-word
    forward-word kill-word
    transpose-words up-case-word)

  if ! zle -l $word_functions[1]; then
    for f in $word_functions; do
      autoload -Uz $f-match
      zle -N zsh-$f $f-match
    done
  fi
  # set the style to shell
  zstyle ':zle:zsh-*' word-style shell
}
zsh-word-movement
unfunction zsh-word-movement
bindkey "\eB" zsh-backward-word
bindkey "\eF" zsh-forward-word
bindkey "\eW" zsh-backward-kill-word

只绑了M-BM-FM-W这三个含大写字母的组合键。其它-match函数的功能以后用到时再加好了。

Category: shell | Tags: zsh shell
8
29
2013
0

不是所有 PAGER 都叫 less

在 Linux 下,最常见的 pager(翻页器)就是 less 了,所以很多时候,我都忘记了还有$PAGER这个环境变量,直到有一天我写了这么个 shell 函数:

repodo () {
  for f in $(cat ~/workspace/.my-repos); do
    echo "\n>>> $f\n"
    cd ~/workspace/$f && stdoutisatty $@
    cd - > /dev/null
  done | less
}

这个函数对于~/workspace/.my-repos中记录的每一个项目,在对应的目录下执行同一条命令,并使用 less 来查看输出。其中,stdoutisatty 是一个把标准输出伪装成 tty 的脚本,这样一些命令就不会因为实际输出到管道而关掉彩色高亮之类的了。

比如

repodo git st

ststatus的 git 别名。

这一句命令就可以查看所有项目的工作区状态了。

后来,我执行这样一条命令,它就出问题了:

repodo git grep string

因为 stdoutisatty 的缘故,git grep 会自动调用翻页器。于是,出现了两个 less 同时要读终端输入。

首先想到的是 git 的--no-pager参数,但这个很显然对其它命令无效。于是才想起自设置之后一直没再搭理的$PAGER环境变量:

repodo () {
  for f in $(cat ~/workspace/.my-repos); do
    echo "\n>>> $f\n"
    cd ~/workspace/$f && PAGER=cat stdoutisatty $@
    cd - > /dev/null
  done | less
}

PAGER指定为cat直接输出,这样就不会有多个 less 在运行了。

但这样还没有结束,因为我的不少脚本里都是直接调用 less 的,现在得改成这样子了:

command | ${PAGER:-less}

或者在 Python 里:

p = subprocess.Popen([os.environ.get('PAGER', 'less')], stdin=subprocess.PIPE,
                      universal_newlines=True)

附:less 默认是会转义来自输入的彩色转义字符序列的。我使用了-FRXM参数,也是通过环境变量传递的:

export LESS=-FRXM

这四个选项的意义是:

-F
如果一屏能显示下,那么显示完就退出
-R
不要转义 ANSI 彩色转义字符序列
-X
不要发布终端初始化和结束字符串。这样才不会使用终端的备用屏幕,less 的输出才会留在主屏幕上(使用-F选项时必须,不然可能看不到东西)
-M
在 less 提示符(最后一行)显示更多信息(比如文件的百分比位置)
Category: shell | Tags: linux shell 环境变量 less
7
3
2013
7

手动保存/读取 zsh 历史记录

关于历史记录,zsh 有很多选项。我的配置是:

HISTFILE=~/.histfile
HISTSIZE=10000
SAVEHIST=10000

# 不保留重复的历史记录项
setopt hist_ignore_all_dups
# 在命令前添加空格,不将此命令添加到记录文件中
setopt hist_ignore_space
# zsh 4.3.6 doesn't have this option
setopt hist_fcntl_lock 2>/dev/null
setopt hist_reduce_blanks

最多保留一万行不重复的历史记录。对其的读取和保存没做额外的配置,因此 zsh 会在启动时读取一次,在退出时保存一次。这样,如果同时开了多个 zsh,它们不会共享启动后的历史记录项,因为还没有写到文件中去。

其实是有选项来方便在多个 zsh 中及时共享历史记录的:

setopt SHARE_HISTORY

但是这样的话,每次显示提示符时 zsh 均会读取一次历史记录,而每当新的历史记录产生时 zsh 都会写入一次。磁盘 I/O 太频繁了,我不喜欢。我只需要在我想的时候,能够手动保存和读取历史记录就可以了。读过长长的文档,发现fc可以做到这点:

# 读取历史记录
fc -IR
# 保存历史记录
fc -IA

-I表示「incremental」,只有新的项目被处理。-R是读取,而-A是写入。千万不要用-IW,这样会丢失原有的历史记录。

Category: shell | Tags: zsh
4
23
2013
7

使用 sed 来切换 hosts 文件项

工作中经常会需要将一个域名映射到本地以调试,但是其余时间又需要让其正常解析。手工修改/etc/hosts文件烦耶!

于是有了以下脚本:

#!/bin/bash

if [[ $UID -eq 0 ]]; then
  sed -i '/^#127\.0\.0\.1\s\+example\.org/s/^#//;t;/^127\.0\.0\.1\s\+example\.org/s/^/#/' \
    /etc/hosts
  systemctl restart dnsmasq
else
  grep -m1 -F 'example.org' /etc/hosts
fi

使用 sed 是因为觉得没必要用 awk 这样复杂的东西,也正好更深入学习下 sed。此代码中用到了t命令,它的语义是:

当当前行的上一个s命令成功(至少替换一次)时,跳转到指定的标签。如果没有指定标签,则跳转到脚本尾部。上边的命令中,当example.org域名这行被注释掉时,s命令成功去掉其前的注释符,然后t命令跳过后边加注释符的s命令,到达脚本尾部。

标签使用冒号定义。以上命令使用标签时如下所示:

  sed -i '/^#127\.0\.0\.1\s\+example\.org/s/^#//;te;/^127\.0\.0\.1\s\+example\.org/s/^/#/;:e' \
    /etc/hosts

当然,以上脚本还做了另一件事:当以普通用户身份执行时,不修改 hosts 文件,而是显示相关行以查看状态。

Category: shell | Tags: sed shell
3
19
2012
14

zsh 命令行编辑技巧三则

zsh 的命令行编辑使用的是 Zsh Line Editor(Zle),功能比 readline 强大不少,只是大量好用的功能都深埋于文档中,难得见识到。最近在看A User's Guide to the Z-Shell,虽然内容有些旧了,但依旧很有用。

首先说一点,以下内容均假定使用的是 Emacs 式键绑定。

暂停当前命令的编辑,先执行点其它命令。这个功能叫push-line,默认绑定在Alt-q。另有一个叫做push-line-or-edit的 widget,我把它绑过来了:

bindkey "\eq" push-line-or-edit

push-line widget 会将当前命令行上的内容放到一个栈上,显示一个新的提示符让你来执行点别的东西。比如刚写了一个长命令的一半,却发现当前目录不对。怎么办呢?readline 里我只好先Ctrl-u,执行之后再Ctrl-y粘贴回来。偶尔会找不到之前 kill 的内容。在 Zsh 里,按下Alt-q,当前命令暂存起来,你可以执行点别的命令,再显示命令提示符时,之前 push 走的命令内容会 pop 回来。而且这个操作是可以嵌套的,因为这是一个

push-line-or-edit widget 多了个 or-edit 后缀。当输入一个if或者for这样的命令时,你可以写成多行,zsh 会自动判断出你的命令尚未写完,显示$PS2提示符。这时,如果想修改之前的某一行怎么办呢?push-line-or-edit widget 会把这些行命令变成一个不带有$PS2提示符的多行命令,默认键绑定中,使用Ctrl-p/n或者方向键移动即可。这个就是 zsh 的多行编辑能力。如果你喜欢使用 zsh 编辑的话,可以试试zed这个运行于 zsh 中的简单文本编辑器:

autoload zed
zed some_small_text_file

按顺序执行若干条历史记录中的命令。比如我读取 3G 网卡短信使用如下的命令序列:

gnokii --smsreader
gnokii --getsms SM 0 end -f sms
smsmboxproc < sms > sms.mbox
mutt -f sms.mbox

如果使用Ctrl-r搜索历史的话,每条命令都搜索岂不麻烦?所以有了accept-line-and-down-history这个 widget,默认绑定于Ctrl-o。先在历史记录里找到第一条需要的命令,按下Ctrl-o,命令执行后,历史记录中的下一条就会出现了。然后接着按Ctrl-o,直到需要执行的命令序列到达最后一条,这次该按Enter了。

最后一个,你是不是经常往命令行上粘贴网址?是的话,你应该知道,网址得用引号括起来,以防止有些字符被 shell 解释了。zsh 带了个功能,可以检测出当前输入的是否是 URL,如果是的话就自动转义那些特殊字符。这样往命令行上粘贴 URL 时就不需要事先打好引号了。使用如下命令启用:

autoload -U url-quote-magic
zle -N self-insert url-quote-magic
Category: shell | Tags: zsh shell
6
29
2011
4

使用 zsh 的 zpty 模块

Zsh 的模块真多呀,最初文档时知道有 ztcp 模块时已惊叹,最近又在邮件列表看到竟然有 zpty 模块,解决了困扰我良久的一个小问题。

会往终端输出彩色字符的程序都知道,如果输出的目的地不是终端,通常彩色转义字符是不需要的,比如重定向到文件,或者通过管道传给 grep 之类的程序。所以不少程序会有个--color=WHEN选项,你可以指定是程序自己决定,还是总是要彩色或者不要彩色。Linux 总是善于提供一堆选项来满足不同的需要。可是,除此之外,ls 还会根据输出目的地是不是终端来确定要不要一行显示多个文件名。更囧的是,只有办法强制 ls 一行显示一个文件名,却没有选项强制它把管道当成终端进行多栏显示。结果就是,当文件比较多时,ls | less会一行一个文件名,即使右边还有大把的空间。强制显示彩色也需要--color=always这么长的参数(而 tree 只需要-C就可以了)。

很早就想写个程序利用专门的伪终端来给 ls 的彩色多栏输出加上翻页器了。现在我终于把它实现了,而且简单很多:

ptyrun () {
  local ptyname=pty-$$
  zmodload zsh/zpty
  zpty $ptyname ${1+"$@"}
  if [[ ! -t 1 ]]; then
    setopt local_traps
    trap '' INT
  fi
  zpty -r $ptyname
  zpty -d $ptyname
}
ptyless () {
  ptyrun $@ | less
}

另外,这个用于 yaourt 查找时也是不错的 ;-)


2014年8月22日更新:采纳评论中的建议,使用管道取代了临时文件。另外,在 Dropbox 可以下载我的 zshrc

Category: shell | Tags: zsh
5
24
2010
7

将du的输出按文件大小排序

du命令的输出结果要么是不人性化的全部以千字节为单位,要么加上-h参数,显示为1K 234M 2G这样易读的数据。可是,我通常想查看那些大文件/目录,或者那些小文件/目录。单单只用sort命令的话,就不得不在脑海转换那些千字节单位的数据了。做为一个Linux用户,电脑能做的我可不想让人脑来做。Google了一下,发现这个帖子提供了一段代码可行,但是输出效果并不理想,于是我略作更改,写出了以下代码:

sdu () {
  du -sk $@ | sort -n | awk '
BEGIN {
  split("K,M,G,T", Units, ",");
  FS="\t";
  OFS="\t";
}
{
  u = 1;
  while ($1 >= 1024) {
    $1 = $1 / 1024;
    u += 1
  }
  $1 = sprintf("%.1f%s", $1, Units[u]);
  sub(/\.0/, "", $1);
  print $0;
}'
}

这段代码使用sort排序原始数据后,再使用awk来转换数字的单位。使用方法为sdu后加要查看大小的文件/目录就可以了。注意我在代码中加了-s参数,如果希望同时查询子目录的话,需要去掉这个参数。

Category: shell | Tags: shell linux

部分静态文件存储由又拍云存储提供。 | Theme: Aeros 2.0 by TheBuckmaker.com