command_kit 0.1.0 → 0.2.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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +15 -0
  3. data/.rubocop.yml +138 -0
  4. data/ChangeLog.md +29 -0
  5. data/Gemfile +3 -0
  6. data/README.md +141 -121
  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.rb +1 -1
  12. data/lib/command_kit/colors.rb +221 -45
  13. data/lib/command_kit/command.rb +1 -1
  14. data/lib/command_kit/commands.rb +4 -4
  15. data/lib/command_kit/help/man.rb +4 -25
  16. data/lib/command_kit/inflector.rb +47 -17
  17. data/lib/command_kit/main.rb +7 -9
  18. data/lib/command_kit/man.rb +44 -0
  19. data/lib/command_kit/open_app.rb +69 -0
  20. data/lib/command_kit/options/option.rb +1 -6
  21. data/lib/command_kit/options/parser.rb +15 -17
  22. data/lib/command_kit/options.rb +2 -2
  23. data/lib/command_kit/os/linux.rb +157 -0
  24. data/lib/command_kit/os.rb +159 -11
  25. data/lib/command_kit/package_manager.rb +200 -0
  26. data/lib/command_kit/pager.rb +46 -4
  27. data/lib/command_kit/printing/indent.rb +2 -2
  28. data/lib/command_kit/printing.rb +1 -1
  29. data/lib/command_kit/sudo.rb +40 -0
  30. data/lib/command_kit/terminal.rb +5 -0
  31. data/lib/command_kit/version.rb +1 -1
  32. data/spec/arguments/argument_spec.rb +1 -1
  33. data/spec/colors_spec.rb +256 -0
  34. data/spec/commands_spec.rb +1 -1
  35. data/spec/exception_handler_spec.rb +1 -1
  36. data/spec/help/man_spec.rb +0 -32
  37. data/spec/inflector_spec.rb +70 -8
  38. data/spec/man_spec.rb +46 -0
  39. data/spec/open_app_spec.rb +85 -0
  40. data/spec/options/option_spec.rb +2 -2
  41. data/spec/os/linux_spec.rb +154 -0
  42. data/spec/os_spec.rb +200 -13
  43. data/spec/package_manager_spec.rb +806 -0
  44. data/spec/pager_spec.rb +71 -6
  45. data/spec/sudo_spec.rb +51 -0
  46. data/spec/terminal_spec.rb +30 -0
  47. data/spec/usage_spec.rb +1 -1
  48. metadata +19 -4
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'strscan'
4
+
3
5
  module CommandKit
4
6
  #
5
7
  # A very simple inflector.
@@ -33,16 +35,37 @@ module CommandKit
33
35
  # @return [String]
34
36
  # The resulting under_scored name.
35
37
  #
38
+ # @raise [ArgumentError]
39
+ # The given string contained non-alpha-numeric characters.
40
+ #
36
41
  def self.underscore(name)
37
- # sourced from: https://github.com/dry-rb/dry-inflector/blob/c918f967ff82611da374eb0847a77b7e012d3fa8/lib/dry/inflector.rb#L286-L287
38
- name = name.to_s.dup
42
+ scanner = StringScanner.new(name.to_s)
43
+ new_string = String.new
44
+
45
+ until scanner.eos?
46
+ if (separator = scanner.scan(/[_-]+/))
47
+ new_string << '_' * separator.length
48
+ else
49
+ if (capitalized = scanner.scan(/[A-Z][a-z\d]+/))
50
+ new_string << capitalized
51
+ elsif (uppercase = scanner.scan(/[A-Z][A-Z\d]*(?=[A-Z_-]|$)/))
52
+ new_string << uppercase
53
+ elsif (lowercase = scanner.scan(/[a-z][a-z\d]*/))
54
+ new_string << lowercase
55
+ else
56
+ raise(ArgumentError,"cannot convert string to underscored: #{scanner.string.inspect}")
57
+ end
39
58
 
40
- name.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
41
- name.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
42
- name.tr!('-','_')
43
- name.downcase!
59
+ if (separator = scanner.scan(/[_-]+/))
60
+ new_string << '_' * separator.length
61
+ elsif !scanner.eos?
62
+ new_string << '_'
63
+ end
64
+ end
65
+ end
44
66
 
45
- name
67
+ new_string.downcase!
68
+ new_string
46
69
  end
47
70
 
48
71
  #
@@ -67,20 +90,27 @@ module CommandKit
67
90
  # @return [String]
68
91
  # The CamelCased name.
69
92
  #
93
+ # @raise [ArgumentError]
94
+ # The given under_scored string contained non-alpha-numeric characters.
95
+ #
70
96
  def self.camelize(name)
71
- name = name.to_s.dup
72
-
73
- # sourced from: https://github.com/dry-rb/dry-inflector/blob/c918f967ff82611da374eb0847a77b7e012d3fa8/lib/dry/inflector.rb#L329-L334
74
- name.sub!(/^[a-z\d]*/,&:capitalize)
75
- name.gsub!(%r{(?:[_-]|(/))([a-z\d]*)}i) do |match|
76
- slash = Regexp.last_match(1)
77
- word = Regexp.last_match(2)
97
+ scanner = StringScanner.new(name.to_s)
98
+ new_string = String.new
78
99
 
79
- "#{slash}#{word.capitalize}"
100
+ until scanner.eos?
101
+ if (word = scanner.scan(/[A-Za-z\d]+/))
102
+ word.capitalize!
103
+ new_string << word
104
+ elsif scanner.scan(/[_-]+/)
105
+ # skip
106
+ elsif scanner.scan(/\//)
107
+ new_string << '::'
108
+ else
109
+ raise(ArgumentError,"cannot convert string to CamelCase: #{scanner.string.inspect}")
110
+ end
80
111
  end
81
112
 
82
- name.gsub!('/','::')
83
- name
113
+ new_string
84
114
  end
85
115
  end
86
116
  end
@@ -49,15 +49,13 @@ module CommandKit
49
49
  # @api public
50
50
  #
51
51
  def start(argv=ARGV, **kwargs)
52
- begin
53
- exit main(argv, **kwargs)
54
- rescue Interrupt
55
- # https://tldp.org/LDP/abs/html/exitcodes.html
56
- exit 130
57
- rescue Errno::EPIPE
58
- # STDOUT pipe broken
59
- exit 0
60
- end
52
+ exit main(argv, **kwargs)
53
+ rescue Interrupt
54
+ # https://tldp.org/LDP/abs/html/exitcodes.html
55
+ exit 130
56
+ rescue Errno::EPIPE
57
+ # STDOUT pipe broken
58
+ exit 0
61
59
  end
62
60
 
63
61
  #
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandKit
4
+ #
5
+ # Allows displaying man pages.
6
+ #
7
+ # ## Examples
8
+ #
9
+ # man "passwd"
10
+ # man "passwd", section: 5
11
+ #
12
+ # @since 0.2.0
13
+ #
14
+ module Man
15
+ #
16
+ # Displays the given man page.
17
+ #
18
+ # @param [String] page
19
+ # The man page file name.
20
+ #
21
+ # @param [Integer, String, nil] section
22
+ # The optional section number to specify.
23
+ #
24
+ # @return [Boolean, nil]
25
+ # Specifies whether the `man` command was successful or not.
26
+ # Returns `nil` when the `man` command is not installed.
27
+ #
28
+ # @example
29
+ # man "passwd"
30
+ #
31
+ # @example Display a man-page from a specific section:
32
+ # man "passwd", section: 5
33
+ #
34
+ # @api public
35
+ #
36
+ def man(page, section: nil)
37
+ if section
38
+ system('man',section.to_s,page.to_s)
39
+ else
40
+ system('man',page.to_s)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,69 @@
1
+ require 'command_kit/os'
2
+ require 'command_kit/env/path'
3
+
4
+ module CommandKit
5
+ #
6
+ # Allows opening a file or a URI with the system's preferred application for
7
+ # that file type or URI scheme.
8
+ #
9
+ # ## Examples
10
+ #
11
+ # open_app_for "movie.avi"
12
+ # open_app_for "https://github.com/postmodern/command_kit.rb"
13
+ #
14
+ # @since 0.2.0
15
+ #
16
+ module OpenApp
17
+ include OS
18
+ include Env::Path
19
+
20
+ #
21
+ # Initializes the command and determines which open command to use.
22
+ #
23
+ # @param [Hash{Symbol => Object}] kwargs
24
+ # Additional keyword arguments.
25
+ #
26
+ # @api public
27
+ #
28
+ def initialize(**kwargs)
29
+ super(**kwargs)
30
+
31
+ @open_command = if macos?
32
+ 'open'
33
+ elsif linux? || bsd?
34
+ if command_installed?('xdg-open')
35
+ 'xdg-open'
36
+ end
37
+ elsif windows?
38
+ if command_installed?('invoke-item')
39
+ 'invoke-item'
40
+ else
41
+ 'start'
42
+ end
43
+ end
44
+ end
45
+
46
+ #
47
+ # Opens a file or URI using the system's preferred application for that
48
+ # file type or URI scheme.
49
+ #
50
+ # @param [String, URI] file_or_uri
51
+ # The file path or URI to open.
52
+ #
53
+ # @return [Boolean, nil]
54
+ # Specifies whether the file or URI was successfully opened or not.
55
+ # If the open command could not be determined, `nil` is returned.
56
+ #
57
+ # @example Open a file:
58
+ # open_app_for "movie.avi"
59
+ #
60
+ # @example Open a URI:
61
+ # open_app_for "https://github.com/postmodern/command_kit.rb"
62
+ #
63
+ def open_app_for(file_or_uri)
64
+ if @open_command
65
+ system(@open_command,file_or_uri.to_s)
66
+ end
67
+ end
68
+ end
69
+ end
@@ -35,11 +35,6 @@ module CommandKit
35
35
  # @return [OptionValue, nil]
36
36
  attr_reader :value
37
37
 
38
- # The option's description.
39
- #
40
- # @return [String]
41
- attr_reader :desc
42
-
43
38
  # The optional block that will receive the parsed option value.
44
39
  #
45
40
  # @return [Proc, nil]
@@ -128,7 +123,7 @@ module CommandKit
128
123
  end
129
124
 
130
125
  #
131
- # The separator characer between the option and option value.
126
+ # The separator character between the option and option value.
132
127
  #
133
128
  # @return ['=', ' ', nil]
134
129
  #
@@ -118,23 +118,21 @@ module CommandKit
118
118
  # @api semipublic
119
119
  #
120
120
  def parse_options(argv)
121
- begin
122
- option_parser.parse(argv)
123
- rescue OptionParser::InvalidOption => error
124
- on_invalid_option(error)
125
- rescue OptionParser::AmbiguousOption => error
126
- on_ambiguous_option(error)
127
- rescue OptionParser::InvalidArgument => error
128
- on_invalid_argument(error)
129
- rescue OptionParser::MissingArgument => error
130
- on_missing_argument(error)
131
- rescue OptionParser::NeedlessArgument => error
132
- on_needless_argument(error)
133
- rescue OptionParser::AmbiguousArgument => error
134
- on_ambiguous_argument(error)
135
- rescue OptionParser::ParseError => error
136
- on_parse_error(error)
137
- end
121
+ option_parser.parse(argv)
122
+ rescue OptionParser::InvalidOption => error
123
+ on_invalid_option(error)
124
+ rescue OptionParser::AmbiguousOption => error
125
+ on_ambiguous_option(error)
126
+ rescue OptionParser::InvalidArgument => error
127
+ on_invalid_argument(error)
128
+ rescue OptionParser::MissingArgument => error
129
+ on_missing_argument(error)
130
+ rescue OptionParser::NeedlessArgument => error
131
+ on_needless_argument(error)
132
+ rescue OptionParser::AmbiguousArgument => error
133
+ on_ambiguous_argument(error)
134
+ rescue OptionParser::ParseError => error
135
+ on_parse_error(error)
138
136
  end
139
137
 
140
138
  #
@@ -22,7 +22,7 @@ module CommandKit
22
22
  # @bar = arg.split(':')
23
23
  # end
24
24
  #
25
- # ### initialize and using ivars
25
+ # ### initialize and using instance variables
26
26
  #
27
27
  # option :number, value: {type: Integer},
28
28
  # desc: 'Numbers' do |num|
@@ -197,7 +197,7 @@ module CommandKit
197
197
  # {Parser#option_parser option parser}.
198
198
  #
199
199
  # @param [Hash{Symbol => Object}] options
200
- # Optional pre-populated options hash.
200
+ # Optional prepopulated options hash.
201
201
  #
202
202
  # @note
203
203
  # The {#option_parser} will populate {#options} and also call any
@@ -0,0 +1,157 @@
1
+ require 'command_kit/os'
2
+
3
+ module CommandKit
4
+ module OS
5
+ #
6
+ # Provides methods for determining the specific type of Linux.
7
+ #
8
+ # ## Example
9
+ #
10
+ # require 'command_kit/command'
11
+ # require 'command_kit/os/linux'
12
+ #
13
+ # class Command < CommandKit::Command
14
+ #
15
+ # include CommandKit::OS::Linux
16
+ #
17
+ # def run
18
+ # if debian_linux?
19
+ # # ...
20
+ # elsif redhat_linux?
21
+ # # ...
22
+ # elsif suse_linux?
23
+ # # ...
24
+ # elsif arch_linux?
25
+ # # ...
26
+ # end
27
+ # end
28
+ # end
29
+ #
30
+ # @since 0.2.0
31
+ #
32
+ module Linux
33
+ #
34
+ # @api private
35
+ #
36
+ module ModuleMethods
37
+ #
38
+ # Extends {ClassMethods} or {ModuleMethods}, depending on whether
39
+ # {OS} is being included into a class or a module..
40
+ #
41
+ # @param [Class, Module] context
42
+ # The class or module which is including {OS}.
43
+ #
44
+ def included(context)
45
+ super(context)
46
+
47
+ if context.class == Module
48
+ context.extend ModuleMethods
49
+ else
50
+ context.extend ClassMethods
51
+ end
52
+ end
53
+ end
54
+
55
+ extend ModuleMethods
56
+
57
+ module ClassMethods
58
+ #
59
+ # Determines the specific Linux distro.
60
+ #
61
+ # @return [:fedora, :redhat, :debian, :suse, :arch, nil]
62
+ # Returns the type of Linux distro or `nil` if the Linux distro could
63
+ # not be determined.
64
+ #
65
+ # @api semipublic
66
+ #
67
+ def linux_distro
68
+ if File.file?('/etc/fedora-release') then :fedora
69
+ elsif File.file?('/etc/redhat-release') then :redhat
70
+ elsif File.file?('/etc/debian_version') then :debian
71
+ elsif File.file?('/etc/SuSE-release') then :suse
72
+ elsif File.file?('/etc/arch-release') then :arch
73
+ end
74
+ end
75
+ end
76
+
77
+ # The Linux distro.
78
+ #
79
+ # @return [:fedora, :redhat, :debian, :suse, :arch, nil]
80
+ #
81
+ # @api public
82
+ attr_reader :linux_distro
83
+
84
+ #
85
+ # Initializes the command.
86
+ #
87
+ # @param [:fedora, :redhat, :debian, :suse, :arch, nil] linux_distro
88
+ # Overrides the default detected Linux distro.
89
+ #
90
+ # @param [Hash{Symbol => Object}] kwargs
91
+ # Additional keyword arguments.
92
+ #
93
+ # @api public
94
+ #
95
+ def initialize(linux_distro: self.class.linux_distro, **kwargs)
96
+ super(**kwargs)
97
+
98
+ @linux_distro = linux_distro
99
+ end
100
+
101
+ #
102
+ # Determines if the current OS is RedHat Linux based distro.
103
+ #
104
+ # @return [Boolean]
105
+ #
106
+ # @api public
107
+ #
108
+ def redhat_linux?
109
+ @linux_distro == :redhat
110
+ end
111
+
112
+ #
113
+ # Determines if the current OS is Fedora Linux based distro.
114
+ #
115
+ # @return [Boolean]
116
+ #
117
+ # @api public
118
+ #
119
+ def fedora_linux?
120
+ @linux_distro == :fedora
121
+ end
122
+
123
+ #
124
+ # Determines if the current OS is Debian Linux based distro.
125
+ #
126
+ # @return [Boolean]
127
+ #
128
+ # @api public
129
+ #
130
+ def debian_linux?
131
+ @linux_distro == :debian
132
+ end
133
+
134
+ #
135
+ # Determines if the current OS is SUSE Linux based distro.
136
+ #
137
+ # @return [Boolean]
138
+ #
139
+ # @api public
140
+ #
141
+ def suse_linux?
142
+ @linux_distro == :suse
143
+ end
144
+
145
+ #
146
+ # Determines if the current OS is Arch Linux based distro.
147
+ #
148
+ # @return [Boolean]
149
+ #
150
+ # @api public
151
+ #
152
+ def arch_linux?
153
+ @linux_distro == :arch
154
+ end
155
+ end
156
+ end
157
+ end