bovem 1.2.6 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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