tty-file 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ad05816965df5179021ae1e70048eac3ae58db78
4
- data.tar.gz: 35e7da73870c9bbb6d01a35584bb0537c110903e
3
+ metadata.gz: 24159965082063c18c0538a86d536db913ac05ee
4
+ data.tar.gz: 4b751ed2950b3f4725333011ce9351dd3d1a1afe
5
5
  SHA512:
6
- metadata.gz: 3dbfc53a540c8f43c130274c02fcf3c2a9cfb0d50cab7b7d0a1e1ba2ab8c92073a61b0c4bf4c0108d005abfc353d65e10190169c689b772c54becfee96bc12e6
7
- data.tar.gz: 92a918fd84a1e846d04c374644781f940f28ea0705df7f9ab0da5df7e87f9d0e39c2bd08621b021eed86da4cb7b2e4703ba5430b64860c571c5fe856ba7308cc
6
+ metadata.gz: e08fc675ca19202aeaa3bbbba1c4236253e8f10355354889e384b41f63cc47e71160ecaa3945fcec062cad473fd9f401fcd7b8c71a2c406a5c28a2c625a2c8c0
7
+ data.tar.gz: 79793d79c75630d2e81228fbbc78ee023d81ba2c425d1dcd4896ea2f72813c363c58b0c39f6b0947f83edcd7f156cb292209bb7ccfbebe294f294195e9aa715f
data/.travis.yml CHANGED
@@ -2,24 +2,24 @@
2
2
  language: ruby
3
3
  sudo: false
4
4
  cache: bundler
5
- script: "bundle exec rake ci"
5
+ script: "bundle update && bundle exec rake ci"
6
6
  rvm:
7
7
  - 2.0.0
8
8
  - 2.1.10
9
9
  - 2.2.5
10
10
  - 2.3.1
11
+ - 2.4.0
11
12
  - ruby-head
12
- - rbx
13
+ - jruby-9.1.1.0
14
+ - rbx-3
15
+ env:
16
+ global:
17
+ - JRUBY_OPTS=''
13
18
  matrix:
14
- include:
15
- - rvm: jruby
16
- env: JRUBY_OPTS='--server -Xcompile.invokedynamic=false -Xcompat.version=2.0'
17
- - rvm: jruby-9.1.2.0
18
- env: JRUBY_OPTS='--server -Xcompile.invokedynamic=false'
19
19
  allow_failures:
20
20
  - rvm: ruby-head
21
- - rvm: jruby
22
- - rvm: rbx
21
+ - rvm: rbx-3
22
+ - rvm: jruby-9.1.1.0
23
23
  fast_finish: true
24
24
  branches:
25
25
  only: master
data/CHANGELOG.md CHANGED
@@ -1,7 +1,25 @@
1
1
  # Change log
2
2
 
3
+ ## [v0.2.0] - 2017-01-22
4
+
5
+ ### Added
6
+ * Add #checksum_file to generate checksum for a file, IO object or String
7
+ * Add #create_dir to create directory structure with directories and files
8
+ * Add #copy_dir to copy directory recurisvely
9
+ * Add #escape_glob_path for escaping glob characters in a path
10
+
11
+ ### Changed
12
+ * Change #binary? to only read max 4Kb of file
13
+ * Change CreateFile to accept context in constructor
14
+ * Change to separate config_options from utility options
15
+
16
+ ### Fixed
17
+ * Fix all aliases being incorrectly defined
18
+ * Fix #copy_file to stop appending to source paths
19
+
3
20
  ## [v0.1.0] - 2016-11-06
4
21
 
5
22
  * Initial implementation and release
6
23
 
24
+ [v0.2.0]: https://github.com/piotrmurach/tty-file/compare/v0.1.0...v0.2.0
7
25
  [v0.1.0]: https://github.com/piotrmurach/tty-file/compare/v0.1.0
data/Gemfile CHANGED
@@ -3,7 +3,7 @@ source 'https://rubygems.org'
3
3
  gemspec
4
4
 
5
5
  group :test do
6
- gem 'simplecov', '~> 0.10.0'
7
- gem 'coveralls', '~> 0.8.2'
8
- gem 'webmock', '~> 2.1.0'
6
+ gem 'simplecov', '~> 0.12.0'
7
+ gem 'coveralls', '~> 0.8.17'
8
+ gem 'webmock', '~> 2.3.0'
9
9
  end
data/README.md CHANGED
@@ -39,16 +39,19 @@ Or install it yourself as:
39
39
  * [1. Usage](#1-usage)
40
40
  * [2. Interface](#2-interface)
41
41
  * [2.1. binary?](#21-binary)
42
- * [2.2. chmod](#22-chmod)
43
- * [2.3. copy_file](#23-copy_file)
44
- * [2.4. create_file](#24-create_file)
45
- * [2.5. diff](#25-diff)
46
- * [2.6. download_file](#26-download_file)
47
- * [2.7. inject_into_file](#27-inject_into_file)
48
- * [2.8. replace_in_file](#28-replace_in_file)
49
- * [2.9. append_to_file](#29-apend_to_file)
50
- * [2.10. prepend_to_file](#30-prepend_to_file)
51
- * [2.11. remove_file](#211-remove_file)
42
+ * [2.2. checksum_file](#22-checksum_file)
43
+ * [2.3. chmod](#23-chmod)
44
+ * [2.4. copy_file](#24-copy_file)
45
+ * [2.5. create_file](#25-create_file)
46
+ * [2.6. copy_dir](#26-copy_dir)
47
+ * [2.7. create_dir](#27-create_dir)
48
+ * [2.8. diff](#28-diff)
49
+ * [2.9. download_file](#29-download_file)
50
+ * [2.10. inject_into_file](#210-inject_into_file)
51
+ * [2.11. replace_in_file](#211-replace_in_file)
52
+ * [2.12. append_to_file](#212-apend_to_file)
53
+ * [2.13. prepend_to_file](#213-prepend_to_file)
54
+ * [2.14. remove_file](#214-remove_file)
52
55
 
53
56
  ## 1. Usage
54
57
 
@@ -70,7 +73,23 @@ To check whether a file is a binary file, i.e. image, executable etc. do:
70
73
  TTY::File.binary?('image.png') # => true
71
74
  ```
72
75
 
73
- ### 2.2. chmod
76
+ ### 2.2. checksum_file
77
+
78
+ To generate checksum for a file, IO object or String use `checksum_file`. By default `MD5` algorithm is used which can be changed by passing second argument.
79
+
80
+ Among supported message digest algorithms are:
81
+
82
+ * `sha`, `sha1`, `sha224`, `sha256`, `sha384`, `sha512`
83
+ * `md2`, `md4`, `md5`
84
+
85
+ For example, to create digest for string using `SHA1` do:
86
+
87
+ ```ruby
88
+ TTY::File.checksum_file("Some content\nThe end", 'sha1')
89
+ # => "289388f187404135e6c15b21460442cf867180dd"
90
+ ```
91
+
92
+ ### 2.3. chmod
74
93
 
75
94
  To change file modes use `chmod` like so:
76
95
 
@@ -92,7 +111,7 @@ TTY::File.chmod('filename.rb', 'u=wrx,g+x')
92
111
 
93
112
  The `u`, `g`, and `o` specify the user, group, and other parts of the mode bits. The `a` symbol is equivalent to `ugo`.
94
113
 
95
- ### 2.3. copy_file
114
+ ### 2.4. copy_file
96
115
 
97
116
  Copies a file content from relative source to relative destination.
98
117
 
@@ -141,7 +160,7 @@ If you wish to preserve original owner, group, permission and modified time use
141
160
  TTY::File.copy_file 'docs/README.md', 'app', preserve: true
142
161
  ```
143
162
 
144
- ### 2.4. create_file
163
+ ### 2.5. create_file
145
164
 
146
165
  To create a file at a given destination with the given content use `create_file`:
147
166
 
@@ -153,9 +172,92 @@ On collision with already existing file, a menu is displayed:
153
172
 
154
173
  You can force to always overwrite file with `:force` option or always skip by providing `:skip`.
155
174
 
156
- ### 2.5. diff
175
+ ### 2.6. copy_dir
176
+
177
+ To recursively copy a directory of files from source to destination location use `copy_directory` or its alias 'copy_dir'.
178
+
179
+ Assuming you have the following directory structure:
180
+
181
+ ```ruby
182
+ # doc/
183
+ # subcommands/
184
+ # command.rb.erb
185
+ # README.md
186
+ # %name%.rb
187
+ ```
188
+
189
+ you can copy `doc` folder to `docs` by invoking:
190
+
191
+ ```ruby
192
+ TTY::File.copy_directory('doc', 'docs', context: ...)
193
+ ```
194
+
195
+ The `context` needs to respond to `name` message and given it returns `foo` value the following directory gets created:
196
+
197
+ ```ruby
198
+ # docs/
199
+ # subcommands/
200
+ # command.rb
201
+ # README.md
202
+ # foo.rb
203
+ ```
204
+
205
+ If you only need to copy top level files use option `recursive: false`:
206
+
207
+ ```ruby
208
+ TTY::File.copy_directory('doc', 'docs', recursive: false)
209
+ ```
210
+
211
+ By passing `:exclude` option you can instruct the method to ignore any files including the given pattern:
157
212
 
158
- To compare files line by line in a system independent way use `diff`:
213
+ ```ruby
214
+ TTY::File.copy_directory('doc', 'docs', exclude: 'subcommands')
215
+ ```
216
+
217
+ ### 2.7. create_dir
218
+
219
+ To create directory use `create_directory` or its alias `create_dir` passing as a first argument file path:
220
+
221
+ ```ruby
222
+ TTY::File.create_dir(/path/to/directory)
223
+ ```
224
+
225
+ or a data structure describing the directory tree including any files with or without content:
226
+
227
+ ```ruby
228
+ tree =
229
+ 'app' => [
230
+ 'README.md',
231
+ ['Gemfile', "gem 'tty-file'"],
232
+ 'lib' => [
233
+ 'cli.rb',
234
+ ['file_utils.rb', "require 'tty-file'"]
235
+ ]
236
+ 'spec' => []
237
+ ]
238
+ ```
239
+
240
+ ```ruby
241
+ TTY::File.create_dir(tree)
242
+ # =>
243
+ # app
244
+ # app/README.md
245
+ # app/Gemfile
246
+ # app/lib
247
+ # app/lib/cli.rb
248
+ # app/lib/file_utils.rb
249
+ # app/spec
250
+ ```
251
+
252
+ As a second argument you can provide a parent directory, otherwise current directory will be assumed:
253
+
254
+ ```ruby
255
+ TTy::File.create_dir(tree, '/path/to/parent/dir')
256
+ ```
257
+
258
+ ### 2.8. diff
259
+
260
+ To compare files line by line in a system independent way use `diff`, or `diff_files`:
159
261
 
160
262
  ```ruby
161
263
  TTY::File.diff('file_a', 'file_b')
@@ -194,7 +296,7 @@ Equally, you can perform a comparison between a file content and a string conten
194
296
  TTY::File.diff('/path/to/file', 'some long text')
195
297
  ```
196
298
 
197
- ### 2.6. download_file
299
+ ### 2.9. download_file
198
300
 
199
301
  To download a content from a given address and to save at a given relative location do:
200
302
 
@@ -217,7 +319,7 @@ TTY::File.download_file("https://gist.github.com/4701967", "doc/README.md", limi
217
319
  # => raises TTY::File::DownloadError
218
320
  ```
219
321
 
220
- ### 2.7. inject_into_file
322
+ ### 2.10. inject_into_file
221
323
 
222
324
  Inject content into a file at a given location
223
325
 
@@ -229,15 +331,23 @@ end
229
331
 
230
332
  You can also use Regular Expressions in `:after` or `:before` to match file location. The `append_to_file` and `prepend_to_file` allow you to add content at the end and the begging of a file.
231
333
 
232
- ### 2.8. replace_in_file
334
+ ### 2.11. replace_in_file
233
335
 
234
- Replace content of a file matching condition.
336
+ Replace content of a file matching condition by calling `replace_in_file` or `gsub_file`
235
337
 
236
338
  ```ruby
237
339
  TTY::File.replace_in_file 'filename.rb', /matching condition/, 'replacement'
238
340
  ```
239
341
 
240
- ### 2.9. append_to_file
342
+ The replacement content can be provided in a block
343
+
344
+ ```ruby
345
+ TTY::File.gsub_file 'filename.rb', /matching condition/ do
346
+ 'replacement'
347
+ end
348
+ ```
349
+
350
+ ### 2.12. append_to_file
241
351
 
242
352
  Appends text to a file. You can provide the text as a second argument:
243
353
 
@@ -253,7 +363,7 @@ TTY::File.append_to_file('Gemfile') do
253
363
  end
254
364
  ```
255
365
 
256
- ### 2.10. prepend_to_file
366
+ ### 2.13. prepend_to_file
257
367
 
258
368
  Prepends text to a file. You can provide the text as a second argument:
259
369
 
@@ -269,7 +379,7 @@ TTY::File.prepend_to_file('Gemfile') do
269
379
  end
270
380
  ```
271
381
 
272
- ### 2.11. remove_file
382
+ ### 2.14. remove_file
273
383
 
274
384
  To remove a file do:
275
385
 
@@ -299,4 +409,4 @@ The gem is available as open source under the terms of the [MIT License](http://
299
409
 
300
410
  ## Copyright
301
411
 
302
- Copyright (c) 2016 Piotr Murach. See LICENSE for further details.
412
+ Copyright (c) 2016-2017 Piotr Murach. See LICENSE for further details.
@@ -1,17 +1,28 @@
1
1
  # encoding: utf-8
2
2
 
3
+ require 'forwardable'
4
+
3
5
  module TTY
4
6
  module File
5
7
  class CreateFile
8
+ extend Forwardable
9
+
6
10
  attr_reader :relative_path, :content, :options, :prompt
7
11
 
8
- def initialize(relative_path, content, options = {})
12
+ def_delegators "@base", :log_status
13
+
14
+ def initialize(base, relative_path, content, options = {})
15
+ @base = base
9
16
  @content = content
10
17
  @options = options
11
18
  @relative_path = convert_encoded_path(relative_path)
12
19
  @prompt = TTY::Prompt.new
13
20
  end
14
21
 
22
+ def context
23
+ options[:context] || @base
24
+ end
25
+
15
26
  def exist?
16
27
  ::File.exist?(relative_path)
17
28
  end
@@ -20,10 +31,6 @@ module TTY
20
31
  ::File.binread(relative_path) == content
21
32
  end
22
33
 
23
- def log_status(*args)
24
- TTY::File.log_status(*args)
25
- end
26
-
27
34
  # Create a file
28
35
  #
29
36
  # @api public
@@ -37,10 +44,6 @@ module TTY
37
44
 
38
45
  protected
39
46
 
40
- def context
41
- options[:context]
42
- end
43
-
44
47
  def convert_encoded_path(filename)
45
48
  filename.gsub(/%(.*?)%/) do |match|
46
49
  method = $1.strip
@@ -69,8 +72,7 @@ module TTY
69
72
  end
70
73
  else
71
74
  log_status(:create, relative_path, options.fetch(:verbose, true), :green)
72
- return if options[:noop]
73
- yield
75
+ yield unless options[:noop]
74
76
  end
75
77
  end
76
78
 
@@ -81,8 +83,7 @@ module TTY
81
83
  choices = [
82
84
  { key: 'y', name: 'yes, overwrite', value: :yes },
83
85
  { key: 'n', name: 'no, do not overwrite', value: :no },
84
- { key: 'q', name: 'quit, abort', value: :quit },
85
- { key: 'd', name: 'diff, compare files line by line', value: :diff }
86
+ { key: 'q', name: 'quit, abort', value: :quit }
86
87
  ]
87
88
  answer = prompt.expand("Overwrite #{relative_path}?", choices)
88
89
  interpret_answer(answer)
@@ -0,0 +1,38 @@
1
+ # encoding: utf-8
2
+
3
+ require 'stringio'
4
+ require 'openssl'
5
+
6
+ module TTY
7
+ module File
8
+ class DigestFile
9
+ attr_reader :source
10
+
11
+ def initialize(source, mode, options)
12
+ @source = source
13
+ @digest = OpenSSL::Digest.new(mode)
14
+ end
15
+
16
+ def call
17
+ if ::FileTest.file?(source.to_s)
18
+ ::File.open(source, 'rb') { |f| checksum_io(f, @digest) }
19
+ else
20
+ non_file = source
21
+ if non_file.is_a?(String)
22
+ non_file = StringIO.new(non_file)
23
+ end
24
+ if non_file.is_a?(StringIO)
25
+ checksum_io(non_file, @digest)
26
+ end
27
+ end
28
+ end
29
+
30
+ def checksum_io(io, digest)
31
+ while (chunk = io.read(1024 * 8))
32
+ digest << chunk
33
+ end
34
+ digest.hexdigest
35
+ end
36
+ end # DigestFile
37
+ end # File
38
+ end # TTY
@@ -2,6 +2,6 @@
2
2
 
3
3
  module TTY
4
4
  module File
5
- VERSION = "0.1.0"
5
+ VERSION = "0.2.0"
6
6
  end # File
7
7
  end # TTY
data/lib/tty/file.rb CHANGED
@@ -4,8 +4,10 @@ require 'pastel'
4
4
  require 'tty-prompt'
5
5
  require 'erb'
6
6
  require 'tempfile'
7
+ require 'pathname'
7
8
 
8
9
  require 'tty/file/create_file'
10
+ require 'tty/file/digest_file'
9
11
  require 'tty/file/download_file'
10
12
  require 'tty/file/differ'
11
13
  require 'tty/file/version'
@@ -16,6 +18,8 @@ module TTY
16
18
  module_function(method)
17
19
  private_class_method(method)
18
20
  end
21
+ # Invalid path erorr
22
+ InvalidPathError = Class.new(ArgumentError)
19
23
 
20
24
  # File permissions
21
25
  U_R = 0400
@@ -43,13 +47,14 @@ module TTY
43
47
  # binary?('image.jpg') # => true
44
48
  #
45
49
  # @return [Boolean]
46
- # Returns `true` if the file is binary
50
+ # Returns `true` if the file is binary, `false` otherwise
47
51
  #
48
52
  # @api public
49
53
  def binary?(relative_path)
50
54
  bytes = ::File.stat(relative_path).blksize
51
55
  bytes = 4096 if bytes > 4096
52
- buffer = ::File.read(relative_path) || ''
56
+ buffer = ::File.read(relative_path, bytes, 0) || ''
57
+ buffer = buffer.force_encoding(Encoding.default_external)
53
58
  begin
54
59
  return buffer !~ /\A[\s[[:print:]]]*\z/m
55
60
  rescue ArgumentError => error
@@ -59,11 +64,40 @@ module TTY
59
64
  end
60
65
  module_function :binary?
61
66
 
67
+ # Create checksum for a file, io or string objects
68
+ #
69
+ # @param [File,IO,String] source
70
+ # the source to generate checksum for
71
+ # @param [String] mode
72
+ # @param [Hash[Symbol]] options
73
+ # @option options [String] :noop
74
+ # No operation
75
+ #
76
+ # @example
77
+ # checksum_file('/path/to/file')
78
+ #
79
+ # @example
80
+ # checksum_file('Some string content', 'md5')
81
+ #
82
+ # @return [String]
83
+ # the generated hex value
84
+ #
85
+ # @api public
86
+ def checksum_file(source, *args, **options)
87
+ mode = args.size.zero? ? 'sha256' : args.pop
88
+ digester = DigestFile.new(source, mode, options)
89
+ digester.call unless options[:noop]
90
+ end
91
+ module_function :checksum_file
92
+
62
93
  # Change file permissions
63
94
  #
64
95
  # @param [String] relative_path
65
96
  # @param [Integer,String] permisssions
66
97
  # @param [Hash[Symbol]] options
98
+ # @option options [Symbol] :noop
99
+ # @option options [Symbol] :verbose
100
+ # @option options [Symbol] :force
67
101
  #
68
102
  # @example
69
103
  # chmod('Gemfile', 0755)
@@ -75,7 +109,7 @@ module TTY
75
109
  # chmod('Gemfile', 'u+x,g+x')
76
110
  #
77
111
  # @api public
78
- def chmod(relative_path, permissions, options = {})
112
+ def chmod(relative_path, permissions, **options)
79
113
  mode = ::File.lstat(relative_path).mode
80
114
  if permissions.to_s =~ /\d+/
81
115
  mode = permissions
@@ -93,6 +127,58 @@ module TTY
93
127
  end
94
128
  module_function :chmod
95
129
 
130
+ # Create directory structure
131
+ #
132
+ # @param [String, Hash] destination
133
+ # the path or data structure describing directory tree
134
+ #
135
+ # @example
136
+ # create_directory('/path/to/dir')
137
+ #
138
+ # @example
139
+ # tree =
140
+ # 'app' => [
141
+ # 'README.md',
142
+ # ['Gemfile', "gem 'tty-file'"],
143
+ # 'lib' => [
144
+ # 'cli.rb',
145
+ # ['file_utils.rb', "require 'tty-file'"]
146
+ # ]
147
+ # 'spec' => []
148
+ # ]
149
+ #
150
+ # create_directory(tree)
151
+ #
152
+ # @return [void]
153
+ #
154
+ # @api public
155
+ def create_directory(destination, *args, **options)
156
+ parent = args.size.nonzero? ? args.pop : nil
157
+ if destination.is_a?(String)
158
+ destination = { destination => [] }
159
+ end
160
+
161
+ destination.each do |dir, files|
162
+ path = parent.nil? ? dir : ::File.join(parent, dir)
163
+ unless ::File.exist?(path)
164
+ ::FileUtils.mkdir_p(path)
165
+ log_status(:create, path, options.fetch(:verbose, true), :green)
166
+ end
167
+
168
+ files.each do |filename, contents|
169
+ if filename.respond_to?(:each_pair)
170
+ create_directory(filename, path, options)
171
+ else
172
+ create_file(::File.join(path, filename), contents, options)
173
+ end
174
+ end
175
+ end
176
+ end
177
+ module_function :create_directory
178
+
179
+ alias create_dir create_directory
180
+ module_function :create_dir
181
+
96
182
  # Create new file if doesn't exist
97
183
  #
98
184
  # @param [String] relative_path
@@ -111,21 +197,27 @@ module TTY
111
197
  # end
112
198
  #
113
199
  # @api public
114
- def create_file(relative_path, *args, &block)
115
- options = args.last.is_a?(Hash) ? args.pop : {}
116
-
200
+ def create_file(relative_path, *args, **options, &block)
117
201
  content = block_given? ? block[] : args.join
118
202
 
119
- CreateFile.new(relative_path, content, options).call
203
+ CreateFile.new(self, relative_path, content, options).call
120
204
  end
121
205
  module_function :create_file
122
206
 
207
+ alias add_file create_file
208
+ module_function :add_file
209
+
123
210
  # Copy file from the relative source to the relative
124
211
  # destination running it through ERB.
125
212
  #
126
213
  # @example
127
214
  # copy_file 'templates/test.rb', 'app/test.rb'
128
215
  #
216
+ # @example
217
+ # vars = OpenStruct.new
218
+ # vars[:name] = 'foo'
219
+ # copy_file 'templates/%name%.rb', 'app/%name%.rb', context: vars
220
+ #
129
221
  # @param [Hash] options
130
222
  # @option options [Symbol] :context
131
223
  # the binding to use for the template
@@ -138,13 +230,8 @@ module TTY
138
230
  # If true log the action status to stdout
139
231
  #
140
232
  # @api public
141
- def copy_file(source_path, *args, &block)
142
- options = args.last.is_a?(Hash) ? args.pop : {}
143
- dest_path = args.first || source_path.sub(/\.erb$/, '')
144
-
145
- if ::File.directory?(dest_path)
146
- dest_path = ::File.join(dest_path, ::File.basename(source_path))
147
- end
233
+ def copy_file(source_path, *args, **options, &block)
234
+ dest_path = (args.first || source_path).sub(/\.erb$/, '')
148
235
 
149
236
  ctx = if (vars = options[:context])
150
237
  vars.instance_eval('binding')
@@ -152,7 +239,6 @@ module TTY
152
239
  instance_eval('binding')
153
240
  end
154
241
 
155
- options[:context] ||= self
156
242
  create_file(dest_path, options) do
157
243
  template = ERB.new(::File.binread(source_path), nil, "-", "@output_buffer")
158
244
  content = template.result(ctx)
@@ -166,14 +252,77 @@ module TTY
166
252
 
167
253
  # Copy file metadata
168
254
  #
255
+ # @param [String] src_path
256
+ # the source file path
257
+ # @param [String] dest_path
258
+ # the destination file path
259
+ #
169
260
  # @api public
170
- def copy_metadata(src_path, dest_path, options = {})
261
+ def copy_metadata(src_path, dest_path, **options)
171
262
  stats = ::File.lstat(src_path)
172
263
  ::File.utime(stats.atime, stats.mtime, dest_path)
173
264
  chmod(dest_path, stats.mode, options)
174
265
  end
175
266
  module_function :copy_metadata
176
267
 
268
+ # Copy directory recursively from source to destination path
269
+ #
270
+ # Any files names wrapped within % sign will be expanded by
271
+ # executing corresponding method and inserting its value.
272
+ # Assuming the following directory structure:
273
+ #
274
+ # app/
275
+ # %name%.rb
276
+ # command.rb.erb
277
+ # README.md
278
+ #
279
+ # Invoking:
280
+ # copy_directory("app", "new_app")
281
+ # The following directory structure should be created where
282
+ # name resolves to 'cli' value:
283
+ #
284
+ # new_app/
285
+ # cli.rb
286
+ # command.rb
287
+ # README
288
+ #
289
+ # @param [Hash[Symbol]] options
290
+ # @option options [Symbol] :preserve
291
+ # If true, the owner, group, permissions and modified time
292
+ # are preserved on the copied file, defaults to false.
293
+ # @option options [Symbol] :recursive
294
+ # If false, copies only top level files, defaults to true.
295
+ # @option options [Symbol] :exclude
296
+ # A regex that specifies files to ignore when copying.
297
+ #
298
+ # @example
299
+ # copy_directory("app", "new_app", recursive: false)
300
+ # copy_directory("app", "new_app", exclude: /docs/)
301
+ #
302
+ # @api public
303
+ def copy_directory(source_path, *args, **options, &block)
304
+ check_path(source_path)
305
+ source = escape_glob_path(source_path)
306
+ dest_path = args.first || source
307
+ opts = {recursive: true}.merge(options)
308
+ pattern = opts[:recursive] ? ::File.join(source, '**') : source
309
+ glob_pattern = ::File.join(pattern, '*')
310
+
311
+ Dir.glob(glob_pattern, ::File::FNM_DOTMATCH).sort.each do |file_source|
312
+ next if ::File.directory?(file_source)
313
+ next if opts[:exclude] && file_source.match(opts[:exclude])
314
+
315
+ dest = ::File.join(dest_path, file_source.gsub(source_path, '.'))
316
+ file_dest = ::Pathname.new(dest).cleanpath.to_s
317
+
318
+ copy_file(file_source, file_dest, **options, &block)
319
+ end
320
+ end
321
+ module_function :copy_directory
322
+
323
+ alias copy_dir copy_directory
324
+ module_function :copy_dir
325
+
177
326
  # Diff files line by line
178
327
  #
179
328
  # @param [String] path_a
@@ -190,7 +339,7 @@ module TTY
190
339
  # diff(file_a, file_b, format: :old)
191
340
  #
192
341
  # @api public
193
- def diff(path_a, path_b, options = {})
342
+ def diff(path_a, path_b, **options)
194
343
  threshold = options[:threshold] || 10_000_000
195
344
  output = ''
196
345
 
@@ -224,7 +373,9 @@ module TTY
224
373
  output
225
374
  end
226
375
  module_function :diff
376
+
227
377
  alias diff_files diff
378
+ module_function :diff_files
228
379
 
229
380
  # Download the content from a given address and
230
381
  # save at the given relative destination. If block
@@ -249,8 +400,7 @@ module TTY
249
400
  # end
250
401
  #
251
402
  # @api public
252
- def download_file(uri, *args, &block)
253
- options = args.last.is_a?(Hash) ? args.pop : {}
403
+ def download_file(uri, *args, **options, &block)
254
404
  dest_path = args.first || ::File.basename(uri)
255
405
 
256
406
  unless uri =~ %r{^https?\://}
@@ -261,13 +411,16 @@ module TTY
261
411
  content = DownloadFile.new(uri, dest_path, options).call
262
412
 
263
413
  if block_given?
264
- content = (block.arity == 1 ? block[content] : block[])
414
+ content = (block.arity.nonzero? ? block[content] : block[])
265
415
  end
266
416
 
267
417
  create_file(dest_path, content, options)
268
418
  end
269
419
  module_function :download_file
270
420
 
421
+ alias get_file download_file
422
+ module_function :get_file
423
+
271
424
  # Prepend to a file
272
425
  #
273
426
  # @param [String] relative_path
@@ -283,8 +436,7 @@ module TTY
283
436
  # end
284
437
  #
285
438
  # @api public
286
- def prepend_to_file(relative_path, *args, &block)
287
- options = args.last.is_a?(Hash) ? args.pop : {}
439
+ def prepend_to_file(relative_path, *args, **options, &block)
288
440
  log_status(:prepend, relative_path, options.fetch(:verbose, true), :green)
289
441
  options.merge!(before: /\A/, verbose: false)
290
442
  inject_into_file(relative_path, *(args << options), &block)
@@ -306,14 +458,15 @@ module TTY
306
458
  # end
307
459
  #
308
460
  # @api public
309
- def append_to_file(relative_path, *args, &block)
310
- options = args.last.is_a?(Hash) ? args.pop : {}
461
+ def append_to_file(relative_path, *args, **options, &block)
311
462
  log_status(:append, relative_path, options.fetch(:verbose, true), :green)
312
463
  options.merge!(after: /\z/, verbose: false)
313
464
  inject_into_file(relative_path, *(args << options), &block)
314
465
  end
315
466
  module_function :append_to_file
467
+
316
468
  alias add_to_file append_to_file
469
+ module_function :add_to_file
317
470
 
318
471
  # Inject content into file at a given location
319
472
  #
@@ -341,9 +494,7 @@ module TTY
341
494
  # end
342
495
  #
343
496
  # @api public
344
- def inject_into_file(relative_path, *args, &block)
345
- options = args.last.is_a?(Hash) ? args.pop : {}
346
-
497
+ def inject_into_file(relative_path, *args, **options, &block)
347
498
  replacement = block_given? ? block[] : args.join
348
499
 
349
500
  flag, match = if options.key?(:after)
@@ -364,7 +515,9 @@ module TTY
364
515
  log_status(:inject, relative_path, options.fetch(:verbose, true), :green)
365
516
  end
366
517
  module_function :inject_into_file
518
+
367
519
  alias insert_into_file inject_into_file
520
+ module_function :insert_into_file
368
521
 
369
522
  # Replace content of a file matching string
370
523
  #
@@ -383,12 +536,9 @@ module TTY
383
536
  # end
384
537
  #
385
538
  # @api public
386
- def replace_in_file(relative_path, *args, &block)
539
+ def replace_in_file(relative_path, *args, **options, &block)
387
540
  check_path(relative_path)
388
- options = args.last.is_a?(Hash) ? args.pop : {}
389
-
390
- contents = IO.read(relative_path)
391
-
541
+ contents = IO.read(relative_path)
392
542
  replacement = (block ? block[] : args[1..-1].join).gsub('\0', '')
393
543
 
394
544
  log_status(:replace, relative_path, options.fetch(:verbose, true), :green)
@@ -400,13 +550,15 @@ module TTY
400
550
  find = args[0]
401
551
  raise "#{find.inspect} not found in #{relative_path}"
402
552
  end
403
- ::File.open(relative_path, 'w') do |file|
553
+ ::File.open(relative_path, 'wb') do |file|
404
554
  file.write(contents)
405
555
  end
406
556
  end
407
557
  end
408
558
  module_function :replace_in_file
559
+
409
560
  alias gsub_file replace_in_file
561
+ module_function :gsub_file
410
562
 
411
563
  # Remove a file or a directory at specified relative path.
412
564
  #
@@ -422,9 +574,7 @@ module TTY
422
574
  # remove_file 'doc/README.md'
423
575
  #
424
576
  # @api public
425
- def remove_file(relative_path, *args)
426
- options = args.last.is_a?(Hash) ? args.pop : {}
427
-
577
+ def remove_file(relative_path, *args, **options)
428
578
  log_status(:remove, relative_path, options.fetch(:verbose, true), :red)
429
579
 
430
580
  return if options[:noop]
@@ -433,14 +583,32 @@ module TTY
433
583
  end
434
584
  module_function :remove_file
435
585
 
586
+ # Escape glob character in a path
587
+ #
588
+ # @param [String] path
589
+ # the path to escape
590
+ #
591
+ # @example
592
+ # escape_glob_path("foo[bar]") => "foo\\[bar\\]"
593
+ #
594
+ # @return [String]
595
+ #
596
+ # @api public
597
+ def escape_glob_path(path)
598
+ path.gsub(/[\\\{\}\[\]\*\?]/) { |x| "\\" + x }
599
+ end
600
+ module_function :escape_glob_path
601
+
436
602
  # Check if path exists
437
603
  #
604
+ # @param [String] path
605
+ #
438
606
  # @raise [ArgumentError]
439
607
  #
440
608
  # @api private
441
609
  def check_path(path)
442
610
  return if ::File.exist?(path)
443
- raise ArgumentError, "File path #{path} does not exist."
611
+ raise InvalidPathError, "File path \"#{path}\" does not exist."
444
612
  end
445
613
  private_module_function :check_path
446
614
 
@@ -467,7 +635,7 @@ module TTY
467
635
  @output.print(message)
468
636
  @output.flush
469
637
  end
470
- module_function :log_status
638
+ private_module_function :log_status
471
639
 
472
640
  # If content is not a path to a file, create a
473
641
  # tempfile and open it instead.
data/tty-file.gemspec CHANGED
@@ -19,9 +19,9 @@ Gem::Specification.new do |spec|
19
19
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
20
  spec.require_paths = ["lib"]
21
21
 
22
- spec.add_dependency 'pastel', '~> 0.6.0'
23
- spec.add_dependency 'tty-prompt', '~> 0.7.0'
24
- spec.add_dependency 'diff-lcs', '~> 1.2.5'
22
+ spec.add_dependency 'pastel', '~> 0.7.0'
23
+ spec.add_dependency 'tty-prompt', '~> 0.10.0'
24
+ spec.add_dependency 'diff-lcs', '~> 1.3.0'
25
25
 
26
26
  spec.add_development_dependency 'bundler', '>= 1.5.0', '< 2.0'
27
27
  spec.add_development_dependency 'rake', '~> 10.0'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tty-file
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Piotr Murach
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2016-11-06 00:00:00.000000000 Z
11
+ date: 2017-01-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: pastel
@@ -16,42 +16,42 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 0.6.0
19
+ version: 0.7.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: 0.6.0
26
+ version: 0.7.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: tty-prompt
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: 0.7.0
33
+ version: 0.10.0
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: 0.7.0
40
+ version: 0.10.0
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: diff-lcs
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: 1.2.5
47
+ version: 1.3.0
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: 1.2.5
54
+ version: 1.3.0
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: bundler
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -122,6 +122,7 @@ files:
122
122
  - lib/tty/file.rb
123
123
  - lib/tty/file/create_file.rb
124
124
  - lib/tty/file/differ.rb
125
+ - lib/tty/file/digest_file.rb
125
126
  - lib/tty/file/download_file.rb
126
127
  - lib/tty/file/version.rb
127
128
  - tasks/console.rake