2012-05-11 18 views
5

これはBashで可能かどうか疑問に思っていますが、拡張している現在の引数を完全に置き換えるためにタブ補完を使用したいと思います。 例を挙げます: ツリー内の任意の数のレベルを上に移動する関数を持っていますので、 を呼び出すことができます。 それは2つのディレクトリをCDにします。 しかし、2番でタブを押すと、その番号がパス(相対パスか絶対パスかどちらでも良い)に展開されるようにしたいと思います。 これはテキストを追加する以外は完全な組み込み関数を使用しているので、 up 2/Volumes/Dev/コマンドライン引数をタブ補完に置き換えるにはどうすればよいですか?

完成したシンボルを置き換えることはできますか?事前に

感謝:)

更新:

chepnerにとても大きな感謝、実際に私のバグがあった場所を明らかに私のコードをチェックするため。私は間違ったvarと比較していました。私が持っていたデバッグコードは値を置き換えないようにしていました。興味がある人々のために

は、ここでのコードだ(とこれを達成するためのより良い方法があるかもしれません):

# Move up N levels of the directory tree 
# Or by typing in some dir in the PWD 
# eg. Assuming your PWD is "/Volumes/Users/natecavanaugh/Documents/stuff" 
#  `up 2` moves up 2 directories to "/Volumes/Users/natecavanaugh" 
#  `up 2/` and pressing tab will autocomplete the dirs in "/Volumes/Users/natecavanaugh" 
#  `up Users` navigate to "/Volumes/Users" 
#  `up us` and pressing tab will autocomplete to "/Volumes/Users" 
function up { 
    dir="../" 
    if [ -n "$1" ]; then 
     if [[ $1 =~ ^[0-9]+$ ]]; then 
      strpath=$(printf "%${1}s"); 
      dir=" ${strpath// /$dir}" 
     else 
      dir=${PWD%/$1/*}/$1 
     fi 
    fi 

    cd $dir 
} 

function _get_up { 
    local cur 
    local dir 
    local results 
    COMPREPLY=() 
    #Variable to hold the current word 
    cur="${COMP_WORDS[COMP_CWORD]}" 

    local lower_cur=`echo ${cur##*/} | tr [:upper:] [:lower:]` 

    # Is the arg a number or number followed by a slash 
    if [[ $cur =~ ^[0-9]+/? ]]; then 
     dir="../" 
     strpath=$(printf "%${cur%%/*}s"); 
     dir=" ${strpath// /$dir}" 

     # Is the arg just a number? 
     if [[ $cur =~ ^[0-9]+$ ]]; then 
      COMPREPLY=($(compgen -W "${dir}")) 
     else 
      if [[ $cur =~ /.*$ ]]; then 
       cur="${cur##*/}" 
      fi 

      results=$(for t in `cd $dir && ls -d */`; do if [[ `echo $t | tr [:upper:] [:lower:]` == "$lower_cur"* ]]; then echo "${t}"; fi done) 

      COMPREPLY=($(compgen -P "$dir" -W "${results}")) 
     fi 
    else 
     # Is the arg a word that we can look for in the PWD 
     results=$(for t in `echo $PWD | tr "/" "\n"`; do if [[ `echo $t | tr [:upper:] [:lower:]` == "$lower_cur"* ]]; then echo "${t}"; fi; done) 

     COMPREPLY=($(compgen -W "${results}")) 
    fi 
} 

#Assign the auto-completion function _get for our command get. 
complete -F _get_up up 
+0

これまでのコードを質問に追加してください。 – chepner

+0

これは意図的なのかどうか分かりませんが、実際には選択できませんが、 'up /'は各祖先ディレクトリを表示します。ニート! – chepner

+0

これはきちんとした考え方であり、賢明なアプローチです。あなたのコードは私にいくつか教えてくれました。しかし、引用符で囲まずに使用すると、 '\'エスケープする必要があるディレクトリ名(埋め込みスペースなど)は適切に処理されません。私の答えはそれに対処するだけでなく、あなたのアプローチを少し変更します。 – mklement0

答えて

1

以下はNateCanaughのコード@に構築し、

  • は、堅牢性を向上させ\を必要とするディレクトリ名を処理する - エスケープする - 例えば、スペースを含む名前 - 正しく;レポート無効なディレクトリ名が(完了せずに)指定された場合、エラー
  • は、コマンド完了アプローチを変更:コマンド完了時に、レベル番号/名プレフィックスは、「/」を終端と対応絶対パスに展開され、必要に応じて、すぐにサブディレクトリに基づく追加の処理を実行することができます。

変形例は以下のとおりです。

# Assume that the working directory is '/Users/jdoe/Documents/Projects/stuff'. 
# `up 2` moves 2 levels up to '/Users/jdoe/Documents' 
# `up 2<tab>` completes to `up /Users/jdoe/Documents/` 
#  Hit enter to change to that path or [type additional characters and] 
#  press tab again to complete based on subdirectories. 
# `up Documents` or `up documents` changes to '/Users/jdoe/Documents' 
# `up Doc<tab>` or `up doc<tab>` completes to `up /Users/jdoe/Documents/` 
#  Hit enter to change to that path or [type additional characters and] 
#  press tab again to complete based on subdirectories. 
#  Note: Case-insensitive completion is only performed if it is turned on 
#  globally via the completion-ignore-case Readline option 
#  (configured, for instance, via ~/.inputrc or /etc/inputrc). 

ここでは、完全なコード(構文の色分けは、不正なコードを示唆することに注意してください、それがケースではありません)です:

# Convenience function for moving up levels in the path to the current working directory. 
# Synopsis: 
#  `up [n]` moves n levels up in the directory hierarchy; default is 1. 
#  `up dirname` changes to the closest ancestral directory by that name, regardless of case. 
#  `up absolutepath` changes to the specified absolute path; primarily used with command completion (see below). 
# Additionally, if command completion via _complete_up() is in effect (<tab> represents pressing the tab key): 
#  `up [n]<tab>` replaces n with the absolute path of the directory n levels up (default is 1). 
#  `up dirnameprefix<tab>` replaces dirnameprefix with the absolute path of the closest ancestral directory whose name starts with the specified name prefix, terminated with '/'. 
#   Whether dirnameprefix is matched case-insensitively or not depends on whether case-insensitive command completion is turned on globally via ~/.inputrc or /etc/inputrc. 
#  In both cases the completed absolute path ends in '/', allowing you to optionally continue completion based on that path's subdirectories. 
# Notes: 
# - Directory names with characters that need escaping when unquoted (such as spaces) are handled correctly. 
# - For command completion, to specify names that need escaping when unquoted, specify them escaped rather than quoted; 
#  e.g., `up my \di<tab>' to match 'my dir' in the ancestral path. 
function up { 

    local dir='../' # By default, go up 1 level. 

    [[ "$1" == '-h' || "$1" == '--help' ]] && { echo -e "usage:\n\t$FUNCNAME [n]\n\t$FUNCNAME dirname\n Moves up N levels in the path to the current working directory, 1 by default.\n If DIRNAME is given, it must be the full name of an ancestral directory (case does not matter).\n If there are multiple matches, the one *lowest* in the hierarchy is changed to." && return 0; } 

    if [[ -n "$1" ]]; then 
     if [[ $1 =~ ^[0-9]+$ ]]; then # A number, specifying the number of levels to go up.    
      local strpath=$(printf "%${1}s") # This creates a string with as many spaces as levels were specified. 
      dir=${strpath// /$dir} # Create the go-up-multiple-levels cd expression by replacing each space with '../' 
     elif [[ $1 =~ ^/ ]]; then # Already an absolute path? Use as is. (Typically, this happens as a result of command-line completion invoked via _complete_up().) 
      dir=$1 
     else # Assumed to be the full name of an ancestral directory (regardless of level), though the case needn't match. 
      # Note: On case-insensitive HFS+ volumes on a Mac (the default), you can actually use case-insensitive names with 'cd' and the resulting working directory will be reported in that case(!). 
      #  This behavior is NOT related to whether case-insensitivity is turned on for command completion or not. 
      # !! Strangely, the 'nocasematch' shopt setting has no effect on variable substitution, so we need to roll our own case-insensitive substitution logic here. 
      local wdLower=$(echo -n "$PWD" | tr '[:upper:]' '[:lower:]') 
      local tokenLower=$(echo -n "$1" | tr '[:upper:]' '[:lower:]') 
      local newParentDirLower=${wdLower%/$tokenLower/*} # If the specified token is a full ancestral directory name (irrespective of case), this substitution will give us its parent path. 
      [[ "$newParentDirLower" == "$wdLower" ]] && { echo "$FUNCNAME: No ancestral directory named '$1' found." 1>&2; return 1; } 
      local targetDirPathLength=$((${#newParentDirLower} + 1 + ${#tokenLower})) 
      # Get the target directory's name in the exact case it's defined. 
      dir=${PWD:0:$targetDirPathLength} 
     fi 
    fi 

    # Change to target directory; use of 'pushd' allows use of 'popd' to return to previous working directory. 
    pushd "$dir" 1>/dev/null 
} 

# Companion function to up(), used for command completion. 
# To install it, run (typically in your bash profile): 
# `complete -o filenames -F _complete_up up` 
# Note: The '-o filenames' option ensures that: 
# (a) paths of directories returned via $COMPREPLY leave the cursor at the terminating "/" for potential further completion 
# (b) paths with embeddes spaces and other characters requiring \-escaping are properly escaped. 
function _complete_up { 

    COMPREPLY=() # Initialize the array variable through which completions must be passed out. 

    # Retrieve the current command-line token, i.e., the one on which completion is being invoked. 
    local curToken=${COMP_WORDS[COMP_CWORD]} 
    # Remove \ chars., presumed to be escape characters in the current token, which is presumed to be *unquoted*. This allows invoking completion on a token with embedded space, e.g., '$FUNCNAME some\ directory' 
    # !! Strictly speaking, we'd have to investigate whether the token was specified with quotes on the command line and, if quoted, NOT unescape. Given that the purpose of this function is expedience, we 
    # !! assume that the token is NOT quoted and that all backslashes are therefore escape characters to be removed. 
    curToken=${curToken//'\'} 

    if [[ $curToken =~ ^/ ]]; then # Token is an absolute path (typically as a result of a previous completion) -> complete with directory names, similar to 'cd' (although the latter, curiously, also completes *file* names). 

     local IFS=$'\n' # Make sure that the output of compgen below is only split along lines, not also along spaces (which the default $IFS would do). 
     COMPREPLY=($(compgen -o dirnames -- "$curToken")) 

    elif [[ $curToken =~ ^[0-9]+/? ]]; then # Token is a number (optionally followed by a slash) -> replace the token to be completed with the absolute path of the directory N levels above, where N is the number specified. 

     # Create a go-up-multiple-levels cd expression that corresponds to the number of levels specified. 
     local strpath=$(printf "%${curToken%%/*}s") # This creates a string with as many spaces as levels were specified. 
     local upDirSpec=${strpath// /../} # Create the go-up-multiple-levels cd expression by replacing each space with '../'   

     # Expand to absolute path (ending in '/' to facilitate optional further completion) and return. 
     local dir=$(cd "$upDirSpec"; echo -n "$PWD/") 
     if [[ "$dir" == '//' ]]; then dir='/'; fi # In case the target dir turns out to be the root dir, we've accidentally created '//' in the previous statement; fix it. 
     # !! Note that the path will appear *unquoted* on the command line and must therefore be properly \-escaped (e.g., a ' ' as '\ '). 
     # !! Escaping is performed automatially by virtue of defining the compspec with '-o filenames' (passed to 'complete'). 
     COMPREPLY=("$dir") 

    else # Token is a name -> look for a prefix match among all the ancestral path components; use the first match found (i.e., the next match up in the hierarchy). 

     # Determine if we should do case-insensitive matching or not, depending on whether cases-insensitive completion was turned on globally via ~/.inputrc or /etc/inputrc. 
     # We do this to be consistent with the default command completion behavior. 
     local caseInsensitive=0   
     bind -v | egrep -i '\bcompletion-ignore-case[[:space:]]+on\b' &>/dev/null && caseInsensitive=1 

     # If we need to do case-INsensitive matching in this function, we need to make sure the 'nocasematch' shell option is (temporarily) turned on. 
     local nocasematchWasOff=0 
     if ((caseInsensitive)); then 
      nocasematchWasOff=1 
      shopt nocasematch >/dev/null && nocasematchWasOff=0 
      ((nocasematchWasOff)) && shopt -s nocasematch >/dev/null 
     fi 

     local pathSoFar='' 
     local matchingPath='' 
     # Note: By letting the loop iterate over ALL components starting at the root, we end up with the *last* match, i.e. the one *lowest* in the hierarchy (closed to the current working folder). 
     # !! We COULD try to return multiple matches, if applicable, but in practice we assume that there'll rarely be paths whose components have identical names or prefixes. 
     # !! Thus, should there be multiple matches, the user can reinvoke the same command to change to the next-higher match (though the command must be typed again), and so forth. 
     local parentPath=${PWD%/*} 
     local IFS='/' # This will break our parent path into components in the 'for' loop below. 
     local name 
     for name in ${parentPath:1}; do 
      pathSoFar+=/$name 
      if [[ "$name" == "$curToken"* ]]; then 
       matchingPath="$pathSoFar/" 
      fi 
     done 

     # Restore the state of 'nocasematch', if necessary. 
     ((caseInsensitive && nocasematchWasOff)) && shopt -u nocasematch >/dev/null 

     # If match was found, return its absolute path (ending in/to facilitate optional further completion). 
     # !! Note that the path will appear *unquoted* on the command line and must therefore be properly \-escaped (e.g., a ' ' as '\ '). 
     # !! Escaping is performed automatially by virtue of defining the compspec with '-o filenames' (passed to 'complete'). 
     [[ -n "$matchingPath" ]] && COMPREPLY=("$matchingPath") 

    fi 
} 

# Assign the auto-completion function for up(). 
complete -o filenames -F _complete_up up 
1

は、それはすることが可能です現在の単語を完全に1つの新しい単語に置き換えます。私はbash 4.2.29で、私はこれを行うことができます。

_xxx() { COMPREPLY=(foo); } 
complete -F _xxx x 
x bar # pressing tab turns this into x foo 

つ以上の可能な補完がある場合は、しかし、問題が発生した、とあなたは、共通のプレフィックスを部分的に完了したことを取得したいです。私の実験によれば、bashは入力した接頭辞に使用可能な補完をマッチさせようとします。

一般に、現在の引数は、何かが一意に定義されている場合は、まったく異なるもので置き換えてください。それ以外の場合は、現在の接頭辞と一致する補完を生成して、ユーザーがそれらの中から選択できるようにする必要があります。しかし、この特定の場合には、唯一の適切なパスでプレフィックス番号を交換した方が良いかもしれない、とデフォルトのbashに他のすべてを残し

local IFS=$'\n' 
COMPREPLY=($(find "${dir}" -maxdepth 1 -type d -iname "${cur#*/}*" -printf "%P\n")) 
if [[ ${#COMPREPLY[@]} -eq 1 ]]; then 
    COMPREPLY=("${dir}${COMPREPLY[0]}") 
fi 

:あなたのケースでは、これらの線に沿って何かをCOMPREPLY=($(compgen -P "$dir" -W "${results}"))を置き換えることができます完了:

_up_prefix() { 
    local dir cur="${COMP_WORDS[COMP_CWORD]}" 
    COMPREPLY=() 

    if [[ ${cur} =~ ^[0-9]+/? ]]; then 
     # Starting with a number, possibly followed by a slash 
     dir=$(printf "%${cur%%/*}s"); 
     dir="${dir// /../}" 
     if [[ ${cur} == */* ]]; then 
      dir="${dir}${cur#*/}" 
     fi 
     COMPREPLY=("${dir}" "${dir}.") # hack to suppress trailing space 
    elif [[ ${cur} != */* ]]; then 
     # Not a digit, and no slash either, so search parent directories 
     COMPREPLY=($(IFS='/'; compgen -W "${PWD}" "${cur}")) 
     if [[ ${#COMPREPLY[@]} -eq 1 ]]; then 
      dir="${PWD%${COMPREPLY[0]}/*}${COMPREPLY[0]}/" 
      COMPREPLY=("${dir}" "${dir}.") # hack as above 
     fi 
    fi 
} 

complete -F _up_prefix -o dirnames up 

コードの読み込みと管理が非常に簡単になり、さらに効率的に起動できます。唯一の欠点は、あなたが以前よりももう一度Tabを押す必要があることです。プレフィックスの代わりに1回、補完候補のリストを実際に表示するために2回以上押す必要があります。それが受け入れられるかどうかあなたの選択。

もう1つ:引数を通常のパスに変更しますが、up関数はそのまま受け入れません。したがって、おそらくあなたは[[ -d $1 ]]チェックでその機能を開始し、そのディレクトリがあればそれを単にcdしてください。さもなければ、あなたの完了は、呼び出された関数に受け入れられない引数を生成します。

関連する問題