命令补全器 objection/console/completer.py
completer.py 实现 objection REPL 的 Tab 补全 CommandCompleter,继承自 prompt_toolkit.completion.Completer。它根据当前输入文本切出 token,遍历 COMMANDS 注册表下钻到最深匹配的子树,再结合 dynamic(动态补全函数)与 flags(可补全 flag)返回候选。Repl 用 FuzzyCompleter(CommandCompleter()) 包一层以支持模糊匹配。
📋 模块概览
| 项目 | 值 |
|---|---|
| 文件路径 | objection/console/completer.py |
| 类型 | prompt_toolkit Completer |
| 被谁调用 | Repl.__init__(FuzzyCompleter(CommandCompleter()),repl.py:31) |
| 依赖 | prompt_toolkit.completion、commands.COMMANDS、utils.helpers.get_tokens、collections |
🎯 解决的问题
- 根据已输入的命令前缀,动态生成下一级子命令候选。
- 支持文件系统类命令的运行时补全(
dynamic按当前设备目录内容返回)。 - 把
--flag也纳入补全,并自动排除已输入的 flag,避免重复建议。 - 对
!开头的 OS 命令直接放弃补全(交给 shell)。
🏗️ 核心结构
CommandCompleter.__init__ — 持有注册表引用
源码:objection/console/completer.py:15-17
python
def __init__(self) -> None:
super(CommandCompleter, self).__init__()
self.COMMANDS = COMMANDSfind_completions(document) — 下钻命令树
源码:objection/console/completer.py:19-97
算法:
- 用
get_tokens(document.text)切词,分出tokens(去掉--flag)与flags(保留--flag)(completer.py:43-48)。 current_suggestions = self.COMMANDS,对每个 token 小写后查匹配:- 有
commands子键 → 下钻(completer.py:66-67)。 - 有
dynamic→ 调用该函数拿动态候选(completer.py:71-72)。 - 有
flags→ 构造{flag: ''}字典,排除已输入 flag(completer.py:75-78)。 - 否则返回
{}(无更深建议,completer.py:82-83)。
- 有
- 对最终
current_suggestions做模糊匹配:保留document.get_word_before_cursor().lower() in k.lower()的 key(completer.py:90-96)。
get_completions(document, complete_event) — prompt_toolkit 钩子
源码:objection/console/completer.py:99-132
python
def get_completions(self, document: Document, complete_event: CompleteEvent) -> Completion:
word_before_cursor = document.get_word_before_cursor()
if document.text.startswith('!'):
return
commands = self.find_completions(document)
if len(commands) <= 0:
return
commands = collections.OrderedDict(sorted(list(commands.items()), key=lambda t: t[0]))
for cmd, extra in commands.items():
meta = extra['meta'] if type(extra) is dict and 'meta' in extra else None
yield Completion(cmd, -(len(word_before_cursor)), display_meta=meta)要点:
!前缀直接return(不补全 OS 命令,completer.py:113-114)。- 候选按字母序排序(
OrderedDict(sorted(...)),completer.py:123)。 - 每个
Completion带上注册表里的meta一句话说明作为display_meta,补全菜单里同时显示命令名与说明。 -(len(word_before_cursor))是光标处替换起点,让 prompt_toolkit 知道从哪里替换。
⚙️ 实现要点
- 数据驱动:补全器不硬编码任何命令,全部从
COMMANDS的commands/dynamic/flags字段推导,新增命令自动获得补全。 - flag 去重:
flags列表会过滤掉已在输入行中出现的--flag,避免重复建议(completer.py:76-78)。 - 大小写不敏感:匹配时统一
.lower()(completer.py:61,94),但返回的候选保留注册表里的原始大小写。 - 与帮助系统一致:补全下钻逻辑与
Repl._find_command_exec_method/_find_command_help同构,三者都"走命令树",保证补全建议与可执行命令一致。 - 模糊层外包:本类只做精确前缀子串匹配,模糊容错由
Repl外层的FuzzyCompleter提供(repl.py:31)。
🔍 源码索引
| 符号 | 位置 |
|---|---|
CommandCompleter 类 | objection/console/completer.py:10 |
__init__ | objection/console/completer.py:15 |
find_completions | objection/console/completer.py:19 |
get_completions | objection/console/completer.py:99 |
🔀 补全决策时序
下图刻画从 prompt_toolkit 触发补全到 yield Completion 对象的完整时序,重点展示 FuzzyCompleter 外包层与 complete_in_thread 异步调用栈。
时序关键点:
- 双层模糊:
CommandCompleter内部只做子串匹配(document.get_word_before_cursor().lower() in k.lower(),completer.py:94),外层FuzzyCompleter再做容错排序。内层是"光标前词是否为候选子串"的精确过滤,外层才允许错位/缺字匹配。 - 线程化补全:
Repl.__init__设complete_in_thread=True(repl.py:48),get_completions在独立线程执行。当dynamic调用涉及设备 I/O(如列出远程目录)较慢时,输入框不会冻结。 dynamic副作用:dynamic函数(如filemanager.list_folders_in_current_fm_directory)每次补全都重新调用,会触发 Frida RPC 查询设备文件系统。这意味着高频按 Tab 会产生多次 RPC 往返——补全延迟主要受设备 I/O 制约。
🧭 补全路径决策状态机
下图刻画单个 token 在 find_completions 循环中如何决定 current_suggestions 的下一步走向。这是一个隐式状态机:每消费一个 token,状态机的"当前候选字典"就迁移一次。
状态迁移规则(基于 completer.py:59-83):
elif链优先级:commands>dynamic>flags> 兜底空 dict。节点同时有多个字段时,靠前的分支胜出。实际注册表无节点同时具备这三者,故无歧义。NotFound不下钻但不报错:若 token 不匹配任何 key,current_suggestions保持不变,继续消费下一个 token(completer.py:59-63的if candidate in ...失败时循环体跳过)。这意味着输入android foo bar时,foo不匹配后bar仍会基于android的子树尝试——但因为current_suggestions仍是android['commands'],bar也不会命中,最终返回整个android['commands']的所有 key 做模糊过滤。FoundLeaf返回空:到达叶子(如ping)后再有 token,直接return {},补全菜单为空(completer.py:82-83)。
📐 候选字典数据流(ASCII 框图)
下图用具体输入 memory list modules --json 展示 current_suggestions 在循环中的逐步变迁,以及 flag 过滤如何工作。
输入: "memory list modules --json"
(光标在末尾, get_word_before_cursor() = "")
┌──────────────────────────────────────────────────────────┐
│ Step 0: 初始化 │
│ tokens = ["memory","list","modules"] (去掉 --json) │
│ flags = ["--json"] │
│ current_suggestions = COMMANDS │
└──────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ Step 1: token="memory" │
│ "memory" in COMMANDS? 是 │
│ 有 'commands'? 是 │
│ → current_suggestions = COMMANDS["memory"]["commands"] │
│ = {dump:{...}, list:{...}, search:{...}, replace, write}│
└──────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ Step 2: token="list" │
│ "list" in current_suggestions? 是 │
│ 有 'commands'? 是 │
│ → current_suggestions = ...["list"]["commands"] │
│ = {modules:{flags:[--json], exec:...}, exports:{...}} │
└──────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ Step 3: token="modules" │
│ "modules" in current_suggestions? 是 │
│ 有 'commands'? 否 │
│ 有 'dynamic'? 否 │
│ 有 'flags'? 是 → 进入 BuildFlags 分支 │
│ → current_suggestions = { │
│ f: '' for f in ["--json"] if f not in ["--json"] │
│ } │
│ = {} ← --json 已输入, 被排除! │
└──────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ Step 4: 模糊过滤 │
│ word_before_cursor = "" (光标紧跟 --json 后) │
│ "" in k.lower() 对任意 k 都成立, 但 current_suggestions={}│
│ → suggestions = {} │
│ → get_completions: len(commands)<=0 → return │
│ → 补全菜单为空 (因为唯一 flag 已用掉) │
└──────────────────────────────────────────────────────────┘
对比: 输入 "memory list modules " (末尾空格, 无 --json)
Step 3 同样进入 BuildFlags, 但 flags=[]
→ current_suggestions = {"--json": ''}
Step 4: word_before_cursor="" → "--json".lower() 含 "" → 命中
→ suggestions = {"--json": ''}
→ yield Completion("--json", ..., display_meta=None)边界情况说明:
- 光标前词为空字符串:
document.get_word_before_cursor()在光标紧跟空格时返回"",而"" in k.lower()对任意字符串都为True(completer.py:94)。所以"输入到子命令后按空格再 Tab"会列出该层全部候选——这是符合预期的"列出所有可能"行为。 meta丢失容忍:get_completions用type(extra) is dict and 'meta' in extra双重判断(completer.py:129)。当current_suggestions来自dynamic()返回的{flag: ''}字典时,值是空串而非 dict,此时meta=None,补全菜单只显示 flag 名不显示说明。!前缀短路:get_completions在find_completions之前就检查document.text.startswith('!')(completer.py:113),直接return不进入命令树遍历。OS 命令的补全交给 shell 层(实际 objection 不提供 shell 补全,仅放行)。
