bulk_replace 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e2f0997f62fecec1406af2ddd15395323a2143fba04fcf97bc0f3ab84aabfd9b
4
+ data.tar.gz: a6328fb89e5b00f6ff2c087276bb2310ce230972b686ce75c219c4ea0ee03efc
5
+ SHA512:
6
+ metadata.gz: 4d7cb74b2fc12a22ce385fed884cdd4e5b060e194ae0337a90a0ff84ef4c81927f51359c1b7b14e1570069e82c240762af01f42bc3e728514cdf0e80998122c1
7
+ data.tar.gz: 807037ff0e93fca6e4175a9b3bdb1f6194843bc671d1af02b85c0380e8729e8c6ead018dab060e289f545ecabf6604478e6292575fdcb96c717418af1a6cb664
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 MikhaiL
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,127 @@
1
+ # bulk-replace
2
+
3
+ A simple CLI tool that bulk-replaces text strings across every file in a directory, writing the results to a new output directory — original files untouched.
4
+
5
+ Built for the case where you have tens or hundreds of config files that share the same placeholder text and need the real value injected all at once. Works with any plain-text files: WireGuard configs, `.env` templates, INI files, YAML, TOML, and more.
6
+
7
+ ## Features
8
+
9
+ - **Two modes** — quick inline replacement or a YAML config for multiple substitutions
10
+ - **Recursive** — processes files in nested subdirectories, preserving the original tree structure
11
+ - **Non-destructive** — always writes to a separate output directory; source files are never modified
12
+ - **Zero dependencies** — pure Ruby, nothing outside the standard library at runtime
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ gem install bulk_replace
18
+ ```
19
+
20
+ Or add it to your `Gemfile`:
21
+
22
+ ```ruby
23
+ gem "bulk_replace"
24
+ ```
25
+
26
+ ## Usage
27
+
28
+ ### Inline mode — one replacement
29
+
30
+ Pass the old and new strings directly on the command line.
31
+
32
+ ```bash
33
+ bulk-replace \
34
+ --from "DNS = 8.8.8.8" \
35
+ --to "DNS = 1.1.1.1" \
36
+ --input ./configs \
37
+ --output ./configs-new
38
+ ```
39
+
40
+ ### Config file mode — multiple replacements
41
+
42
+ Describe all substitutions in a YAML file and pass it with `--config`.
43
+
44
+ ```bash
45
+ bulk-replace \
46
+ --config replacements.yml \
47
+ --input ./configs \
48
+ --output ./configs-new
49
+ ```
50
+
51
+ **`replacements.yml`**
52
+
53
+ ```yaml
54
+ replacements:
55
+ - from: "PrivateKey = PLACEHOLDER"
56
+ to: "PrivateKey = wOEI9rqqbDwnN8/Bpp/ZkEMEKlMDxVMB8PJHA0XXXXXXX="
57
+
58
+ - from: "DNS = 8.8.8.8"
59
+ to: "DNS = 1.1.1.1"
60
+
61
+ - from: "Endpoint = 203.0.113.1:51820"
62
+ to: "Endpoint = 203.0.113.99:51820"
63
+ ```
64
+
65
+ A fully annotated template is provided in [`replacements.example.yml`](replacements.example.yml).
66
+
67
+ ### All options
68
+
69
+ | Flag | Description |
70
+ |------|-------------|
71
+ | `--from TEXT` | Text to find (literal, case-sensitive) |
72
+ | `--to TEXT` | Replacement text |
73
+ | `--config PATH` | Path to a YAML config file |
74
+ | `--input DIR` | Directory containing the source files |
75
+ | `--output DIR` | Directory to write the updated files into |
76
+
77
+ `--config` and `--from`/`--to` are mutually exclusive — use one or the other.
78
+
79
+ ## Example: WireGuard key injection
80
+
81
+ You generated 50 WireGuard peer configs with a placeholder private key. Now you want to inject the real keys.
82
+
83
+ ```
84
+ configs/
85
+ peer01.conf
86
+ peer02.conf
87
+ ...
88
+ peer50.conf
89
+ ```
90
+
91
+ Each file contains:
92
+
93
+ ```ini
94
+ [Interface]
95
+ PrivateKey = PLACEHOLDER
96
+ Address = 10.8.0.1/24
97
+ DNS = 8.8.8.8
98
+ ```
99
+
100
+ Run:
101
+
102
+ ```bash
103
+ bulk-replace \
104
+ --from "PrivateKey = PLACEHOLDER" \
105
+ --to "PrivateKey = <your_actual_key>" \
106
+ --input ./configs \
107
+ --output ./configs-ready
108
+ ```
109
+
110
+ Result in `configs-ready/` — all files updated, originals intact.
111
+
112
+ ## Development
113
+
114
+ ```bash
115
+ git clone https://github.com/mik2win/bulk-replace
116
+ cd bulk-replace
117
+ bundle install
118
+ bundle exec rspec
119
+ ```
120
+
121
+ ## Contributing
122
+
123
+ Bug reports and pull requests are welcome at [github.com/mik2win/bulk-replace](https://github.com/mik2win/bulk-replace).
124
+
125
+ ## License
126
+
127
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
data/exe/bulk-replace ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../lib/bulk_replace'
5
+
6
+ begin
7
+ BulkReplace::CLI.new(ARGV).run
8
+ rescue StandardError => e
9
+ warn e.message
10
+ exit 1
11
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'optparse'
4
+ require_relative 'replacement_set'
5
+ require_relative 'runner'
6
+
7
+ module BulkReplace
8
+ class CLI
9
+ def initialize(argv)
10
+ @argv = argv
11
+ @options = {}
12
+ end
13
+
14
+ def run
15
+ parser.parse!(@argv)
16
+ validate!
17
+ Runner.new(
18
+ input_dir: @options[:input],
19
+ output_dir: @options[:output],
20
+ replacements: replacements
21
+ ).run
22
+ end
23
+
24
+ private
25
+
26
+ def parser
27
+ OptionParser.new do |o|
28
+ o.on('--from FROM', 'String to replace') { |v| @options[:from] = v }
29
+ o.on('--to TO', 'Replacement string') { |v| @options[:to] = v }
30
+ o.on('--config PATH', 'YAML config file') { |v| @options[:config] = v }
31
+ o.on('--input DIR', 'Input directory') { |v| @options[:input] = v }
32
+ o.on('--output DIR', 'Output directory') { |v| @options[:output] = v }
33
+ end
34
+ end
35
+
36
+ def validate!
37
+ raise ArgumentError, '--input is required' unless @options[:input]
38
+ raise ArgumentError, '--output is required' unless @options[:output]
39
+ raise ArgumentError, 'provide --config or both --from and --to' unless valid_mode?
40
+ end
41
+
42
+ def valid_mode?
43
+ @options[:config] || (@options[:from] && @options[:to])
44
+ end
45
+
46
+ def replacements
47
+ if @options[:config]
48
+ ReplacementSet.from_config(@options[:config])
49
+ else
50
+ ReplacementSet.from_inline(from: @options[:from], to: @options[:to])
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+
5
+ module BulkReplace
6
+ class DirectoryWalker
7
+ def initialize(input_dir)
8
+ @root = Pathname.new(input_dir)
9
+ end
10
+
11
+ def each_file(&block)
12
+ @root.glob('**/*').select(&:file?).each do |path|
13
+ block.call(path.relative_path_from(@root), path)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BulkReplace
4
+ class FileProcessor
5
+ def initialize(replacements)
6
+ @replacements = replacements
7
+ end
8
+
9
+ def process(content)
10
+ @replacements.reduce(content) { |text, r| text.gsub(r.from, r.to) }
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BulkReplace
4
+ Replacement = Data.define(:from, :to)
5
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+ require_relative 'replacement'
5
+
6
+ module BulkReplace
7
+ class ReplacementSet
8
+ def self.from_config(path)
9
+ raw = YAML.safe_load(File.read(path), symbolize_names: true)
10
+ unless raw.is_a?(Hash) && raw[:replacements].is_a?(Array)
11
+ raise ArgumentError,
12
+ "Config must contain a 'replacements' key"
13
+ end
14
+
15
+ raw[:replacements].map { |r| Replacement.new(**r) }
16
+ end
17
+
18
+ def self.from_inline(from:, to:)
19
+ [Replacement.new(from: from, to: to)]
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+ require_relative 'directory_walker'
5
+ require_relative 'file_processor'
6
+
7
+ module BulkReplace
8
+ class Runner
9
+ def initialize(input_dir:, output_dir:, replacements:)
10
+ @walker = DirectoryWalker.new(input_dir)
11
+ @processor = FileProcessor.new(replacements)
12
+ @output = Pathname.new(output_dir)
13
+ end
14
+
15
+ def run
16
+ @walker.each_file { |rel, abs| write(rel, @processor.process(abs.read)) }
17
+ end
18
+
19
+ private
20
+
21
+ def write(relative_path, content)
22
+ dest = @output.join(relative_path)
23
+ dest.dirname.mkpath
24
+ dest.write(content)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BulkReplace
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'bulk_replace/version'
4
+ require_relative 'bulk_replace/replacement'
5
+ require_relative 'bulk_replace/replacement_set'
6
+ require_relative 'bulk_replace/file_processor'
7
+ require_relative 'bulk_replace/directory_walker'
8
+ require_relative 'bulk_replace/runner'
9
+ require_relative 'bulk_replace/cli'
10
+
11
+ module BulkReplace; end
metadata ADDED
@@ -0,0 +1,63 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bulk_replace
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - mik2win
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rspec
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '3.13'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '3.13'
26
+ executables:
27
+ - bulk-replace
28
+ extensions: []
29
+ extra_rdoc_files: []
30
+ files:
31
+ - LICENSE
32
+ - README.md
33
+ - exe/bulk-replace
34
+ - lib/bulk_replace.rb
35
+ - lib/bulk_replace/cli.rb
36
+ - lib/bulk_replace/directory_walker.rb
37
+ - lib/bulk_replace/file_processor.rb
38
+ - lib/bulk_replace/replacement.rb
39
+ - lib/bulk_replace/replacement_set.rb
40
+ - lib/bulk_replace/runner.rb
41
+ - lib/bulk_replace/version.rb
42
+ homepage: https://github.com/mik2win/bulk-replace
43
+ licenses:
44
+ - MIT
45
+ metadata: {}
46
+ rdoc_options: []
47
+ require_paths:
48
+ - lib
49
+ required_ruby_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: 3.2.0
54
+ required_rubygems_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: '0'
59
+ requirements: []
60
+ rubygems_version: 3.6.9
61
+ specification_version: 4
62
+ summary: Bulk-replace text strings across files in a directory
63
+ test_files: []