command_kit 0.1.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +18 -3
  3. data/.rubocop.yml +141 -0
  4. data/ChangeLog.md +165 -0
  5. data/Gemfile +3 -0
  6. data/README.md +186 -118
  7. data/Rakefile +3 -2
  8. data/command_kit.gemspec +4 -4
  9. data/examples/command.rb +1 -1
  10. data/gemspec.yml +7 -0
  11. data/lib/command_kit/arguments/argument.rb +2 -2
  12. data/lib/command_kit/arguments.rb +36 -7
  13. data/lib/command_kit/colors.rb +702 -53
  14. data/lib/command_kit/command.rb +2 -3
  15. data/lib/command_kit/commands/auto_load.rb +8 -1
  16. data/lib/command_kit/commands/help.rb +3 -2
  17. data/lib/command_kit/commands/subcommand.rb +1 -1
  18. data/lib/command_kit/commands.rb +24 -9
  19. data/lib/command_kit/env/path.rb +1 -1
  20. data/lib/command_kit/file_utils.rb +46 -0
  21. data/lib/command_kit/help/man.rb +17 -33
  22. data/lib/command_kit/inflector.rb +47 -17
  23. data/lib/command_kit/interactive.rb +9 -0
  24. data/lib/command_kit/main.rb +7 -9
  25. data/lib/command_kit/man.rb +44 -0
  26. data/lib/command_kit/open_app.rb +69 -0
  27. data/lib/command_kit/options/option.rb +41 -27
  28. data/lib/command_kit/options/option_value.rb +3 -2
  29. data/lib/command_kit/options/parser.rb +17 -22
  30. data/lib/command_kit/options.rb +102 -14
  31. data/lib/command_kit/os/linux.rb +157 -0
  32. data/lib/command_kit/os.rb +159 -11
  33. data/lib/command_kit/package_manager.rb +200 -0
  34. data/lib/command_kit/pager.rb +46 -4
  35. data/lib/command_kit/printing/indent.rb +4 -4
  36. data/lib/command_kit/printing.rb +14 -3
  37. data/lib/command_kit/program_name.rb +9 -0
  38. data/lib/command_kit/sudo.rb +40 -0
  39. data/lib/command_kit/terminal.rb +5 -0
  40. data/lib/command_kit/version.rb +1 -1
  41. data/spec/arguments/argument_spec.rb +1 -1
  42. data/spec/arguments_spec.rb +84 -1
  43. data/spec/colors_spec.rb +357 -70
  44. data/spec/command_spec.rb +77 -6
  45. data/spec/commands/auto_load_spec.rb +33 -2
  46. data/spec/commands_spec.rb +101 -29
  47. data/spec/env/path_spec.rb +6 -0
  48. data/spec/exception_handler_spec.rb +1 -1
  49. data/spec/file_utils_spec.rb +59 -0
  50. data/spec/fixtures/template.erb +5 -0
  51. data/spec/help/man_spec.rb +54 -57
  52. data/spec/inflector_spec.rb +70 -8
  53. data/spec/man_spec.rb +46 -0
  54. data/spec/open_app_spec.rb +85 -0
  55. data/spec/options/option_spec.rb +38 -2
  56. data/spec/options/option_value_spec.rb +55 -0
  57. data/spec/options/parser_spec.rb +0 -10
  58. data/spec/options_spec.rb +328 -0
  59. data/spec/os/linux_spec.rb +164 -0
  60. data/spec/os_spec.rb +200 -13
  61. data/spec/package_manager_spec.rb +806 -0
  62. data/spec/pager_spec.rb +71 -6
  63. data/spec/printing/indent_spec.rb +7 -5
  64. data/spec/printing_spec.rb +23 -1
  65. data/spec/program_name_spec.rb +8 -0
  66. data/spec/sudo_spec.rb +51 -0
  67. data/spec/terminal_spec.rb +30 -0
  68. data/spec/usage_spec.rb +1 -1
  69. metadata +23 -4
@@ -0,0 +1,200 @@
1
+ require 'command_kit/os'
2
+ require 'command_kit/os/linux'
3
+ require 'command_kit/env/path'
4
+ require 'command_kit/sudo'
5
+
6
+ module CommandKit
7
+ #
8
+ # Allows installing packages using the system's package manager.
9
+ #
10
+ # Supports the following package managers:
11
+ #
12
+ # * Linux
13
+ # * Debian / Ubuntu
14
+ # * `apt`
15
+ # * RedHat / Fedora
16
+ # * `dnf`
17
+ # * `yum`
18
+ # * Arch
19
+ # * `pacman`
20
+ # * SUSE / OpenSUSE
21
+ # * `zypper`
22
+ # * macOS
23
+ # * `brew`
24
+ # * `port`
25
+ # * FreeBSD
26
+ # * `pkg`
27
+ # * OpenBSD
28
+ # * `pkg_add`
29
+ #
30
+ # ## Examples
31
+ #
32
+ # unless install_packages("nmap")
33
+ # print_error "failed to install nmap"
34
+ # exit -1
35
+ # end
36
+ #
37
+ # ### Installing multiple packages
38
+ #
39
+ # install_packages apt: ["libxml2-dev", ...],
40
+ # dnf: ["libxml2-devel", ...],
41
+ # brew: ["libxml2", ...],
42
+ # ...
43
+ #
44
+ # @since 0.2.0
45
+ #
46
+ module PackageManager
47
+ include OS
48
+ include OS::Linux
49
+ include Env::Path
50
+ include Sudo
51
+
52
+ # The detected package manager.
53
+ #
54
+ # @return [:apt, :dnf, :yum, :zypper, :pacman, :brew, :pkg, :pkg_add, nil]
55
+ attr_reader :package_manager
56
+
57
+ #
58
+ # Initializes the command and determines which open command to use.
59
+ #
60
+ # @param [:apt, :dnf, :yum, :zypper, :pacman, :brew, :pkg, :pkg_add, nil] package_manager
61
+ # The explicit package manager to use. If `nil`, the package manager will
62
+ # be detected.
63
+ #
64
+ def initialize(package_manager: nil, **kwargs)
65
+ super(**kwargs)
66
+
67
+ @package_manager = package_manager || begin
68
+ if macos?
69
+ if command_installed?('brew') then :brew
70
+ elsif command_installed?('port') then :port
71
+ end
72
+ elsif linux?
73
+ if redhat_linux?
74
+ if command_installed?('dnf') then :dnf
75
+ elsif command_installed?('yum') then :yum
76
+ end
77
+ elsif debian_linux?
78
+ :apt if command_installed?('apt')
79
+ elsif suse_linux?
80
+ :zypper if command_installed?('zypper')
81
+ elsif arch_linux?
82
+ :pacman if command_installed?('pacman')
83
+ end
84
+ elsif freebsd?
85
+ :pkg if command_installed?('pkg')
86
+ elsif openbsd?
87
+ :pkg_add if command_installed?('pkg_add')
88
+ end
89
+ end
90
+ end
91
+
92
+ #
93
+ # Installs the packages using the system's package manager.
94
+ #
95
+ # @param [Array<String>, String] packages
96
+ # A list of package name(s) to install.
97
+ #
98
+ # @param [Boolean] yes
99
+ # Assume yes for all user prompts.
100
+ #
101
+ # @param [Array<String>, String] apt
102
+ # List of `apt` specific package names.
103
+ #
104
+ # @param [Array<String>, String] brew
105
+ # List of `brew` specific package names.
106
+ #
107
+ # @param [Array<String>, String] dnf
108
+ # List of `dnf` specific package names.
109
+ #
110
+ # @param [Array<String>, String] pacman
111
+ # List of `pacman` specific package names.
112
+ #
113
+ # @param [Array<String>, String] pkg
114
+ # List of `pkg` specific package names.
115
+ #
116
+ # @param [Array<String>, String] pkg_add
117
+ # List of `pkg_add` specific package names.
118
+ #
119
+ # @param [Array<String>, String] port
120
+ # List of `port` specific package names.
121
+ #
122
+ # @param [Array<String>, String] yum
123
+ # List of `yum` specific package names.
124
+ #
125
+ # @param [Array<String>, String] zypper
126
+ # List of `zypper` specific package names.
127
+ #
128
+ # @return [Boolean, nil]
129
+ # Specifies whether the packages were successfully installed or not.
130
+ # If the package manager command could not be determined, `nil` is
131
+ # returned.
132
+ #
133
+ # @example Install a package
134
+ # install_packages "nmap", ...
135
+ #
136
+ # @example Install a list of packages per package-manager
137
+ # install_packages apt: ["libxml2-dev", ...],
138
+ # dnf: ["libxml2-devel", ...],
139
+ # brew: ["libxml2", ...],
140
+ # ...
141
+ #
142
+ def install_packages(*packages, yes: false,
143
+ apt: nil,
144
+ brew: nil,
145
+ dnf: nil,
146
+ pacman: nil,
147
+ pkg: nil,
148
+ pkg_add: nil,
149
+ port: nil,
150
+ yum: nil,
151
+ zypper: nil)
152
+ specific_package_names = case @package_manager
153
+ when :apt then apt
154
+ when :brew then brew
155
+ when :dnf then dnf
156
+ when :pacman then pacman
157
+ when :pkg then pkg
158
+ when :pkg_add then pkg_add
159
+ when :port then port
160
+ when :yum then yum
161
+ when :zypper then zypper
162
+ end
163
+ packages += Array(specific_package_names)
164
+
165
+ case @package_manager
166
+ when :apt
167
+ args = []
168
+ args << '-y' if yes
169
+
170
+ sudo('apt','install',*args,*packages)
171
+ when :brew
172
+ system('brew','install',*packages)
173
+ when :dnf, :yum
174
+ args = []
175
+ args << '-y' if yes
176
+
177
+ sudo(@package_manager.to_s,'install',*args,*packages)
178
+ when :pacman
179
+ missing_packages = `pacman -T #{Shellwords.shelljoin(packages)}`.split
180
+
181
+ if missing_packages.empty?
182
+ return true
183
+ end
184
+
185
+ sudo('pacman','-S',*missing_packages)
186
+ when :pkg
187
+ args = []
188
+ args << '-y' if yes
189
+
190
+ sudo('pkg','install',*args,*packages)
191
+ when :pkg_add
192
+ sudo('pkg_add',*packages)
193
+ when :port
194
+ sudo('port','install',*packages)
195
+ when :zypper
196
+ sudo('zypper','-n','in','-l',*packages)
197
+ end
198
+ end
199
+ end
200
+ end
@@ -5,6 +5,8 @@ require 'command_kit/env/path'
5
5
  require 'command_kit/stdio'
6
6
  require 'command_kit/terminal'
7
7
 
8
+ require 'shellwords'
9
+
8
10
  module CommandKit
9
11
  #
10
12
  # Allows opening a pager, such as `less` or `more`.
@@ -50,7 +52,7 @@ module CommandKit
50
52
  # Keyword arguments.
51
53
  #
52
54
  # @note
53
- # Respects the `PAGER` env variable, or attemps to find `less` or
55
+ # Respects the `PAGER` env variable, or attempts to find `less` or
54
56
  # `more` by searching the `PATH` env variable.
55
57
  #
56
58
  # @api public
@@ -58,7 +60,7 @@ module CommandKit
58
60
  def initialize(**kwargs)
59
61
  super(**kwargs)
60
62
 
61
- @pager = env.fetch('PAGER') do
63
+ @pager_command = env.fetch('PAGER') do
62
64
  PAGERS.find do |command|
63
65
  bin = command.split(' ',2).first
64
66
 
@@ -94,14 +96,14 @@ module CommandKit
94
96
  # @api public
95
97
  #
96
98
  def pager
97
- if !stdout.tty? || @pager.nil?
99
+ if !stdout.tty? || @pager_command.nil?
98
100
  # fallback to stdout if the process does not have a terminal or we could
99
101
  # not find a suitable pager command.
100
102
  yield stdout
101
103
  return
102
104
  end
103
105
 
104
- io = IO.popen(@pager,'w')
106
+ io = IO.popen(@pager_command,'w')
105
107
  pid = io.pid
106
108
 
107
109
  begin
@@ -144,5 +146,45 @@ module CommandKit
144
146
  stdout.puts(data)
145
147
  end
146
148
  end
149
+
150
+ #
151
+ # Pipes a command into the pager.
152
+ #
153
+ # @param [#to_s] command
154
+ # The program or command to run.
155
+ #
156
+ # @param [Array<#to_s>] arguments
157
+ # Additional arguments for the program.
158
+ #
159
+ # @return [Boolean]
160
+ # Indicates whether the command exited successfully or not.
161
+ #
162
+ # @note
163
+ # If multiple arguments are given, they will be shell-escaped and executed
164
+ # as a single command.
165
+ # If a single command is given, it will not be shell-escaped to allow
166
+ # executing compound shell commands.
167
+ #
168
+ # @example Pipe a single command into the pager:
169
+ # run_in_pager 'find', '.', '-name', '*.md'
170
+ #
171
+ # @example Pipe a compound command into the pager:
172
+ # run_in_pager "wc -l /path/to/wordlists/*.txt | sort -n"
173
+ #
174
+ # @api public
175
+ #
176
+ # @since 0.2.0
177
+ #
178
+ def pipe_to_pager(command,*arguments)
179
+ if @pager_command
180
+ unless arguments.empty?
181
+ command = Shellwords.shelljoin([command, *arguments])
182
+ end
183
+
184
+ system("#{command} | #{@pager_command}")
185
+ else
186
+ system(command.to_s,*arguments.map(&:to_s))
187
+ end
188
+ end
147
189
  end
148
190
  end
@@ -20,7 +20,7 @@ module CommandKit
20
20
  #
21
21
  module Indent
22
22
  #
23
- # Initializes the indententation level to zero.
23
+ # Initializes the indentation level to zero.
24
24
  #
25
25
  def initialize(**kwargs)
26
26
  @indent = 0
@@ -40,7 +40,7 @@ module CommandKit
40
40
  # increased.
41
41
  #
42
42
  # @return [Integer]
43
- # If no block is given, the indententation level will be returned.
43
+ # If no block is given, the indentation level will be returned.
44
44
  #
45
45
  # @example
46
46
  # puts "values:"
@@ -65,10 +65,10 @@ module CommandKit
65
65
  #
66
66
  def indent(n=2)
67
67
  if block_given?
68
+ original_indent = @indent
69
+
68
70
  begin
69
- original_indent = @indent
70
71
  @indent += n
71
-
72
72
  yield
73
73
  ensure
74
74
  @indent = original_indent
@@ -9,7 +9,7 @@ module CommandKit
9
9
  module Printing
10
10
  include Stdio
11
11
 
12
- # Platform independency new-line constant
12
+ # Platform independent new-line constant
13
13
  #
14
14
  # @return [String]
15
15
  #
@@ -23,12 +23,23 @@ module CommandKit
23
23
  # The error message.
24
24
  #
25
25
  # @example
26
- # print_error "Error: invalid input"
26
+ # print_error "error: invalid input"
27
+ # # error: invalid input
28
+ #
29
+ # @example When CommandKit::CommandName is included:
30
+ # print_error "invalid input"
31
+ # # foo: invalid input
27
32
  #
28
33
  # @api public
29
34
  #
30
35
  def print_error(message)
31
- stderr.puts message
36
+ if respond_to?(:command_name)
37
+ # if #command_name is available, prefix all error messages with it
38
+ stderr.puts "#{command_name}: #{message}"
39
+ else
40
+ # if #command_name is not available, just print the error message as-is
41
+ stderr.puts message
42
+ end
32
43
  end
33
44
 
34
45
  #
@@ -60,5 +60,14 @@ module CommandKit
60
60
  def program_name
61
61
  self.class.program_name
62
62
  end
63
+
64
+ #
65
+ # @see #program_name
66
+ #
67
+ # @since 0.3.0
68
+ #
69
+ def command_name
70
+ program_name
71
+ end
63
72
  end
64
73
  end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'command_kit/os'
4
+
5
+ module CommandKit
6
+ #
7
+ # Allows running commands with `sudo`.
8
+ #
9
+ # @since 0.2.0
10
+ #
11
+ module Sudo
12
+ include OS
13
+
14
+ #
15
+ # Runs the command under sudo, if the user isn't already root.
16
+ #
17
+ # @param [String] command
18
+ # The command to execute.
19
+ #
20
+ # @param [Array<String>] arguments
21
+ # Additional arguments for the command.
22
+ #
23
+ # @return [Boolean, nil]
24
+ # Specifies whether the command was successfully ran or not.
25
+ #
26
+ # @api public
27
+ #
28
+ def sudo(command,*arguments)
29
+ if windows?
30
+ system('runas','/user:administrator',command,*arguments)
31
+ else
32
+ if Process.uid == 0
33
+ system(command,*arguments)
34
+ else
35
+ system('sudo',command,*arguments)
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -71,6 +71,11 @@ module CommandKit
71
71
  IO.respond_to?(:console) && stdout.tty?
72
72
  end
73
73
 
74
+ #
75
+ # @since 0.2.0
76
+ #
77
+ alias tty? terminal?
78
+
74
79
  #
75
80
  # Returns the terminal object, if {Stdio#stdout stdout} is connected to a
76
81
  # terminal.
@@ -1,4 +1,4 @@
1
1
  module CommandKit
2
2
  # command_kit version
3
- VERSION = "0.1.0"
3
+ VERSION = "0.3.0"
4
4
  end
@@ -126,7 +126,7 @@ describe CommandKit::Arguments::Argument do
126
126
  let(:repeats) { false }
127
127
 
128
128
  it "must return the usage name unchanged" do
129
- expect(subject.usage).to eq("#{usage}")
129
+ expect(subject.usage).to eq(usage)
130
130
  end
131
131
  end
132
132
  end
@@ -137,6 +137,56 @@ describe CommandKit::Arguments do
137
137
 
138
138
  subject { command_class.new }
139
139
 
140
+ describe "#main" do
141
+ module TestArguments
142
+ class TestCommand
143
+
144
+ include CommandKit::Arguments
145
+
146
+ argument :argument1, required: true,
147
+ usage: 'ARG1',
148
+ desc: "Argument 1"
149
+
150
+ argument :argument2, required: false,
151
+ usage: 'ARG2',
152
+ desc: "Argument 2"
153
+
154
+ end
155
+ end
156
+
157
+ let(:command_class) { TestArguments::TestCommand }
158
+
159
+ context "when given the correct number of arguments" do
160
+ let(:argv) { %w[arg1 arg2] }
161
+
162
+ it "must parse options before validating the number of arguments" do
163
+ expect {
164
+ expect(subject.main(argv)).to eq(0)
165
+ }.to_not output.to_stderr
166
+ end
167
+ end
168
+
169
+ context "when given fewer than the required number of arguments" do
170
+ let(:argv) { %w[] }
171
+
172
+ it "must print an error message and return 1" do
173
+ expect {
174
+ expect(subject.main(argv)).to eq(1)
175
+ }.to output("#{subject.command_name}: insufficient number of arguments.#{$/}").to_stderr
176
+ end
177
+ end
178
+
179
+ context "when given more than the total number of arguments" do
180
+ let(:argv) { %w[foo bar baz] }
181
+
182
+ it "must print an error message and return 1" do
183
+ expect {
184
+ expect(subject.main(argv)).to eq(1)
185
+ }.to output("#{subject.command_name}: too many arguments given.#{$/}").to_stderr
186
+ end
187
+ end
188
+ end
189
+
140
190
  describe "#help_arguments" do
141
191
  context "when #arguments returns {}" do
142
192
  module TestArguments
@@ -153,7 +203,7 @@ describe CommandKit::Arguments do
153
203
  end
154
204
  end
155
205
 
156
- context "when #arguments returns an Array" do
206
+ context "when #arguments returns a Hash" do
157
207
  module TestArguments
158
208
  class MultipleArguments
159
209
  include CommandKit::Arguments
@@ -183,6 +233,39 @@ describe CommandKit::Arguments do
183
233
  ].join($/)
184
234
  ).to_stdout
185
235
  end
236
+
237
+ context "when one the argument has an Array for a description" do
238
+ module TestArguments
239
+ class MultiLineArgumentDescription
240
+ include CommandKit::Arguments
241
+
242
+ argument :foo, desc: "Foo option"
243
+ argument :bar, desc: [
244
+ "Bar option",
245
+ "Line 2",
246
+ "Line 3"
247
+ ]
248
+ argument :baz, desc: "Baz option"
249
+ end
250
+ end
251
+
252
+ let(:command_class) { TestArguments::MultiLineArgumentDescription }
253
+
254
+ it "must print out each line of a multi-line argument description" do
255
+ expect { subject.help_arguments }.to output(
256
+ [
257
+ '',
258
+ "Arguments:",
259
+ " #{foo_argument.usage.ljust(33)}#{foo_argument.desc}",
260
+ " #{bar_argument.usage.ljust(33)}#{bar_argument.desc[0]}",
261
+ " #{' '.ljust(33)}#{bar_argument.desc[1]}",
262
+ " #{' '.ljust(33)}#{bar_argument.desc[2]}",
263
+ " #{baz_argument.usage.ljust(33)}#{baz_argument.desc}",
264
+ ''
265
+ ].join($/)
266
+ ).to_stdout
267
+ end
268
+ end
186
269
  end
187
270
  end
188
271