命令行式指令
基于argparse,PepperBot将经典的命令行指令,移植到了机器人的世界中,使得机器人的指令更加灵活、易用
而且,PepperBot支持将任意的消息片段作为参数,而不仅仅局限于纯文本
比如,我们可以要求用户提供一张图片,可以直接这样使用
async def initial(self, sender: CommandSender, image_result: Image = CLIArgument()):
    await sender.send_message(
        Text(f"收到了你发送的图片"),
        image_result,  # 可以直接获取到用户发送的图片
    )
参数定义
Positional(位置) 参数、Optional 参数
什么是Positional(位置) 参数?
比如git clone <repo>,repo就是Positional(位置) 参数
什么是Optional 参数?
比如git clone <repo> --branch <branch>,branch就是Optional 参数
定义参数
在PepperBot中,我们通过CLIArgument和CLIOption来定义Positional(位置) 参数和Optional 参数
from pepperbot.store.command import CLIArgument, CLIOption
class GitCommand:
    async def initial(self, sender: CommandSender, repo:str= CLIArgument()):
        pass
    @sub_command()
    async def clone(self, sender: CommandSender, repo:str= CLIArgument(), branch:str= CLIOption()):
        pass
参数的类型
在开头,我们展示了用图片(Image)作为类型的例子
事实上,定义类型就是这么简单,只要提供一个类型注解即可
class GitCommand:
    async def initial(self, sender: CommandSender, repo:str= CLIArgument()):
        pass
具体有哪些可用的类型?可以参考消息片段
除此之外,还可以设置bool、int、float等类型
repo:bool= CLIArgument()
repo:int= CLIArgument()
repo:float= CLIArgument()
当为这三种类型时,PepperBot会自动将用户输入的参数转换为对应的类型
也可以设置Any类型,这样,用户输入的参数就会原样返回
class TestCommand:
    async def initial(self, sender: CommandSender, any_type:Any= CLIArgument()):
        if type(any_type) in (str, int, float,bool):
            await sender.send_message(Text(f"你输入的参数为 {any_type}"))
        else:
            await sender.send_message(
                Text(f"你输入的参数为 "),
                any_type, # 此时是消息片段,可以直接发送
            )
        return self.initial
参数的数量
可以通过提供List类型,来标注该参数可以接受多个参数(至少 1 个,如果是必选参数)
class TestCommand:
    async def initial(self, sender: CommandSender, list_arguments:List[str]= CLIArgument()):
        result = "_".join(list_arguments)
        await sender.send_message(Text(f"你输入的参数为 {list_arguments}"))
        return self.initial
可选、必选、默认值
对于一个命令行指令,一般来说,Positional(位置) 参数都是必选的,而Optional 参数都是可选的
在PepperBot中,你可以实现可选的Positional(位置) 参数,也可以实现必选的Optional 参数
但是一般还是建议按照惯例来
那么,具体如何实现呢?
通过Optional类型注解
# 必选的Positional(位置) 参数
repo:str= CLIArgument()
# 可选的Positional(位置) 参数
repo:Optional[str]= CLIArgument()
# 必选的Optional 参数
branch:str= CLIOption()
# 可选的Optional 参数
branch:Optional[str]= CLIOption()
这个可以和 List 结合使用
repo :Optional[List[str]]= CLIArgument()
如果设置了Optional,那么就可以顺便为参数设置默认值
class GitCommand:
    async def clone(self, branch:Optional[str]= CLIOption(default="master")):
        pass
默认值可以是一个函数,可以返回动态的默认值
def random_branch():
    return random.choice(["master", "dev", "test"])
class GitCommand:
    async def clone(self, branch:Optional[str]= CLIOption(default=random_branch)):
        pass
Optional 参数的缩写
命令行指令中,经常可以看到这样的缩写
git clone -b master
这里的-b就是branch的缩写
那么,如何实现这样的缩写呢?
class GitCommand:
    async def clone(self, branch:Optional[str]= CLIOption(short_name="b")):
        pass
用户输入参数的格式
参数格式有两点要求
- 参数数量要和定义了的命令行参数的数量一致,所以必选的参数必须要有,可选的参数可以没有
- 参数类型要和对应参数一致
比如这样的定义
class TestCommand:
    async def choose_game(self, game: str = CLIArgument(), sender: CommandSender, npc : int = CLIArgument()):
        sender.send_message(Text(f"你选择的是 {game} 的 {npc},需要查询他的什么装备呢?稀有度为何?"))
我们定义了两个参数,第一个参数为 game,字符串类型,第二个参数为 npc,是 int 类型
可以看到,我们在统计参数的时候,跳过了sender这样的保留参数,只统计了默认值为CLIArgument()(或者CLIOption)的参数
用户输入参数时的顺序,应该和定义的顺序一致,比如,第一个参数因为game,第二个参数为npc
如果用户输入的参数的类型,与我们定义的不一致,比如
这里,npc 应该为 int 类型,而我们提供的是字符串"某人物",那么,当 PepperBot 试图将字符串"某人物"转换为 int 类型时,自然会失败
在 PepperBot 中,当用户的输入不符合要求时,会自动发送格式提示,比如
子指令
什么是子指令(命令)?
比如最常见的 git,直接git,此时,git就是根指令(root command,或者理解为,最上层/最顶层的指令)
而git commit、git clone中的commit、clone就是子指令(sub command)
这是 2 层嵌套的情况,如果 3 层、4 层又会是什么样的情况呢?
git commit level3
git commit level3 level4
如何实现子指令
通过sub_command装饰器
from pepperbot.extensions.command import sub_command
@as_command()
class GitCommand:
    async def initial(self, sender: CommandSender):
        pass
    @sub_command() # 或者 @sub_command(initial)
    async def commit(self):
        pass
    @sub_command(commit)
    async def level3(self):
        pass
通过这样的定义,我们实现了 3 层嵌套的子指令
- git
    - commit
        - level3
子指令的别名
有时候,我们希望实际解析时,使用的指令名和定义的 method 不一致,可以这样实现
class GitCommand:
    async def initial(self, sender: CommandSender):
        pass
    @sub_command(name="commit")
    async def another_name(self):
        pass
子指令的调度逻辑
对于嵌套指令,PepperBot只会执行最底层的指令,比如
@as_command()
class GitCommand:
    async def initial(self, sender: CommandSender):
        await sender.send_message(Text("git"))
        return self.initial
    @sub_command() # 或者 @sub_command(initial)
    async def commit(self, sender: CommandSender):
        await sender.send_message(Text("commit"))
        return self.initial
    @sub_command(commit)
    async def level3(self, sender: CommandSender):
        await sender.send_message(Text("level3"))
        return self.initial
不能返回子指令
在指令的状态,我们提到,可以返回一个method,作为下次用户交互时被调用的方法
PepperBot不允许返回子指令,只允许返回最顶层的指令(root command)
比如上方的例子,commit和level3都是子指令,不允许返回,只允许返回initial
在子指令中,获取父指令的参数
一般来说,对于每一级指令,都可以实现可选参数,一般不实现位置参数
git --x xxx commit --y yyy any --z zzz
比如现在有三级指令,git、commit、any,各自都有自己的位置参数--x、--y、--z
假设我这样定义了指令
@as_command()
class GitCommand:
    async def initial(self, sender: CommandSender, x: Optional[str] = CLIOption()):
        pass
    @sub_command()
    async def commit(self, sender: CommandSender, y: Optional[str] = CLIOption()):
        pass
    @sub_command(commit)
    async def any(self, sender: CommandSender, z: Optional[str] = CLIOption(), context: Dict):
        pass
PepperBot会将父指令的参数,都注入到context中的cli_arguments中,比如
{
    "cli_arguments": {
        "x": "xxx",
        "y": "yyy",
    }
}
帮助信息
针对“根指令”的帮助信息
针对“子指令”的帮助信息
不要太“聪明”
不要同时实现可选的位置参数和子指令
为什么?因为argparse中,子指令的本质,就是可选的位置参数
假设我们实现了一个可选的位置参数,同时实现了一个子指令,也就是说,我们现在有两个可选的位置参数了
class GitCommand:
    async def initial(self, x: Optional[str] = CLIArgument()):
        pass
    @sub_command()
    async def commit(self):
        pass
这种情况,argparse无法处理,因为它无法判断,用户输入的参数,是属于哪个可选的位置参数的
如果我们非要实现这样的效果呢?可以用Option参数来模拟,argparse可以正常处理可选的Option参数
class GitCommand:
    async def initial(self, sender: CommandSender, x: Optional[str] = CLIOption()):
        pass
    @sub_command()
    async def commit(self, sender: CommandSender, y: Optional[str] = CLIOption()):
        pass
此时,我们既可以
也可以
事实上,常见的命令行指令,比如git,docker,都没有在父指令中,实现可选的位置参数,有实现,也是在最底层的指令中
主流的选择,就是只在父指令中,实现可选的Option参数
不建议在一个指令中,实现多个 root command
class MultiTopLevel:
    async def initial(self, initial_optional: Optional[str] = CLIOption()):
        return self.another
    @sub_command(initial)
    async def a(self, sender: CommandSender, context: Dict):
        parent_argument = context["cli_arguments"]["initial_optional"]
        await sender.send_message(Text(f"a {parent_argument}"))
        return self.another
    @sub_command(initial)
    async def b(self):
        return self.another
    async def another(self, another_optional: Optional[str] = CLIOption()):
        pass
    @sub_command(another)
    async def c(self, sender: CommandSender, context: Dict):
        parent_argument = context["cli_arguments"]["another_optional"]
        await sender.send_message(Text(f"c {parent_argument}"))
    @sub_command(another)
    async def d(self):
        pass
在上方的例子中,我们实现了两个root command,initial和another
并且,initial返回another
从模拟调用中,我们可以看到,第一次执行时,此时的root command是initial,所以我们可以调用a和b
第二次执行时,此时的root command是another,所以我们可以调用c和d
虽然确实可能有这样的使用场景,但是不建议在一个指令中,实现多个root command,因为这样会导致代码的可读性变差,而且有点烧脑
那么建议怎么用呢?
只实现一个有子指令的root command,其他 root command,不实现子指令,只利用命令行的参数解析能力,来自动解析参数
class MultiTopLevel:
    async def initial(self, sender: CommandSender, x: Optional[str] = CLIOption()):
        pass
    @sub_command(initial)
    async def a(self, sender: CommandSender):
        await sender.send_message(Text("a"))
    @sub_command(initial)
    async def b(self):
        pass
    async def another(self, sender: CommandSender, y: Optional[str] = CLIOption()):
        pass
如果想实现多个root command,可以拆分成多个指令
class Command1:
    async def initial(self, sender: CommandSender, x: Optional[str] = CLIOption()):
        pass
    @sub_command(initial)
    async def a(self, sender: CommandSender):
        await sender.send_message(Text("a"))
    @sub_command(initial)
    async def b(self):
        pass
class Command2:
    async def initial(self, sender: CommandSender, y: Optional[str] = CLIOption()):
        pass
    @sub_command(initial)
    async def c(self, sender: CommandSender):
        await sender.send_message(Text("c"))
    @sub_command(initial)
    async def d(self):
        pass