由在ZSH运行上条命令展开

三 19 2015 Published by under Linux/Ubuntu

很多时候运行完命令才忘了sudo,那么

  • 方向上键到上一条命令,然后方向左键或home键到命令最左端,添加"sudo ",回车
  • Ctrl-P回到上一条命令,Ctrl-A回到最左端,添加"sudo ",回车
  • sudo !!,回车

很明显第三个是最简单的。
同样很多时候,一条命令的输出太长,需要pipe到less中或者Grep中,那么用alias或者函数会方便很多。可是"!!"没法写入到.zshrc中。

Google相关信息,出现频率最高的是这个命令:fc -nl -1,可以直接在得到历史记录中的上一条命令。

不过有一个问题:当一个命令的前面出现若干空格时,这条命令不会出现在历史记录中,那么这时候fc命令无法返回我们想获得的那条命令。
同时,当上条命令中出现任意alias时,通过fc命令返回上条命令会出现command not found提示,貌似是因为alias默认无法在non-interactive shell中生效。

那么,fc -nl -1两个问题

  1. 上个命令中有alias时无效
  2. 上个命令中最开始有若干空格时无效

先解决问题1

既然上个命令中有alias无法识别,那么将命令中的alias还原成原始命令,问题即可解决。
zsh中有个函数_expand_alias,其作用是当当前光标所在的word是一个alisa时,会自动将alisa还须成该alias的值,也就是原始的命令。
有函数了,那么还需要个条件来触发函数。
在ZSH中,每一个函数都可以当做一个Widget,通过Widget绑定到某个按键组合从而可以实现对Widget的触发。

那么就这样,

# 新建一个函数
expand_alias_space () {
    zle _expand_alias
    zle self-insert
}
# 新建一个Widget
zle -N expand_alias_space
# 绑定到空格键
bindkey " " expand_alias_space

效果图:

当命令中只有一个alias时,输入完alias后直接就回车,不输入空格就无法触发Widget,就无法expand alias了。
所以对于Enter键也同样要绑定Widget,不过这个Widget会略微有所不同。

# 新建一个函数
expand_alias_enter () {
    if [[ -z $BUFFER ]]
    then
        zle clear-screen
    else
        zle _expand_alias
        zle accept-line
    fi
}
# 新建一个Widget
zle -N expand_alias_enter
# 绑定到回车键
bindkey "^M" expand_alias_enter

expand_alias_enter函数首先判断当前输入内容是否为空,如果为空,就帮个忙,把屏幕清一下,如果不为空,就expand alias,然后执行命令。
效果图:

OK,问题1解决。

然后就是问题2

解决了问题1那问题2就很简单了。
既然部分情况下命令不会被记录到历史中,那么咱来手动记录。
每次回车的时候把当前记录记录下来就可以了吧。
修改expand_alias_enter函数为:

expand_alias_enter () {
    if [[ -z $BUFFER ]]
    then
        zle clear-screen
    else
        zle _expand_alias
        zle accept-line
        # 保存当前命令
        LAST_COMMAND=$CURRENT_COMMAND
        CURRENT_COMMAND=$BUFFER
    fi
}

这样上次执行的命令保存在$LAST_COMMAND中,那么eval $LAST_COMMAND就可以执行上次命令。
嗯,问题2解决。

任务完成

其实还是有问题的,如果两条相邻的命令都执行了eval $LAST_COMMAND,当前命令执行eval $LASZT_COMMAND,也就是上条命令,而上条命令也执行eval $LAST_COMMAND,此时$LAST_COMMAND中保存的还是上条命令,那继续执行上条命令,然后就死循环了:

zsh: job table full or recursion limit exceeded

这个问题解决起来就比较麻烦了。

思路

要消除循环,就要将$LAST_COMMAND的值替换成具体的命令。
那么就需要在用到了$LAST_COMMAND的函数中将当前函数中执行过的所有命令都顺序存放在某个buffer中,在执行完函数后,检测下buffer,如果不为空,那么就将其中的值作为本次执行过的命令内容,存放在$CURRENT_COMMAND中。
这样虽然历史记录中显示的是函数名,但是$CURRENT_COMMAND中记录的是这个函数对应的所有命令,不包含$LAST_COMMAND,也就不会出现循环问题。

实现

函数

下面实现一个显示上次执行过的命令的函数:

<pre><code>fun()
{
    # 将本函数要执行的命令赋给BUFFER_FOR_LAST_COMMAND
    BUFFER_FOR_LAST_COMMAND="echo \[`echo $LAST_COMMAND`\]"
    # 执行该BUFFER
    eval $BUFFER_FOR_LAST_COMMAND
}
</code></pre>

执行完函数后检测变量$BUFFER_FOR_LAST_COMMAND,如果不为空,将其值赋给$CURRENT_COMMAND,然后删除变量$BUFFER_FOR_LAST_COMMAND
这个过程要在执行完函数后触发,Widgetzle-line-init可以满足要求。

function zle-line-init {
    if [ -n "$BUFFER_FOR_LAST_COMMAND" ]
    then
        # 如果BUFFER不为空,则将其值作为本次运行的命令
        CURRENT_COMMAND=$BUFFER_FOR_LAST_COMMAND
        eval unset -v $BUFFER_FOR_LAST_COMMAND 
    fi

    zle reset-prompt
}
zle -N zle-line-init

效果如图:

Alias

至于alias可以用函数实现,就这样。

任务完成

总感觉第二部分有问题,但又不知道到底什么地方有问题,先就这样,以后再改。

这是第一篇用Markdown写的博文,在马克飞象上完成。界面手感都不错,就是vim模式下切换到普通模式比较麻烦,需要将输入法切换到英文模式才能操作,不方便。用dillinger.io导出。导出后的样式和马克飞象中差太多。

有时间配置vim的Markdown插件,配合fcitx.vim至少可以少按几下Shift,虽然插件有很明示的延时。

感谢依云提供的Widget,当用来执行固定操作时这种方式更合适。

sudo-command-line() {
    # If current buffer is empth, get the last command
    [[ -z $BUFFER ]] && zle up-history
    # If the command not start with sudo
    [[ $BUFFER != sudo\ * ]] && {
      typeset -a bufs
      bufs=(${(z)BUFFER})
      # If the first word in BUFFER is an alias, replace is with
      # it's value
      if (( $+aliases[$bufs[1]] )); then
bufs[1]=$aliases[$bufs[1]]
      fi
      bufs=(sudo $bufs)
      BUFFER=$bufs
    }
    zle end-of-line
}
zle -N sudo-command-line
bindkey "\e\e" sudo-command-line

本文链接地址: 由在ZSH运行上条命令展开



No responses yet

发表评论