не удается использовать ‘~’ в автозаполнении zsh

#autocomplete #zsh #tab-completion #zsh-completion

#автозаполнение #zsh #вкладка-завершение #zsh-завершение

Вопрос:

Я использую zsh и хочу использовать функцию, которую я написал, для замены cd. Эта функция дает вам возможность перейти в родительский каталог:

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

Вы также можете перейти в подкаталог родительского каталога:

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

Если первая часть пути не является родительским каталогом, он будет работать как обычный компакт-диск. Я надеюсь, что это имеет смысл.

В итоге, когда в /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" != *"/"*"/"* ]] amp;amp; 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 ] amp;amp; [[ "$d" != "." ]] amp;amp; [[ "$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) == "~" ]] amp;amp; return 1
        while [[ $PWD == $pwd_bu ]]; do
            popd >/dev/null
        done
        local pwd_nw=$PWD
        [[ $(dirs) != "~" ]] amp;amp; 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" != *"/"*"/"* ]] amp;amp; 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 ] amp;amp; [[ "$d" != "." ]] amp;amp; [[ "$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 это означает, что я могу ввести «загрузить», и он завершится «Загрузками». С cl это не работает. Не происходит при использовании обычной функциональности cd.

Есть ли способ исправить (некоторые из этих) проблем? Надеюсь, вы, ребята, понимаете мои вопросы. Мне трудно объяснить проблему.

Спасибо за вашу помощь!

Комментарии:

1. Я обновил свой ответ. Пожалуйста, посмотрите.

Ответ №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 завершит этот путь для вас на каждом этапе пути.

Комментарии:

1. Идея умная, но если пользователь уже установил для своего cdpath фиксированное значение, это приведет к разрушению его настроек.

2. @MarlonRichet : Проблема в том, что вам нужно изменить эту функцию, чтобы применить изменение. В типичном примере мы настроили вашу функцию где-то в .zshrc, и после некоторого времени ее использования пользователь решает установить явное cdpath на лету. Функция не знает об этом изменении. Это будет работать, только если пользователь решит установить cdpath по умолчанию во время определения функции, а не изменять его впоследствии.

3. @user1934428 cdr zsh-autocomplete Для этого я использую plus. Таким образом, мне не нужно ничего настраивать. Я могу просто ввести любую часть любого каталога, который я недавно посетил, нажать Tab, и он завершит весь путь для меня.

4. @user1934428 cdr поддерживает последние каталоги в текстовом файле, поэтому они сохраняются между сеансами, и вы можете вручную добавлять новые каталоги в файл (что также возможно из командной строки).

5. @Tijn Но в конце cl() написано: «В противном случае перейдите в родительский каталог или дочерний каталог этого родительского каталога». Таким образом, дочерние элементы родительских каталогов должны быть допустимыми целями. Тогда почему вы не хотите их завершать?