教你编写控制台程序
前言¶
本文章所有示例代码均在 GitHub。
建议阅读本文后可以扩展性的阅读Term::CLI模块提供的Tutorial。
无论是出于何目的,我们对一个程序运维到一定程度后,都会想要为其编写一个控制台程序,以便于我们更好的管理它。本文将教你如何使用 Perl 编写一个控制台程序。
对于编写控制台第一件事就是要确定是否有可用的开源项目可以使用或者参考,这样我们往往都可以达到事半功倍的效果,并且也可以提升个人的编程能力。在 Perl 中,我们可以使用 Term::CLI 模块来编写控制台程序。
安装 Term::CLI¶
编写控制台程序¶
在安装完 Term::CLI 后,我们就可以开始编写控制台程序了。首先,我们需要创建一个文件夹,用于存放我们的控制台程序,然后在该文件夹下创建一个 myapp.pl
文件,用于存放我们的控制台程序的主要逻辑。
基本框架¶
在 myapp.pl
中写入以下代码,创建一个app的控制台:
- 控制台的提示词为
app
- 在控制台中输入
help
时,会输出help
。 - 在控制台中输入
exit
时,会退出控制台。
#!/bin/env perl
use strict;
use warnings;
use Term::CLI;
my $cli = Term::CLI->new(
name => 'app',
prompt => 'app> ',
commands => [
Term::CLI::Command->new(
name => 'exit',
callback => sub { exit 0 },
),
Term::CLI::Command->new(
name => 'help',
callback => sub {
my ($self, %args) = @_;
print "help\n";
return (%args, status => 0);
},
),
],
);
# 跳过空行和注释行
while ( my $input = $cli->readline(skip => qr/^\s*(?:#.*)?$/) )
{
$cli->execute_line($input);
}
Term::CLI->new
:创建一个控制台对象name
:名称;一些默认的文件名会使用该名称prompt
:控制台的提示符command
:控制台的命令Term::CLI::Command->new
:创建一个命令对象name
:命令的名称callback
:命令的回调函数;当用户输入命令时,会调用该回调函数my ($self, %args) = @_;
:获取命令的参数return (%args, status => 0);
:返回命令的参数,其中status
为命令的状态码。如果异常应该返回-1
,正常返回0
。注意:这里是一种固定返回格式,下文会对此展开说明,这里先不展开。
运行上述程序,将会得到以下结果:
app> # 首次登陆控制台,会显示提示符
app> # 按 <Tab> 键,会显示所有的命令
exit help
app> help # 输入help,打印help
help
app> exit # 输入exit,退出控制台
记录输入的历史记录¶
记录输入的历史记录是一个很有用的功能,它可以让我们在下次使用控制台时,可以通过上下键来选择历史记录中的命令。在 Term::CLI 中,我们可以通过cleanup
、$cli->read_history
和$cli->write_history
来实现该功能。
#省略...
my $cli = Term::CLI->new(
name => 'app',
prompt => 'app> ',
cleanup => sub {
my ($self) = @_; # 因为是回调函数,所以第一个参数为控制台对象
# 将历史记录写到文件 `~/.app_history` 中
# 如果写入失败,会将错误信息写入到 `$self->error` 中
$self->write_history
or warn "cannot write history: " . $self->error . "\n";
},
commands => [
# 省略...
],
);
$cli->read_history; # 从文件 `~/.app_history` 读取历史记录
# 跳过空行和注释行
# while ( my $input = $cli->readline(skip => qr/^\s*(?:#.*)?$/) )
# 省略...
给命令添加说明¶
在使用命令行的时候,我们希望能够通过help
命令来查看命令的说明,这样我们就可以更好的使用命令行。在 Term::CLI 中,我们可以通过Term::CLI::Command->new
的description
、summary
和usage
参数来设置说明,并通过完善help
命令来实现该功能。
# 省略...
# 创建一个 `help` 命令对象;后面可以被复用到每个命令中
my $cmd_help = Term::CLI::Command->new(
name => 'help',
description => '查看帮助信息',
summary => '查看帮助信息',
usage => 'help [ command ]',
callback => \&cb_help, # 设置命令的回调函数为 `cb_help`
);
my $cli = Term::CLI->new(
# 省略...
commands => [
Term::CLI::Command->new(
name => 'exit',
description => '退出控制台',
summary => '退出控制台',
usage => 'exit',
callback => sub { exit 0 },
),
$cmd_help, # 引用 `help` 命令对象
],
);
sub cb_help
{
my ($self, %args) = @_;
my $line = $args{command_line}; # 提取命令行
#
# 概述:提取前一个命令对象(后续称为父命令):当我们输入`help`时,其实会默认加入一个根对象,
# 根对象名称就是控制台的名称,即`app`,即`app help`。因此这里提取的是`app`命令对象。
#
# 详述:
# 通过 `command_path` 可以获取到命令对象的路径,比如这里的路径为:`app help`,
# 因此,`$args{command_path}->[-2]` 意思是倒数第二个命令对象,即 `app` 命令对象,
# 同理,`$args{command_path}->[-1]` 意思是倒数第一个命令对象,即 `help` 命令对象。
my $pcmd = $args{command_path}->[-2];
my ($help_cmd) = ($line =~ /help (\w+)$/); # 提取 `help` 之后的命令
if (defined $help_cmd)
{
#
# 如果要查看指定命令的使用说明,则打印该命令的详细说明和使用方法,比如:help help
# 效果类似于:
# app> help help
# ===== 详细说明 =====
# 查看帮助信息
#
# ===== 使用方法 =====
# help [ command ]
#
my $cmd = $pcmd->find_command($help_cmd); # 查找命令对象
$cmd || return (%args, status => -1, error => $pcmd->error); # 如果命令不存在,则返回错误
print "===== 详细说明 =====\n";
print $cmd->description . "\n\n";
print "===== 使用方法 =====\n";
print $cmd->usage . "\n\n";
}
else
{
#
# 如果输入了 `help` 命令,但是没有输入命令名称,则打印所有属于父命令的子命令的概述
# 效果类似于:
# app> help
# exit 退出控制台
# help 查看帮助信息
foreach my $name ($pcmd->command_names)
{
my $cmd = $pcmd->find_command($name);
my $summary = $cmd->summary // "unknwon";
printf "%-15s\%s\n", $name, $summary;
}
}
return (%args, status => 0);
}
加上上述代码后,执行效果如下:
app> help
exit 退出控制台
help 查看帮助信息
app> help help
===== 详细说明 =====
查看帮助信息
===== 使用方法 =====
help [ command ]
app>
给命令定义参数¶
参数分为可选参数和必选参数,可选参数可以通过-
来指定,比如ls -l
,而必选参数则不需要-
,比如ls /path/to/dest
。
- 必选参数:通过
Term::CLI::Command->new
的arguments
参数来实现该功能。 - 可选参数:通过
Term::CLI::Command->new
的options
参数来实现该功能。
假设我们现在想添加一个ls [OPTIONS] <PATH>
命令,该命令可以接收-l
可选参数,如果输入了-l
参数,则会打印出<PATH>
路径下的所有文件的详细信息,否则只打印文件名。
# 省略...
my $cli = Term::CLI->new(
# 省略...
commands => [
Term::CLI::Command->new(
name => 'exit',
description => '退出控制台',
summary => '退出控制台',
usage => 'exit',
callback => sub { exit 0 },
),
$cmd_help, # 引用 `help` 命令对象
Term::CLI::Command->new(
name => 'ls',
description => '查看指定路径下的文件',
summary => '查看指定路径下的文件',
usage => "ls [OPTIONS] <PATH>\n".
"\n".
"PATH: 要查看的路径\n".
"\n".
"OPTIONS:\n".
" -l 显示文件详细信息",
arguments => [
Term::CLI::Argument::String->new(
name => 'path',
min_length => 1,
max_length => 128,
),
],
options => [ "l" ],
callback => \&cb_ls,
)
],
);
sub cb_ls
{
my ($self, %args) = @_;
return (%args) if ($args{status} < 0);
my @arguments = @{$args{arguments}}; # 提取命令行参数
my %options = %{$args{options}}; # 提取命令行选项
my $path = $arguments[0];
my @files;
if (-d $path)
{
opendir(my $dh, $path)
|| return (%args, status => -1, error => "无法打开目录:$!");
@files = readdir($dh);
closedir($dh);
}
elsif (-e $path)
{
push @files, $path;
}
else
{
return (%args, status => -1, error => "无效的路径:$path")
}
unless(exists $options{l})
{
# 如果没有输入 `-l` 参数,则只打印文件名
foreach my $file (@files)
{
next if ($file eq '.' || $file eq '..');
print "$file\n";
}
}
else
{
# 如果输入了 `-l` 参数,则打印文件详细信息
foreach my $file (@files)
{
next if ($file eq '.' || $file eq '..');
my $filepath = File::Spec->catfile($path, $file);
my @stat = stat($filepath);
printf "%-20s %10d %s\n",
$file, $stat[7], scalar localtime($stat[9]);
}
}
return (%args, status => 0);
}
# 省略...
执行上述代码效果与下面相似。
没有输入-l
和<PATH>
参数:
输入了-l
参数,但是没有输入<PATH>
参数:
app> ls -l .
.devcontainer 160 Fri Jun 30 09:53:02 2023
.git 480 Fri Jun 30 10:03:14 2023
LICENSE 1063 Fri Jun 30 09:53:02 2023
README-zh.md 244 Fri Jun 30 09:57:39 2023
README.md 363 Fri Jun 30 10:00:52 2023
myapp-zh.pl 5417 Fri Jun 30 14:23:17 2023
myapp.pl 0 Fri Jun 30 14:31:58 2023
查看<PATH>
路径下的文件:
app> ls -l /
sys 0 Fri Jun 30 09:09:30 2023
lib64 4096 Mon Jun 12 00:00:00 2023
lib 4096 Tue Jun 13 03:31:02 2023
mnt 4096 Mon Jun 12 00:00:00 2023
etc 4096 Fri Jun 30 14:54:53 2023
dev 340 Fri Jun 30 09:09:30 2023
proc 0 Fri Jun 30 09:09:30 2023
var 4096 Tue Jun 27 13:54:05 2023
home 4096 Tue Jun 27 13:53:25 2023
opt 4096 Mon Jun 12 00:00:00 2023
media 4096 Mon Jun 12 00:00:00 2023
boot 4096 Sun Apr 2 11:55:00 2023
sbin 4096 Tue Jun 27 13:52:39 2023
usr 4096 Mon Jun 12 00:00:00 2023
srv 4096 Mon Jun 12 00:00:00 2023
bin 4096 Tue Jun 27 13:53:58 2023
root 4096 Tue Jun 27 13:54:01 2023
run 4096 Mon Jun 12 00:00:00 2023
tmp 4096 Fri Jun 30 15:04:00 2023
vscode 4096 Fri Apr 29 09:53:23 2022
workspaces 4096 Tue Jun 27 13:54:05 2023
.dockerenv 0 Tue Jun 27 13:54:04 2023
自定义补全内容¶
补全是一个很有用的功能,可以让用户在输入命令时,通过Tab
键来自动补全命令名称、参数、选项等。
比如在前文的ls
命令的<PATH>
参数,在我们没有开发补全功能前,它只是一个单纯的字符串参数,因此无法补全路径。这样在使用上将会特别难受。
我们期望用户输入了ls /bi
,然后按下Tab
键,则会自动补全为ls /bin/
,如果按下两次Tab
键,则会列出/bin/
目录下的所有文件。
为了实现上述需求,我们设计了3个步骤:
- 实现一个
Term::CLI::Command::Argument::String
的子类,叫做MyCLI::Argument
用于支持自定义补全功能; - 将
<PATH>
参数的类型从Term::CLI::Argument::String
改为`MyCLI::Argument; - 定义补全方法
complete_path
,用于补全路径。
“MyCLI::Argument.pm”实现如下:
package MyCLI::Argument;
use strict;
use warnings;
use Term::CLI::Argument::String;
# 为了方便,我们继承了 Term::CLI::Argument::String
our @ISA = qw(Term::CLI::Argument::String);
# 重写 `new` 方法
sub new
{
my ($class, %args) = @_;
my $self = $class->SUPER::new(%args);
$self->{complete} = $args{complete} if exists $args{complete};
bless $self, $class;
return $self;
}
# 重写 `complete` 方法
sub complete
{
my ($self, $text, $state) = @_;
# 如果用户没有指定 `complete` 方法,我们就调用父类的 `complete` 方法
return $self->SUPER::complete($text, $state) unless $self->{complete};
# 如果用户指定了 `complete` 方法,我们就调用它
return $self->{complete}->($self, $text, $state);
}
1;
在myapp.pl
中改动如下:
# 省略...
# 添加Perl搜索路径:当前工程所在目录
use FindBin qw($Bin);
use lib "$Bin";
use Term::CLI 0.058002;
# 导入自定义的命令行参数类
use MyCLI::Argument;
use File::Glob qw(bsd_glob);
# 省略...
my $cli = Term::CLI->new(
# 省略...
commands => [
Term::CLI::Command->new(
name => 'ls',
description => '查看指定路径下的文件',
summary => '查看指定路径下的文件',
usage => "ls [OPTIONS] <PATH>\n".
"\n".
"PATH: 要查看的路径;默认为当前路径,即`.`\n".
"\n".
"OPTIONS:\n".
" -l 显示文件详细信息",
arguments => [
# 参数类型从 `Term::CLI::Argument::String` 改为 `MyCLI::Argument`
MyCLI::Argument->new(
name => 'path',
min_length => 1,
max_length => 128,
complete => \&complete_path, # 设置补全方法
),
],
options => [ "l" ],
callback => \&cb_ls,
)
],
);
# 省略...
# 函数实现引用了:<https://metacpan.org/dist/Term-CLI/source/lib/Term/CLI/Argument/Filename.pm>
sub complete_path
{
my ($self, $text, $state) = @_;
my @list = bsd_glob("$text*");
return if @list == 0;
if (@list == 1)
{
if (-d $list[0])
{
# Dumb trick to get readline to expand a directory
# with a trailing "/", but *not* add a space.
# Simulates the way GNU readline does it.
return ("$list[0]/", "$list[0]//");
}
return @list;
}
# Add filetype suffixes if there is more than one possible completion.
foreach (@list) {
lstat;
if ( -l _ ) { $_ .= q{@}; next } # symbolic link
if ( -d _ ) { $_ .= q{/}; next } # directory
if ( -c _ ) { $_ .= q{%}; next } # character special
if ( -b _ ) { $_ .= q{#}; next } # block special
if ( -S _ ) { $_ .= q{=}; next } # socket
if ( -p _ ) { $_ .= q{=}; next } # fifo
if ( -x _ ) { $_ .= q{*}; next } # executable
}
return @list;
}
在上述代码中,为了方便程序执行,我们将程序所在的目录也添加进了搜索路径,这样就可以直接执行myapp.pl
了:
进入控制台后,效果如下:
perl -I . 04-自定义补全内容.pl
app> ls -l /bin/
/bin/bash* /bin/date* /bin/kill* /bin/readlink* /bin/uncompress*
/bin/bunzip2* /bin/dd* /bin/ln* /bin/rm* /bin/vdir*
/bin/bzcat* /bin/df* /bin/login* /bin/rmdir* /bin/wdctl*
/bin/bzcmp@ /bin/dir* /bin/ls* /bin/rnano@ /bin/ypdomainname@
/bin/bzdiff* /bin/dmesg* /bin/lsblk* /bin/run-parts* /bin/zcat*
/bin/bzegrep@ /bin/dnsdomainname@ /bin/mkdir* /bin/rzsh@ /bin/zcmp*
/bin/bzexe* /bin/domainname@ /bin/mknod* /bin/sed* /bin/zdiff*
/bin/bzfgrep@ /bin/echo* /bin/mktemp* /bin/sh@ /bin/zegrep*
/bin/bzgrep* /bin/egrep* /bin/more* /bin/sleep* /bin/zfgrep*
/bin/bzip2* /bin/false* /bin/mount* /bin/ss* /bin/zforce*
/bin/bzip2recover* /bin/fgrep* /bin/mountpoint* /bin/stty* /bin/zgrep*
/bin/bzless@ /bin/findmnt* /bin/mv* /bin/su* /bin/zless*
/bin/bzmore* /bin/fuser* /bin/nano* /bin/sync* /bin/zmore*
/bin/cat* /bin/grep* /bin/netstat* /bin/tar* /bin/znew*
/bin/chgrp* /bin/gunzip* /bin/nisdomainname@ /bin/tempfile* /bin/zsh*
/bin/chmod* /bin/gzexe* /bin/pidof@ /bin/touch* /bin/zsh5*
/bin/chown* /bin/gzip* /bin/ps* /bin/true*
/bin/cp* /bin/hostname* /bin/pwd* /bin/umount*
/bin/dash* /bin/ip* /bin/rbash@ /bin/uname*
给控制台工具本身添加命令行参数¶
作为一个合格的控制台工具,我们还需要给它本身添加命令行参数,比如--help
、--version
等。接着我们就增加这些命令行参数。
# 省略...
use Term::CLI 0.058002;
# 导入自定义的命令行参数类
use MyCLI::Argument;
use File::Glob qw(bsd_glob);
use Getopt::Long;
my $conf_help;
my $conf_version;
GetOptions(
'help|h' => \$conf_help,
'version|v' => \$conf_version,
);
if ($conf_help)
{
print <<EOF;
Usage: $0 [OPTIONS]
OPTIONS:
-h, --help 显示帮助信息
-v, --version 显示版本信息
EOF
exit 0;
}
if ($conf_version)
{
print "v1.0.0\n";
exit 0;
}
# 省略...
上述代码中使用Getopt::Long
去解析命令行参数,然后根据参数的值来执行不同的逻辑。
运行效果如下所示:
vscode ➜ /workspaces/编写控制台程序 (main ✗) $ perl 05-给控制台工具本身添加命令行参数.pl -h
Usage: 05-给控制台工具本身添加命令行参数.pl [OPTIONS]
OPTIONS:
-h, --help 显示帮助信息
-v, --version 显示版本信息
vscode ➜ /workspaces/编写控制台程序 (main ✗) $ perl 05-给控制台工具本身添加命令行参数.pl -v
v1.0.0
添加子命令¶
为了演示子命令,我们新增一个新的命令show
,它拥有两个子命令分别为memory
和disk
,分别用于显示内存和磁盘的信息。
要实现子命令只需要在Term::CLI::Command->new
的commands
参数中引用子命令对应的Term::CLI::Command
对象即可。
# 省略...
my $cli = Term::CLI->new(
# 省略...
commands => [
# 省略...
# 新增命令`show`,它拥有两个子命令分别为`memory`和`disk`,分别用于显示内存和磁盘的信息。
Term::CLI::Command->new(
name => 'show',
description => '显示系统信息',
summary => '显示系统信息',
usage => "show [OPTIONS] <SUBCOMMAND>\n".
"\n".
"SUBCOMMAND:\n".
" memory 显示内存信息\n".
" cpu 显示CPU信息",
commands => [
Term::CLI::Command->new(
name => 'memory',
description => '显示内存信息',
summary => '显示内存信息',
usage => 'show memory',
callback => \&cb_show_memory,
),
Term::CLI::Command->new(
name => 'disk',
description => '显示磁盘信息',
summary => '显示磁盘信息',
usage => 'show disk',
callback => \&cb_show_disk,
),
]
)
],
);
# 省略...
sub cb_show_memory
{
my ($self, %args) = @_;
return (%args) if ($args{status} < 0);
my $memory = `free -h`;
print $memory;
return (%args, status => 0);
}
sub cb_show_disk
{
my ($self, %args) = @_;
return (%args) if ($args{status} < 0);
my $disk = `df -h`;
print $disk;
return (%args, status => 0);
}
执行上述程序后,效果如下:
vscode ➜ /workspaces/编写控制台程序 (main ✗) $ perl 06-添加子命令.pl
app> help
exit 退出控制台
help 查看帮助信息
ls 查看指定路径下的文件
show 显示系统信息
app> show
disk memory
app> show memory
total used free shared buff/cache available
Mem: 7.8Gi 1.4Gi 606Mi 305Mi 5.8Gi 5.8Gi
Swap: 1.0Gi 33Mi 990Mi
app> show disk
Filesystem Size Used Avail Use% Mounted on
overlay 95G 81G 8.6G 91% /
tmpfs 64M 0 64M 0% /dev
shm 64M 0 64M 0% /dev/shm
/dev/vda1 95G 81G 8.6G 91% /vscode
grpcfuse 234G 220G 14G 95% /workspaces/编写控制台程序
tmpfs 3.9G 0 3.9G 0% /proc/acpi
tmpfs 3.9G 0 3.9G 0% /sys/firmware
app>
有时候我们还会有一种特殊的需求,比如:我们希望可以进入到show
节点里面,然后单独的执行子命令 memory
等,而不是敲完整的命令 show memory
。这在命令比较长的时候会比较方便。
为此我们需要介绍Term::CLI::Command->new
的require_sub_command
参数,它的值为1
时表示需要子命令,为0
时表示不需要子命令,默认是为1
。
因此此时在上述程序中执行show
会报错,如下所示:
此时我们来修改一下show
命令的Term::CLI::Command->new
,将require_sub_command
的值改为0
即可,并且实现一个cb_show
的回调函数,用于处理show
命令的逻辑,它将会进入到show
节点里面,然后在该节点中可以单独的执行子命令 memory
等,而不是敲完整的命令 show memory
。
# 省略...
Term::CLI::Command->new(
name => 'show',
description => '显示系统信息',
summary => '显示系统信息',
usage => "show [OPTIONS] <SUBCOMMAND>\n".
"\n".
"SUBCOMMAND:\n".
" memory 显示内存信息\n".
" cpu 显示CPU信息",
callback => \&cb_show,
require_sub_command => 0, # 设置为0,表示不强制要求子命令
commands => [
Term::CLI::Command->new(
name => 'memory',
description => '显示内存信息',
summary => '显示内存信息',
usage => 'show memory',
callback => \&cb_show_memory,
),
Term::CLI::Command->new(
name => 'disk',
description => '显示磁盘信息',
summary => '显示磁盘信息',
usage => 'show disk',
callback => \&cb_show_disk,
),
]
)
# 省略...
sub cb_show
{
my ($self, %args) = @_;
# 忽略非直接执行`show`命令的情况
unless ($args{command_line} =~ /^show\s*$/)
{
return %args;
}
# 进入 show 节点
while (my $input = $self->readline(prompt => "show> ", skip => qr/^\s*(?:#.*)?$/))
{
my (%args) = $self->execute_line($input);
if ($args{status} != 0)
{
print "ERROR: " . $args{error} . "\n";
}
}
return %args;
}
详解Term::CLI::Command¶
可参阅手册:Term::CLI::Command
用new
方法创建一个命令行对象,然后作为Term::CLI
的commands
参数传入即可。Term::CLI::Comman
本身也可引用Term::CLI::Command
,这样就可以实现子命令的功能。
new
方法中有以下参数:
name
:命令行名称description
:命令行描述summary
:命令行摘要usage
:命令行使用说明arguments
:命令行参数options
:命令行可选参数callback
: 命令行回调函数require_sub_command
: 是否需要子命令;有时候我们有一些命令虽然有子命令,但是并不要求用户在输入的时候要携带子命令,比如git
命令,用户可以输入git
,也可以输入git commit
,这里的commit
就是子命令,但是git
命令并不要求用户输入子命令,因此这里的require_sub_command
就是用来控制这种情况的,当require_sub_command
为0
时,表示子命令不是必须的。commands
: 子命令
那么命令树是如何调用的呢?理解命令树的工作原理对于我们编写命令行程序是非常有帮助的。
假设我们执行一条命令 show disk --verbose /etc
,其命令树的解析流程如下:
(cli-root)
|
+--> Command 'show'
|
+--> Command 'disk'
|
+--> Option '--verbose'
|
+--> Argument '/etc'
这时它将按以下顺序调用:
- 调用
disk
的callback
- 调用
show
的callback
- 调用
Term::CLI
对象
且前者的返回值会传递给后者,后者要以此作判断。也如上示例所示,一个命令callback
的入参%args
实际上是前一个命令callback
的返回值。而对于首个调用的Term::CLI
对象,它的%args
是由Term::CLI
对象的execute_line
方法传递过来的,它的%args
中的command_line
就是用户输入的命令行,command_path
就是命令树的路径,arguments
就是命令行中的参数,options
就是命令行中的可选参数,status
就是命令行的状态,error
就是命令行的错误信息。
详解入参¶
callback
调用方式如下所示:
OBJ->callback->(OBJ,
status => Int,
error => Str,
options => HashRef,
arguments => ArrayRef[Value],
command_line => Str,
command_path => ArrayRef[InstanceOf['Term::CLI::Command']],
);
OBJ
:Term::CLI::Command
对象
status
:表示到目前为止解析/执行的状态。它有以下含义:
-
< 0 负的状态值表示解析错误。这是一个信号,表明不应该采取任何行动,但需要进行一些错误处理。实际的解析错误可以在error键下找到。在这种情况下,链中的某个回调函数(例如Term::CLI对象上的回调函数)通常会将错误打印到STDERR。
-
0 命令行解析为有效,并且到目前为止执行成功。
-
> 0 在执行动作过程中出现了一些错误。回调函数需要自行设置此状态。
error
:在状态为负的情况下,这里将包含解析错误。在所有其他情况下,它可能包含有用的信息,也可能不包含。
options
:引用一个包含所有命令行选项的哈希。与Getopt::Long设置的选项哈希兼容。
arguments
:引用一个包含命令所有参数的数组,每个值都是标量值。
unprocessed
:引用一个数组,该数组包含命令行中尚未解析为参数或子命令的所有单词。在解析错误的情况下,这通常包含元素,否则应为空。
command_line
:传递给Term::CLI::execute
方法的完整命令行。
command_path
:引用一个包含“解析树”的数组,即表示到达此点的命令和子命令的对象引用列表:
- 列表中的第一个项目始终是顶级的Term::CLI对象。
processed
:更详细的“解析树”:表示到达此点的命令行上所有元素的哈希列表,不包括Term::CLI对象本身。
[
{
element => InstanceOf['Term::CLI::Command'],
value => String
},
{
element => InstanceOf['Term::CLI::Argument'],
value => String
},
{
element => InstanceOf['Term::CLI::Argument'],
value => String
},
...
]
注意
command_path
,arguments
和options
应被视为只读。
即使在错误的情况下,也会调用回调,因此在进行任何操作之前,您应 始终检查状态。
详解返回值¶
从调用树的原理(前文)中我们了解到,前者调用的结果会作为后者的入参,这样我们才能根据前者的命令执行结果来判断后者是否需要执行,或者怎么做。
通常每个命令执行完后都应当把%args
返回回去,以传递给后者,而命令需要关注的是%args
中的status
和error
。
习惯性的写法是:
这样可以保证%args
中的其他值不会被覆盖,只设置了我们需要的status
和error
。