【问题标题】:can't use '~' in zsh autocompletion不能在 zsh 自动补全中使用 '~'
【发布时间】:2020-10-21 12:32:16
【问题描述】:

我使用 zsh,我想使用我编写的函数来替换 cd。 此功能使您能够移动到父目录:

$ pwd
/a/b/c/d
$ cl b
$ pwd
/a/b

您也可以移动到父目录的子目录:

$ pwd
/a/b/c/d
$ cl b/e
$ pwd
/a/b/e

如果路径的第一部分不是父目录,它将像普通的 cd 一样运行。我希望这是有道理的。

综上所述,在/a/b/c/d的时候,我希望能够移动到/a、/a/b、/a/b/c、/a/b/c/的所有子目录d 和任何以 /、~/ 或 ../(或 ./)开头的绝对路径。 我希望这是有道理的。

这是我写的函数:

cl () {
    local first=$( echo $1 | cut -d/ -f1 )
    if [ $# -eq 0 ]; then
        # cl without any arguments moves back to the previous directory
        cd - > /dev/null
    elif [ -d $first ]; then
        # If the first argument is an existing normal directory, move there
        cd $1
    else
        # Otherwise, move to a parent directory
        cd ${PWD%/$first/*}/$1
    fi
}

可能有更好的方法(欢迎提示),但到目前为止我还没有遇到任何问题。

现在我想添加自动完成功能。这是我目前所拥有的:

_cl() {
    pth=${words[2]}
    opts=""
    new=${pth##*/}
    [[ "$pth" != *"/"*"/"* ]] && middle="" || middle="${${pth%/*}#*/}/"
    if [[ "$pth" != *"/"* ]]; then
        # If this is the start of the path
        # In this case we should also show the parent directories
        opts+="  "
        first=""
        d="${${PWD#/}%/*}/"
        opts+="${d//\/// }"
        dir=$PWD
    else
        first=${pth%%/*}
        if [[ "$first" == "" ]]; then
            # path starts with "/"
            dir="/$middle"
        elif [[ "$first" == "~" ]]; then
            # path starts with "~/"
            dir="$HOME/$middle"
        elif [ -d $first ]; then
            # path starts with a directory in the current directory
            dir="$PWD/$first/$middle"
        else
            # path starts with parent directory
            dir=${PWD%/$first/*}/$first/$middle
        fi
        first=$first/
    fi
    # List al sub directories of the $dir directory
    if [ -d "$dir" ]; then
        for d in $(ls -a $dir); do
            if [ -d $dir/$d ] && [[ "$d" != "." ]] && [[ "$d" != ".." ]]; then
                opts+="$first$middle$d/ "
            fi
        done
    fi
    _multi_parts / "(${opts})"
    return 0
}
compdef _cl cl

同样,这可能不是最好的方法,但它确实有效……有点。

其中一个问题是我键入 cl ~/,它用 cl ~/ 替换它,并且不建议我的主文件夹中的任何目录。有没有办法让它工作?

编辑

cl () {
    local first=$( echo $1 | cut -d/ -f1 )
    if [ $# -eq 0 ]; then
        # cl without any arguments moves back to the previous directory
        local pwd_bu=$PWD
        [[ $(dirs) == "~" ]] && return 1
        while [[ $PWD == $pwd_bu ]]; do
            popd >/dev/null
        done
        local pwd_nw=$PWD
        [[ $(dirs) != "~" ]] && popd >/dev/null
        pushd $pwd_bu >/dev/null
        pushd $pwd_nw >/dev/null
    elif [ -d $first ]; then
        pushd $1 >/dev/null # If the first argument is an existing normal directory, move there
    else
        pushd ${PWD%/$first/*}/$1 >/dev/null # Otherwise, move to a parent directory or a child of that parent directory
    fi
}
_cl() {
    _cd
    pth=${words[2]}
    opts=""
    new=${pth##*/}
    local expl
    # Generate the visual formatting and store it in `$expl`
    _description -V ancestor-directories expl 'ancestor directories'
    [[ "$pth" != *"/"*"/"* ]] && middle="" || middle="${${pth%/*}#*/}/"
    if [[ "$pth" != *"/"* ]]; then
        # If this is the start of the path
        # In this case we should also show the parent directories
        local ancestor=$PWD:h
        while (( $#ancestor > 1 )); do
            # -f: Treat this as a file (incl. dirs), so you get proper highlighting.
            # -Q: Don't quote (escape) any of the characters.
            # -W: Specify the parent of the dir we're adding.
            # ${ancestor:h}: The parent ("head") of $ancestor.
            # ${ancestor:t}: The short name ("tail") of $ancestor.
            compadd "$expl[@]" -fQ -W "${ancestor:h}/" - "${ancestor:t}"
            # Move on to the next parent.
            ancestor=$ancestor:h
        done
    else
        # $first is the first part of the path the user typed in.
        # it it is part of the current direoctory, we know the user is trying to go back to a directory
        first=${pth%%/*}
        # $middle is the rest of the provided path
        if [ ! -d $first ]; then
            # path starts with parent directory
            dir=${PWD%/$first/*}/$first
            first=$first/
            # List all sub directories of the $dir/$middle directory
            if [ -d "$dir/$middle" ]; then
                for d in $(ls -a $dir/$middle); do
                    if [ -d $dir/$middle/$d ] && [[ "$d" != "." ]] && [[ "$d" != ".." ]]; then
                        compadd "$expl[@]" -fQ -W $dir/ - $first$middle$d
                    fi
                done
            fi
        fi
    fi
}
compdef _cl cl

这是我自己得到的。它确实有效(有点)但有几个问题:

  • 返回父目录时,补全通常有效。但是当您转到 paretn 目录的子目录时,建议是错误的(它们显示您输入的完整路径,而不仅仅是子目录)。结果确实有效
  • 我使用语法高亮,但我输入的路径只是白色的(当使用转到父目录时。正常的 cd 函数是彩色的)
  • 在我的 zshrc 中有一行:
zstyle ':completion:*' matcher-list 'm:{a-z}={A-Za-z}' '+l:|=* r:|=*'

使用 cd 这意味着我可以输入“load”,它会完成“Downloads”。使用 cl,这是行不通的。使用普通 cd 功能时不会发生事件。

有没有办法解决(其中一些)问题? 我希望你们能理解我的问题。我觉得很难解释这个问题。

感谢您的帮助!

【问题讨论】:

  • 我更新了我的答案。请看一看。

标签: autocomplete zsh tab-completion zsh-completion


【解决方案1】:

应该这样做:

_cl() {
  # Store the number of matches generated so far.
  local -i nmatches=$compstate[nmatches]

  # Call the built-in completion for `cd`. No need to reinvent the wheel.
  _cd

  # ${PWD:h}: The parent ("head") of the present working dir.
  local ancestor=$PWD:h expl

  # Generate the visual formatting and store it in `$expl`
  # -V: Don't sort these items; show them in the order we add them.
  _description -V ancestor-directories expl 'ancestor directories'

  while (( $#ancestor > 1 )); do
    # -f: Treat this as a file (incl. dirs), so you get proper highlighting.
    # -W: Specify the parent of the dir we're adding.
    # ${ancestor:h}: The parent ("head") of $ancestor.
    # ${ancestor:t}: The short name ("tail") of $ancestor.
    compadd "$expl[@]" -f -W ${ancestor:h}/ - $ancestor:t

    # Move on to the next parent.
    ancestor=$ancestor:h
  done

  # Return true if we've added any matches.
  (( compstate[nmatches] > nmatches ))
}

# Define the function above as generating completions for `cl`.
compdef _cl cl

# Alternatively, instead of the line above:
# 1. Create a file `_cl` inside a dir that's in your `$fpath`.
# 2. Paste the _contents_ of the function `_cl` into this file.
# 3. Add `#compdef cl` add the top of the file.
# `_cl` will now get loaded automatically when you run `compinit`.

另外,我会像这样重写你的 cl 函数,所以它不再依赖于 cut 或其他外部命令:

cl() {
  if (( $# == 0 )); then
    # `cl` without any arguments moves back to the previous directory.
    cd -
  elif [[ -d $1 || -d $PWD/$1 ]]; then
    # If the argument is an existing absolute path or direct child, move there.
    cd $1
  else
    # Get the longest prefix that ends with the argument.
    local ancestor=${(M)${PWD:h}##*$1}
    if [[ -d $ancestor ]]; then
      # Move there, if it's an existing dir.
      cd $ancestor
    else
      # Otherwise, print to stderr and return false.
      print -u2 "$0: no such ancestor '$1'"
      return 1
    fi
  fi
}

替代解决方案

有一种更简单的方法可以完成所有,而无需编写cd 替换或任何完成代码:

cdpath() {
  # `$PWD` is always equal to the present working directory.
  local dir=$PWD

  # In addition to searching all children of `$PWD`, `cd` will also search all 
  # children of all of the dirs in the array `$cdpath`.
  cdpath=()

  # Add all ancestors of `$PWD` to `$cdpath`.
  while (( $#dir > 1 )); do
    # `:h` is the direct parent.
    dir=$dir:h
    cdpath+=( $dir )
  done
}

# Run the function above whenever we change directory.
add-zsh-hook chpwd cdpath

Zsh 对cd 的完成代码自动将$cdpath 考虑在内。甚至不需要配置它。 :)

作为一个工作原理的例子,假设你在/Users/marlon/.zsh/prezto/modules/history-substring-search/external/

  • 您现在可以键入cd pre 并按Tab,Zsh 将完成它到cd prezto。之后,按 Enter 将直接转到/Users/marlon/.zsh/prezto/
  • 或者假设还存在/Users/marlon/.zsh/prezto/modules/prompt/external/agnoster/。当你在前一个目录时,你可以cd prompt/external/agnoster直接进入后者,Zsh会为你完成这条路径的每一步。

【讨论】:

  • 这个想法很聪明,但是如果用户已经将他的 cdpath 设置为固定值,这将破坏他的设置。
  • @MarlonRichet :问题是您必须修改此函数才能应用更改。在一个典型的例子中,我们在 .zshrc 中的某个地方设置了你的函数,并且在使用了一段时间后,用户决定即时设置一个显式的cdpath。该函数不知道此更改。只有当用户决定在定义函数时设置默认的 cdpath 并且之后不更改它,它才会起作用。
  • @user1934428 我为此使用cdr 加上zsh-autocomplete。这样,我不必配置任何东西。我可以输入我最近访问过的任何目录的任何部分,按 Tab,它会为我完成整个路径。
  • @user1934428 cdr 在文本文件中维护最近的目录,因此它们在会话之间持续存在,您可以手动将新目录添加到文件中(也可以从命令行)。
  • @Tijn 但它在cl() 的末尾说:“否则,移动到父目录或该父目录的子目录”。因此,父目录的子级应该是有效的目标。那你为什么不想完成它们呢?
猜你喜欢
  • 1970-01-01
  • 2014-09-16
  • 1970-01-01
  • 2020-06-18
  • 2017-02-21
  • 1970-01-01
  • 1970-01-01
  • 2018-07-30
  • 1970-01-01
相关资源
最近更新 更多