command_kit 0.1.0 → 0.3.0
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.
- checksums.yaml +4 -4
- data/.github/workflows/ruby.yml +18 -3
- data/.rubocop.yml +141 -0
- data/ChangeLog.md +165 -0
- data/Gemfile +3 -0
- data/README.md +186 -118
- data/Rakefile +3 -2
- data/command_kit.gemspec +4 -4
- data/examples/command.rb +1 -1
- data/gemspec.yml +7 -0
- data/lib/command_kit/arguments/argument.rb +2 -2
- data/lib/command_kit/arguments.rb +36 -7
- data/lib/command_kit/colors.rb +702 -53
- data/lib/command_kit/command.rb +2 -3
- data/lib/command_kit/commands/auto_load.rb +8 -1
- data/lib/command_kit/commands/help.rb +3 -2
- data/lib/command_kit/commands/subcommand.rb +1 -1
- data/lib/command_kit/commands.rb +24 -9
- data/lib/command_kit/env/path.rb +1 -1
- data/lib/command_kit/file_utils.rb +46 -0
- data/lib/command_kit/help/man.rb +17 -33
- data/lib/command_kit/inflector.rb +47 -17
- data/lib/command_kit/interactive.rb +9 -0
- data/lib/command_kit/main.rb +7 -9
- data/lib/command_kit/man.rb +44 -0
- data/lib/command_kit/open_app.rb +69 -0
- data/lib/command_kit/options/option.rb +41 -27
- data/lib/command_kit/options/option_value.rb +3 -2
- data/lib/command_kit/options/parser.rb +17 -22
- data/lib/command_kit/options.rb +102 -14
- data/lib/command_kit/os/linux.rb +157 -0
- data/lib/command_kit/os.rb +159 -11
- data/lib/command_kit/package_manager.rb +200 -0
- data/lib/command_kit/pager.rb +46 -4
- data/lib/command_kit/printing/indent.rb +4 -4
- data/lib/command_kit/printing.rb +14 -3
- data/lib/command_kit/program_name.rb +9 -0
- data/lib/command_kit/sudo.rb +40 -0
- data/lib/command_kit/terminal.rb +5 -0
- data/lib/command_kit/version.rb +1 -1
- data/spec/arguments/argument_spec.rb +1 -1
- data/spec/arguments_spec.rb +84 -1
- data/spec/colors_spec.rb +357 -70
- data/spec/command_spec.rb +77 -6
- data/spec/commands/auto_load_spec.rb +33 -2
- data/spec/commands_spec.rb +101 -29
- data/spec/env/path_spec.rb +6 -0
- data/spec/exception_handler_spec.rb +1 -1
- data/spec/file_utils_spec.rb +59 -0
- data/spec/fixtures/template.erb +5 -0
- data/spec/help/man_spec.rb +54 -57
- data/spec/inflector_spec.rb +70 -8
- data/spec/man_spec.rb +46 -0
- data/spec/open_app_spec.rb +85 -0
- data/spec/options/option_spec.rb +38 -2
- data/spec/options/option_value_spec.rb +55 -0
- data/spec/options/parser_spec.rb +0 -10
- data/spec/options_spec.rb +328 -0
- data/spec/os/linux_spec.rb +164 -0
- data/spec/os_spec.rb +200 -13
- data/spec/package_manager_spec.rb +806 -0
- data/spec/pager_spec.rb +71 -6
- data/spec/printing/indent_spec.rb +7 -5
- data/spec/printing_spec.rb +23 -1
- data/spec/program_name_spec.rb +8 -0
- data/spec/sudo_spec.rb +51 -0
- data/spec/terminal_spec.rb +30 -0
- data/spec/usage_spec.rb +1 -1
- 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
|
data/lib/command_kit/pager.rb
CHANGED
@@ -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
|
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
|
-
@
|
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? || @
|
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(@
|
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
|
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
|
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
|
data/lib/command_kit/printing.rb
CHANGED
@@ -9,7 +9,7 @@ module CommandKit
|
|
9
9
|
module Printing
|
10
10
|
include Stdio
|
11
11
|
|
12
|
-
# Platform
|
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 "
|
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
|
-
|
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
|
#
|
@@ -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
|
data/lib/command_kit/terminal.rb
CHANGED
data/lib/command_kit/version.rb
CHANGED
data/spec/arguments_spec.rb
CHANGED
@@ -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
|
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
|
|