7

我正在使用 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 函数再次返回 cwd。

我不知道如何通过制表符完成获取 subdir 的内容。请帮忙!

4

7 回答 7

7

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

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

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

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

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

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

于 2015-12-02T20:04:12.813 回答
4

使用 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 模块来查找完整路径。Glob 适用于绝对路径和相对路径。找到路径后,我删除了“固定”部分,它是 begidx 之前的子字符串。

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

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

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

我在作为目录的结果中附加了一个“/”,因为这样可以更轻松地通过制表符完成遍历目录(否则您需要为每个目录按两次制表键),并且它使用户清楚哪些完成项是目录,哪些是文件。

最后删除路径的“固定”部分,因此 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()
于 2014-12-02T18:44:49.153 回答
1

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

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() 中的“文本”,而是直接使用解析“行”参数。如果您有更好的想法,请告诉我。

于 2013-05-30T22:56:08.313 回答
1

我对 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 目录结构创建此代码。

于 2013-09-12T02:59:51.380 回答
1

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()
于 2016-10-20T11:17:35.337 回答
0

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

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)]
于 2017-10-11T18:01:24.887 回答
0

我通过这样做来实现这一点:

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

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

=)

于 2016-06-17T14:41:10.273 回答