开发工具 开发一个命令行工具,一般要遵循那些传统?

dggy · 2021年02月02日 · 最后由 aline57963 回复于 2021年02月04日 · 641 次阅读

当然,不遵循传统,理论上是没任何问题的
但是遵循传统,能让其他人,更方便使用这个工具

比如,开发了一个叫 ppz 的工具

  • 要提供 man 页面
  • ppz --version 打印版本
  • ...

还有,各种参数,要遵循什么格式

有没有什么机构,指定了专门的规范,感谢指点

没有规范

想怎么搞就怎么搞。

跟一些命令行库的 README,一般就足够使用,我偏好这个库 https://github.com/jekyll/mercenary

要系统的了解命令行程序的惯例的话,有本书叫《Build Awesome Command-Line Applications in Ruby 2》

Rei 回复

感谢!

感谢 @Rei 的提示,发现了 OptionParser
感觉把 OptionParser 提供的功能用一下,就 ok 了

dggy 回复

mercenary 跟 OptionParser 语法近似,多了子命令的语法,不需要子命令的话确实 OptionParser 够用了。

Rei 回复

好的~

前几天看到了这个,也贴出来:https://github.com/erikhuda/thor

bindiry 回复

哈哈~刚好前阵子用 thor 做过一个小小的脚手架命令行,thor 那种基于 Class 的编写方式,体验挺不错。

翻译了一篇关于OptionParser 的文档,共勉。
原文链接

OptionParser

简介

OptionParser 是一个用于“解析命令行参数”的类。和 GetoptLong相比,它功能更丰富,使用起来更简单,并且是一种更具 Ruby 风格的解决方案。

译者注:GetoptLong 和 OptionParser 功能类似,但偏向 C 语言风格

特性
  • “参数说明”和“处理参数的代码”写在一起。(译者注:即“把代码和文档写一起”或“代码即文档”,听起来很神奇,下文有例子)
  • 它可以输出参数的使用说明,你不必单独维护使用说明。(译者注:因为,写一个程序时,往往要单独写一分文档,来介绍使用方法)
  • “可选”和“必须”的参数声明很优雅。
  • 参数自动转化成某个特定的类。(译者注:比如数字、比如日期)
  • 可以设置参数的合法值的集合。(译者注:比如某个参数的值只能是 1 或 2 或 3)

所有的这些特性将在下面的例子中演示。详参mack_switch

最小的例子
require 'optparse'

options = {}
OptionParser.new do |opts|
  opts.banner = "Usage: example.rb [options]"

  opts.on("-v", "--[no-]verbose", "Run verbosely") do |v|
    options[:verbose] = v
  end
end.parse!

p options
p ARGV
生成“帮助”文档

OptionParser可以自动生成“帮助”文档,来介绍你写的命令:

require 'optparse'

Options = Struct.new(:name)

class Parser
  def self.parse(options)
    args = Options.new("world")

    opt_parser = OptionParser.new do |opts|
      opts.banner = "Usage: example.rb [options]"

      opts.on("-nNAME", "--name=NAME", "Name to say hello to") do |n|
        args.name = n
      end

      opts.on("-h", "--help", "Prints this help") do
        puts opts
        exit
      end
    end

    opt_parser.parse!(options)
    return args
  end
end
options = Parser.parse %w[--help]

#=>
   # Usage: example.rb [options]
   #     -n, --name=NAME                  Name to say hello to
   #     -h, --help                       Prints this help

必须有值的参数

译者注: 有的参数不需要值,比如 --help,一般用来查看一个命令的使用方法 有的参数必须有值,比如登录 mysql,填写用户名:mysql -uUSERNAME

对于那些必须有值的参数,“参数说明字符串”里需要包含大写的“参数名”。如果使用一个“必须有值的参数”,但未传入值时,将会抛出一个异常。

require 'optparse'

options = {}
OptionParser.new do |parser|
  parser.on("-r", "--require LIBRARY",
            "Require the LIBRARY before executing your script") do |lib|
    puts "You required #{lib}!"
  end
end.parse!

使用:

$ ruby optparse-test.rb -r
optparse-test.rb:9:in `<main>': missing argument: -r (OptionParser::MissingArgument)
$ ruby optparse-test.rb -r my-library
You required my-library!
类型转化

OptionParser 可以把参数值转化成某个对象。
OptionParser 附带了一些开箱即用的类型转化,它们是:

  • Date - 所有 Date.parse 接受的东西
  • DateTime - 所有 DateTime.parse 接受的东西
  • Time - 所有 Time.httpdateTime.parse 接受的东西
  • URI - 所有 URI.parse 接受的东西
  • Shellwords - 所有 Shellwords.shellwords 接受的东西
  • String - 所有的非空字符串
  • Integer - 所有整数。将转化八进制。(比如:124、-3、040)
  • Float - 所有浮点型数。(比如:10、3.14、-100E+13)
  • Numeric - 所有整数、浮点型数、有理数。(比如:1、3.4、1/3)
  • DecimalInteger - 类似 Integer,但不支持八进制。
  • OctalInteger - 类似 Integer,但不支持十进制。
  • DecimalNumeric - 十进制整数或浮点型数。
  • TrueClass - 接受 truefalseyesno+-,默认值是 true
  • FalseClass - 跟 TrueClass 一样,但默认值是 false
  • Array - 被逗号分开的字符串。(比如:1,2,3)
  • Regexp - 正则表达式。也包括参数。

我们也可以添加自己的类型转化,详见下文。

使用内置的“转化”

作为一个例子,这里使用内置的 Time 转化。其他内置的转化,也是一样的。OptionParser 会把这个参数当作一个 Time 来解析。如果成功了,这个 Time 对象被传给“处理块”(handle block)。否则会抛出一个异常。

require 'optparse'
require 'optparse/time'
OptionParser.new do |parser|
  parser.on("-t", "--time [TIME]", Time, "Begin execution at given time") do |time|
    p time
  end
end.parse!

使用:

$ ruby optparse-test.rb  -t nonsense
... invalid argument: -t nonsense (OptionParser::InvalidArgument)
$ ruby optparse-test.rb  -t 10-11-12
2010-11-12 00:00:00 -0500
$ ruby optparse-test.rb  -t 9:30
2014-08-13 09:30:00 -0400
创建自定义转化

OptionParser 上的 accept 方法用于创建“转化”。它声明“某个类的转化块(conversion block)”。下面的例子使用它(accept)来获取一个 User 对象,在 on 处理器(on handler)接收到它之前。

require 'optparse'

User = Struct.new(:id, :name)

def find_user id
  not_found = ->{ raise "No User Found for id #{id}" }
  [ User.new(1, "Sam"),
    User.new(2, "Gandalf") ].find(not_found) do |u|
    u.id == id
  end
end

op = OptionParser.new
op.accept(User) do |user_id|
  find_user user_id.to_i
end

op.on("--user ID", User) do |user|
  puts user
end

op.parse!

使用:

$ ruby optparse-test.rb --user 1
#<struct User id=1, name="Sam">
$ ruby optparse-test.rb --user 2
#<struct User id=2, name="Gandalf">
$ ruby optparse-test.rb --user 3
optparse-test.rb:15:in `block in find_user': No User Found for id 3 (RuntimeError)
存储参数到一个 Hash

orderparse 等方法的 into 参数用来存储命令行参数到一个 Hash

require 'optparse'

params = {}
OptionParser.new do |opts|
  opts.on('-a')
  opts.on('-b NUM', Integer)
  opts.on('-v', '--verbose')
end.parse!(into: params)

p params

使用:

$ ruby optparse-test.rb -a
{:a=>true}
$ ruby optparse-test.rb -a -v
{:a=>true, :verbose=>true}
$ ruby optparse-test.rb -a -b 100
{:a=>true, :b=>100}
完整的例子

下面的例子是一个完整的 Ruby 程序。你可以运行它,然后观察各种参数的效果。这也许是学习 optparse 的特性的最佳途径。

require 'optparse'
require 'optparse/time'
require 'ostruct'
require 'pp'

class OptparseExample
  Version = '1.0.0'

  CODES = %w[iso-2022-jp shift_jis euc-jp utf8 binary]
  CODE_ALIASES = { "jis" => "iso-2022-jp", "sjis" => "shift_jis" }

  class ScriptOptions
    attr_accessor :library, :inplace, :encoding, :transfer_type,
                  :verbose, :extension, :delay, :time, :record_separator,
                  :list

    def initialize
      self.library = []
      self.inplace = false
      self.encoding = "utf8"
      self.transfer_type = :auto
      self.verbose = false
    end

    def define_options(parser)
      parser.banner = "Usage: example.rb [options]"
      parser.separator ""
      parser.separator "Specific options:"

      # add additional options
      perform_inplace_option(parser)
      delay_execution_option(parser)
      execute_at_time_option(parser)
      specify_record_separator_option(parser)
      list_example_option(parser)
      specify_encoding_option(parser)
      optional_option_argument_with_keyword_completion_option(parser)
      boolean_verbose_option(parser)

      parser.separator ""
      parser.separator "Common options:"
      # No argument, shows at tail.  This will print an options summary.
      # Try it and see!
      parser.on_tail("-h", "--help", "Show this message") do
        puts parser
        exit
      end
      # Another typical switch to print the version.
      parser.on_tail("--version", "Show version") do
        puts Version
        exit
      end
    end

    def perform_inplace_option(parser)
      # Specifies an optional option argument
      parser.on("-i", "--inplace [EXTENSION]",
                "Edit ARGV files in place",
                "(make backup if EXTENSION supplied)") do |ext|
        self.inplace = true
        self.extension = ext || ''
        self.extension.sub!(/\A\.?(?=.)/, ".")  # Ensure extension begins with dot.
      end
    end

    def delay_execution_option(parser)
      # Cast 'delay' argument to a Float.
      parser.on("--delay N", Float, "Delay N seconds before executing") do |n|
        self.delay = n
      end
    end

    def execute_at_time_option(parser)
      # Cast 'time' argument to a Time object.
      parser.on("-t", "--time [TIME]", Time, "Begin execution at given time") do |time|
        self.time = time
      end
    end

    def specify_record_separator_option(parser)
      # Cast to octal integer.
      parser.on("-F", "--irs [OCTAL]", OptionParser::OctalInteger,
                "Specify record separator (default \\0)") do |rs|
        self.record_separator = rs
      end
    end

    def list_example_option(parser)
      # List of arguments.
      parser.on("--list x,y,z", Array, "Example 'list' of arguments") do |list|
        self.list = list
      end
    end

    def specify_encoding_option(parser)
      # Keyword completion.  We are specifying a specific set of arguments (CODES
      # and CODE_ALIASES - notice the latter is a Hash), and the user may provide
      # the shortest unambiguous text.
      code_list = (CODE_ALIASES.keys + CODES).join(', ')
      parser.on("--code CODE", CODES, CODE_ALIASES, "Select encoding",
                "(#{code_list})") do |encoding|
        self.encoding = encoding
      end
    end

    def optional_option_argument_with_keyword_completion_option(parser)
      # Optional '--type' option argument with keyword completion.
      parser.on("--type [TYPE]", [:text, :binary, :auto],
                "Select transfer type (text, binary, auto)") do |t|
        self.transfer_type = t
      end
    end

    def boolean_verbose_option(parser)
      # Boolean switch.
      parser.on("-v", "--[no-]verbose", "Run verbosely") do |v|
        self.verbose = v
      end
    end
  end

  #
  # Return a structure describing the options.
  #
  def parse(args)
    # The options specified on the command line will be collected in
    # *options*.

    @options = ScriptOptions.new
    @args = OptionParser.new do |parser|
      @options.define_options(parser)
      parser.parse!(args)
    end
    @options
  end

  attr_reader :parser, :options
end  # class OptparseExample

example = OptparseExample.new
options = example.parse(ARGV)
pp options # example.options
pp ARGV
Shell

在一些现代化 Shell 中(例如 bash、zsh 等),你可以使用“命令自动补全”来完成参数输入。(译者注:tab 键)

更多文档

上面的例子应该足够教会你使用这个类。如果有问题,去 bugs.ruby-lang.org

还有一个也可供楼主参考,Linux 下自带的命令行工具普遍遵循 KISS 原则:即每个工具只完成一个简单功能,复杂功能由管道组合多个简单工具完成

需要 登录 后方可回复, 如果你还没有账号请 注册新账号