bovem 1.2.6 → 2.0.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 (50) hide show
  1. data/.travis.yml +1 -1
  2. data/Gemfile +1 -1
  3. data/README.md +5 -2
  4. data/bovem.gemspec +11 -9
  5. data/doc/Bovem.html +7 -25
  6. data/doc/Bovem/Configuration.html +30 -43
  7. data/doc/Bovem/Console.html +225 -4360
  8. data/doc/Bovem/ConsoleMethods.html +125 -0
  9. data/doc/Bovem/ConsoleMethods/Interactions.html +575 -0
  10. data/doc/Bovem/ConsoleMethods/Interactions/ClassMethods.html +231 -0
  11. data/doc/Bovem/ConsoleMethods/Logging.html +2218 -0
  12. data/doc/Bovem/ConsoleMethods/Logging/ClassMethods.html +212 -0
  13. data/doc/Bovem/ConsoleMethods/Output.html +1213 -0
  14. data/doc/Bovem/ConsoleMethods/StyleHandling.html +274 -0
  15. data/doc/Bovem/ConsoleMethods/StyleHandling/ClassMethods.html +653 -0
  16. data/doc/Bovem/Errors.html +3 -3
  17. data/doc/Bovem/Errors/InvalidConfiguration.html +3 -3
  18. data/doc/Bovem/Errors/InvalidLogger.html +3 -3
  19. data/doc/Bovem/Logger.html +112 -12
  20. data/doc/Bovem/Shell.html +58 -1888
  21. data/doc/Bovem/ShellMethods.html +125 -0
  22. data/doc/Bovem/ShellMethods/Directories.html +484 -0
  23. data/doc/Bovem/ShellMethods/Execute.html +565 -0
  24. data/doc/Bovem/ShellMethods/General.html +450 -0
  25. data/doc/Bovem/ShellMethods/Read.html +451 -0
  26. data/doc/Bovem/ShellMethods/Write.html +676 -0
  27. data/doc/Bovem/Version.html +6 -6
  28. data/doc/_index.html +145 -4
  29. data/doc/class_list.html +1 -1
  30. data/doc/file.README.html +10 -7
  31. data/doc/frames.html +1 -1
  32. data/doc/index.html +10 -7
  33. data/doc/method_list.html +119 -79
  34. data/doc/top-level-namespace.html +3 -3
  35. data/lib/bovem.rb +1 -1
  36. data/lib/bovem/configuration.rb +24 -13
  37. data/lib/bovem/console.rb +566 -497
  38. data/lib/bovem/errors.rb +1 -1
  39. data/lib/bovem/logger.rb +4 -4
  40. data/lib/bovem/shell.rb +482 -305
  41. data/lib/bovem/version.rb +4 -4
  42. data/locales/en.yml +43 -0
  43. data/locales/it.yml +43 -0
  44. data/spec/bovem/configuration_spec.rb +3 -3
  45. data/spec/bovem/console_spec.rb +17 -10
  46. data/spec/bovem/logger_spec.rb +1 -1
  47. data/spec/bovem/shell_spec.rb +9 -5
  48. data/spec/coverage_helper.rb +1 -1
  49. data/spec/spec_helper.rb +1 -1
  50. metadata +32 -22
@@ -1,6 +1,6 @@
1
1
  # encoding: utf-8
2
2
  #
3
- # This file is part of the bovem gem. Copyright (C) 2012 and above Shogun <shogun_panda@me.com>.
3
+ # This file is part of the bovem gem. Copyright (C) 2013 and above Shogun <shogun_panda@me.com>.
4
4
  # Licensed under the MIT license, which can be found at http://www.opensource.org/licenses/mit-license.php.
5
5
  #
6
6
 
@@ -1,16 +1,16 @@
1
1
  # encoding: utf-8
2
2
  #
3
- # This file is part of the bovem gem. Copyright (C) 2012 and above Shogun <shogun_panda@me.com>.
3
+ # This file is part of the bovem gem. Copyright (C) 2013 and above Shogun <shogun_panda@me.com>.
4
4
  # Licensed under the MIT license, which can be found at http://www.opensource.org/licenses/mit-license.php.
5
5
  #
6
6
 
7
7
  module Bovem
8
8
  # A custom logger.
9
+ #
10
+ # @attr [Time] start_time The start time of first line. This allows to show a `T+0.1234` information into the log.
11
+ # @attr [IO|String] device The file or device to log messages to.
9
12
  class Logger < ::Logger
10
- # The start time of first line. This allows to show a `T+0.1234` information into the log.
11
13
  mattr_accessor :start_time
12
-
13
- # The file or device to log messages to.
14
14
  attr_reader :device
15
15
 
16
16
  # Creates a new logger.
@@ -1,175 +1,249 @@
1
1
  # encoding: utf-8
2
2
  #
3
- # This file is part of the bovem gem. Copyright (C) 2012 and above Shogun <shogun_panda@me.com>.
3
+ # This file is part of the bovem gem. Copyright (C) 2013 and above Shogun <shogun_panda@me.com>.
4
4
  # Licensed under the MIT license, which can be found at http://www.opensource.org/licenses/mit-license.php.
5
5
  #
6
6
 
7
7
  module Bovem
8
- # A utility class for most common shell operation.
9
- class Shell
10
- # A {Console Console} instance.
11
- attr_accessor :console
12
-
13
- # Returns a unique instance for Shell.
14
- #
15
- # @return [Shell] A new instance.
16
- def self.instance
17
- @instance ||= ::Bovem::Shell.new
18
- end
8
+ # Methods of the {Shell Shell} class.
9
+ module ShellMethods
10
+ # General methods.
11
+ module General
12
+ # Handles general failure of a file/directory method.
13
+ #
14
+ # @param e [Exception] The occurred exception.
15
+ # @param access_error [String|Symbol] The message to show in case of access errors.
16
+ # @param not_found_error [String|Symbol] The message to show in case of a not found entry.
17
+ # @param general_error [String|Symbol] The message to show in case of other errors.
18
+ # @param entries [Array] The list of entries which failed.
19
+ # @param fatal [Boolean] If quit in case of fatal errors.
20
+ # @param show_errors [Boolean] Whether to show errors.
21
+ def handle_failure(e, access_error, not_found_error, general_error, entries, fatal, show_errors)
22
+ error_type = fatal ? :fatal : :error
23
+ message = e.message.gsub(/.+ - (.+)/, "\\1")
24
+ locale = self.i18n.shell
25
+
26
+ case e.class.to_s
27
+ when "Errno::EACCES" then @console.send(error_type, locale.send(access_error, message))
28
+ when "Errno::ENOENT" then @console.send(error_type, locale.send(not_found_error, message))
29
+ else show_general_failure(e, general_error, entries, fatal) if show_errors
30
+ end
31
+ end
19
32
 
20
- # Initializes a new Shell.
21
- def initialize
22
- @console = ::Bovem::Console.instance
33
+ # Shows errors when a directory creation failed.
34
+ #
35
+ # @param e [Exception] The occurred exception.
36
+ # @param entries [Array] The list of entries which failed.
37
+ # @param fatal [Boolean] If quit in case of fatal errors.
38
+ def show_general_failure(e, general_error, entries, fatal)
39
+ locale = self.i18n.shell
40
+
41
+ @console.error(locale.send(general_error))
42
+ @console.with_indentation(11) do
43
+ entries.each do |entry| @console.write(entry) end
44
+ end
45
+ @console.write(locale.error(e.class.to_s, e), "\n", 5)
46
+ Kernel.exit(-1) if fatal
47
+ end
23
48
  end
24
49
 
25
- # Runs a command into the shell.
26
- #
27
- # @param command [String] The string to run.
28
- # @param message [String] A message to show before running.
29
- # @param run [Boolean] If `false`, it will just print a message with the full command that will be run.
30
- # @param show_exit [Boolean] If show the exit status.
31
- # @param show_output [Boolean] If show command output.
32
- # @param show_command [Boolean] If show the command that will be run.
33
- # @param fatal [Boolean] If quit in case of fatal errors.
34
- # @return [Hash] An hash with `status` and `output` keys.
35
- def run(command, message = nil, run = true, show_exit = true, show_output = false, show_command = false, fatal = true)
36
- rv = {:status => 0, :output => ""}
37
- command = command.ensure_string
38
-
39
- # Show the command
40
- self.console.begin(message) if message.present?
41
-
42
-
43
- if !run then # Print a message
44
- self.console.warn("Will run command: {mark=bright}\"#{command}\"{/mark}...")
45
- self.console.status(:ok) if show_exit
46
- else # Run
47
- output = ""
48
-
49
- self.console.info("Running command: {mark=bright}\"#{command}\"{/mark}...") if show_command
50
- rv[:status] = ::Open4::popen4(command + " 2>&1") { |pid, stdin, stdout, stderr|
51
- stdout.each_line do |line|
52
- output << line
53
- Kernel.print line if show_output
50
+ # Methods to find or check entries.
51
+ module Read
52
+ # Tests a path against a list of test.
53
+ #
54
+ # Valid tests are every method available in http://www.ruby-doc.org/core-1.9.3/FileTest.html (plus `read`, `write`, `execute`, `exec`, `dir`). Trailing question mark can be omitted.
55
+ #
56
+ # @param path [String] The path to test.
57
+ # @param tests [Array] The list of tests to perform.
58
+ def check(path, tests)
59
+ path = path.ensure_string
60
+
61
+ tests.ensure_array.all? {|test|
62
+ # Adjust test name
63
+ test = test.ensure_string.strip
64
+
65
+ test = case test
66
+ when "read" then "readable"
67
+ when "write" then "writable"
68
+ when "execute", "exec" then "executable"
69
+ when "dir" then "directory"
70
+ else test
54
71
  end
55
- }.exitstatus
56
72
 
57
- rv[:output] = output
73
+ # Execute test
74
+ test += "?" if test !~ /\?$/
75
+ FileTest.respond_to?(test) ? FileTest.send(test, path) : nil
76
+ }
58
77
  end
59
78
 
60
- # Return
61
- self.console.status(rv[:status] == 0 ? :ok : :fail) if show_exit
62
- exit(rv[:status]) if fatal && rv[:status] != 0
63
- rv
64
- end
79
+ # Find a list of files in directories matching given regexps or patterns.
80
+ #
81
+ # You can also pass a block to perform matching. The block will receive a single argument and the path will be considered matched if the blocks not evaluates to `nil` or `false`.
82
+ #
83
+ # Inside the block, you can call `Find.prune` to stop searching in the current directory.
84
+ #
85
+ # @param directories [String] A list of directories where to search files.
86
+ # @param patterns [Array] A list of regexps or patterns to match files. If empty, every file is returned. Ignored if a block is provided.
87
+ # @param by_extension [Boolean] If to only search in extensions. Ignored if a block is provided.
88
+ # @param case_sensitive [Boolean] If the search is case sensitive. Only meaningful for string patterns.
89
+ # @param block [Proc] An optional block to perform matching instead of pattern matching.
90
+ def find(directories, patterns = [], by_extension = false, case_sensitive = false, &block)
91
+ rv = []
92
+
93
+ directories = directories.ensure_array.compact {|d| File.expand_path(d.ensure_string) }
94
+ patterns = normalize_patterns(patterns, by_extension, case_sensitive)
65
95
 
66
- # Tests a path against a list of test.
67
- #
68
- # Valid tests are every method available in http://www.ruby-doc.org/core-1.9.3/FileTest.html (plus `read`, `write`, `execute`, `exec`, `dir`). Trailing question mark can be omitted.
69
- #
70
- #
71
- # @param path [String] The path to test.
72
- # @param tests [Array] The list of tests to perform.
73
- def check(path, tests)
74
- path = path.ensure_string
75
-
76
- tests.ensure_array.all? {|test|
77
- # Adjust test name
78
- test = test.ensure_string.strip
79
-
80
- test = case test
81
- when "read" then "readable"
82
- when "write" then "writable"
83
- when "execute", "exec" then "executable"
84
- when "dir" then "directory"
85
- else test
96
+ directories.each do |directory|
97
+ if self.check(directory, [:directory, :readable, :executable]) then
98
+ Find.find(directory) do |entry|
99
+ found = patterns.blank? ? true : match_pattern(entry, patterns, by_extension, &block)
100
+
101
+ rv << entry if found
102
+ end
103
+ end
86
104
  end
87
105
 
88
- # Execute test
89
- test += "?" if test !~ /\?$/
90
- FileTest.respond_to?(test) ? FileTest.send(test, path) : nil
91
- }
92
- end
106
+ rv
107
+ end
93
108
 
94
- # Deletes a list of files.
95
- #
96
- # @param files [Array] The list of files to remove
97
- # @param run [Boolean] If `false`, it will just print a list of message that would be deleted.
98
- # @param show_errors [Boolean] If show errors.
99
- # @param fatal [Boolean] If quit in case of fatal errors.
100
- # @return [Boolean] `true` if operation succeeded, `false` otherwise.
101
- def delete(files, run = true, show_errors = false, fatal = true)
102
- rv = true
103
- files = files.ensure_array.compact.collect {|f| File.expand_path(f.ensure_string) }
104
-
105
- if !run then
106
- self.console.warn("Will remove file(s):")
107
- self.console.with_indentation(11) do
108
- files.each do |file| self.console.write(file) end
109
+ private
110
+ # Normalizes a set of patterns to find.
111
+ #
112
+ # @param patterns [Array] A list of regexps or patterns to match files. If empty, every file is returned. Ignored if a block is provided.
113
+ # @param by_extension [Boolean] If to only search in extensions. Ignored if a block is provided.
114
+ # @param case_sensitive [Boolean] If the search is case sensitive. Only meaningful for string patterns.
115
+ # @return [Array] The normalized patterns.
116
+ def normalize_patterns(patterns, by_extension, case_sensitive)
117
+ # Adjust patterns
118
+ patterns = patterns.ensure_array.compact.collect {|p| p.is_a?(::Regexp) ? p : Regexp.new(Regexp.quote(p.ensure_string)) }
119
+ patterns = patterns.collect {|p| /(#{p.source})$/ } if by_extension
120
+ patterns = patterns.collect {|p| /#{p.source}/i } if !case_sensitive
121
+ patterns
109
122
  end
110
- else
111
- rv = catch(:rv) do
112
- begin
113
- FileUtils.rm_r(files, {:noop => false, :verbose => false, :secure => true})
114
- throw(:rv, true)
115
- rescue Errno::EACCES => e
116
- self.console.send(fatal ? :fatal : :error, "Cannot remove following non writable file: {mark=bright}#{e.message.gsub(/.+ - (.+)/, "\\1")}{/mark}")
117
- rescue Errno::ENOENT => e
118
- self.console.send(fatal ? :fatal : :error, "Cannot remove following non existent file: {mark=bright}#{e.message.gsub(/.+ - (.+)/, "\\1")}{/mark}")
119
- rescue Exception => e
120
- if show_errors then
121
- self.console.error("Cannot remove following file(s):")
122
- self.console.with_indentation(11) do
123
- files.each do |file| self.console.write(file) end
123
+
124
+ # Matches an entry against a set of patterns or a procedure.
125
+ #
126
+ # @param entry [String] The entry to search.
127
+ # @param patterns [Array] A list of regexps or patterns to match files. If empty, every file is returned. Ignored if a block is provided.
128
+ # @param by_extension [Boolean] If to only search in extensions. Ignored if a block is provided.
129
+ # @param block [Proc|nil] An optional block to run instead of pattern matching.
130
+ # @return [Boolean] `true` if entry matched, `false` otherwise.
131
+ def match_pattern(entry, patterns, by_extension, &block)
132
+ catch(:found) do
133
+ if block then
134
+ throw(:found, true) if block.call(entry)
135
+ else
136
+ patterns.each do |pattern|
137
+ throw(:found, true) if pattern.match(entry) && (!by_extension || !File.directory?(entry))
124
138
  end
125
- self.console.write("due to this error: [#{e.class.to_s}] #{e}.", "\n", 5)
126
- Kernel.exit(-1) if fatal
127
139
  end
128
- end
129
140
 
130
- false
141
+ false
142
+ end
131
143
  end
132
- end
133
-
134
- rv
135
144
  end
136
145
 
137
- # Copies or moves a set of files or directory to another location.
138
- #
139
- # @param src [String|Array] The entries to copy or move. If is an Array, `dst` is assumed to be a directory.
140
- # @param dst [String] The destination. **Any existing entries will be overwritten.** Any required directory will be created.
141
- # @param operation [Symbol] The operation to perform. Valid values are `:copy` or `:move`.
142
- # @param run [Boolean] If `false`, it will just print a list of message that would be copied or moved.
143
- # @param show_errors [Boolean] If show errors.
144
- # @param fatal [Boolean] If quit in case of fatal errors.
145
- # @return [Boolean] `true` if operation succeeded, `false` otherwise.
146
- def copy_or_move(src, dst, operation, run = true, show_errors = false, fatal = true)
147
- rv = true
148
- operation = :copy if operation != :move
149
- single = !src.is_a?(Array)
150
-
151
- if single then
152
- src = File.expand_path(src)
153
- else
154
- src = src.collect {|s| File.expand_path(s) }
146
+ # Methods to copy or move entries.
147
+ module Write
148
+ # Copies a set of files or directory to another location.
149
+ #
150
+ # @param src [String|Array] The entries to copy. If is an Array, `dst` is assumed to be a directory.
151
+ # @param dst [String] The destination. **Any existing entries will be overwritten.** Any required directory will be created.
152
+ # @param run [Boolean] If `false`, it will just print a list of message that would be copied or moved.
153
+ # @param show_errors [Boolean] If show errors.
154
+ # @param fatal [Boolean] If quit in case of fatal errors.
155
+ # @return [Boolean] `true` if operation succeeded, `false` otherwise.
156
+ def copy(src, dst, run = true, show_errors = false, fatal = true)
157
+ self.copy_or_move(src, dst, :copy, run, show_errors, fatal)
155
158
  end
156
159
 
157
- dst = File.expand_path(dst.ensure_string)
160
+ # Moves a set of files or directory to another location.
161
+ #
162
+ # @param src [String|Array] The entries to move. If is an Array, `dst` is assumed to be a directory.
163
+ # @param dst [String] The destination. **Any existing entries will be overwritten.** Any required directory will be created.
164
+ # @param run [Boolean] If `false`, it will just print a list of message that would be deleted.
165
+ # @param show_errors [Boolean] If show errors.
166
+ # @param fatal [Boolean] If quit in case of fatal errors.
167
+ # @return [Boolean] `true` if operation succeeded, `false` otherwise.
168
+ def move(src, dst, run = true, show_errors = false, fatal = true)
169
+ self.copy_or_move(src, dst, :move, run, show_errors, fatal)
170
+ end
158
171
 
159
- if !run then
160
- if single then
161
- self.console.warn("Will #{operation} a file:")
162
- self.console.write("From: {mark=bright}#{File.expand_path(src.ensure_string)}{/mark}", "\n", 11)
163
- self.console.write("To: {mark=bright}#{dst}{/mark}", "\n", 11)
172
+ # Copies or moves a set of files or directory to another location.
173
+ #
174
+ # @param src [String|Array] The entries to copy or move. If is an Array, `dst` is assumed to be a directory.
175
+ # @param dst [String] The destination. **Any existing entries will be overwritten.** Any required directory will be created.
176
+ # @param operation [Symbol] The operation to perform. Valid values are `:copy` or `:move`.
177
+ # @param run [Boolean] If `false`, it will just print a list of message that would be copied or moved.
178
+ # @param show_errors [Boolean] If show errors.
179
+ # @param fatal [Boolean] If quit in case of fatal errors.
180
+ # @return [Boolean] `true` if operation succeeded, `false` otherwise.
181
+ def copy_or_move(src, dst, operation, run = true, show_errors = false, fatal = true)
182
+ rv = true
183
+ locale = self.i18n.shell
184
+ operation, operation_s, single, src, dst = sanitize_copy_or_move_arguments(operation, src, dst)
185
+
186
+ if !run then
187
+ dry_run_copy_or_move(single, operation_s, src, dst)
164
188
  else
165
- self.console.warn("Will #{operation} following entries:")
166
- self.console.with_indentation(11) do
167
- src.each do |s| self.console.write(s) end
189
+ rv = catch(:rv) do
190
+ dst_dir = prepare_destination(single, src, dst, operation_s, show_errors, fatal)
191
+ check_sources(src, operation_s, fatal)
192
+ execute_copy_or_move(src, dst, dst_dir, single, operation, operation_s, show_errors, fatal)
193
+ true
168
194
  end
169
- self.console.write("to directory: {mark=bright}#{dst}{/mark}", "\n", 5)
170
195
  end
171
- else
172
- rv = catch(:rv) do
196
+
197
+ rv
198
+ end
199
+
200
+ private
201
+ # Sanitizes arguments for copy or move.
202
+ #
203
+ # @param operation [String] The operation which will be executed.
204
+ # @param src [String|Array] The entries to copy or move. If is an Array, `dst` is assumed to be a directory.
205
+ # @param dst [String] The destination. **Any existing entries will be overwritten.** Any required directory will be created.
206
+ # @return [Array] A list of sanitized arguments.
207
+ def sanitize_copy_or_move_arguments(operation, src, dst)
208
+ operation = :copy if operation != :move
209
+ single = !src.is_a?(Array)
210
+ src = single ? File.expand_path(src) : src.collect {|s| File.expand_path(s) }
211
+
212
+ [operation, self.i18n.shell.send(operation), single, src, File.expand_path(dst.ensure_string)]
213
+ end
214
+
215
+ # Shows which copy or move operation are going to executed.
216
+ #
217
+ # @param single [Boolean] Whether `src` is a single file or directory.
218
+ # @param operation [String] The operation which will be executed.
219
+ # @param src [String|Array] The entries to copy or move. If is an Array, `dst` is assumed to be a directory.
220
+ # @param dst [String] The destination. **Any existing entries will be overwritten.** Any required directory will be created.
221
+ def dry_run_copy_or_move(single, operation, src, dst)
222
+ locale = self.i18n.shell
223
+
224
+ if single then
225
+ @console.warn(locale.copy_move_single_dry(operation))
226
+ @console.write(locale.copy_move_from(File.expand_path(src.ensure_string)), "\n", 11)
227
+ @console.write(locale.copy_move_to(dst), "\n", 11)
228
+ else
229
+ @console.warn(locale.copy_move_multi_dry(operation))
230
+ @console.with_indentation(11) do
231
+ src.each do |s| @console.write(s) end
232
+ end
233
+ @console.write(locale.copy_move_to_multi(dst), "\n", 5)
234
+ end
235
+ end
236
+
237
+ # Prepares a destination directory for a copy or move.
238
+ #
239
+ # @param single [Boolean] Whether `src` is a single file or directory.
240
+ # @param src [String|Array] The entries to copy or move. If is an Array, `dst` is assumed to be a directory.
241
+ # @param dst [String] The destination. **Any existing entries will be overwritten.** Any required directory will be created.
242
+ # @param operation [String] The operation which will be executed.
243
+ # @param show_errors [Boolean] If show errors.
244
+ # @param fatal [Boolean] If quit in case of fatal errors.
245
+ # @return [String] The prepared destination.
246
+ def prepare_destination(single, src, dst, operation, show_errors, fatal)
173
247
  dst_dir = single ? File.dirname(dst) : dst
174
248
  has_dir = self.check(dst_dir, :dir)
175
249
 
@@ -178,210 +252,313 @@ module Bovem
178
252
  throw(:rv, false) if !has_dir
179
253
 
180
254
  if single && self.check(dst, :dir) then
181
- @console.send(fatal ? :fatal : :error, "Cannot #{operation} file {mark=bright}#{src}{/mark} to {mark=bright}#{dst}{/mark} because it is currently a directory.")
255
+ @console.send(fatal ? :fatal : :error, self.i18n.shell.copy_move_single_to_directory(operation, src, dst))
182
256
  throw(:rv, false)
183
257
  end
184
258
 
259
+ dst_dir
260
+ end
261
+
262
+ # Checks every sources for a copy or move.
263
+ #
264
+ # @param src [String|Array] The entries to copy or move. If is an Array, `dst` is assumed to be a directory.
265
+ # @param operation [String] The operation which will be executed.
266
+ # @param fatal [Boolean] If quit in case of fatal errors.
267
+ def check_sources(src, operation, fatal)
185
268
  # Check that every file is existing
186
269
  src.ensure_array.each do |s|
187
270
  if !self.check(s, :exists) then
188
- @console.send(fatal ? :fatal : :error, "Cannot #{operation} non existent file {mark=bright}#{s}{/mark}.")
271
+ @console.send(fatal ? :fatal : :error, self.i18n.shell.copy_move_src_not_found(operation, s))
189
272
  throw(:rv, false)
190
273
  end
191
274
  end
275
+ end
276
+
277
+ # Executes the copy or move operation.
278
+ #
279
+ # @param src [String|Array] The entries to copy or move. If is an Array, `dst` is assumed to be a directory.
280
+ # @param dst [String] The destination. **Any existing entries will be overwritten.** Any required directory will be created.
281
+ # @param dst_dir [String] The destination directory.
282
+ # @param single [Boolean] Whether `src` is a single file or directory.
283
+ # @param operation [Symbol] The operation to perform. Valid values are `:copy` or `:move`.
284
+ # @param operation_s [String] The string representation of the operation to perform.
285
+ # @param show_errors [Boolean] If show errors.
286
+ # @param fatal [Boolean] If quit in case of fatal errors.
287
+ def execute_copy_or_move(src, dst, dst_dir, single, operation, operation_s, show_errors, fatal)
288
+ locale = self.i18n.shell
192
289
 
193
- # Do operation
194
290
  begin
195
- FileUtils.send(operation == :move ? :mv : :cp_r, src, dst, {:noop => false, :verbose => false})
291
+ FileUtils.send(operation == :move ? :mv : :cp_r, src, dst, {noop: false, verbose: false})
196
292
  rescue Errno::EACCES => e
197
- if single then
198
- @console.send(fatal ? :fatal : :error, "Cannot #{operation} file {mark=bright}#{src}{/mark} to non writable directory {mark=bright}#{dst_dir}{/mark}.")
199
- else
200
- self.console.error("Cannot #{operation} following file(s) to non writable directory {mark=bright}#{dst}{/mark}:")
201
- self.console.with_indentation(11) do
202
- src.each do |s| self.console.write(s) end
203
- end
204
- Kernel.exit(-1) if fatal
205
- end
206
-
207
- throw(:rv, false)
293
+ single_msg = locale.copy_move_dst_not_writable_single(operation_s, src, dst_dir)
294
+ multi_msg = locale.copy_move_dst_not_writable_single(operation_s, dst)
295
+ handle_copy_move_failure(single, src, show_errors, fatal, single_msg, multi_msg, nil)
208
296
  rescue Exception => e
209
- if single then
210
- @console.send(fatal ? :fatal : :error, "Cannot #{operation} file {mark=bright}#{src}{/mark} to directory {mark=bright}#{dst_dir}{/mark} due to this error: [#{e.class.to_s}] #{e}.", "\n", 5)
211
- else
212
- self.console.error("Cannot #{operation} following entries to {mark=bright}#{dst}{/mark}:")
213
- self.console.with_indentation(11) do
214
- src.each do |s| self.console.write(s) end
215
- end
216
- self.console.write("due to this error: [#{e.class.to_s}] #{e}.", "\n", 5)
217
- Kernel.exit(-1) if fatal
218
- end
219
-
220
- throw(:rv, false)
297
+ single_msg = locale.copy_move_error_single(operation_s, src, dst_dir, e.class.to_s, e)
298
+ multi_msg = locale.copy_move_error_multi(operation_s, dst)
299
+ handle_copy_move_failure(single, src, show_errors, fatal, single_msg, multi_msg, locale.error(e.class.to_s, e))
221
300
  end
222
-
223
- true
224
301
  end
225
- end
226
302
 
227
- rv
228
- end
229
-
230
- # Copies a set of files or directory to another location.
231
- #
232
- # @param src [String|Array] The entries to copy. If is an Array, `dst` is assumed to be a directory.
233
- # @param dst [String] The destination. **Any existing entries will be overwritten.** Any required directory will be created.
234
- # @param run [Boolean] If `false`, it will just print a list of message that would be copied or moved.
235
- # @param show_errors [Boolean] If show errors.
236
- # @param fatal [Boolean] If quit in case of fatal errors.
237
- # @return [Boolean] `true` if operation succeeded, `false` otherwise.
238
- def copy(src, dst, run = true, show_errors = false, fatal = true)
239
- self.copy_or_move(src, dst, :copy, run, show_errors, fatal)
240
- end
303
+ # Handles a failure on copy or move.
304
+ #
305
+ # @param single [Boolean] Whether `src` is a single file or directory.
306
+ # @param src [String|Array] The entries to copy or move. If is an Array, `dst` is assumed to be a directory.
307
+ # @param show_errors [Boolean] If show errors.
308
+ # @param fatal [Boolean] If quit in case of fatal errors.
309
+ # @param single_msg [String] The message to show in case of a single source.
310
+ # @param multi_msg [String] The starting message to show in case of multiple sources.
311
+ # @param error [String|nil] The ending message to show in case of multiple sources.
312
+ def handle_copy_move_failure(single, src, show_errors, fatal, single_msg, multi_msg, error)
313
+ if single then
314
+ @console.send(fatal ? :fatal : :error, single_msg, "\n", 5)
315
+ else
316
+ @console.error(multi_msg)
317
+ @console.with_indentation(11) do
318
+ src.each do |s| @console.write(s) end
319
+ end
320
+ @console.write(error, "\n", 5) if error
321
+ Kernel.exit(-1) if fatal
322
+ end
241
323
 
242
- # Moves a set of files or directory to another location.
243
- #
244
- # @param src [String|Array] The entries to move. If is an Array, `dst` is assumed to be a directory.
245
- # @param dst [String] The destination. **Any existing entries will be overwritten.** Any required directory will be created.
246
- # @param run [Boolean] If `false`, it will just print a list of message that would be deleted.
247
- # @param show_errors [Boolean] If show errors.
248
- # @param fatal [Boolean] If quit in case of fatal errors.
249
- # @return [Boolean] `true` if operation succeeded, `false` otherwise.
250
- def move(src, dst, run = true, show_errors = false, fatal = true)
251
- self.copy_or_move(src, dst, :move, run, show_errors, fatal)
324
+ throw(:rv, false)
325
+ end
252
326
  end
253
327
 
254
- # Executes a block of code in another directory.
255
- #
256
- # @param directory [String] The new working directory.
257
- # @param restore [Boolean] If to restore the original working directory.
258
- # @param show_messages [Boolean] Show informative messages about working directory changes.
259
- # @return [Boolean] `true` if the directory was valid and the code executed, `false` otherwise.
260
- def within_directory(directory, restore = true, show_messages = false)
261
- rv = false
262
- original = Dir.pwd
263
- directory = File.expand_path(directory.ensure_string)
264
-
265
- if self.check(directory, [:directory, :executable]) then
266
- begin
267
- self.console.info("Moving into directory {mark=bright}#{directory}{/mark}") if show_messages
268
- Dir.chdir(directory)
269
- rv = true
270
- rescue Exception => e
328
+ # Methods to run commands or delete entries.
329
+ module Execute
330
+ # Runs a command into the shell.
331
+ #
332
+ # @param command [String] The string to run.
333
+ # @param message [String] A message to show before running.
334
+ # @param run [Boolean] If `false`, it will just print a message with the full command that will be run.
335
+ # @param show_exit [Boolean] If show the exit status.
336
+ # @param show_output [Boolean] If show command output.
337
+ # @param show_command [Boolean] If show the command that will be run.
338
+ # @param fatal [Boolean] If quit in case of fatal errors.
339
+ # @return [Hash] An hash with `status` and `output` keys.
340
+ def run(command, message = nil, run = true, show_exit = true, show_output = false, show_command = false, fatal = true)
341
+ rv = {status: 0, output: ""}
342
+ command = command.ensure_string
343
+ locale = self.i18n.shell
344
+
345
+ # Show the command
346
+ @console.begin(message) if message.present?
347
+
348
+ if !run then # Print a message
349
+ @console.warn(locale.run_dry(command))
350
+ @console.status(:ok) if show_exit
351
+ else # Run
352
+ rv = execute_command(command, show_command, show_output)
271
353
  end
354
+
355
+ # Return
356
+ @console.status(rv[:status] == 0 ? :ok : :fail) if show_exit
357
+ exit(rv[:status]) if fatal && rv[:status] != 0
358
+ rv
272
359
  end
273
360
 
274
- yield if rv && block_given?
361
+ # Deletes a list of files.
362
+ #
363
+ # @param files [Array] The list of files to remove
364
+ # @param run [Boolean] If `false`, it will just print a list of message that would be deleted.
365
+ # @param show_errors [Boolean] If show errors.
366
+ # @param fatal [Boolean] If quit in case of fatal errors.
367
+ # @return [Boolean] `true` if operation succeeded, `false` otherwise.
368
+ def delete(files, run = true, show_errors = false, fatal = true)
369
+ rv = true
370
+ locale = self.i18n.shell
371
+ files = files.ensure_array.compact.collect {|f| File.expand_path(f.ensure_string) }
372
+
373
+ if !run then
374
+ @console.warn(locale.remove_dry)
375
+ @console.with_indentation(11) do
376
+ files.each do |file| @console.write(file) end
377
+ end
378
+ else
379
+ rv = catch(:rv) do
380
+ begin
381
+ FileUtils.rm_r(files, {noop: false, verbose: false, secure: true})
382
+ throw(:rv, true)
383
+ rescue => e
384
+ handle_failure(e, :remove_unwritable, :remove_not_found, :remove_error, files, fatal, show_errors)
385
+ end
275
386
 
276
- if rv && original then
277
- begin
278
- self.console.info("Moving back into directory {mark=bright}#{original}{/mark}") if show_messages
279
- Dir.chdir(original) if restore
280
- rescue Exception => e
281
- rv = false
387
+ false
388
+ end
282
389
  end
390
+
391
+ rv
283
392
  end
284
393
 
285
- rv
394
+ private
395
+ # Runs a command into the shell.
396
+ #
397
+ # @param command [String] The string to run.
398
+ # @param show_command [Boolean] If show the command that will be run.
399
+ # @param show_output [Boolean] If show command output.
400
+ # @return [Hash] An hash with `status` and `output` keys.
401
+ def execute_command(command, show_command, show_output)
402
+ output = ""
403
+
404
+ @console.info(self.i18n.shell.run(command)) if show_command
405
+ status = ::Open4::popen4(command + " 2>&1") { |pid, stdin, stdout, stderr|
406
+ stdout.each_line do |line|
407
+ output << line
408
+ Kernel.print line if show_output
409
+ end
410
+ }.exitstatus
411
+
412
+ {status: status, output: output}
413
+ end
286
414
  end
287
415
 
288
- # Creates a list of directories, included missing parent directories.
289
- #
290
- # @param directories [Array] The list of directories to create.
291
- # @param mode [Fixnum] Initial permissions for the new directories.
292
- # @param run [Boolean] If `false`, it will just print a list of directories that would be created.
293
- # @param show_errors [Boolean] If show errors.
294
- # @param fatal [Boolean] If quit in case of fatal errors.
295
- # @return [Boolean] `true` if operation succeeded, `false` otherwise.
296
- def create_directories(directories, mode = 0755, run = true, show_errors = false, fatal = true)
297
- rv = true
298
-
299
- # Adjust directory
300
- directories = directories.ensure_array.compact {|d| File.expand_path(d.ensure_string) }
301
-
302
- if !run then # Just print
303
- self.console.warn("Will create directories:")
304
- self.console.with_indentation(11) do
305
- directories.each do |directory| self.console.write(directory) end
416
+ # Methods to interact with directories.
417
+ module Directories
418
+ # Executes a block of code in another directory.
419
+ #
420
+ # @param directory [String] The new working directory.
421
+ # @param restore [Boolean] If to restore the original working directory.
422
+ # @param show_messages [Boolean] Show informative messages about working directory changes.
423
+ # @return [Boolean] `true` if the directory was valid and the code executed, `false` otherwise.
424
+ def within_directory(directory, restore = true, show_messages = false)
425
+ rv = false
426
+ locale = self.i18n.shell
427
+
428
+ directory = File.expand_path(directory.ensure_string)
429
+ original = Dir.pwd
430
+
431
+ rv = enter_directory(directory, show_messages, locale.move_in(directory))
432
+ yield if rv && block_given?
433
+ rv = enter_directory(original, show_messages, locale.move_out(directory)) if rv && restore
434
+
435
+ rv
436
+ end
437
+
438
+ # Creates a list of directories, included missing parent directories.
439
+ #
440
+ # @param directories [Array] The list of directories to create.
441
+ # @param mode [Fixnum] Initial permissions for the new directories.
442
+ # @param run [Boolean] If `false`, it will just print a list of directories that would be created.
443
+ # @param show_errors [Boolean] If show errors.
444
+ # @param fatal [Boolean] If quit in case of fatal errors.
445
+ # @return [Boolean] `true` if operation succeeded, `false` otherwise.
446
+ def create_directories(directories, mode = 0755, run = true, show_errors = false, fatal = true)
447
+ rv = true
448
+
449
+ # Adjust directory
450
+ directories = directories.ensure_array.compact {|d| File.expand_path(d.ensure_string) }
451
+
452
+ if !run then # Just print
453
+ dry_run_directory_creation(directories)
454
+ else
455
+ directories.each do |directory|
456
+ rv = rv && try_create_directory(directory, mode, fatal, directories, show_errors)
457
+ break if !rv
458
+ end
306
459
  end
307
- else
308
- directories.each do |directory|
309
- rv = catch(:rv) do
310
- # Perform tests
311
- if self.check(directory, :directory) then
312
- self.console.send(fatal ? :fatal : :error, "The directory {mark=bright}#{directory}{/mark} already exists.")
313
- elsif self.check(directory, :exist) then
314
- self.console.send(fatal ? :fatal : :error, "Path {mark=bright}#{directory}{/mark} is currently a file.")
315
- else
316
- begin # Create directory
317
- FileUtils.mkdir_p(directory, {:mode => mode, :noop => false, :verbose => false})
318
- throw(:rv, true)
319
- rescue Errno::EACCES => e
320
- self.console.send(fatal ? :fatal : :error, "Cannot create following directory due to permission denied: {mark=bright}#{e.message.gsub(/.+ - (.+)/, "\\1")}{/mark}.")
321
- rescue Exception => e
322
- if show_errors then
323
- self.console.error("Cannot create following directories:")
324
- self.console.with_indentation(11) do
325
- directories.each do |directory| self.console.write(directory) end
326
- end
327
- self.console.write("due to this error: [#{e.class.to_s}] #{e}.", "\n", 5)
328
- Kernel.exit(-1) if fatal
329
- end
330
- end
331
- end
332
460
 
461
+ rv
462
+ end
463
+
464
+ private
465
+ # Change current working directory.
466
+ #
467
+ # @param directory [String] The directory which move into.
468
+ # @param show_message [Boolean] Whether to show or not message.
469
+ # @param message [String] The message to show.
470
+ # @return [Boolean] `true` if operation succeeded, `false` otherwise.
471
+ def enter_directory(directory, show_message, message)
472
+ begin
473
+ raise ArgumentError if !self.check(directory, [:directory, :executable])
474
+ @console.info(message) if show_message
475
+ Dir.chdir(directory)
476
+ true
477
+ rescue Exception => e
333
478
  false
334
479
  end
480
+ end
335
481
 
336
- break if !rv
482
+ # Show which directory are going to be created.
483
+ # @param directories [Array] The list of directories to create.
484
+ def dry_run_directory_creation(directories)
485
+ @console.warn(self.i18n.shell.mkdir_dry)
486
+ @console.with_indentation(11) do
487
+ directories.each do |directory| @console.write(directory) end
488
+ end
337
489
  end
338
- end
339
490
 
340
- rv
341
- end
491
+ # Tries to creates a directory.
492
+ #
493
+ # @param directory [String] The directory to create.
494
+ # @param mode [Fixnum] Initial permissions for the new directories.
495
+ # @param fatal [Boolean] If quit in case of fatal errors.
496
+ # @param directories [Array] The list of directories to create.
497
+ # @param show_errors [Boolean] If show errors.
498
+ # @return [Boolean] `true` if operation succeeded, `false` otherwise.
499
+ def try_create_directory(directory, mode, fatal, directories, show_errors)
500
+ rv = false
501
+ locale = self.i18n.shell
502
+
503
+ # Perform tests
504
+ if self.check(directory, :directory) then
505
+ @console.send(fatal ? :fatal : :error, locale.mkdir_existing(directory))
506
+ elsif self.check(directory, :exist) then
507
+ @console.send(fatal ? :fatal : :error, locale.mkdir_file(directory))
508
+ else
509
+ rv = create_directory(directory, mode, fatal, directories, show_errors)
510
+ end
342
511
 
343
- # Find a list of files in directories matching given regexps or patterns.
344
- #
345
- # You can also pass a block to perform matching. The block will receive a single argument and the path will be considered matched if the blocks not evaluates to `nil` or `false`.
346
- #
347
- # Inside the block, you can call `Find.prune` to stop searching in the current directory.
348
- #
349
- # @param directories [String] A list of directories where to search files.
350
- # @param patterns [Array] A list of regexps or patterns to match files. If empty, every file is returned. Ignored if a block is provided.
351
- # @param by_extension [Boolean] If to only search in extensions. Ignored if a block is provided.
352
- # @param case_sensitive [Boolean] If the search is case sensitive. Only meaningful for string patterns.
353
- def find(directories, patterns = [], by_extension = false, case_sensitive = false)
354
- rv = []
355
-
356
- # Adjust directory
357
- directories = directories.ensure_array.compact {|d| File.expand_path(d.ensure_string) }
358
-
359
- # Adjust patterns
360
- patterns = patterns.ensure_array.compact.collect {|p| p.is_a?(::Regexp) ? p : Regexp.new(Regexp.quote(p.ensure_string)) }
361
- patterns = patterns.collect {|p| /(#{p.source})$/ } if by_extension
362
- patterns = patterns.collect {|p| /#{p.source}/i } if !case_sensitive
363
-
364
- directories.each do |directory|
365
- if self.check(directory, [:directory, :readable, :executable]) then
366
- Find.find(directory) do |entry|
367
- found = patterns.blank? ? true : catch(:found) do
368
- if block_given? then
369
- throw(:found, true) if yield(entry)
370
- else
371
- patterns.each do |pattern|
372
- throw(:found, true) if pattern.match(entry) && (!by_extension || !File.directory?(entry))
373
- end
374
- end
512
+ rv
513
+ end
375
514
 
376
- false
377
- end
515
+ # Creates a directory.
516
+ #
517
+ # @param directory [String] The directory to create.
518
+ # @param mode [Fixnum] Initial permissions for the new directories.
519
+ # @param fatal [Boolean] If quit in case of fatal errors.
520
+ # @param directories [Array] The list of directories to create.
521
+ # @param show_errors [Boolean] If show errors.
522
+ # @return [Boolean] `true` if operation succeeded, `false` otherwise.
523
+ def create_directory(directory, mode, fatal, directories, show_errors)
524
+ rv = false
378
525
 
379
- rv << entry if found
526
+ begin # Create directory
527
+ FileUtils.mkdir_p(directory, {mode: mode, noop: false, verbose: false})
528
+ rv = true
529
+ rescue Exception => e
530
+ handle_failure(e, :mkdir_denied, nil, :mkdir_error, directories, fatal, show_errors)
380
531
  end
532
+
533
+ rv
381
534
  end
382
- end
535
+ end
536
+ end
537
+
538
+ # A utility class for most common shell operation.
539
+ #
540
+ # @attr [Console] console # A console instance.
541
+ class Shell
542
+ include Lazier::I18n
543
+ include Bovem::ShellMethods::General
544
+ include Bovem::ShellMethods::Read
545
+ include Bovem::ShellMethods::Write
546
+ include Bovem::ShellMethods::Execute
547
+ include Bovem::ShellMethods::Directories
548
+
549
+ attr_accessor :console
550
+
551
+ # Returns a unique instance for Shell.
552
+ #
553
+ # @return [Shell] A new instance.
554
+ def self.instance
555
+ @instance ||= ::Bovem::Shell.new
556
+ end
383
557
 
384
- rv
558
+ # Initializes a new Shell.
559
+ def initialize
560
+ @console = ::Bovem::Console.instance
561
+ self.i18n_setup(:bovem, ::File.absolute_path(::Pathname.new(::File.dirname(__FILE__)).to_s + "/../../locales/"))
385
562
  end
386
563
  end
387
564
  end