构建 CLI 时,argv、选项、环境变量、stdin、stdout 和 stderr 是否有可接受的约定?

构建 CLI 时,argv、选项、环境变量、stdin、stdout 和 stderr 是否有可接受的约定?

我倾向于谈论命令行应用程序是多么简单。通常,我会说“它们从标准输入读取并写入标准输出和标准错误,就是这样。”然后,我画了一张图,如下所示:

在此输入图像描述

但后来我开始认为这实际上并不是故事的结局。命令行应用程序从标准输入读取并写入标准错误和标准输出,以及...

  • 读取配置文件(从任意数量的位置,如/etc~.
  • 读取环境变量
  • 读取命令行开关
  • 读取命令行标志
  • 读取命令行参数
  • 设置退出状态

命令行应用程序最终看起来更像是:

在此输入图像描述

客观上来说,这比简单的 stdin、stdout 和 stderr 更复杂。

我整天使用命令行应用程序。随着时间的推移,我对它们形成了自己的看法和直觉:

  • 标准 - 通常是数据,而不是参数(除非我使用xargs
  • 标准输出 - 产品,最好是列式数据
  • 标准错误 - 用于在出现问题时记录错误或错误消息
  • 退出状态 - 神圣,0 表示成功,1 表示失败(或者其他类型的失败)
  • 参数 - 很重要,但阅读方式与标准不同
  • 标志 - 类似于参数,但不太重要
  • 开关 - 类似于标志,但要么打开要么关闭
  • 环境变量 - 有点像参数,但它们之间存在某种哲学差异
  • 配置文件 - 有点像环境变量,您可以控制开关或标志,但您可以将它们保留在版本控制中,并且通常使用它们来防止不得不使用开关和标志的令人沮丧的组合,或者向每个人展示您的点文件有多酷是

这些指南往往适用于我使用的各种命令行工具。不过,当谈到制作它们供其他人使用时,我希望有一个参考。

例如,以下是用 Ruby 编写的命令行应用程序:

#!/usr/bin/env ruby
# somecli

require 'optparse'
require 'yaml'

options = {}

etc_config = File.join('etc', 'somecli')
if File.exist? etc_config
  options.merge! YAML.load_file(etc_config)
end

home_config = File.join(ENV['HOME'], '.somecli')
if File.exist? home_config
  options.merge! YAML.load_file(home_config)
end

current_working_directory_config = '.somecli'
if File.exist? current_working_directory_config
  options.merge! YAML.load_file(current_working_directory_config)
end

OptionParser.new do |opts|
  opts.on("-s", "--[no-]switch") do |s|
    options[:switch] = s
  end
  opts.on("-a", "--[no-]another-switch") do |as|
    options[:'another-switch'] = as
  end
  opts.on("-y", "--[no-]yet-another-switch") do |yas|
    options[:'yet-another-switch'] = yas
  end
  opts.on("-y", "--[no-]even-yet-another-switch") do |eyas|
    options[:'even-yet-another-switch'] = eyas
  end
  opts.on("--flag FLAG") do |f|
    options[:flag] = f
  end
end.parse!

puts "ARGV=#{ARGV.inspect}"
puts "options=#{options.inspect}"
puts "ENV['cats']=#{ENV['cats'].inspect}"
unless STDIN.tty?
  puts "STDIN.read=#{STDIN.read.inspect}"
end
$stderr.puts "stderr: hello world!"
$stdout.puts "stdout: hello world!"
exit 0

运行时看起来像

echo -n foo bar baz | ./somecli -s -f flap jacks; echo $?
ARGV=["jacks"]
options={:"another-switch"=>true, :"even-yet-another-switch"=>true, :switch=>true, :flag=>"flap"}
ENV['cats']="flapjacks"
STDIN.read="foo bar baz"
stderr: hello world!
stdout: hello world!
0

我想在自述文件中提供一个更正式的文档的链接,该文档指导其输入和输出约定以及设计决策。

我知道POSIX存在用于决定系统上安装哪些常见实用程序,除了在构建命令行应用程序时我想知道两件事:

  1. 还有其他我错过的将输入输入到命令行程序的常见方法吗?
  2. 是否存在有关如何组织各种输入和输出的约定的权威文档?

答案1

POSIX.1/Single UNIX 规范中记录了一些命令行实用程序的一般准则。

第 12.2 节:实用程序语法指南

答案2

我认为除了常识之外没有任何约定。

至于输入输出方式,或者一般来说,沟通方法,您提到了一种方法,但只是部分方法。它正在读取和写入文件。 (您提到读取配置文件,或者一般来说,文件描述符.)

与文件类似的还有管道和套接字。例如。 TCP、UDP 或 Unix 套接字,这可以被认为是一种方式进程间通信(IPC)。其他类型的 IPC 包括:

  • 共享内存和映射内存
  • 信号
  • 信号量
  • D总线

相关内容