diff options
author | Gearnode <bryan@frimin.fr> | 2019-08-25 12:08:52 +0200 |
---|---|---|
committer | Gearnode <bryan@frimin.fr> | 2019-08-25 12:08:52 +0200 |
commit | 24fe02a2809f0722e3bd1d0215fbd66a60e54379 (patch) | |
tree | 2d7483f61e175c5b9721a6cfea10bd45893511d7 |
initial import
-rw-r--r-- | .gitignore | 0 | ||||
-rw-r--r-- | LICENSE | 201 | ||||
-rw-r--r-- | README.md | 1 | ||||
-rwxr-xr-x | examples/arguments | 14 | ||||
-rwxr-xr-x | examples/commands | 29 | ||||
-rwxr-xr-x | examples/minimal | 4 | ||||
-rwxr-xr-x | examples/options | 23 | ||||
-rw-r--r-- | lib/cmdline.rb | 433 |
8 files changed, 705 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/.gitignore @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2019 Bryan Frimin + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..87ce8f8 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# rb-cmdline diff --git a/examples/arguments b/examples/arguments new file mode 100755 index 0000000..a3f1220 --- /dev/null +++ b/examples/arguments @@ -0,0 +1,14 @@ +#!/usr/bin/env ruby + +require("rb-cmdline") + +cmd = Cmdline.new +cmd.add_argument("foo", "the first argument") +cmd.add_argument("bar", "the second argument") +cmd.add_trailing_arguments("name", "a trailing argument") + +cmd.parse(Cmdline.argv) + +printf("foo: %s\n", cmd.argument_value("foo")) +printf("bar: %s\n", cmd.argument_value("bar")) +printf("names: %s\n", cmd.trailing_arguments_values("name")) diff --git a/examples/commands b/examples/commands new file mode 100755 index 0000000..ca643bf --- /dev/null +++ b/examples/commands @@ -0,0 +1,29 @@ +#!/usr/bin/env ruby + +require("rb-cmdline") + +cmd = Cmdline.new + +cmd.add_command("foo", "subcommand 1") +cmd.add_command("bar", "subcommand 2") + +cmd.parse(Cmdline.argv) + +commands = { + "foo" => -> (args) { + printf("running command \"foo\" with arguments %s\n", args[1..]) + }, + "bar" => -> (args) { + cmd = CLI.new + cmd.add_option("n", "", "value", "an example value") + cmd.parse(args) + + printf("running command \"bar\" with arguments %s\n", args[1..]) + + if cmd.is_option_set("n") + printf("n: %s\n", cmd.option_value("n")) + end + } +} + +commands[cmd.command_name].call(cmd.command_name_and_arguments) diff --git a/examples/minimal b/examples/minimal new file mode 100755 index 0000000..7ec826c --- /dev/null +++ b/examples/minimal @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby + +cmd = Cmdline.new +cmd.parse(Cmdline.argv) diff --git a/examples/options b/examples/options new file mode 100755 index 0000000..87878db --- /dev/null +++ b/examples/options @@ -0,0 +1,23 @@ +#!/usr/bin/env ruby + +require("rb-cmdline") + +cmd = Cmdline.new +cmd.add_option("a", "option-a", "value", "a simple option") +cmd.set_option_default("a", "42") +cmd.add_option("b", "", "value", "an short option") +cmd.add_option("", "option-c", "value", "a long option") +cmd.add_flag("d", "flag-d", "a simple flag") +cmd.parse(Cmdline.argv) + +printf("a: %s\n", cmd.option_value("a")) + +if cmd.is_option_set("b") + printf("b: %s\n", cmd.option_value("b")) +end + +if cmd.is_option_set("option-c") + printf("option-c: %s\n", cmd.option_value("option-c")) +end + +printf("d: %s\n", cmd.is_option_set("d")) diff --git a/lib/cmdline.rb b/lib/cmdline.rb new file mode 100644 index 0000000..780630b --- /dev/null +++ b/lib/cmdline.rb @@ -0,0 +1,433 @@ +# Copyright 2019 Bryan Frimin +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +module Cmdline + Option = Struct.new( + :short_name, + :long_name, + :value_string, + :description, + :default, + :set, + :value, + keyword_init: true + ) + + class Option + def initialize(*) + super + self.short_name ||= "" + self.long_name ||= "" + self.value_string ||= "" + self.description ||= "" + self.default ||= "" + self.set ||= false + self.value ||= "" + end + + def sort_key + return short_name unless short_name.empty? + return long_name unless long_name.empty? + "" + end + end + + Argument = Struct.new( + :name, + :description, + :trailing, + :value, + :trailing_values, + keyword_init: true + ) + + class Argument + def initialize(*) + super + self.name ||= "" + self.description ||= "" + self.trailing ||= false + self.value ||= "" + self.trailing_values ||= [] + end + end + + Command = Struct.new( + :name, + :description, + keyword_init: true + ) + + class Command + def initialize(*) + super + self.name ||= "" + self.description ||= "" + end + end + + CmdLine = Struct.new( + :options, + :arguments, + :commands, + :command, + :command_arguments, + :program_name, + keyword_init: true + ) + + class CmdLine + def initialize(*) + super + self.options ||= {} + self.commands ||= {} + self.command ||= "" + self.arguments ||= [] + self.command_arguments ||= [] + self.program_name ||= "" + end + + def add_flag(short, long, description) + option = Option.new( + short_name: short.to_s, + long_name: long.to_s, + value_string: "", + description: description.to_s + ) + + addopt(option) + end + + def add_option(short, long, value, description) + option = Option.new( + short_name: short.to_s, + long_name: long.to_s, + value_string: value.to_s, + description: description.to_s + ) + + addopt(option) + end + + def set_option_default(name, value) + option = self.options[name] + raise(ArgumentError, "unknown option") if option.nil? + raise(ArgumentError, "flags cannot have a default value") if option.value_string.empty? + + option.default = value + end + + def add_argument(name, description) + argument = Argument.new( + name: name.to_s, + description: description.to_s + ) + + addarg(argument) + end + + def add_trailing_arguments(name, description) + argument = Argument.new( + name: name.to_s, + description: description.to_s, + trailing: true + ) + + addarg(argument) + end + + def add_command(name, description) + if self.arguments.size.zero? + add_argument("command", "the command to execute") + elsif self.arguments.first.name != "command" + raise(ArgumentError, "cannot have both arguments and commands") + end + + cmd = Command.new( + name: name.to_s, + description: description.to_s + ) + + self.commands[cmd.name] = cmd + end + + def die(format, *args) + msg = sprintf(format, *args) + STDERR.puts("error: #{msg}") + exit(1) + end + + def parse(args) + die("empty argument array") if args.size == 0 + + self.program_name = args.shift + + while args.size > 0 + arg = args.first + + if arg == "--" + args.shift + break + end + + is_short = arg.size == 2 && arg[0] == "-" && arg[1] != "-" + is_long = arg.size > 2 && arg[0,2] == "--" + + if is_short || is_long + key = if is_short + arg[1,2] + else + arg[2..] + end + + opt = self.options[key] + die("unknown option \"%s\"", key) if opt.nil? + + opt.set = true + + if opt.value_string.empty? + args = args[1..] + else + die("missing value for option \"%s\"", key) if args.size < 2 + opt.value = args[1] + args = args[2..] + end + else + break + end + end + + if self.arguments.size > 0 && !is_option_set("help") + last = self.arguments.last + + min = self.arguments.size + min -= 1 if last.trailing + + die("missing argument(s)") if args.size < min + + min.times do |i| + self.arguments[i].value = args[i] + end + args = args[min..] + + if last.trailing + last.trailing_values = args + args = args[args.size..] + end + end + + if self.commands.size > 0 + self.command = self.arguments.first.value + self.command_arguments = args + end + + if !is_option_set("help") + if self.commands.size > 0 + cmd = self.commands[self.command] + if cmd.nil? + die("unknown command \"%s\"", self.command) + end + elsif args.size > 0 + die("invalid extra argument(s)") + end + end + + if is_option_set("help") + print_usage + exit(0) + end + end + + def print_usage + usage = sprintf("Usage: %s OPTIONS", self.program_name) + if self.arguments.size > 0 + self.arguments.each do |arg| + if arg.trailing + usage << sprintf(" [<%s> ...]", arg.name) + else + usage << sprintf(" <%s>", arg.name) + end + end + end + + usage << "\n\n" + + opt_strs = {} + max_width = 0 + + self.options.each do |_, opt| + next if opt_strs[opt] + + buf = "" + + if opt.short_name != "" + buf << sprintf("-%s", opt.short_name) + end + + if opt.long_name != "" + if opt.short_name != "" + buf << ", " + end + + buf << sprintf("--%s", opt.long_name) + end + + if opt.value_string != "" + buf << sprintf(" <%s>", opt.value_string) + end + + opt_strs[opt] = buf + + if buf.size > max_width + max_width = buf.size + end + end + + if self.commands.size > 0 + self.commands.each do |name, _| + max_width = name.size if name.size > max_width + end + elsif self.arguments.size > 0 + self.arguments.each do |arg| + max_width = arg.name.size if arg.name.size > max_width + end + end + + # Print options + usage << "OPTIONS\n\n" + + opts = [] + opt_strs.each do |opt, _| + opts << opt + end + + # TODO: sort options + + opts.each do |opt| + usage << sprintf("%-*s %s", max_width, opt_strs[opt], opt.description) + usage << sprintf(" (default: %s)", opt.default) unless opt.default.empty? + usage << "\n" + end + + if self.commands.size > 0 + usage << "\nCOMMANDS\n\n" + names = [] + self.commands.each do |name, _| + names << name + end + names.sort! + + names.each do |name| + cmd = self.commands[name] + usage << sprintf("%-*s %s\n", max_width, cmd.name, cmd.description) + end + elsif self.arguments.size > 0 + usage << "\nARGUMENTS\n\n" + + self.arguments.each do |arg| + usage << sprintf("%-*s %s\n", max_width, arg.name, arg.description) + end + end + + printf(usage) + end + + def is_option_set(name) + opt = self.options[name] + raise(ArgumentError, "unknown option") if opt.nil? + opt.set + end + + def option_value(name) + opt = self.options[name] + raise(ArgumentError, "unknown option") if opt.nil? + + return opt.value if opt.set + opt.default + end + + def argument_value(name) + self.arguments.each do |arg| + if arg.name == name + return arg.name + end + end + raise(ArgumentError, "unknown argument") + end + + def trailing_arguments_values(name) + raise(ArgumentError, "empty argument array") if self.arguments.empty? + last = self.arguments.last + raise(ArgumentError, "no trailing arguments") unless last.trailing + last.trailing_values + end + + def command_name + raise(RuntimeError, "no command defined") if self.commands.empty? + self.command + end + + def command_arguments_values + raise(RuntimeError, "no command defined") if self.commands.empty? + self.command_arguments + end + + def command_name_and_arguments + raise(RuntimeError, "no command defined") if self.commands.empty? + [self.command, *self.command_arguments] + end + + private + + def addopt(opt) + if !opt.short_name.empty? + if opt.short_name.size != 1 + raise(ArgumentError, "option short names must be one character long") + end + + self.options[opt.short_name] = opt + end + + if !opt.long_name.empty? + if opt.long_name.size < 2 + raise(ArgumentError, "option long names must be at least two characters long") + end + + self.options[opt.long_name] = opt + end + end + + def addarg(arg) + raise(ArgumentError, "cannot have both arguments and commands") if self.commands.size > 0 + if self.arguments.size > 0 + last = self.arguments.last + if last.trailing + raise(ArgumentError, "cannot add argument after trailing argument") + end + end + + self.arguments << arg + end + end + + def self.new + cmd = CmdLine.new + cmd.add_flag("h", "help", "print help and exit") + cmd + end + + def self.argv + ARGV.dup.unshift($0) + end +end |