Implementing filename completion with cmd is a bit tricky because the
underlying readline library interprets special characters such as '/' and '-'
(and others) as separators, and this sets which substring within the line is to
be replaced by the completions.
For example,
> load /hom<tab>
calls complete_load() with
text='hom', line='load /hom', begidx=6, endidx=9
text is line[begidx:endidx]
'text' is not "/hom" because the readline library parsed the line and
returns the string after the '/' separator. The complete_load() should return
a list of completion strings that begin with "hom", not "/hom", since the
completions will replace the substring starting at the begidx. If
complete_load() function incorrectly returns ['/home'], the line becomes,
> load //home
which is not good.
Other characters are considered to be separators by readline, not just slashes,
so you cannot assume the substring before 'text' is a parent directory. For
example:
> load /home/mike/my-file<tab>
calls complete_load() with
text='file', line='load /home/mike/my-file', begidx=19, endidx=23
Assuming /home/mike contains the files my-file1 and my-file2, the completions
should be ['file1', 'file2'], not ['my-file1', 'my-file2'], nor
['/home/mike/my-file1', '/home/mike/my-file2']. If you return the full paths, the result is:
> load /home/mike/my-file/home/mike/my-file1
The approach I took was to use the glob module to find the full paths. Glob
works for absolute paths and relative paths. After finding the paths, I remove
the "fixed" portion, which is the substring before the begidx.
First, parse the fixed portion argument, which is the substring between the space
and the begidx.
index = line.rindex(' ', 0, begidx) # -1 if not found
fixed = line[index + 1: begidx]
The argument is between the space and the end of the line. Append a star to make
a glob search pattern.
I append a '/' to results which are directories, as this makes it easier
to traverse directories with tab completion (otherwise you need to hit the
tab key twice for each directory), and it makes it obvious to the user
which completion items are directories and which are files.
Finally remove the "fixed" portion of the paths, so readline will replace
just the "text" part.
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()