路径书签/别名,用来给目录取个名字,要用时快速跳转,它不是用来代替:z.lua / z.sh / autojump 这类第一梯队的 cd 辅助工具的,而是作为他们的一个补充。

先前我想找个现成的路径书签的小插件,找到这个:pm

把现有目录添加成书签要:

pm add my-project

跳到这个书签对应的目录要:

pm go my-project

然后列出所有书签要:

pm list

删除书签要:

pm remove my-project

我又看了好几个书签软件,都大同小异,又难用,实现又啰嗦,这玩意儿居然写出 500 行以上的代码来,真是匪夷所思。所以我打算用十行代码实现一个更优雅的书签功能。

目标1:少打字

同样一个功能多打一个字母,做一千次就多打了 1K 的内容,能省则省,我不明白为什么这些插件做的都那么啰嗦,输入完命令名还得再输入 add/remove/go/list 之一的参数,然后才是书签名,要我来做,我会把命令名起短一点,比如叫做 m :

m         # 列出当前所有书签
m foo     # 跳转到名为 foo 的标签
m +foo    # 将当前路径添加成书签 foo
m -foo    # 删除名为 foo 书签
m /foo    # 搜索名称里包含 foo 的书签

大部分时候都频率最高的就是 “跳转” 和 “列出所有书签”,所以尽可能的减少他们输入的字符数,m foo 就是跳转到名字为 foo 的书签,比 pm go foo 少打四个字符,m 后面没参数,就是列出所有书签,pm list 少打六个字符。

其他几项功能也比 pm 少打很多字,用 +/- 三个符号代表:增加,搜索和删除,也很直白。

目标2:代码少

我实在不明白一个简单的插件为什么会写成 500-600 行的翔。我们充分利用 shell 的功能,那么把当前目录添加到书签,就是:

ln -snf "$(pwd)" "$MARKPATH/$1" 

其中 $MARKPATH 指向 ~/.local/share/marks 之类的地方,可以自定义,添加书签就是在 $MARKPATH 下面做一个软连接,指向当前目录即可。

删除书签:

rm -i "$MARKPATH/$1" 

删除书签就是删除软连接,还会用 rm 命令的 -i 参数问下你是否真的要删除。

跳转书签:

cd -P "$MARKPATH/$1"

不就完了?使用 -P 参数跳转到实际路径,搞那么多花花绕干嘛?

其他的搜索书签就是 find 命令,列出书签就是 ls 命令格式化一下。

实际代码

实际代码用 case 来判断参数前缀,增加了一些边际情况处理,保存成 m.sh

function m() {
    MARKPATH="${MARKPATH:-$HOME/.local/share/marks}"
    [ -d "$MARKPATH" ] || mkdir -p -m 700 "$MARKPATH" 2> /dev/null
    case "$1" in
        +*)            # m +foo  - add new bookmark for $PWD
            ln -snf "$(pwd)" "$MARKPATH/${1:1}" 
            ;;
        -*)            # m -foo  - delete a bookmark named "foo"
            rm -i "$MARKPATH/${1:1}" 
            ;;
        /*)            # m /bar  - search bookmarks matching "bar"
            find "$MARKPATH" -type l -name "*${1:1}*" | \
                awk -F "/" '{print $NF}' | MARKPATH="$MARKPATH" xargs -I'{}'\
                sh -c 'echo "{} ->" $(readlink "$MARKPATH/{}")'
            ;;
        "")            # m       - list all bookmarks
            command ls -1 "$MARKPATH/" | MARKPATH="$MARKPATH" xargs -I'{}' \
                sh -c 'echo "{} ->" $(readlink "$MARKPATH/{}")'
            ;;
        *)             # m foo   - cd to the bookmark directory
            local dest="$(readlink "$MARKPATH/$1" 2> /dev/null)"
            [ -d "$dest" ] && cd "$dest" || echo "No such mark: $1"
            ;;
    esac
}

就是这么简短,是用的时候在你的 .bashrc / .zshrc 中添加一下:

source /path/to/m.sh

就能用 m foo 进行跳转,m 列出所有书签,m +foo 添加书签了。懒得多弄一个 m.sh 的话,还可以直接把这个函数帖到你的 rc 文件或者 init.sh 里面。

增加补全

上面提到的好几个书签插件都没做补全,咱们代码量虽少,补全方面也不含糊:

if [ -n "$BASH_VERSION" ]; then
    function _cdmark_complete() {
        local MARKPATH="${MARKPATH:-$HOME/.local/share/marks}"
        local curword="${COMP_WORDS[COMP_CWORD]}"
        if [[ "$curword" == "-"* ]]; then
            COMPREPLY=($(find "$MARKPATH" -type l -name "${curword:1}*" \
                2> /dev/null | awk -F "/" '{print "-"$NF}'))
        else
            COMPREPLY=($(find "$MARKPATH" -type l -name "${curword}*" \
                2> /dev/null | awk -F "/" '{print $NF}'))
        fi
    }
    complete -F _cdmark_complete m
elif [ -n "$ZSH_VERSION" ]; then
    function _cdmark_complete() {
        local MARKPATH="${MARKPATH:-$HOME/.local/share/marks}"
        if [[ "${1}${2}" == "-"* ]]; then
            reply=($(command ls -1 "$MARKPATH" 2> /dev/null | \
                awk '{print "-"$0}'))
        else
            reply=($(command ls -1 "$MARKPATH" 2> /dev/null))
        fi
    }
    compctl -K _cdmark_complete m
fi

该补全代码写的稍微啰嗦了点,但是主要是同时兼容 bash/zsh,有了这些补全代码,你输入完 m 命令后按 tab ,能帮你补全书签名称。

好了,前后代码加起来,不超过 50 行,比其他那些 400-500 行的垃圾插件优雅多了。

所以说,灵活应用各类终端工具,玩得转 awk/xargs/sed 等,就能让你事半功倍。

10-23 03:01