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,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