command_kit 0.1.0.pre1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,57 @@
1
+ module CommandKit
2
+ #
3
+ # Retrieves the current program name (`$PROGRAM_NAME`).
4
+ #
5
+ module ProgramName
6
+ module ModuleMethods
7
+ #
8
+ # Extends {ClassMethods} or {ModuleMethods}, depending on whether
9
+ # {ProgramName} is being included into a class or a module.
10
+ #
11
+ # @param [Class, Module] context
12
+ # The class or module which is including {ProgramName}.
13
+ #
14
+ def included(context)
15
+ super(context)
16
+
17
+ if context.class == Module
18
+ context.extend ModuleMethods
19
+ else
20
+ context.extend ClassMethods
21
+ end
22
+ end
23
+ end
24
+
25
+ extend ModuleMethods
26
+
27
+ #
28
+ # Class-level methods.
29
+ #
30
+ module ClassMethods
31
+ # List of `$PROGRAM_NAME`s that should be ignored.
32
+ IGNORED_PROGRAM_NAMES = [
33
+ '-e', # ruby -e "..."
34
+ 'irb', # running in irb
35
+ 'rspec' # running in rspec
36
+ ]
37
+
38
+ #
39
+ # The current program name (`$PROGRAM_NAME`).
40
+ #
41
+ # @return [String, nil]
42
+ # The `$PROGRAM_NAME` or `nil` if the `$PROGRAM_NAME` is `-e`, `irb`,
43
+ # or `rspec`.
44
+ #
45
+ def program_name
46
+ $PROGRAM_NAME unless IGNORED_PROGRAM_NAMES.include?($PROGRAM_NAME)
47
+ end
48
+ end
49
+
50
+ #
51
+ # @see ClassMethods#program_name
52
+ #
53
+ def program_name
54
+ self.class.program_name
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,138 @@
1
+ module CommandKit
2
+ #
3
+ # Provides access to stdin, stdout, and stderr streams.
4
+ #
5
+ # ## Examples
6
+ #
7
+ # class MyCmd
8
+ # include CommandKit::Stdio
9
+ #
10
+ # def main
11
+ # end
12
+ # end
13
+ #
14
+ # ## Testing
15
+ #
16
+ # Can be initialized with custom stdin, stdout, and stderr streams for testing
17
+ # purposes.
18
+ #
19
+ # stdin = StringIO.new
20
+ # stdout = StringIO.new
21
+ # stderr = StringIO.new
22
+ # MyCmd.new(stdin: stdin, stdout: stdout, stderr: stderr)
23
+ #
24
+ module Stdio
25
+ #
26
+ # Initializes {#stdin}, {#stdout}, and {#stderr}.
27
+ #
28
+ # @param [IO] stdin
29
+ # The stdin input stream. Defaults to `$stdin`.
30
+ #
31
+ # @param [IO] stdout
32
+ # The stdout output stream. Defaults to `$stdout`.
33
+ #
34
+ # @param [IO] stderr
35
+ # The stderr error output stream. Defaults to `$stderr`.
36
+ #
37
+ def initialize(stdin: nil, stdout: nil, stderr: nil, **kwargs)
38
+ @stdin = stdin
39
+ @stdout = stdout
40
+ @stderr = stderr
41
+
42
+ super(**kwargs)
43
+ end
44
+
45
+ #
46
+ # Returns the stdin input stream.
47
+ #
48
+ # @return [$stdin, IO]
49
+ # The initialized `@stdin` value or `$stdin`.
50
+ #
51
+ def stdin
52
+ @stdin || $stdin
53
+ end
54
+
55
+ #
56
+ # Returns the stdout output stream.
57
+ #
58
+ # @return [$stdout, IO]
59
+ # The initialized `@stdout` value or `$stdout`.
60
+ #
61
+ def stdout
62
+ @stdout || $stdout
63
+ end
64
+
65
+ #
66
+ # Returns the stderr error output stream.
67
+ #
68
+ # @return [$stderr, IO]
69
+ # The initialized `@stderr` value or `$stderr`.
70
+ #
71
+ def stderr
72
+ @stderr || $stderr
73
+ end
74
+
75
+ #
76
+ # Calls `stdin.gets`.
77
+ #
78
+ def gets(*arguments)
79
+ stdin.gets(*arguments)
80
+ end
81
+
82
+ #
83
+ # Calls `stdin.readline`.
84
+ #
85
+ def readline(*arguments)
86
+ stdin.readline(*arguments)
87
+ end
88
+
89
+ #
90
+ # Calls `stdin.readlines`.
91
+ #
92
+ def readlines(*arguments)
93
+ stdin.readlines(*arguments)
94
+ end
95
+
96
+ # NOTE: intentionally do not override `Kenrel#p` or `Kernel#pp` to not
97
+ # hijack echo-debugging.
98
+
99
+ #
100
+ # Calls `stdout.putc`.
101
+ #
102
+ def putc(*arguments)
103
+ stdout.putc(*arguments)
104
+ end
105
+
106
+ #
107
+ # Calls `stdout.puts`.
108
+ #
109
+ def puts(*arguments)
110
+ stdout.puts(*arguments)
111
+ end
112
+
113
+ #
114
+ # Calls `stdout.print`.
115
+ #
116
+ def print(*arguments)
117
+ stdout.print(*arguments)
118
+ end
119
+
120
+ #
121
+ # Calls `stdout.printf`.
122
+ #
123
+ def printf(*arguments)
124
+ stdout.printf(*arguments)
125
+ end
126
+
127
+ #
128
+ # Overrides `Kernel.abort` to print to {#stderr}.
129
+ #
130
+ # @param [String, nil] message
131
+ # The optional abort message.
132
+ #
133
+ def abort(message=nil)
134
+ stderr.puts(message) if message
135
+ exit(1)
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,102 @@
1
+ require 'command_kit/command_name'
2
+ require 'command_kit/help'
3
+
4
+ module CommandKit
5
+ #
6
+ # Defines the usage string for a command class.
7
+ #
8
+ # ## Examples
9
+ #
10
+ # include CommandKit::Usage
11
+ #
12
+ # usage "[options] ARG1 ARG2 [ARG3 ...]"
13
+ #
14
+ module Usage
15
+ include CommandName
16
+ include Help
17
+
18
+ module ModuleMethods
19
+ #
20
+ # Extends {ClassMethods} or {ModuleMethods}, depending on whether {Usage}
21
+ # is being included into a class or module.
22
+ #
23
+ # @param [Class, Module] context
24
+ # The class or module which is including {Usage}.
25
+ #
26
+ def included(context)
27
+ super
28
+
29
+ if context.class == Module
30
+ context.extend ModuleMethods
31
+ else
32
+ context.extend ClassMethods
33
+ end
34
+ end
35
+ end
36
+
37
+ extend ModuleMethods
38
+
39
+ #
40
+ # Class-level methods.
41
+ #
42
+ module ClassMethods
43
+ #
44
+ # Gets or sets the class'es usage string(s).
45
+ #
46
+ # @param [String, Array<String>, nil] new_usage
47
+ # If a new_usage argument is given, it will set the class'es usage
48
+ # string(s).
49
+ #
50
+ # @return [String, Array<String>]
51
+ # The class'es or superclass'es usage string(s).
52
+ #
53
+ def usage(new_usage=nil)
54
+ if new_usage
55
+ @usage = new_usage
56
+ else
57
+ @usage || (superclass.usage if superclass.kind_of?(ClassMethods))
58
+ end
59
+ end
60
+ end
61
+
62
+ #
63
+ # Similar to {ClassMethods#usage .usage}, but prepends
64
+ # {CommandName#command_name command_name}.
65
+ #
66
+ # @return [Array<String>, String, nil]
67
+ #
68
+ def usage
69
+ case (usage = self.class.usage)
70
+ when Array
71
+ usage.map { |command| "#{command_name} #{command}" }
72
+ when String
73
+ "#{command_name} #{usage}"
74
+ end
75
+ end
76
+
77
+ #
78
+ # Prints the `usage: ...` output.
79
+ #
80
+ def help_usage
81
+ case (usage = self.usage)
82
+ when Array
83
+ puts "usage: #{usage[0]}"
84
+
85
+ usage[1..].each do |command|
86
+ puts " #{command}"
87
+ end
88
+ when String
89
+ puts "usage: #{usage}"
90
+ end
91
+ end
92
+
93
+ #
94
+ # Prints the usage.
95
+ #
96
+ # @see #help_usage
97
+ #
98
+ def help
99
+ help_usage
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,4 @@
1
+ module CommandKit
2
+ # command_kit version
3
+ VERSION = "0.1.0.pre1"
4
+ end
@@ -0,0 +1,138 @@
1
+ require 'command_kit/command_name'
2
+ require 'command_kit/env/home'
3
+
4
+ module CommandKit
5
+ #
6
+ # Provides access to [XDG directories].
7
+ #
8
+ # * `~/.config`
9
+ # * `~/.local/share`
10
+ # * `~/.cache`
11
+ #
12
+ # ## Environment Variables
13
+ #
14
+ # * `XDG_CONFIG_HOME` - The directory that should contain user-specific
15
+ # configuration. Defaults to `~/.config/`.
16
+ # * `XDG_DATA_HOME` - The directory that should contain user-specific data.
17
+ # Defaults to `~/.local/share/`.
18
+ # * `XDG_CACHE_HOME` - The directory that should contain user-specific cache
19
+ # data. Defaults to `~/.cache/`.
20
+ #
21
+ # [XDG directories]: https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
22
+ #
23
+ module XDG
24
+ include CommandName
25
+ include Env::Home
26
+
27
+ module ModuleMethods
28
+ #
29
+ # Extends {ClassMethods} or {ModuleMethods}, depending on whether {XDG} is
30
+ # being included into a class or a module..
31
+ #
32
+ # @param [Class, Module] context
33
+ # The class or module which is including {XDG}.
34
+ #
35
+ def included(context)
36
+ super(context)
37
+
38
+ if context.class == Module
39
+ context.extend ModuleMethods
40
+ else
41
+ context.extend ClassMethods
42
+ end
43
+ end
44
+ end
45
+
46
+ extend ModuleMethods
47
+
48
+ module ClassMethods
49
+ #
50
+ # Gets or sets the XDG sub-directory name used by the command.
51
+ #
52
+ # @param [#to_s, nil] new_namespace
53
+ # If a new_namespace argument is given, it will set the class'es
54
+ # {#xdg_namespace} string.
55
+ #
56
+ # @return [String]
57
+ # The class'es or superclass'es {#xdg_namespace}. Defaults to
58
+ # {CommandName::ClassMethods#command_name} if no {#xdg_namespace} has
59
+ # been defined.
60
+ #
61
+ def xdg_namespace(new_namespace=nil)
62
+ if new_namespace
63
+ @xdg_namespace = new_namespace.to_s
64
+ else
65
+ @xdg_namespace || (superclass.xdg_namespace if superclass.kind_of?(ClassMethods)) || command_name
66
+ end
67
+ end
68
+ end
69
+
70
+ # The `~/.config/<xdg_namespace>` directory.
71
+ #
72
+ # @return [String]
73
+ attr_reader :config_dir
74
+
75
+ # The `~/.local/share/<xdg_namespace>` directory.
76
+ #
77
+ # @return [String]
78
+ attr_reader :local_share_dir
79
+
80
+ # The `~/.cache/<xdg_namespace>` directory.
81
+ #
82
+ # @return [String]
83
+ attr_reader :cache_dir
84
+
85
+ #
86
+ # Initializes {#config_dir}, {#local_share_dir}, and {#cache_dir}.
87
+ #
88
+ # @param [Hash{Symbol => Object}] kwargs
89
+ # Additional keyword arguments.
90
+ #
91
+ # @note
92
+ # If the `$XDG_CONFIG_HOME` env variable is set, then {#config_dir} will
93
+ # be initialized to effectively `$XDG_CONFIG_HOME/<xdg_namespace>`.
94
+ # Otherwise {#config_dir} will be initialized to
95
+ # `~/.config/<xdg_namespace>`.
96
+ #
97
+ # @note
98
+ # If the `$XDG_DATA_HOME` env variable is set, then {#local_share_dir}
99
+ # will be initialized to effectively `$XDG_DATA_HOME/<xdg_namespace>`.
100
+ # Otherwise {#local_share_dir} will be initialized to
101
+ # `~/.local/share/<xdg_namespace>`.
102
+ #
103
+ # @note
104
+ # If the `$XDG_CACHE_HOME` env variable is set, then {#cache_dir} will
105
+ # be initialized to effectively `$XDG_CACHE_HOME/<xdg_namespace>`.
106
+ # Otherwise {#cache_dir} will be initialized to
107
+ # `~/.cache/<xdg_namespace>`.
108
+ #
109
+ def initialize(**kwargs)
110
+ super(**kwargs)
111
+
112
+ xdg_config_home = env.fetch('XDG_CONFIG_HOME') do
113
+ File.join(home_dir,'.config')
114
+ end
115
+
116
+ @config_dir = File.join(xdg_config_home,xdg_namespace)
117
+
118
+ xdg_data_home = env.fetch('XDG_DATA_HOME') do
119
+ File.join(home_dir,'.local','share')
120
+ end
121
+
122
+ @local_share_dir = File.join(xdg_data_home,xdg_namespace)
123
+
124
+ xdg_cache_home = env.fetch('XDG_CACHE_HOME') do
125
+ File.join(home_dir,'.cache')
126
+ end
127
+
128
+ @cache_dir = File.join(xdg_cache_home,xdg_namespace)
129
+ end
130
+
131
+ #
132
+ # @see ClassMethods#xdg_namespace
133
+ #
134
+ def xdg_namespace
135
+ self.class.xdg_namespace
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,169 @@
1
+ require 'spec_helper'
2
+ require 'command_kit/arguments/argument'
3
+
4
+ describe Arguments::Argument do
5
+ let(:name) { :foo }
6
+ let(:type) { String }
7
+ let(:usage) { 'FOO' }
8
+ let(:default) { 'foo' }
9
+ let(:required) { true }
10
+ let(:repeats) { false }
11
+ let(:desc) { 'Foo argument' }
12
+
13
+ subject do
14
+ described_class.new name, type: type,
15
+ usage: usage,
16
+ default: default,
17
+ required: required,
18
+ repeats: repeats,
19
+ desc: desc
20
+ end
21
+
22
+ describe "#initialize" do
23
+ context "when the type: keyword is given" do
24
+ subject { described_class.new(name, type: type, desc: desc) }
25
+
26
+ it "must set #type" do
27
+ expect(subject.type).to eq(type)
28
+ end
29
+ end
30
+
31
+ context "when the type: keyword is not given" do
32
+ subject { described_class.new(name, desc: desc) }
33
+
34
+ it "default #type to String" do
35
+ expect(subject.type).to eq(String)
36
+ end
37
+ end
38
+
39
+ context "when the usage: keyword is given" do
40
+ subject { described_class.new(name, usage: usage, desc: desc) }
41
+
42
+ it "must include usage: in #usage" do
43
+ expect(subject.usage).to include(usage)
44
+ end
45
+ end
46
+
47
+ context "when the usage: keyword is not given" do
48
+ subject { described_class.new(name, desc: desc) }
49
+
50
+ it "should use uppercased argument name in #usage" do
51
+ expect(subject.usage).to include(name.to_s.upcase)
52
+ end
53
+ end
54
+
55
+ context "when the default: keyword is given" do
56
+ subject { described_class.new(name, default: default, desc: desc) }
57
+
58
+ it "must set #default" do
59
+ expect(subject.default).to eq(default)
60
+ end
61
+ end
62
+
63
+ context "when the default: keyword is not given" do
64
+ subject { described_class.new(name, desc: desc) }
65
+
66
+ it "default #default to String" do
67
+ expect(subject.default).to eq(nil)
68
+ end
69
+ end
70
+
71
+ context "when the required: keyword is given" do
72
+ subject { described_class.new(name, required: required, desc: desc) }
73
+
74
+ it "must set #required" do
75
+ expect(subject.required).to eq(required)
76
+ end
77
+ end
78
+
79
+ context "when the required: keyword is not given" do
80
+ subject { described_class.new(name, desc: desc) }
81
+
82
+ it "default #required to String" do
83
+ expect(subject.required).to eq(true)
84
+ end
85
+ end
86
+
87
+ context "when the repeats: keyword is given" do
88
+ subject { described_class.new(name, repeats: repeats, desc: desc) }
89
+
90
+ it "must set #repeats" do
91
+ expect(subject.repeats).to eq(repeats)
92
+ end
93
+ end
94
+
95
+ context "when the repeats: keyword is not given" do
96
+ subject { described_class.new(name, desc: desc) }
97
+
98
+ it "default #repeats to String" do
99
+ expect(subject.repeats).to eq(false)
100
+ end
101
+ end
102
+
103
+ context "when the desc: keyword is given" do
104
+ subject { described_class.new(name, desc: desc) }
105
+
106
+ it "must set #desc" do
107
+ expect(subject.desc).to eq(desc)
108
+ end
109
+ end
110
+
111
+ context "when the desc: keyword is not given" do
112
+ it do
113
+ expect {
114
+ described_class.new(name)
115
+ }.to raise_error(ArgumentError)
116
+ end
117
+ end
118
+ end
119
+
120
+ describe "#repeats?" do
121
+ context "when initialized with repeats: true" do
122
+ let(:repeats) { true }
123
+
124
+ it { expect(subject.repeats?).to be(true) }
125
+ end
126
+
127
+ context "when initialized with repeats: false" do
128
+ let(:repeats) { false }
129
+
130
+ it { expect(subject.repeats?).to be(false) }
131
+ end
132
+ end
133
+
134
+ describe "#usage" do
135
+ let(:usage) { 'FOO' }
136
+
137
+ context "initialized with required: true" do
138
+ let(:required) { true }
139
+
140
+ it "must return the usage unchanged" do
141
+ expect(subject.usage).to eq(usage)
142
+ end
143
+ end
144
+
145
+ context "initialized with required: false" do
146
+ let(:required) { false }
147
+
148
+ it "must wrap the usage in [ ] brackets" do
149
+ expect(subject.usage).to eq("[#{usage}]")
150
+ end
151
+ end
152
+
153
+ context "initialized with repeats: true" do
154
+ let(:repeats) { true }
155
+
156
+ it "must append the ... ellipses" do
157
+ expect(subject.usage).to eq("#{usage} ...")
158
+ end
159
+ end
160
+
161
+ context "initialized with repeats: false" do
162
+ let(:repeats) { false }
163
+
164
+ it "must return the usage name unchanged" do
165
+ expect(subject.usage).to eq("#{usage}")
166
+ end
167
+ end
168
+ end
169
+ end