3
3
2019
16

使用 cgroups net_cls 来让 docker 走代理

我这里 docker hub 连不上或者连上了访问很慢,根本没法用。本来我常规代理的办法,要么是 proxychains,要么是用 iptables 代理特定的 IP 段。至于 docker 嘛,亚马逊的 IP 段那么多,它用到的域名我也不是很清楚,一点点加好麻烦。作为系统服务,用 proxychains 不仅得修改 systemd 服务配置,而且不知道会不会出什么幺蛾子。最近刚好在某个地方看到这一手,就试试啰。

其实用法很简单的。去 /sys/fs/cgroup/net_cls 下建立个目录,往 net_cls.classid 里写一个整数(支持十六进制的 0x 表示法),然后把 dockerd 的 pid 写到 cgroup.procs 里去。最后用 iptables 代理这部分流量即可。现在都用 443 端口啦,所以只要代理它便好,也避免影响了别的东西:

iptables -t nat -A OUTPUT -p tcp --dport 443 -m cgroup --cgroup 0x110001 -j REDIRECT --to-ports XXX

XXX 是 ss-redir 的端口啦。

注意不要把进程的 pid 往 tasks 文件里写。那里得写的是 task 的 id 而不是 process 的 id,也就是说(用内核的术语来说)是线程的 pid 而不是进程的 tgid(thread group id)。所以非要写 tasks 文件的话,得把 docker 所有的线程的 pid 都写进去才行。真是混乱呢……画个表格好了:

用户态 内核 相关系统调用
pid tgid getpid, kill
tid pid gettid, tgkill
process task group fork, clone without CLONE_THREAD
thread task clone with CLONE_THREAD

另外如果更新过内核的话,那句 iptables 有可能会找不到模块的。(所以更新内核之后还是重启一下以避免尴尬吧。)

Category: shell | Tags: iptables linux cgroups 网络 代理
9
16
2018
10

人生苦短,我用 skim

前两天我又看到了基于子序列匹配的字符串过滤工具 fzf 的绚丽效果了。实际上我很早就听说了这个工具,只是懒得动手配置。此次提及,我发现 fzf 已经在官方软件源里了,而我也正好有时间,所以打算试一试。

然后呢,Arch Linux CN 群组里艾穎初提到 skim 这么一个工具。了解了一下,这个就是 Rust 版本的 fzf,并且在 archlinuxcn 源里也有(git 版本,即 skim-git)。这太好了,就是它了!

skim 的操作很简单。文章开头的链接里已经有效果演示了。常用的也就是输入子序列去过滤,然后再输入一个进一步过滤,直到看到想要的。使用 ! 前缀可以反向过滤,^ 匹配开头 $ 匹配结尾。Ctrl-p/n 来上下移动。提示符那里也支持通常的行编辑。

到现在为止,我自行实现了 sk-cd、sk-search-history、sk-vim-mru 三个功能。另外使用了自带的 completion.zsh 文件。由于各种不满意,没有使用自带的 key-bindings.zsh 文件(也就包含 cd 和历史命令搜索功能啦)。

completion.zsh 里目前有两个功能。kill 时通过 ps 补全进程 pid。这个想法很好,以后我可能专门做一个通用的方便 strace 啊 lsof 啊 gdb 啊之类的用。

另一个是遇到两个星号(**)时按 Tab 补全,查找并替换成当前目录下的文件。

我实现的 sk-cd 是从 autojump 取目录列表,然后喂给 skim。于是就成了交互式的 autojump~这是一个我很需要的功能。原来我都是通过 Tab 补全列出可能的项,然后再 Tab 过去选的,有些慢也有些麻烦。

sk-search-history 就是在历史命令里找东西。因为遇到特殊字符时无法正确地加载预览,我并没有开启预览功能。反正找到的命令只会放在命令行上,并不会自动执行的,选错了可以及时取消。

以上两个功能分别绑定到 Alt-s d 和 Alt-s r 上。我使用 Alt-s 作为 skim 快捷键的开头,以便保留 zsh 原本的快捷键,避免冲突,特别是以后可能会有更多功能被加入。我在 Vim 里,也是类似的做法,Alt-q 是 easymotion 的开头快捷键,Alt-d 是 denite 的开头快捷键。

sk-vim-mru 仅仅是个命令了。使用的数据是 mru.vim 的历史记录文件。然后做了两个函数:vim-mru 使用 Vim 编辑文件,vv-mru 使用我自己的 vv 命令在已有的 gVim 里编辑文件。

我做的版本和 skim 自带版本,最大的差别在于,我的版本会尽量使用全部的窗口空间,而 skim 自带的总是会使用 40% 窗口高度。(所以我有个函数用来获取当前光标位置,有需要的可以自己拿去用。)

如果你想用我的配置,可以 wget https://github.com/lilydjwg/dotzsh/raw/master/plugins/sk-tools.zsh 回去,然后 source 一下就好。有需要的话(比如数据来源、键绑定等)可以自行修改。


2018年09月17日更新:我尝试了一下把 sk-search-history 映射到 Ctrl-r 上,然后很快就放弃了。因为 skim 的结果是不可预测的,而默认的 Ctrl-r 的结果是完全可预测的(只要还记得;当然你不能开(那个让我在服务器上误杀过进程的)实时历史共享)。可预测性对提高效率非常关键,因为你不需要中断思维,停下来等结果。

Category: shell | Tags: linux shell zsh Rust
3
14
2017
22

我的 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
4

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
20

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

| Theme: Aeros 2.0 by TheBuckmaker.com