docscribe 1.4.1 → 1.5.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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +588 -104
  3. data/lib/docscribe/cli/check_for_comments.rb +183 -0
  4. data/lib/docscribe/cli/config_builder.rb +180 -36
  5. data/lib/docscribe/cli/formatters/json.rb +294 -0
  6. data/lib/docscribe/cli/formatters/sarif.rb +235 -0
  7. data/lib/docscribe/cli/formatters/text.rb +208 -0
  8. data/lib/docscribe/cli/formatters.rb +26 -0
  9. data/lib/docscribe/cli/generate.rb +296 -125
  10. data/lib/docscribe/cli/init.rb +58 -14
  11. data/lib/docscribe/cli/options.rb +410 -133
  12. data/lib/docscribe/cli/rbs_gen.rb +529 -0
  13. data/lib/docscribe/cli/run.rb +503 -189
  14. data/lib/docscribe/cli/sigs.rb +366 -0
  15. data/lib/docscribe/cli/update_types.rb +103 -0
  16. data/lib/docscribe/cli.rb +35 -9
  17. data/lib/docscribe/config/defaults.rb +16 -12
  18. data/lib/docscribe/config/emit.rb +18 -0
  19. data/lib/docscribe/config/filtering.rb +37 -31
  20. data/lib/docscribe/config/loader.rb +20 -13
  21. data/lib/docscribe/config/plugin.rb +2 -1
  22. data/lib/docscribe/config/rbs.rb +68 -27
  23. data/lib/docscribe/config/sorbet.rb +40 -17
  24. data/lib/docscribe/config/sorting.rb +2 -1
  25. data/lib/docscribe/config/template.rb +10 -1
  26. data/lib/docscribe/config/utils.rb +12 -9
  27. data/lib/docscribe/config.rb +3 -4
  28. data/lib/docscribe/infer/ast_walk.rb +1 -1
  29. data/lib/docscribe/infer/constants.rb +15 -0
  30. data/lib/docscribe/infer/literals.rb +39 -26
  31. data/lib/docscribe/infer/names.rb +24 -16
  32. data/lib/docscribe/infer/params.rb +57 -13
  33. data/lib/docscribe/infer/raises.rb +23 -15
  34. data/lib/docscribe/infer/returns.rb +784 -199
  35. data/lib/docscribe/infer.rb +28 -28
  36. data/lib/docscribe/inline_rewriter/collector.rb +816 -430
  37. data/lib/docscribe/inline_rewriter/doc_block.rb +323 -150
  38. data/lib/docscribe/inline_rewriter/doc_builder.rb +1837 -648
  39. data/lib/docscribe/inline_rewriter/source_helpers.rb +119 -71
  40. data/lib/docscribe/inline_rewriter/tag_sorter.rb +165 -107
  41. data/lib/docscribe/inline_rewriter.rb +1144 -727
  42. data/lib/docscribe/parsing.rb +29 -10
  43. data/lib/docscribe/plugin/base/collector_plugin.rb +3 -3
  44. data/lib/docscribe/plugin/base/tag_plugin.rb +1 -2
  45. data/lib/docscribe/plugin/context.rb +28 -18
  46. data/lib/docscribe/plugin/registry.rb +49 -23
  47. data/lib/docscribe/plugin/tag.rb +9 -14
  48. data/lib/docscribe/plugin.rb +54 -22
  49. data/lib/docscribe/types/provider_chain.rb +4 -2
  50. data/lib/docscribe/types/rbs/collection_loader.rb +2 -3
  51. data/lib/docscribe/types/rbs/provider.rb +127 -62
  52. data/lib/docscribe/types/rbs/type_formatter.rb +286 -77
  53. data/lib/docscribe/types/signature.rb +22 -42
  54. data/lib/docscribe/types/sorbet/base_provider.rb +51 -27
  55. data/lib/docscribe/types/sorbet/rbi_provider.rb +3 -3
  56. data/lib/docscribe/types/sorbet/source_provider.rb +3 -2
  57. data/lib/docscribe/types/yard/formatter.rb +100 -0
  58. data/lib/docscribe/types/yard/parser.rb +240 -0
  59. data/lib/docscribe/types/yard/types.rb +52 -0
  60. data/lib/docscribe/version.rb +1 -1
  61. metadata +34 -2
@@ -14,91 +14,92 @@ module Docscribe
14
14
  module Generate
15
15
  PLUGIN_TYPES = %w[tag collector].freeze
16
16
 
17
+ NEXT_STEPS_TEMPLATE = <<~TEXT
18
+ Next steps:
19
+ 1. Open %<path>s and implement the plugin logic.
20
+ %<hint>s
21
+
22
+ 2. Register the plugin in your docscribe_plugins.rb:
23
+
24
+ require_relative '%<require_path>s'
25
+ Docscribe::Plugin::Registry.register(%<base_name>s.new)
26
+
27
+ 3. Add the file to docscribe.yml:
28
+
29
+ plugins:
30
+ require:
31
+ - ./docscribe_plugins
32
+ TEXT
33
+
17
34
  class << self
18
35
  # Run the `generate` subcommand.
19
36
  #
20
- # @param [Array<String>] argv
21
- # @raise [OptionParser::InvalidOption]
37
+ # @param [Array<String>] argv command line arguments
22
38
  # @return [Integer] exit code
23
39
  def run(argv)
24
- opts = {
25
- output: nil,
26
- stdout: false
27
- }
28
-
29
- parser = OptionParser.new do |o|
30
- o.banner = <<~TEXT
31
- Usage: docscribe generate <type> <PluginName> [options]
32
-
33
- Types:
34
- tag Generate a TagPlugin skeleton
35
- collector Generate a CollectorPlugin skeleton
36
-
37
- Options:
38
- TEXT
39
-
40
- o.on('--output DIR', 'Directory to write the plugin file (default: .)') { |v| opts[:output] = v }
41
- o.on('--stdout', 'Print the generated plugin to STDOUT instead of writing a file') { opts[:stdout] = true }
42
- o.on('-h', '--help', 'Show this help') do
43
- puts o
44
- return 0
45
- end
46
- end
40
+ opts, parser = parse_generate_options(argv)
41
+ return 0 if opts[:help]
42
+
43
+ plugin_type, class_name = extract_generate_args(argv)
44
+ result = validate_generate_args(plugin_type, class_name, parser)
45
+ return result if result
46
+
47
+ content = render(plugin_type, class_name)
48
+ dispatch_output(content, plugin_type, class_name, opts)
49
+ end
50
+
51
+ private
52
+
53
+ # Parse options for the generate subcommand.
54
+ #
55
+ # @private
56
+ # @param [Array<String>] argv command line arguments
57
+ # @raise [OptionParser::InvalidOption]
58
+ # @return [(Hash<Symbol, Object>, OptionParser)]
59
+ def parse_generate_options(argv)
60
+ opts = { output: nil, stdout: false, help: false }
61
+ parser = build_option_parser(opts)
47
62
 
48
63
  begin
49
64
  parser.parse!(argv)
50
65
  rescue OptionParser::InvalidOption => e
51
66
  warn e.message
52
67
  warn parser
53
- return 1
54
- end
55
-
56
- plugin_type = argv.shift
57
- class_name = argv.shift
58
-
59
- unless plugin_type && class_name
60
- warn 'Error: both <type> and <PluginName> are required.'
61
- warn parser
62
- return 1
63
- end
64
-
65
- unless PLUGIN_TYPES.include?(plugin_type)
66
- warn "Error: unknown type #{plugin_type.inspect}. Must be one of: #{PLUGIN_TYPES.join(', ')}."
67
- return 1
68
68
  end
69
69
 
70
- unless valid_constant?(class_name)
71
- warn "Error: #{class_name.inspect} is not a valid Ruby constant name."
72
- return 1
73
- end
74
-
75
- content = render(plugin_type, class_name)
76
-
77
- if opts[:stdout]
78
- puts content
79
- return 0
80
- end
81
-
82
- write_plugin(content, plugin_type: plugin_type, class_name: class_name, output_dir: opts[:output] || '.')
70
+ [opts, parser]
83
71
  end
84
72
 
85
- private
73
+ # Extract plugin_type and class_name from remaining argv.
74
+ #
75
+ # @private
76
+ # @param [Array<String>] argv command line arguments
77
+ # @return [(String?, String?)]
78
+ def extract_generate_args(argv)
79
+ [argv.shift, argv.shift]
80
+ end
86
81
 
87
- # Check whether a string is a valid Ruby constant name.
82
+ # Validate generate arguments and return exit code on failure.
88
83
  #
89
84
  # @private
90
- # @param [String] str
91
- # @return [Boolean]
92
- def valid_constant?(str)
93
- !!(str =~ /\A[A-Z][A-Za-z0-9]*(?:::[A-Z][A-Za-z0-9]*)*\z/)
85
+ # @param [String?] plugin_type 'tag' or 'collector'
86
+ # @param [String?] class_name CamelCase plugin class name
87
+ # @param [OptionParser] parser option parser instance
88
+ # @return [Integer, nil] exit code or nil if valid
89
+ def validate_generate_args(plugin_type, class_name, parser)
90
+ return 1 unless args_provided?(plugin_type, class_name, parser)
91
+ return 1 unless known_type?(plugin_type)
92
+ return 1 unless valid_name?(class_name)
93
+
94
+ nil
94
95
  end
95
96
 
96
97
  # Render plugin boilerplate for the given type and class name.
97
98
  #
98
99
  # @private
99
- # @param [String] plugin_type 'tag' or 'collector'
100
- # @param [String] class_name CamelCase plugin class name
101
- # @return [String]
100
+ # @param [String?] plugin_type 'tag' or 'collector'
101
+ # @param [String?] class_name CamelCase plugin class name
102
+ # @return [String?]
102
103
  def render(plugin_type, class_name)
103
104
  case plugin_type
104
105
  when 'tag' then tag_template(class_name)
@@ -106,70 +107,10 @@ module Docscribe
106
107
  end
107
108
  end
108
109
 
109
- # Write the generated content to a file.
110
- #
111
- # @private
112
- # @param [String] content
113
- # @param [String] plugin_type
114
- # @param [String] class_name
115
- # @param [String] output_dir
116
- # @return [Integer] exit code
117
- def write_plugin(content, plugin_type:, class_name:, output_dir:)
118
- filename = "#{underscore(class_name)}.rb"
119
- path = File.join(output_dir, filename)
120
-
121
- if File.exist?(path)
122
- warn "Error: #{path} already exists. Remove it first or use --stdout."
123
- return 1
124
- end
125
-
126
- require 'fileutils'
127
- FileUtils.mkdir_p(output_dir)
128
- File.write(path, content)
129
- puts "Created: #{path}"
130
- puts
131
- puts next_steps(plugin_type, path)
132
- 0
133
- end
134
-
135
- # Print registration instructions after file creation.
136
- #
137
- # @private
138
- # @param [String] plugin_type
139
- # @param [String] path
140
- # @return [String]
141
- def next_steps(plugin_type, path)
142
- base_name = File.basename(path, '.rb').split('_').map(&:capitalize).join
143
-
144
- implement_hint = case plugin_type
145
- when 'tag'
146
- 'Implement the #call method to return Array<Docscribe::Plugin::Tag>.'
147
- when 'collector'
148
- 'Implement the #collect method to return Array<{anchor_node:, doc:}>.'
149
- end
150
-
151
- <<~TEXT
152
- Next steps:
153
- 1. Open #{path} and implement the plugin logic.
154
- #{implement_hint}
155
-
156
- 2. Register the plugin in your docscribe_plugins.rb:
157
-
158
- require_relative '#{path.delete_suffix('.rb')}'
159
- Docscribe::Plugin::Registry.register(#{base_name}.new)
160
-
161
- 3. Add the file to docscribe.yml:
162
-
163
- plugins:
164
- require:
165
- - ./docscribe_plugins
166
- TEXT
167
- end
168
-
169
110
  # Template for a TagPlugin.
170
111
  #
171
112
  # @private
172
- # @param [String] class_name
113
+ # @param [String?] class_name CamelCase plugin class name
173
114
  # @return [String]
174
115
  def tag_template(class_name)
175
116
  <<~RUBY
@@ -227,7 +168,7 @@ module Docscribe
227
168
  # Template for a CollectorPlugin.
228
169
  #
229
170
  # @private
230
- # @param [String] class_name
171
+ # @param [String?] class_name CamelCase plugin class name
231
172
  # @return [String]
232
173
  def collector_template(class_name)
233
174
  <<~RUBY
@@ -292,10 +233,166 @@ module Docscribe
292
233
  RUBY
293
234
  end
294
235
 
236
+ # Write generated plugin to a file or print to STDOUT based on options.
237
+ #
238
+ # @private
239
+ # @param [String?] content generated plugin source code
240
+ # @param [String?] plugin_type 'tag' or 'collector'
241
+ # @param [String?] class_name CamelCase plugin class name
242
+ # @param [Hash<Symbol, Object>] opts parsed options hash
243
+ # @return [Integer] exit code
244
+ def dispatch_output(content, plugin_type, class_name, opts)
245
+ if opts[:stdout]
246
+ puts content
247
+ return 0
248
+ end
249
+
250
+ write_plugin(content, plugin_type: plugin_type, class_name: class_name, output_dir: opts[:output] || '.')
251
+ end
252
+
253
+ # Write the generated content to a file.
254
+ #
255
+ # @private
256
+ # @param [String?] content file content to write
257
+ # @param [String?] plugin_type 'tag' or 'collector'
258
+ # @param [String?] class_name CamelCase plugin class name
259
+ # @param [String?] output_dir output directory path
260
+ # @return [Integer] exit code
261
+ def write_plugin(content, plugin_type:, class_name:, output_dir:)
262
+ path = plugin_path(class_name, output_dir)
263
+
264
+ return 1 if file_exists?(path)
265
+
266
+ write_to_file(output_dir, path, content)
267
+ print_created(plugin_type, path)
268
+ 0
269
+ end
270
+
271
+ # Build the OptionParser for the generate subcommand.
272
+ #
273
+ # @private
274
+ # @param [Hash<Symbol, Object>] opts mutable parsed options hash
275
+ # @return [OptionParser]
276
+ def build_option_parser(opts)
277
+ OptionParser.new do |opt|
278
+ opt.banner = parser_banner
279
+ register_output_option(opt, opts)
280
+ register_stdout_option(opt, opts)
281
+ register_help_option(opt, opts)
282
+ end
283
+ end
284
+
285
+ # Return the usage banner for the generate subcommand parser.
286
+ #
287
+ # @private
288
+ # @return [String]
289
+ def parser_banner
290
+ <<~TEXT
291
+ Usage: docscribe generate <type> <PluginName> [options]
292
+
293
+ Types:
294
+ tag Generate a TagPlugin skeleton
295
+ collector Generate a CollectorPlugin skeleton
296
+
297
+ Options:
298
+ TEXT
299
+ end
300
+
301
+ # Register the --output option on the OptionParser.
302
+ #
303
+ # @private
304
+ # @param [OptionParser] opt option parser instance
305
+ # @param [Hash<Symbol, Object>] opts mutable parsed options hash
306
+ # @return [void]
307
+ def register_output_option(opt, opts)
308
+ opt.on('--output DIR', 'Directory to write the plugin file (default: .)') { |v| opts[:output] = v }
309
+ end
310
+
311
+ # Register the --stdout option on the OptionParser.
312
+ #
313
+ # @private
314
+ # @param [OptionParser] opt option parser instance
315
+ # @param [Hash<Symbol, Object>] opts mutable parsed options hash
316
+ # @return [void]
317
+ def register_stdout_option(opt, opts)
318
+ opt.on('--stdout', 'Print the generated plugin to STDOUT instead of writing a file') { opts[:stdout] = true }
319
+ end
320
+
321
+ # Register the -h/--help option on the OptionParser.
322
+ #
323
+ # @private
324
+ # @param [OptionParser] opt option parser instance
325
+ # @param [Hash<Symbol, Object>] opts mutable parsed options hash
326
+ # @return [void]
327
+ def register_help_option(opt, opts)
328
+ opt.on('-h', '--help', 'Show this help') do
329
+ puts opt
330
+ opts[:help] = true
331
+ end
332
+ end
333
+
334
+ # Validate that both plugin_type and class_name arguments were provided.
335
+ #
336
+ # @private
337
+ # @param [String?] plugin_type plugin type argument
338
+ # @param [String?] class_name plugin class name argument
339
+ # @param [OptionParser] parser option parser instance
340
+ # @return [Boolean]
341
+ def args_provided?(plugin_type, class_name, parser)
342
+ return true if plugin_type && class_name
343
+
344
+ warn 'Error: both <type> and <PluginName> are required.'
345
+ warn parser
346
+ false
347
+ end
348
+
349
+ # Validate that the plugin type is one of the recognized types.
350
+ #
351
+ # @private
352
+ # @param [String?] plugin_type plugin type to validate
353
+ # @return [Boolean]
354
+ def known_type?(plugin_type)
355
+ return true if PLUGIN_TYPES.include?(plugin_type)
356
+
357
+ warn "Error: unknown type #{plugin_type.inspect}. Must be one of: #{PLUGIN_TYPES.join(', ')}."
358
+ false
359
+ end
360
+
361
+ # Validate that the class name is a valid Ruby constant name.
362
+ #
363
+ # @private
364
+ # @param [String?] class_name class name to validate
365
+ # @return [Boolean]
366
+ def valid_name?(class_name)
367
+ return true if valid_constant?(class_name)
368
+
369
+ warn "Error: #{class_name.inspect} is not a valid Ruby constant name."
370
+ false
371
+ end
372
+
373
+ # Check whether a string is a valid Ruby constant name.
374
+ #
375
+ # @private
376
+ # @param [String?] str string constant to validate
377
+ # @return [Boolean]
378
+ def valid_constant?(str)
379
+ !!(str =~ /\A[A-Z][A-Za-z0-9]*(?:::[A-Z][A-Za-z0-9]*)*\z/)
380
+ end
381
+
382
+ # Build the file path for the generated plugin.
383
+ #
384
+ # @private
385
+ # @param [String?] class_name CamelCase plugin class name
386
+ # @param [String?] output_dir output directory
387
+ # @return [String] full file path
388
+ def plugin_path(class_name, output_dir)
389
+ File.join(output_dir || '.', "#{underscore(class_name || '')}.rb")
390
+ end
391
+
295
392
  # Convert CamelCase to snake_case for file naming.
296
393
  #
297
394
  # @private
298
- # @param [String] str
395
+ # @param [String] str CamelCase string to convert
299
396
  # @return [String]
300
397
  def underscore(str)
301
398
  str
@@ -303,6 +400,80 @@ module Docscribe
303
400
  .gsub(/([a-z\d])([A-Z])/, '\1_\2')
304
401
  .downcase
305
402
  end
403
+
404
+ # Check whether the target plugin file already exists and warn if so.
405
+ #
406
+ # @private
407
+ # @param [String] path file path to check
408
+ # @return [Boolean]
409
+ def file_exists?(path)
410
+ return false unless File.exist?(path)
411
+
412
+ warn "Error: #{path} already exists. Remove it first or use --stdout."
413
+ true
414
+ end
415
+
416
+ # Create the output directory and write the plugin file.
417
+ #
418
+ # @private
419
+ # @param [String?] output_dir output directory path
420
+ # @param [String] path full plugin file path
421
+ # @param [String?] content file content to write
422
+ # @return [void]
423
+ def write_to_file(output_dir, path, content)
424
+ require 'fileutils'
425
+ FileUtils.mkdir_p(output_dir || '.')
426
+ File.write(path, content)
427
+ end
428
+
429
+ # Print the creation message and next steps after generating a plugin.
430
+ #
431
+ # @private
432
+ # @param [String?] plugin_type 'tag' or 'collector'
433
+ # @param [String] path file path of the created plugin
434
+ # @return [void]
435
+ def print_created(plugin_type, path)
436
+ puts "Created: #{path}"
437
+ puts
438
+ puts next_steps(plugin_type, path)
439
+ end
440
+
441
+ # Print registration instructions after file creation.
442
+ #
443
+ # @private
444
+ # @param [String?] plugin_type 'tag' or 'collector'
445
+ # @param [String] path file path
446
+ # @return [String]
447
+ def next_steps(plugin_type, path)
448
+ format(NEXT_STEPS_TEMPLATE,
449
+ path: path,
450
+ hint: generate_implement_hint(plugin_type),
451
+ require_path: path.delete_suffix('.rb'),
452
+ base_name: plugin_base_name(path))
453
+ end
454
+
455
+ # Derive a CamelCase base name from a snake_case file path.
456
+ #
457
+ # @private
458
+ # @param [String] path file path
459
+ # @return [String] CamelCase class name
460
+ def plugin_base_name(path)
461
+ File.basename(path, '.rb').split('_').map(&:capitalize).join
462
+ end
463
+
464
+ # Generate an implementation hint string for the given plugin type.
465
+ #
466
+ # @private
467
+ # @param [String?] plugin_type 'tag' or 'collector'
468
+ # @return [String, nil] hint text
469
+ def generate_implement_hint(plugin_type)
470
+ case plugin_type
471
+ when 'tag'
472
+ 'Implement the #call method to return Array<Docscribe::Plugin::Tag>.'
473
+ when 'collector'
474
+ 'Implement the #collect method to return Array<{anchor_node:, doc:}>.'
475
+ end
476
+ end
306
477
  end
307
478
  end
308
479
  end
@@ -5,7 +5,16 @@ require 'docscribe/config'
5
5
 
6
6
  module Docscribe
7
7
  module CLI
8
+ # Generate starter Docscribe configuration.
8
9
  module Init
10
+ BANNER = <<~TEXT
11
+ Usage: docscribe init [options]
12
+
13
+ Generate a starter docscribe.yml configuration file.
14
+
15
+ Options:
16
+ TEXT
17
+
9
18
  class << self
10
19
  # Create or print a starter Docscribe configuration file.
11
20
  #
@@ -18,30 +27,65 @@ module Docscribe
18
27
  # @param [Array<String>] argv command-line arguments for `docscribe init`
19
28
  # @return [Integer] process exit code
20
29
  def run(argv)
21
- opts = {
22
- config: 'docscribe.yml',
23
- force: false,
24
- stdout: false
25
- }
30
+ opts = parse_init_options(argv)
31
+ return 0 if opts[:help]
32
+
33
+ yaml = Docscribe::Config.default_yaml
26
34
 
35
+ if opts[:stdout]
36
+ puts yaml
37
+ return 0
38
+ end
39
+
40
+ write_init_config(opts, yaml)
41
+ end
42
+
43
+ private
44
+
45
+ # Parse CLI options for `docscribe init`.
46
+ #
47
+ # @private
48
+ # @param [Array<String>] argv command-line arguments for `docscribe init`
49
+ # @return [Hash<Symbol, Object>] parsed options
50
+ def parse_init_options(argv)
51
+ opts = default_init_options
52
+ build_init_parser(opts).parse!(argv)
53
+ opts
54
+ end
55
+
56
+ # Return the default options hash for the init command.
57
+ #
58
+ # @private
59
+ # @return [Hash<Symbol, String, Boolean>]
60
+ def default_init_options
61
+ { config: 'docscribe.yml', force: false, stdout: false, help: false }
62
+ end
63
+
64
+ # Build and return an OptionParser for the init command.
65
+ #
66
+ # @private
67
+ # @param [Hash<Symbol, Object>] opts options hash that the parser populates
68
+ # @return [OptionParser]
69
+ def build_init_parser(opts)
27
70
  OptionParser.new do |o|
28
- o.banner = 'Usage: docscribe init [options]'
71
+ o.banner = BANNER
29
72
  o.on('--config PATH', 'Where to write the config (default: docscribe.yml)') { |v| opts[:config] = v }
30
73
  o.on('-f', '--force', 'Overwrite if the file already exists') { opts[:force] = true }
31
74
  o.on('--stdout', 'Print config template to STDOUT instead of writing a file') { opts[:stdout] = true }
32
75
  o.on('-h', '--help', 'Show this help') do
76
+ opts[:help] = true
33
77
  puts o
34
- return 0
35
78
  end
36
- end.parse!(argv)
37
-
38
- yaml = Docscribe::Config.default_yaml
39
-
40
- if opts[:stdout]
41
- puts yaml
42
- return 0
43
79
  end
80
+ end
44
81
 
82
+ # Write the config template to a file.
83
+ #
84
+ # @private
85
+ # @param [Hash<Symbol, Object>] opts parsed options
86
+ # @param [String] yaml config template content
87
+ # @return [Integer] exit code
88
+ def write_init_config(opts, yaml)
45
89
  path = opts[:config]
46
90
  if File.exist?(path) && !opts[:force]
47
91
  warn "Config already exists: #{path} (use --force to overwrite)"