command_kit 0.1.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (98) hide show
  1. checksums.yaml +7 -0
  2. data/.document +3 -0
  3. data/.github/workflows/ruby.yml +29 -0
  4. data/.gitignore +7 -0
  5. data/.rspec +1 -0
  6. data/.yardopts +1 -0
  7. data/ChangeLog.md +29 -0
  8. data/Gemfile +14 -0
  9. data/LICENSE.txt +20 -0
  10. data/README.md +283 -0
  11. data/Rakefile +23 -0
  12. data/command_kit.gemspec +60 -0
  13. data/gemspec.yml +14 -0
  14. data/lib/command_kit.rb +1 -0
  15. data/lib/command_kit/arguments.rb +161 -0
  16. data/lib/command_kit/arguments/argument.rb +111 -0
  17. data/lib/command_kit/arguments/argument_value.rb +81 -0
  18. data/lib/command_kit/arguments/usage.rb +6 -0
  19. data/lib/command_kit/colors.rb +355 -0
  20. data/lib/command_kit/command.rb +42 -0
  21. data/lib/command_kit/command_name.rb +95 -0
  22. data/lib/command_kit/commands.rb +299 -0
  23. data/lib/command_kit/commands/auto_load.rb +153 -0
  24. data/lib/command_kit/commands/auto_load/subcommand.rb +90 -0
  25. data/lib/command_kit/commands/auto_require.rb +138 -0
  26. data/lib/command_kit/commands/command.rb +12 -0
  27. data/lib/command_kit/commands/help.rb +43 -0
  28. data/lib/command_kit/commands/parent_command.rb +21 -0
  29. data/lib/command_kit/commands/subcommand.rb +51 -0
  30. data/lib/command_kit/console.rb +141 -0
  31. data/lib/command_kit/description.rb +89 -0
  32. data/lib/command_kit/env.rb +43 -0
  33. data/lib/command_kit/env/home.rb +71 -0
  34. data/lib/command_kit/env/path.rb +71 -0
  35. data/lib/command_kit/examples.rb +99 -0
  36. data/lib/command_kit/exception_handler.rb +55 -0
  37. data/lib/command_kit/help.rb +62 -0
  38. data/lib/command_kit/help/man.rb +125 -0
  39. data/lib/command_kit/inflector.rb +84 -0
  40. data/lib/command_kit/main.rb +103 -0
  41. data/lib/command_kit/options.rb +179 -0
  42. data/lib/command_kit/options/option.rb +171 -0
  43. data/lib/command_kit/options/option_value.rb +90 -0
  44. data/lib/command_kit/options/parser.rb +227 -0
  45. data/lib/command_kit/options/quiet.rb +53 -0
  46. data/lib/command_kit/options/usage.rb +6 -0
  47. data/lib/command_kit/options/verbose.rb +55 -0
  48. data/lib/command_kit/options/version.rb +62 -0
  49. data/lib/command_kit/os.rb +47 -0
  50. data/lib/command_kit/pager.rb +115 -0
  51. data/lib/command_kit/printing.rb +32 -0
  52. data/lib/command_kit/printing/indent.rb +78 -0
  53. data/lib/command_kit/program_name.rb +57 -0
  54. data/lib/command_kit/stdio.rb +138 -0
  55. data/lib/command_kit/usage.rb +102 -0
  56. data/lib/command_kit/version.rb +4 -0
  57. data/lib/command_kit/xdg.rb +138 -0
  58. data/spec/arguments/argument_spec.rb +169 -0
  59. data/spec/arguments/argument_value_spec.rb +126 -0
  60. data/spec/arguments_spec.rb +213 -0
  61. data/spec/colors_spec.rb +470 -0
  62. data/spec/command_kit_spec.rb +8 -0
  63. data/spec/command_name_spec.rb +130 -0
  64. data/spec/command_spec.rb +49 -0
  65. data/spec/commands/auto_load/subcommand_spec.rb +82 -0
  66. data/spec/commands/auto_load_spec.rb +128 -0
  67. data/spec/commands/auto_require_spec.rb +142 -0
  68. data/spec/commands/fixtures/test_auto_load/cli/commands/test1.rb +10 -0
  69. data/spec/commands/fixtures/test_auto_load/cli/commands/test2.rb +10 -0
  70. data/spec/commands/fixtures/test_auto_require/lib/test_auto_require/cli/commands/test1.rb +10 -0
  71. data/spec/commands/help_spec.rb +66 -0
  72. data/spec/commands/parent_command_spec.rb +40 -0
  73. data/spec/commands/subcommand_spec.rb +99 -0
  74. data/spec/commands_spec.rb +767 -0
  75. data/spec/console_spec.rb +201 -0
  76. data/spec/description_spec.rb +203 -0
  77. data/spec/env/home_spec.rb +46 -0
  78. data/spec/env/path_spec.rb +78 -0
  79. data/spec/env_spec.rb +123 -0
  80. data/spec/examples_spec.rb +235 -0
  81. data/spec/exception_handler_spec.rb +103 -0
  82. data/spec/help_spec.rb +119 -0
  83. data/spec/inflector_spec.rb +104 -0
  84. data/spec/main_spec.rb +179 -0
  85. data/spec/options/option_spec.rb +258 -0
  86. data/spec/options/option_value_spec.rb +67 -0
  87. data/spec/options/parser_spec.rb +265 -0
  88. data/spec/options_spec.rb +137 -0
  89. data/spec/os_spec.rb +46 -0
  90. data/spec/pager_spec.rb +154 -0
  91. data/spec/printing/indent_spec.rb +130 -0
  92. data/spec/printing_spec.rb +76 -0
  93. data/spec/program_name_spec.rb +62 -0
  94. data/spec/spec_helper.rb +6 -0
  95. data/spec/stdio_spec.rb +264 -0
  96. data/spec/usage_spec.rb +237 -0
  97. data/spec/xdg_spec.rb +191 -0
  98. metadata +156 -0
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandKit
4
+ module Help
5
+ #
6
+ # Allows displaying a man-page instead of the usual `--help` output.
7
+ #
8
+ # ## Environment Variables
9
+ #
10
+ # * `TERM` - Specifies the type of terminal. When set to `DUMB`, it will
11
+ # disable man-page help output.
12
+ #
13
+ module Man
14
+ module ModuleMethods
15
+ #
16
+ # Extends {ClassMethods} or {ModuleMethods}, depending on whether
17
+ # {Help::Man} is being included into a class or a module.
18
+ #
19
+ # @param [Class, Module] context
20
+ # The class or module including {Man}.
21
+ #
22
+ def included(context)
23
+ super(context)
24
+
25
+ if context.class == Module
26
+ context.extend ModuleMethods
27
+ else
28
+ context.extend ClassMethods
29
+ end
30
+ end
31
+ end
32
+
33
+ extend ModuleMethods
34
+
35
+ #
36
+ # Defines class-level methods.
37
+ #
38
+ module ClassMethods
39
+ #
40
+ # Gets or sets the directory where man-pages are stored.
41
+ #
42
+ # @param [String, nil] new_man_dir
43
+ # If a String is given, it will set The class'es man-page directory.
44
+ #
45
+ # @return [String, nil]
46
+ # The class'es or superclass'es man-page directory.
47
+ #
48
+ # @example
49
+ # man_dir File.expand_path('../../../man',__FILE__)
50
+ #
51
+ def man_dir(new_man_dir=nil)
52
+ if new_man_dir
53
+ @man_dir = File.expand_path(new_man_dir)
54
+ else
55
+ @man_dir || (superclass.man_dir if superclass.kind_of?(ClassMethods))
56
+ end
57
+ end
58
+ end
59
+
60
+ #
61
+ # Determines if displaying man pages is supported.
62
+ #
63
+ # @return [Boolean]
64
+ # Indicates whether the `TERM` environment variable is not `dumb`
65
+ # and `$stdout` is a TTY.
66
+ #
67
+ def self.supported?
68
+ ENV['TERM'] != 'dumb' && $stdout.tty?
69
+ end
70
+
71
+ #
72
+ # Returns the man-page file name for the given command name.
73
+ #
74
+ # @param [String] command
75
+ # The given command name.
76
+ #
77
+ # @return [String]
78
+ # The man-page file name.
79
+ #
80
+ def man_page(command=command_name)
81
+ "#{command}.1"
82
+ end
83
+
84
+ #
85
+ # Displays the given man page.
86
+ #
87
+ # @param [String] page
88
+ # The man page file name.
89
+ #
90
+ # @return [Boolean, nil]
91
+ # Specifies whether the `man` command was successful or not.
92
+ # Returns `nil` when the `man` command is not installed.
93
+ #
94
+ def man(page=man_page)
95
+ system('man',page)
96
+ end
97
+
98
+ #
99
+ # Displays the {#man_page} instead of the usual `--help` output.
100
+ #
101
+ # @raise [NotImplementedError]
102
+ # {ClassMethods#man_dir man_dir} does not have a value.
103
+ #
104
+ # @note
105
+ # if `TERM` is `dumb` or `$stdout` is not a TTY, fallsback to printing
106
+ # the usual `--help` output.
107
+ #
108
+ def help
109
+ if Man.supported?
110
+ unless self.class.man_dir
111
+ raise(NotImplementedError,"#{self.class}.man_dir not set")
112
+ end
113
+
114
+ man_path = File.join(self.class.man_dir,man_page)
115
+
116
+ if man(man_path).nil?
117
+ super
118
+ end
119
+ else
120
+ super
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandKit
4
+ #
5
+ # A very simple inflector.
6
+ #
7
+ # @note
8
+ # If you need something more powerful, checkout
9
+ # [dry-inflector](https://dry-rb.org/gems/dry-inflector/0.1/)
10
+ #
11
+ module Inflector
12
+ #
13
+ # Removes the namespace from a constant name.
14
+ #
15
+ # @param [#to_s] name
16
+ # The constant name.
17
+ #
18
+ # @return [String]
19
+ # The class or module's name, without the namespace.
20
+ #
21
+ def self.demodularize(name)
22
+ name.to_s.split('::').last
23
+ end
24
+
25
+ #
26
+ # Converts a CamelCased name to an under_scored name.
27
+ #
28
+ # @param [#to_s] name
29
+ # The CamelCased name.
30
+ #
31
+ # @return [String]
32
+ # The resulting under_scored name.
33
+ #
34
+ def self.underscore(name)
35
+ # sourced from: https://github.com/dry-rb/dry-inflector/blob/c918f967ff82611da374eb0847a77b7e012d3fa8/lib/dry/inflector.rb#L286-L287
36
+ name = name.to_s.dup
37
+
38
+ name.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
39
+ name.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
40
+ name.tr!('-','_')
41
+ name.downcase!
42
+
43
+ name
44
+ end
45
+
46
+ #
47
+ # Replaces all underscores with dashes.
48
+ #
49
+ # @param [#to_s] name
50
+ # The under_scored name.
51
+ #
52
+ # @return [String]
53
+ # The dasherized name.
54
+ #
55
+ def self.dasherize(name)
56
+ name.to_s.tr('_','-')
57
+ end
58
+
59
+ #
60
+ # Converts an under_scored name to a CamelCased name.
61
+ #
62
+ # @param [String] name
63
+ # The under_scored name.
64
+ #
65
+ # @return [String]
66
+ # The CamelCased name.
67
+ #
68
+ def self.camelize(name)
69
+ name = name.to_s.dup
70
+
71
+ # sourced from: https://github.com/dry-rb/dry-inflector/blob/c918f967ff82611da374eb0847a77b7e012d3fa8/lib/dry/inflector.rb#L329-L334
72
+ name.sub!(/^[a-z\d]*/,&:capitalize)
73
+ name.gsub!(%r{(?:[_-]|(/))([a-z\d]*)}i) do |match|
74
+ slash = Regexp.last_match(1)
75
+ word = Regexp.last_match(2)
76
+
77
+ "#{slash}#{word.capitalize}"
78
+ end
79
+
80
+ name.gsub!('/','::')
81
+ name
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,103 @@
1
+ module CommandKit
2
+ #
3
+ # Defines a `main` method.
4
+ #
5
+ # ## Examples
6
+ #
7
+ # include CommandKit::Main
8
+ #
9
+ # def main(argv=[])
10
+ # # ...
11
+ # return 0
12
+ # end
13
+ #
14
+ module Main
15
+ module ModuleMethods
16
+ #
17
+ # Extends {ClassMethods} or {ModuleMethods}, depending on whether {Main}
18
+ # is being included into a class or a module.
19
+ #
20
+ # @param [Class, Module] context
21
+ # The class or module which is including {Main}.
22
+ #
23
+ def included(context)
24
+ super(context)
25
+
26
+ if context.class == Module
27
+ context.extend ModuleMethods
28
+ else
29
+ context.extend ClassMethods
30
+ end
31
+ end
32
+ end
33
+
34
+ extend ModuleMethods
35
+
36
+ #
37
+ # Class-level methods.
38
+ #
39
+ module ClassMethods
40
+ #
41
+ # Starts the command and then exits.
42
+ #
43
+ # @param [Array<String>] argv
44
+ # The Array of command-line arguments.
45
+ #
46
+ def start(argv=ARGV, **kwargs)
47
+ begin
48
+ exit main(argv, **kwargs)
49
+ rescue Interrupt
50
+ # https://tldp.org/LDP/abs/html/exitcodes.html
51
+ exit 130
52
+ rescue Errno::EPIPE
53
+ # STDOUT pipe broken
54
+ exit 0
55
+ end
56
+ end
57
+
58
+ #
59
+ # Initializes the command class with the given keyword arguments, then
60
+ # calls {Main#main main} with the given `argv`.
61
+ #
62
+ # @param [Array<String>] argv
63
+ # The Array of command-line arguments.
64
+ #
65
+ # @param [Hash{Symbol => Object}] kwargs
66
+ # Additional keyword arguments to initialize the command class with.
67
+ #
68
+ # @return [Integer]
69
+ # The exit status of the command.
70
+ #
71
+ def main(argv=[], **kwargs)
72
+ new(**kwargs).main(argv)
73
+ end
74
+ end
75
+
76
+ #
77
+ # Place-holder `main` method, which parses options, before calling {#run}.
78
+ #
79
+ # @param [Array<String>] argv
80
+ # The Array of command-line arguments.
81
+ #
82
+ # @return [Integer]
83
+ # The exit status code.
84
+ #
85
+ def main(argv=[])
86
+ run(*argv)
87
+ return 0
88
+ rescue SystemExit => system_exit
89
+ return system_exit.status
90
+ end
91
+
92
+ #
93
+ # Place-holder method for command business logic.
94
+ #
95
+ # @param [Array<Object>] args
96
+ # Additional arguments for the command.
97
+ #
98
+ # @abstract
99
+ #
100
+ def run(*args)
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,179 @@
1
+ require 'command_kit/options/option'
2
+ require 'command_kit/options/parser'
3
+
4
+ module CommandKit
5
+ #
6
+ # Provides a thin DSL for defining options as attributes.
7
+ #
8
+ # ## Examples
9
+ #
10
+ # include CommandKit::Options
11
+ #
12
+ # option :foo, type: String,
13
+ # short: '-f',
14
+ # desc: "Foo option"
15
+ #
16
+ # option :bar, type: String,
17
+ # short: '-b',
18
+ # usage: 'STR:STR:...',
19
+ # desc: "Bar option" do |arg|
20
+ # @bar = arg.split(':')
21
+ # end
22
+ #
23
+ # option :number, type: Integer,
24
+ # desc: 'Numbers' do |num|
25
+ # @numbers << num
26
+ # end
27
+ #
28
+ # def initialize
29
+ # super
30
+ #
31
+ # @numbers = []
32
+ # end
33
+ #
34
+ module Options
35
+ include Parser
36
+
37
+ module ModuleMethods
38
+ #
39
+ # Extends {ClassMethods} or {ModuleMethods}, depending on whether
40
+ # {Options} is being included into a class or a module.
41
+ #
42
+ # @param [Class, Module] context
43
+ # The class or module which is including {Options}.
44
+ #
45
+ def included(context)
46
+ super(context)
47
+
48
+ if context.class == Module
49
+ context.extend ModuleMethods
50
+ else
51
+ context.extend ClassMethods
52
+ end
53
+ end
54
+ end
55
+
56
+ extend ModuleMethods
57
+
58
+ #
59
+ # Defines class-level methods.
60
+ #
61
+ module ClassMethods
62
+ #
63
+ # All defined options for the command class.
64
+ #
65
+ # @return [Hash{Symbol => Option}]
66
+ #
67
+ def options
68
+ @options ||= if superclass.kind_of?(ClassMethods)
69
+ superclass.options.dup
70
+ else
71
+ {}
72
+ end
73
+ end
74
+
75
+ #
76
+ # Defines an {Option option} for the class.
77
+ #
78
+ # @param [Symbol] name
79
+ # The option name.
80
+ #
81
+ # @yield [(value)]
82
+ # If a block is given, it will be passed the parsed option value.
83
+ #
84
+ # @yieldparam [Object, nil] value
85
+ # The parsed option value.
86
+ #
87
+ # @return [Option]
88
+ #
89
+ # @example Define an option:
90
+ # option :foo, desc: "Foo option"
91
+ #
92
+ # @example With a custom short option:
93
+ # option :foo, short: '-f',
94
+ # desc: "Foo option"
95
+ #
96
+ # @example With a custom long option:
97
+ # option :foo, short: '--foo-opt',
98
+ # desc: "Foo option"
99
+ #
100
+ # @example With a custom usage string:
101
+ # option :foo, value: {usage: 'FOO'},
102
+ # desc: "Foo option"
103
+ #
104
+ # @example With a custom block:
105
+ # option :foo, desc: "Foo option" do |value|
106
+ # @foo = Foo.new(value)
107
+ # end
108
+ #
109
+ # @example With a custom type:
110
+ # option :foo, value: {type: Integer},
111
+ # desc: "Foo option"
112
+ #
113
+ # @example With a default value:
114
+ # option :foo, value: {type: Integer, default: 1},
115
+ # desc: "Foo option"
116
+ #
117
+ # @example With a required value:
118
+ # option :foo, value: {type: String, required: true},
119
+ # desc: "Foo option"
120
+ #
121
+ # @example With a custom option value Hash map:
122
+ # option :flag, value: {
123
+ # type: {
124
+ # 'enabled' => :enabled,
125
+ # 'yes' => :enabled,
126
+ # 'disabled' => :disabled,
127
+ # 'no' => :disabled
128
+ # }
129
+ # },
130
+ # desc: "Flag option"
131
+ #
132
+ # @example With a custom option value Array enum:
133
+ # option :enum, value: {type: %w[yes no]},
134
+ # desc: "Enum option"
135
+ #
136
+ # @example With a custom option value Regexp:
137
+ # option :date, value: {type: /(\d+)-(\d+)-(\d{2,4})/},
138
+ # desc: "Regexp optin" do |date,d,m,y|
139
+ # # ...
140
+ # end
141
+ #
142
+ def option(name,**kwargs,&block)
143
+ options[name] = Option.new(name,**kwargs,&block)
144
+ end
145
+ end
146
+
147
+ # Hash of parsed option values.
148
+ #
149
+ # @return [Hash{Symbol => Object}]
150
+ attr_reader :options
151
+
152
+ #
153
+ # Initializes {#options} and populates the
154
+ # {Parser#option_parser option parser}.
155
+ #
156
+ # @param [Hash{Symbol => Object}] options
157
+ # Optional pre-populated options hash.
158
+ #
159
+ def initialize(options: {}, **kwargs)
160
+ @options = options
161
+
162
+ super(**kwargs)
163
+
164
+ self.class.options.each_value do |option|
165
+ option_parser.on(*option.usage,option.type,option.desc) do |arg,*captures|
166
+ @options[option.name] = if arg.nil?
167
+ option.default_value
168
+ else
169
+ arg
170
+ end
171
+
172
+ if option.block
173
+ instance_exec(*arg,*captures,&option.block)
174
+ end
175
+ end
176
+ end
177
+ end
178
+ end
179
+ end