【问题标题】:filename tab-completion in Cmd.cmd of PythonPython 的 Cmd.cmd 中的文件名制表符补全
【发布时间】:2013-05-30 00:42:41
【问题描述】:

我正在使用 Python 的 Cmd.cmd 制作一个命令行工具,我想添加一个带有文件名参数的“加载”命令,它支持制表符补全。

参考thisthis,我疯了这样的代码:

import os, cmd, sys, yaml
import os.path as op
import glob as gb

def _complete_path(path):
    if op.isdir(path):
        return gb.glob(op.join(path, '*'))
    else:
        return gb.glob(path+'*')

class CmdHandler(cmd.Cmd):

    def do_load(self, filename):
        try:
            with open(filename, 'r') as f:
                self.cfg = yaml.load(f)
        except:
            print 'fail to load the file "{:}"'.format(filename)

    def complete_load(self, text, line, start_idx, end_idx):
        return _complete_path(text)

这对 cwd 很有效,但是,当我想进入 subdir 时,在 subdir/ 之后,complete_load 函数的“文本”变为空白,因此 _complete_path func 再次返回 cwd。

我不知道如何通过 tab-completion 获取 subdir 的内容。 请帮忙!

【问题讨论】:

  • 我很难相信没有人回答这个问题

标签: python cmd tab-completion


【解决方案1】:

您的主要问题是 readline 库根据其默认分隔符集来分隔事物:

import readline
readline.get_completer_delims()
# yields ' \t\n`~!@#$%^&*()-=+[{]}\\|;:\'",<>/?'

当一个文件名的制表符完成时,我会删除除空格之外的所有内容。

import readline
readline.set_completer_delims(' \t\n')

设置分隔符后,补全函数的“文本”参数应该更符合您的预期。

这也解决了制表符补全复制部分文本的常见问题。

【讨论】:

    【解决方案2】:

    用 cmd 实现文件名完成有点棘手,因为 底层的 readline 库解释特殊字符,例如 '/' 和 '-' (和其他)作为分隔符,这将设置该行中的哪个子字符串 替换为补全。

    例如,

    > load /hom<tab>
    

    调用 complete_load() 与

    text='hom', line='load /hom', begidx=6, endidx=9
    text is line[begidx:endidx]
    

    'text' 不是 "/hom" 因为 readline 库解析了该行并且 返回 '​​/' 分隔符后的字符串。 complete_load() 应该返回 以“hom”而不是“/hom”开头的完成字符串列表,因为 完成将替换从 begidx 开始的子字符串。如果 complete_load() 函数错误返回 ['/home'],行变为,

    > load //home
    

    这不好。

    其他字符被 readline 认为是分隔符,而不仅仅是斜杠, 所以你不能假设 'text' 之前的子字符串是父目录。为了 示例:

    > load /home/mike/my-file<tab>
    

    调用 complete_load() 与

    text='file', line='load /home/mike/my-file', begidx=19, endidx=23
    

    假设 /home/mike 包含文件 my-file1 和 my-file2,完成 应该是 ['file1', 'file2'],而不是 ['my-file1', 'my-file2'],也不是 ['/home/mike/my-file1', '/home/mike/my-file2']。如果返回完整路径,结果是:

    > load /home/mike/my-file/home/mike/my-file1
    

    我采用的方法是使用 glob 模块来查找完整路径。全球 适用于绝对路径和相对路径。找到路径后,我删除 “固定”部分,即begidx之前的子字符串。

    首先,解析固定部分参数,即空格之间的子字符串 和begidx。

    index = line.rindex(' ', 0, begidx)  # -1 if not found
    fixed = line[index + 1: begidx]
    

    参数在空格和行尾之间。附加一颗星来制作 全局搜索模式。

    我在作为目录的结果后面附加一个“/”,因为这样更容易 使用制表符完成遍历目录(否则您需要点击 每个目录的 tab 键两次),这对用户来说很明显 哪些补全项是目录,哪些是文件。

    最后删除路径的“固定”部分,所以 readline 将替换 只是“文本”部分。

    import os
    import glob
    import cmd
    
    def _append_slash_if_dir(p):
        if p and os.path.isdir(p) and p[-1] != os.sep:
            return p + os.sep
        else:
            return p
    
    class MyShell(cmd.Cmd):
        prompt = "> "
    
        def do_quit(self, line):
            return True
    
        def do_load(self, line):
            print("load " + line)
    
        def complete_load(self, text, line, begidx, endidx):
            before_arg = line.rfind(" ", 0, begidx)
            if before_arg == -1:
                return # arg not found
    
            fixed = line[before_arg+1:begidx]  # fixed portion of the arg
            arg = line[before_arg+1:endidx]
            pattern = arg + '*'
    
            completions = []
            for path in glob.glob(pattern):
                path = _append_slash_if_dir(path)
                completions.append(path.replace(fixed, "", 1))
            return completions
    
    MyShell().cmdloop()
    

    【讨论】:

    • 我想切片会比调用 replace() 更有效。只需要计算切片的索引,类似于 begidx - before_arg。
    • 我尝试了类似的方法,不幸的是它不处理路径中的空格。 shlex.split(line) 无法帮助,因为它删除了行元素之间的空白,因此无法在该元素中找到正确的元素和位置。真不幸。
    【解决方案3】:

    我不认为这是最好的答案,但我得到了我想要的功能:

    def _complete_path(text, line):
        arg = line.split()[1:]
        dir, base = '', ''
        try: 
            dir, base = op.split(arg[-1])
        except:
            pass
        cwd = os.getcwd()
        try: 
            os.chdir(dir)
        except:
            pass
        ret = [f+os.sep if op.isdir(f) else f for f in os.listdir('.') if f.startswith(base)]
        if base == '' or base == '.': 
            ret.extend(['./', '../'])
        elif base == '..':
            ret.append('../')
        os.chdir(cwd)
        return ret
    
        .............................
    
        def complete_load(self, text, line, start_idx, end_idx):
            return _complete_path(text, line)
    

    我没有使用 complete_cmd() 中的“文本”,而是直接使用解析“行”参数。 如果您有更好的想法,请告诉我。

    【讨论】:

      【解决方案4】:

      我对 jinserk 有相同的想法,但方式不同。这是我的代码:

      def complete_load(self, text, line, begidx, endidx):
          arg = line.split()[1:]
      
          if not arg:
              completions = os.listdir('./')
          else:
              dir, part, base = arg[-1].rpartition('/')
              if part == '':
                  dir = './'
              elif dir == '':
                  dir = '/'            
      
              completions = []
              for f in os.listdir(dir):
                  if f.startswith(base):
                      if os.path.isfile(os.path.join(dir,f)):
                          completions.append(f)
                      else:
                          completions.append(f+'/')
      
          return completions
      

      如果您有更好的想法,请告诉我。 注意:我认为这种方法只适用于 Unix 系列操作系统,因为我基于 Unix 目录结构创建此代码。

      【讨论】:

        【解决方案5】:

        我使用shlex 来解析该行。与其他一些解决方案相比,我支持引用和转义路径(即带有空格的路径)并且完成适用于任何光标位置。我没有进行广泛的测试,因此您的里程可能会有所不同。

        def path_completion(self, text, line, startidx, endidx):
            try:
                glob_prefix = line[:endidx]
        
                # add a closing quote if necessary
                quote = ['', '"', "'"]
                while len(quote) > 0:
                    try:
                        split = [s for s in shlex.split(glob_prefix + quote[0]) if s.strip()]
                    except ValueError as ex:
                        assert str(ex) == 'No closing quotation', 'Unexpected shlex error'
                        quote = quote[1:]
                    else:
                        break
                assert len(quote) > 0, 'Could not find closing quotation'
        
                # select relevant line segment
                glob_prefix = split[-1] if len(split) > 1 else ''
        
                # expand tilde
                glob_prefix = os.path.expanduser(glob_prefix)
        
                # find matches
                matches = glob.glob(glob_prefix + '*')
        
                # append os.sep to directories
                matches = [match + os.sep if Path(match).is_dir() else match for match in matches]
        
                # cutoff prefixes
                cutoff_idx = len(glob_prefix) - len(text)
                matches = [match[cutoff_idx:] for match in matches]
        
                return matches
            except:
                traceback.print_exc()
        

        【讨论】:

          【解决方案6】:

          我做到了这一点:

          def complete_listFolder(self, text, line, begidx, endidx):
              path = os.path.relpath(os.path.normpath(line.split()[1]))
                      if not os.path.isdir(path) and not os.path.isfile(path):
                          baseName = os.path.basename(path)
                          dirName = os.path.dirname(path)
                          return fnmatch.filter(os.listdir(dirName), baseName + "*")
          
                      completions = [completion for completion in os.listdir(path)]    
                      return completions
          

          当然还有很多需要改进的地方,但希望这会有所帮助。

          =)

          【讨论】:

            【解决方案7】:

            这对我有用。如果您不在课堂上使用,请删除“自我”。

            def _complete_path(self, path):
                if os.path.isdir(path):
                    return gb.glob(os.path.join(path, '*'))
                else:
                    return gb.glob(path + '*')
            
            def complete_load(self, text, line, start_idx, end_idx):
                mline = line.split(' ')[-1]
                offs = len(mline) - len(text)
                completions = []
                if line.split()[-2] == '-p':
                    completions = self._complete_path(mline)
                return [s[offs:] for s in completions if s.startswith(mline)]
            

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 2010-12-28
              • 1970-01-01
              • 2013-08-16
              • 2021-04-02
              • 2010-12-23
              • 1970-01-01
              • 2010-10-19
              • 2011-04-02
              相关资源
              最近更新 更多