我正在使用Python的Cmd.cmd创建一个命令行工具,我想添加一个" load"带有filename参数的命令,它支持tab-completion。
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 / then" text"之后complete_load函数变为空白,因此_complete_path func会再次返回cwd。
我不知道如何使用tab-completion获取subdir的内容。 请帮忙!
答案 0 :(得分:5)
您的主要问题是readline库基于它的默认分隔符集来分隔:
import readline
readline.get_completer_delims()
# yields ' \t\n`~!@#$%^&*()-=+[{]}\\|;:\'",<>/?'
当选项卡填写文件名时,我会删除此内容但空格。
import readline
readline.set_completer_delims(' \t\n')
设置分隔符后,&#39;文本&#39;完成函数的参数应该更符合您的预期。
这也解决了标签完成时复制文本部分的常见问题。
答案 1 :(得分:3)
使用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模块查找完整路径。水珠 适用于绝对路径和相对路径。找到路径后,我删除 “fixed”部分,它是begidx之前的子字符串。
首先,解析fixed部分参数,它是空格之间的子字符串 和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()
答案 2 :(得分: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()中的“text”,而是直接使用解析“line”参数。 如果您有任何更好的想法,请告诉我。
答案 3 :(得分: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()
答案 4 :(得分:0)
我和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 :(得分: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
当然,有很多改进但希望这会有所帮助。
=)
答案 6 :(得分: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)]