bashvar 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: 8850287d0fbbd4d625419ecfc3b6c4abd674ca801fb9a761b7fd7017126f5b32
4
+ data.tar.gz: 3780ba02f1ce62dbc87235bc201c371d3c172c0fc362084d16e09216e74396b0
5
+ SHA512:
6
+ metadata.gz: b406e5f831b5770ca9527020f1461509107df50fe211c95a78a9686d447f93c1a726e7b32a056c393c4c8c0200d4f505fea40ea0e195035463fecbb7a018f2d3
7
+ data.tar.gz: 20f5d3384d6d59f64f0e210249997ae9d20c40291a978c86d6a8c99cf220fda1c4ade21b2eb7732e43e0363a298ea72c2191d44ecdb3be52cfde656467958ed9
data/.fasterer.yml ADDED
@@ -0,0 +1,3 @@
1
+ exclude_paths:
2
+ - 'spec/**/*.rb'
3
+ - 'vendor/**/*.rb'
@@ -0,0 +1,23 @@
1
+ name: Ruby Gem
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - v*
7
+
8
+ jobs:
9
+ build:
10
+ name: Build + Publish
11
+ runs-on: ubuntu-latest
12
+ permissions:
13
+ contents: read
14
+ id-token: write
15
+ packages: write
16
+
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ - uses: ruby/setup-ruby@v1
20
+ with:
21
+ ruby-version: '3.4.5'
22
+ bundler-cache: true
23
+ - uses: rubygems/release-gem@v1
@@ -0,0 +1,36 @@
1
+ # This workflow uses actions that are not certified by GitHub.
2
+ # They are provided by a third-party and are governed by
3
+ # separate terms of service, privacy policy, and support
4
+ # documentation.
5
+ # This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake
6
+ # For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby
7
+
8
+ name: Ruby
9
+
10
+ on:
11
+ push:
12
+ branches: [ "master" ]
13
+ pull_request:
14
+ branches: [ "master" ]
15
+
16
+ permissions:
17
+ contents: read
18
+
19
+ jobs:
20
+ test:
21
+ runs-on: ubuntu-latest
22
+ steps:
23
+ - uses: actions/checkout@v4
24
+ - name: Set up Ruby
25
+ # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
26
+ # change this to (see https://github.com/ruby/setup-ruby#versioning):
27
+ uses: ruby/setup-ruby@v1
28
+ with:
29
+ ruby-version: '3.4.5'
30
+ bundler-cache: true # runs 'bundle install' and caches installed gems automatically
31
+ - name: Run rubocop
32
+ run: bundle exec rubocop --format simple
33
+ - name: Run fasterer
34
+ run: bundle exec fasterer
35
+ - name: Run tests
36
+ run: bundle exec rspec
data/.gitignore ADDED
@@ -0,0 +1,8 @@
1
+ # Ignore bundler config.
2
+ .bundle
3
+
4
+ # Ignore the vendor directory.
5
+ /vendor/bundle
6
+
7
+ # Ignore generated files.
8
+ /Gemfile.lock
data/.rubocop.yml ADDED
@@ -0,0 +1,37 @@
1
+ require: rubocop-performance
2
+
3
+ AllCops:
4
+ NewCops: enable
5
+ SuggestExtensions: false
6
+ TargetRubyVersion: 3.2
7
+ Exclude:
8
+ - 'vendor/**/*'
9
+
10
+ Gemspec/DevelopmentDependencies:
11
+ EnforcedStyle: gemspec
12
+
13
+ Layout/LineLength:
14
+ Max: 160
15
+ Exclude:
16
+ - '*.gemspec'
17
+
18
+ Metrics/AbcSize:
19
+ Max: 45
20
+ Metrics/BlockLength:
21
+ Exclude:
22
+ - '*.gemspec'
23
+ - 'spec/**/*'
24
+ Metrics/CyclomaticComplexity:
25
+ Max: 10
26
+ Metrics/MethodLength:
27
+ Max: 35
28
+ Metrics/ParameterLists:
29
+ Max: 8
30
+ Metrics/PerceivedComplexity:
31
+ Max: 12
32
+
33
+ Naming/FileName:
34
+ Enabled: false
35
+
36
+ Style/Documentation:
37
+ Enabled: false
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
6
+
7
+ gem 'rspec', '>= 3.0'
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Daisuke Fujimura
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,88 @@
1
+ # bashvar
2
+
3
+ `bashvar` is a small Ruby library that parses the output produced by running the following Bash snippet:
4
+
5
+ ```bash
6
+ compgen -v | while read -r var; do declare -p "$var" 2>/dev/null; done
7
+ ```
8
+
9
+ It provides a robust, dependency-free way to marshal Bash variables into Ruby data structures.
10
+
11
+ ## Project Origin
12
+
13
+ This library was initially scaffolded with assistance from Google Gemini CLI. All code has been reviewed and modified by human contributors. See LICENSE (MIT) for terms.
14
+
15
+ ## Installation
16
+
17
+ Add this line to your application's Gemfile:
18
+
19
+ ```ruby
20
+ gem 'bashvar'
21
+ ```
22
+
23
+ And then execute:
24
+
25
+ ```bash
26
+ $ bundle install
27
+ ```
28
+
29
+ Or install it yourself as:
30
+
31
+ ```bash
32
+ $ gem install bashvar
33
+ ```
34
+
35
+ ## Usage
36
+
37
+ First, capture your Bash variables into a file (e.g., `/tmp/bash_vars.txt`):
38
+
39
+ ```bash
40
+ echo "$(compgen -v | while read -r v; do declare -p "$v" 2>/dev/null; done)" > /tmp/bash_vars.txt
41
+ ```
42
+
43
+ Then, parse them in Ruby:
44
+
45
+ ```ruby
46
+ require "bashvar"
47
+
48
+ input = File.read("/tmp/bash_vars.txt")
49
+ vars = BashVar.parse(input)
50
+
51
+ puts vars["HOME"]
52
+ puts vars["PATH"]
53
+
54
+ # Example for array/hash variables
55
+ require 'pp' # For pretty printing
56
+ pp vars["LIST"] if vars.key?("LIST")
57
+ pp vars["MAP"] if vars.key?("MAP")
58
+ ```
59
+
60
+ ## Supported Bash Declarations → Ruby Types
61
+
62
+ | Bash Flags | Example `declare -p` line | Ruby Value Type | Notes |
63
+ | --------------------------------------------------- | ------------------------------------- | -------------------------------------- | ------------------------------------------------------------------------------------------ |
64
+ | `--` (none), `-x`, `-r`, combos w/out `a`, `A`, `i` | `declare -- HOME="/home/u"` | `String` | Attribute flags ignored for value type. Export/readonly not preserved. |
65
+ | `-i` | `declare -i COUNT="42"` | `Integer` (if parseable) else `String` | Numeric conversion best‑effort via `Integer()`; fallback to raw decoded string. |
66
+ | `-a` | `declare -a LIST='([0]="a" [1]="b")'` | `Array<String>` | Indices respected; assigning to `result[index] = value` (Ruby auto-expands & fills `nil`). |
67
+ | `-A` | `declare -A MAP='([k]="v" [x]="y")'` | `Hash<String,String>` | Keys preserved as strings exactly as given inside brackets. |
68
+
69
+ ### Escapes / Special Characters
70
+
71
+ Double‑quoted values and ANSI-C quoted (`$'...'`) values emitted by `declare -p` may contain backslash escapes (e.g., `\n`, `\t`, `\"`, `\\`). These are decoded to their actual characters in the returned Ruby value.
72
+
73
+ ## Limitations
74
+
75
+ - Does not preserve attribute metadata (export, readonly, nameref, etc.). Only value typing.
76
+ - Does not attempt to resolve `nameref` (`-n`) targets.
77
+ - Does not evaluate arithmetic expressions beyond what Bash already evaluated in the `declare -p` output.
78
+ - Does not parse function definitions.
79
+
80
+ ## Contributing
81
+
82
+ Bug reports and pull requests are welcome on GitHub at [https://github.com/fd00/bashvar](https://github.com/fd00/bashvar).
83
+
84
+ ## License
85
+
86
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
87
+
88
+ ```
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
data/bashvar.gemspec ADDED
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/bashvar/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'bashvar'
7
+ spec.version = BashVar::VERSION
8
+ spec.authors = ['Daisuke Fujimura']
9
+ spec.email = ['booleanlabel@gmail.com']
10
+
11
+ spec.summary = 'Parse Bash declare -p output into Ruby data structures.'
12
+ spec.description = "A simple, dependency-free Ruby library to parse the output of Bash's `declare -p` command, converting shell variables into corresponding Ruby types like String, Integer, Array, and Hash."
13
+ spec.homepage = 'https://github.com/fd00/bashvar'
14
+ spec.license = 'MIT'
15
+ spec.required_ruby_version = '>= 3.2.0'
16
+
17
+ spec.metadata['homepage_uri'] = spec.homepage
18
+ spec.metadata['source_code_uri'] = spec.homepage
19
+ spec.metadata['rubygems_mfa_required'] = 'true'
20
+
21
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
22
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(spec|features)/}) }
23
+ end
24
+ spec.bindir = 'exe'
25
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
26
+ spec.require_paths = ['lib']
27
+
28
+ spec.add_development_dependency 'fasterer', '>= 0.11.0'
29
+ spec.add_development_dependency 'rake', '>= 13.3.0'
30
+ spec.add_development_dependency 'rspec', '>= 3.13.1'
31
+ spec.add_development_dependency 'rubocop', '>= 1.78.0'
32
+ spec.add_development_dependency 'rubocop-performance', '>= 1.25.0'
33
+ end
data/bashvar.md ADDED
@@ -0,0 +1,490 @@
1
+ # Coding Agent Implementation Spec: `bashvar` Gem
2
+
3
+ ## 1. Project Overview
4
+
5
+ `bashvar` is a small Ruby library that parses the output produced by running the following Bash snippet:
6
+
7
+ ```bash
8
+ compgen -v | while read -r var; do declare -p "$var" 2>/dev/null; done
9
+ ```
10
+
11
+ The library exposes a single public entrypoint class ` with a **class method **`. The `input` argument is a `String` containing one or more lines of `declare -p` output. The method returns a **Ruby **``**) → Ruby object (scalar **``**, **``**)** depending on the Bash variable attributes present in the `declare` flags.
12
+
13
+ Goal: Provide a robust, dependency‑free way to marshal Bash variables into Ruby data structures.
14
+
15
+ ---
16
+
17
+ ## 2. Supported Bash Declarations → Ruby Types
18
+
19
+ | Bash Flags | Example `declare -p` line | Ruby Value Type | Notes |
20
+ | --------------------------------------------------- | ------------------------------------- | -------------------------------------- | ------------------------------------------------------------------------------------------ |
21
+ | `--` (none), `-x`, `-r`, combos w/out `a`, `A`, `i` | `declare -- HOME="/home/u"` | `String` | Attribute flags ignored for value type. Export/readonly not preserved. |
22
+ | `-i` | `declare -i COUNT="42"` | `Integer` (if parseable) else `String` | Numeric conversion best‑effort via `Integer()`; fallback to raw decoded string. |
23
+ | `-a` | `declare -a LIST='([0]="a" [1]="b")'` | `Array<String>` | Indices respected; assigning to `result[index] = value` (Ruby auto-expands & fills `nil`). |
24
+ | `-A` | `declare -A MAP='([k]="v" [x]="y")'` | `Hash<String,String>` | Keys preserved as strings exactly as given inside brackets. |
25
+
26
+ ### Escapes / Special Characters
27
+
28
+ Double‑quoted values emitted by `declare -p` may contain backslash escapes (e.g., `\n`, `\t`, `\"`, `\\`). These must be decoded to their actual characters in the returned Ruby value. See §5.
29
+
30
+ ### Multi‑line Logical Values
31
+
32
+ `declare -p` emits each variable on **one physical output line**; however, the value *content* may include embedded newlines represented as escape sequences. After decoding, returned Ruby strings may contain actual "\n" line breaks.
33
+
34
+ ---
35
+
36
+ ## 3. Non‑Goals / Out‑of‑Scope (Initial Version)
37
+
38
+ - Do **not** preserve attribute metadata (export, readonly, nameref, etc.). Only value typing.
39
+ - Do **not** attempt to resolve `nameref` (`-n`) targets.
40
+ - Do **not** evaluate arithmetic expressions beyond what Bash already evaluated in the `declare -p` output. (E.g., if user ran `declare -i X=1+2`, `declare -p` will show resolved value; parse that.)
41
+ - Do **not** parse function definitions (input stream should be only `declare -p` lines; see §10 defensive parsing).
42
+
43
+ ---
44
+
45
+ ## 4. Public API
46
+
47
+ ```ruby
48
+ class BashVar
49
+ # Parse a string of one-or-more Bash `declare -p` lines into a Ruby Hash.
50
+ #
51
+ # @param input [String] Multi-line string containing the raw output of
52
+ # `compgen -v | while read -r var; do declare -p "$var"; done`.
53
+ # @return [Hash{String => (String, Integer, Array, Hash)}]
54
+ # Variable name mapped to decoded Ruby value.
55
+ #
56
+ # Type mapping rules:
57
+ # -a -> Array
58
+ # -A -> Hash
59
+ # -i -> Integer (fallback String)
60
+ # else -> String
61
+ #
62
+ def self.parse(input)
63
+ ...
64
+ end
65
+ end
66
+ ```
67
+
68
+ ### Error Handling
69
+
70
+ - Method **must never raise** on malformed lines; skip unparseable entries.
71
+ - Recoverable parse anomalies (e.g., unknown escape `\\z`) → leave literal `z` w/o backslash.
72
+ - Return empty `{}` if `input.nil?` or blank.
73
+
74
+ ---
75
+
76
+ ## 5. Escape Decoding Rules
77
+
78
+ Implement a helper that converts Bash `declare -p` backslash escapes within **double‑quoted** (`"..."`) and **ANSI-C quoted** (`$'...'`) strings into real characters.
79
+
80
+ Decode these sequences:
81
+
82
+ | Escape | Char |
83
+ | ------ | --------------- |
84
+ | `\\n` | newline ("\n") |
85
+ | `\\r` | carriage return |
86
+ | `\\t` | tab |
87
+ | `\\v` | vertical tab |
88
+ | `\\f` | form feed |
89
+ | `\\b` | backspace |
90
+ | `\\a` | bell |
91
+ | `\\\\` | backslash |
92
+ | `\\"` | double quote |
93
+
94
+ Fallback: `\\X` → `X` (drop backslash) for any other single char `X`.
95
+
96
+ **ANSI-C Quoted** strings (`$'...'`) are decoded using the rules above.
97
+
98
+ **Single‑quoted** strings are taken verbatim (contents between quotes, no escape decoding except strip outer quotes).
99
+
100
+ **Unquoted** values: return literal raw string (after strip); no unescaping.
101
+
102
+ ---
103
+
104
+ ## 6. Parsing Array / Assoc Array Bodies
105
+
106
+ `declare -p` for arrays uses a Bash-ish repr inside single quotes:
107
+
108
+ ```bash
109
+ declare -a LIST='([0]="foo" [1]="bar" [5]="baz")'
110
+ # body: ([0]="foo" [1]="bar" [5]="baz")
111
+
112
+ declare -A MAP='([key1]="val1" [key two]="val 2")'
113
+ ```
114
+
115
+ ### Steps
116
+
117
+ 1. Strip leading/trailing quotes around the full raw_value.
118
+ 2. Confirm it starts with `(` and ends with `)`; empty `()` → return `[]` or `{}`.
119
+ 3. Tokenize repeated pattern: `[KEY]=VALUE`
120
+ - `KEY` = any run of characters up to closing `]` (do *not* unescape inside KEY; pass literally, then strip surrounding quotes if any were inserted—in practice, `declare -p` prints keys w/o quoting unless whitespace, but handle `'...'` & `"..."`).
121
+ - `VALUE` = one shell word: either double‑quoted, single‑quoted, or bare.
122
+ 4. For `-a` (indexed array): interpret `KEY.to_i` as index. Assign: `ary[index] = decoded_value`. This will auto‑fill `nil` gaps if indices skip.
123
+ 5. For `-A` (assoc): use decoded KEY string as-is: `h[key] = decoded_value`.
124
+
125
+ ### Tokenization Regex Suggestion
126
+
127
+ A tolerant scan is sufficient:
128
+
129
+ ```ruby
130
+ pairs = body.scan(/\[([^\]]*)\]=((?:\"(?:\\.|[^\"])*\")|(?:'(?:\\.|[^'])*')|[^\s)]+)/)
131
+ ```
132
+
133
+ - Captures KEY in group 1.
134
+ - Captures VALUE in group 2 (quoted or bare token).
135
+ - Stops at whitespace or `)` boundary.
136
+
137
+ After capture, feed VALUE through same scalar parser used for top-level scalars.
138
+
139
+ ---
140
+
141
+ ## 7. Integer Parsing (`-i`)
142
+
143
+ - After scalar decode, attempt `Integer(value, 10)`.
144
+ - If conversion fails (raises `ArgumentError`), return decoded string unchanged.
145
+ - Accept leading `+`/`-`; whitespace trimmed.
146
+
147
+ ---
148
+
149
+ ## 8. Robustness Requirements
150
+
151
+ - Ignore leading/trailing whitespace around lines.
152
+ - Support combined flags (e.g., `-xi`, `-irx`, etc.) — detect presence via `flags.include?("i")` etc.
153
+ - Lines missing `name=` pattern → skip.
154
+ - Lines beginning `declare -f` or `declare -F` (functions) → skip.
155
+ - Unknown flags → ignore.
156
+
157
+ ---
158
+
159
+ ## 9. Performance Expectations
160
+
161
+ - Input size typically small (<5k vars) but parser should be linear in number of lines.
162
+ - Avoid heavy backtracking regex; prefer single pass per line.
163
+ - No external gem dependencies.
164
+
165
+ ---
166
+
167
+ ## 10. Line Grammar to Match
168
+
169
+ Target lines generally resemble:
170
+
171
+ ```
172
+ declare -xr PATH="/usr/bin"
173
+ declare -- HOME="/home/u"
174
+ declare -i COUNT="42"
175
+ declare -a LIST='([0]="foo" [1]="bar")'
176
+ declare -A MAP='([k]="v" [z]="w")'
177
+ ```
178
+
179
+ Use this top-level regex skeleton (safe match):
180
+
181
+ ```ruby
182
+ /^declare\s+(-[A-Za-z]+)?\s+([A-Za-z_][A-Za-z0-9_]*)=(.*)$/
183
+ ```
184
+
185
+ Group 1 = flags (optional)
186
+ Group 2 = var name
187
+ Group 3 = raw value (rest of line)
188
+
189
+ > Do not assume there's exactly one space between tokens; use `\s+`.
190
+
191
+ ---
192
+
193
+ ## 11. Implementation Sketch
194
+
195
+ ```ruby
196
+ class BashVar
197
+ class << self
198
+ def parse(input)
199
+ return {} if input.nil? || input.strip.empty?
200
+
201
+ vars = {}
202
+ input.each_line do |line|
203
+ line = line.strip
204
+ next unless line.start_with?("declare")
205
+ m = line.match(/^declare\s+(-[A-Za-z]+)?\s+([A-Za-z_][A-Za-z0-9_]*)=(.*)$/)
206
+ next unless m
207
+
208
+ flags = m[1] || ""
209
+ name = m[2]
210
+ raw = m[3]
211
+
212
+ # skip functions explicitly
213
+ next if flags.include?("f") && !flags.match?(/-[^A-Za-z]*[ai]/) # crude but protects `declare -f`
214
+
215
+ value = case
216
+ when flags.include?("a") || flags.include?("A")
217
+ parse_array_like(raw, assoc: flags.include?("A"))
218
+ when flags.include?("i")
219
+ parse_integer(raw)
220
+ else
221
+ parse_scalar(raw)
222
+ end
223
+
224
+ vars[name] = value
225
+ end
226
+ vars
227
+ end
228
+
229
+ private
230
+
231
+ def parse_scalar(raw)
232
+ raw = raw.strip
233
+ if raw.start_with?("$'"') && raw.end_with?("'"')
234
+ # ANSI-C Quoting
235
+ decode_dquoted(raw[2..-2])
236
+ elsif raw.start_with?('"') && raw.end_with?('"')
237
+ # Double-quoted
238
+ decode_dquoted(raw[1..-2])
239
+ elsif raw.start_with?("'"') && raw.end_with?("'"')
240
+ # Single-quoted
241
+ raw[1..-2]
242
+ else
243
+ # Unquoted
244
+ raw
245
+ end
246
+ end
247
+
248
+ def parse_integer(raw)
249
+ v = parse_scalar(raw)
250
+ begin
251
+ Integer(v, 10)
252
+ rescue ArgumentError, TypeError
253
+ v
254
+ end
255
+ end
256
+
257
+ def parse_array_like(raw, assoc: false)
258
+ s = raw.strip
259
+ # expect quoted wrapper; tolerate missing quotes
260
+ if (s.start_with?("'"') && s.end_with?("'"')) || (s.start_with?('"') && s.end_with?('"'))
261
+ s = s[1..-2]
262
+ end
263
+ s = s.strip
264
+ return(assoc ? {} : []) unless s.start_with?("(") && s.end_with?(" )") || s.end_with?(")")
265
+
266
+ # trim parens
267
+ s = s[1..-2].strip
268
+
269
+ result = assoc ? {} : []
270
+
271
+ # scan pairs
272
+ s.scan(/\[([^\]]*)\]=((?:\"(?:\\.|[^\"])*\")|(?:'(?:\\.|[^'])*')|[^\s)]+)/) do |k, v|
273
+ key_str = parse_scalar(k.strip.gsub(/^\"|\"$/, '').gsub(/^'|'$/, '')) # keys seldom quoted; defensive
274
+ val_str = parse_scalar(v)
275
+ if assoc
276
+ result[key_str] = val_str
277
+ else
278
+ idx = key_str.to_i
279
+ result[idx] = val_str
280
+ end
281
+ end
282
+ result
283
+ end
284
+
285
+ ESCAPE_MAP = {
286
+ 'n' => "\n", 'r' => "\r", 't' => "\t", 'v' => "\v", 'f' => "\f", 'b' => "\b", 'a' => "\a", '\\' => "\\", '"' => '"'
287
+ }.freeze
288
+
289
+ def decode_dquoted(str)
290
+ str.gsub(/\\(.)/) { ESCAPE_MAP[$1] || $1 }
291
+ end
292
+ end
293
+ end
294
+ ```
295
+
296
+ > **NOTE:** The above is a sketch; the Coding Agent should refine regex boundaries, whitespace tolerance, and key quoting logic. Add unit tests first (TDD recommended).
297
+
298
+ ---
299
+
300
+ ## 12. Test Matrix (RSpec Suggested)
301
+
302
+ Create `spec/bashvar_spec.rb` with the scenarios below. Use `RSpec.describe BashVar do ... end`.
303
+
304
+ ### 12.1 Basic Scalar
305
+
306
+ Input:
307
+
308
+ ```
309
+ declare -- NAME="hello"
310
+ ```
311
+
312
+ Expect: `{"NAME"=>"hello"}`
313
+
314
+ ### 12.2 Integer OK
315
+
316
+ ```
317
+ declare -i COUNT="42"
318
+ ```
319
+
320
+ Expect: `{"COUNT"=>42}`
321
+
322
+ ### 12.3 Integer Fallback (non-numeric)
323
+
324
+ ```
325
+ declare -i FAILSAFE="notnum"
326
+ ```
327
+
328
+ Expect: `{"FAILSAFE"=>"notnum"}`
329
+
330
+ ### 12.4 Indexed Array Sequential
331
+
332
+ ```
333
+ declare -a LIST='([0]="a" [1]="b")'
334
+ ```
335
+
336
+ Expect: `{"LIST"=>["a","b"]}`
337
+
338
+ ### 12.5 Indexed Array Sparse
339
+
340
+ ```
341
+ declare -a SPARSE='([2]="x" [5]="y")'
342
+ ```
343
+
344
+ Expect: `{"SPARSE"=>[nil,nil,"x",nil,nil,"y"]}`
345
+
346
+ ### 12.6 Assoc Array
347
+
348
+ ```
349
+ declare -A MAP='([k]="v" [x]="y")'
350
+ ```
351
+
352
+ Expect: `{"MAP"=>{"k"=>"v","x"=>"y"}}`
353
+
354
+ ### 12.7 Escapes & Newlines
355
+
356
+ ```
357
+ declare -- MULTI="line1\nline2\tindent\"quote\""
358
+ ```
359
+
360
+ Expect value with actual newline and tab, and embedded quotes.
361
+
362
+ ### 12.8 Mixed Input Multi-line
363
+
364
+ Combine all above in one input string; ensure parser accumulates all.
365
+
366
+ ### 12.9 Ignore Functions
367
+
368
+ Ensure lines like `declare -f myfunc` are skipped.
369
+
370
+ ### 12.10 Ignore Garbage
371
+
372
+ Random lines that don’t match grammar are ignored; parser should not raise.
373
+
374
+ ### 12.11 ANSI-C Quoted String
375
+
376
+ Input:
377
+ ```
378
+ declare -- ANSI_C_STRING=$'line1\nline2\tindent'
379
+ ```
380
+
381
+ Expect: `{"ANSI_C_STRING" => "line1\nline2\tindent"}`
382
+
383
+ ---
384
+
385
+ ## 13. Gem Packaging Requirements
386
+
387
+ ### 13.1 `bashvar.gemspec`
388
+
389
+ Include:
390
+
391
+ - name: `bashvar`
392
+ - summary: "Parse Bash declare -p output into Ruby data structures"
393
+ - description: longer text
394
+ - authors placeholder
395
+ - email placeholder
396
+ - version from `lib/bashvar/version.rb`
397
+ - required_ruby_version ">= 2.6"
398
+ - license: MIT
399
+ - files via `git ls-files -z` or `Dir.glob`.
400
+
401
+ ### 13.2 `lib/bashvar/version.rb`
402
+
403
+ ```ruby
404
+ class BashVar
405
+ VERSION = "0.1.0"
406
+ end
407
+ ```
408
+
409
+ ### 13.3 `lib/bashvar.rb`
410
+
411
+ (Full class implementation from §11.)
412
+
413
+ ---
414
+
415
+ ## 14. Namespacing Guidance
416
+
417
+ To align gem name (`bashvar`) with Ruby constant style:
418
+
419
+ - Top-level module & Public class: `BashVar` (entrypoint requested by user).
420
+ - Users can:
421
+ ```ruby
422
+ require 'bashvar'
423
+ BashVar.parse(str)
424
+ ```
425
+
426
+ ---
427
+
428
+ ## 15. README.md Template (generate)
429
+
430
+ Should include:
431
+
432
+ - What problem it solves
433
+ - Install steps (`gem install bashvar` / Gemfile)
434
+ - Minimal usage example
435
+ - Bash snippet to produce input
436
+ - Supported types table
437
+ - Limitations / roadmap
438
+
439
+ ---
440
+
441
+ ## 16. Rake Tasks
442
+
443
+ Provide default Rakefile that loads Bundler::GemTasks so `rake build`, `rake install`, `rake release` work.
444
+
445
+ ---
446
+
447
+ ## 17. Lint & Style
448
+
449
+ - Use frozen string literal magic comment.
450
+ - RuboCop optional (do not enforce unless configured).
451
+ - 100 char line length soft.
452
+
453
+ ---
454
+
455
+ ## 18. Deliverables Checklist for Coding Agent
456
+
457
+ -
458
+
459
+ ---
460
+
461
+ ## 19. Example End-to-End Usage Snippet (README excerpt)
462
+
463
+ ```bash
464
+ # capture bash vars into a file
465
+ echo "$(compgen -v | while read -r v; do declare -p "$v" 2>/dev/null; done)" > /tmp/bash_vars.txt
466
+ ```
467
+
468
+ ```ruby
469
+ require "bashvar"
470
+ input = File.read("/tmp/bash_vars.txt")
471
+ vars = BashVar.parse(input)
472
+ puts vars["HOME"]
473
+ puts vars["PATH"]
474
+ pp vars["LIST"] if vars.key?("LIST")
475
+ ```
476
+
477
+ ---
478
+
479
+ ### Final Notes to Coding Agent
480
+
481
+ - Please implement with **unit tests first** where practical.
482
+ - Parser should favor **forgiving** behavior: skip or fallback, never crash.
483
+ - Keep runtime deps zero.
484
+ - Provide internal docstrings / YARD tags.
485
+
486
+ > When done, ensure `bundle exec rspec` passes and `gem build` succeeds without warnings.
487
+
488
+ ---
489
+
490
+ End of spec.
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BashVar
4
+ VERSION = '0.1.0'
5
+ end
data/lib/bashvar.rb ADDED
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'bashvar/version'
4
+
5
+ class BashVar
6
+ class << self
7
+ def parse(input)
8
+ return {} if input.nil? || input.strip.empty?
9
+
10
+ vars = {}
11
+ input.each_line do |line|
12
+ line = line.strip
13
+
14
+ # Main regex for parsing declare lines
15
+ m = line.match(/^declare\s+(-[A-Za-z\-]+)?\s+([A-Za-z_][A-Za-z0-9_]*)=(.*)$/)
16
+ next unless m
17
+
18
+ flags = m[1] || ''
19
+ name = m[2]
20
+ raw = m[3]
21
+
22
+ # skip functions explicitly
23
+ next if flags.include?('f') && !flags.match?(/-[^A-Za-z]*[ai]/) # crude but protects `declare -f`
24
+
25
+ value = if flags.include?('a') || flags.include?('A')
26
+ parse_array_like(raw, assoc: flags.include?('A'))
27
+ elsif flags.include?('i')
28
+ parse_integer(raw)
29
+ else
30
+ parse_scalar(raw)
31
+ end
32
+
33
+ vars[name] = value
34
+ end
35
+ vars
36
+ end
37
+
38
+ ESCAPE_MAP = {
39
+ 'n' => "\n", 'r' => "\r", 't' => "\t", 'v' => "\v", 'f' => "\f", 'b' => "\b", 'a' => "\a", '\\' => '\\', '"' => '"'
40
+ }.freeze
41
+
42
+ private_constant :ESCAPE_MAP
43
+
44
+ private
45
+
46
+ def parse_scalar(raw)
47
+ raw = raw.strip
48
+ if raw.start_with?("$'") && raw.end_with?("'") # ANSI-C Quoting
49
+ decode_bash_escapes(raw[2..-2])
50
+ elsif raw.start_with?('"') && raw.end_with?('"') # Double-quoted
51
+ decode_bash_escapes(raw[1..-2])
52
+ elsif raw.start_with?("'") && raw.end_with?("'") # Single-quoted
53
+ raw[1..-2]
54
+ else # Unquoted
55
+ raw
56
+ end
57
+ end
58
+
59
+ def parse_integer(raw)
60
+ v = parse_scalar(raw)
61
+ begin
62
+ Integer(v, 10)
63
+ rescue ArgumentError, TypeError
64
+ v
65
+ end
66
+ end
67
+
68
+ def parse_array_like(raw, assoc: false)
69
+ s = raw.strip
70
+ # expect quoted wrapper; tolerate missing quotes
71
+ # Corrected string comparisons for quotes
72
+ s = s[1..-2] if (s.start_with?("'") && s.end_with?("'")) || (s.start_with?('"') && s.end_with?('"'))
73
+ s = s.strip
74
+ # Corrected: Removed parentheses around return value, and fixed the logical grouping
75
+ return assoc ? {} : [] unless s.start_with?('(') && s.end_with?(' )', ')')
76
+
77
+ # trim parens
78
+ s = s[1..-2].strip
79
+
80
+ result = assoc ? {} : []
81
+
82
+ # scan pairs
83
+ # Corrected regex: proper escaping for quotes and backslashes
84
+ s.scan(/\[([^\]]*)\]=("(?:\\.|[^"])*"|'(?:\\.|[^'])*'|[^\s)]+)/) do |k, v|
85
+ # Corrected gsub for keys: use correct Ruby string literal syntax
86
+ key_str = parse_scalar(k.strip.gsub(/^"|"$/, '').gsub(/^'|'$/, '')) # keys seldom quoted; defensive
87
+ val_str = parse_scalar(v)
88
+ if assoc
89
+ result[key_str] = val_str
90
+ else
91
+ idx = key_str.to_i
92
+ result[idx] = val_str
93
+ end
94
+ end
95
+ result
96
+ end
97
+
98
+ def decode_bash_escapes(str)
99
+ str.gsub(/\\(.)/) { ESCAPE_MAP[::Regexp.last_match(1)] || ::Regexp.last_match(1) }
100
+ end
101
+ end
102
+ end
metadata ADDED
@@ -0,0 +1,128 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bashvar
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Daisuke Fujimura
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: fasterer
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: 0.11.0
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: 0.11.0
26
+ - !ruby/object:Gem::Dependency
27
+ name: rake
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 13.3.0
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: 13.3.0
40
+ - !ruby/object:Gem::Dependency
41
+ name: rspec
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: 3.13.1
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: 3.13.1
54
+ - !ruby/object:Gem::Dependency
55
+ name: rubocop
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: 1.78.0
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: 1.78.0
68
+ - !ruby/object:Gem::Dependency
69
+ name: rubocop-performance
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: 1.25.0
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: 1.25.0
82
+ description: A simple, dependency-free Ruby library to parse the output of Bash's
83
+ `declare -p` command, converting shell variables into corresponding Ruby types like
84
+ String, Integer, Array, and Hash.
85
+ email:
86
+ - booleanlabel@gmail.com
87
+ executables: []
88
+ extensions: []
89
+ extra_rdoc_files: []
90
+ files:
91
+ - ".fasterer.yml"
92
+ - ".github/workflows/gem-push.yml"
93
+ - ".github/workflows/ruby.yml"
94
+ - ".gitignore"
95
+ - ".rubocop.yml"
96
+ - Gemfile
97
+ - LICENSE
98
+ - README.md
99
+ - Rakefile
100
+ - bashvar.gemspec
101
+ - bashvar.md
102
+ - lib/bashvar.rb
103
+ - lib/bashvar/version.rb
104
+ homepage: https://github.com/fd00/bashvar
105
+ licenses:
106
+ - MIT
107
+ metadata:
108
+ homepage_uri: https://github.com/fd00/bashvar
109
+ source_code_uri: https://github.com/fd00/bashvar
110
+ rubygems_mfa_required: 'true'
111
+ rdoc_options: []
112
+ require_paths:
113
+ - lib
114
+ required_ruby_version: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - ">="
117
+ - !ruby/object:Gem::Version
118
+ version: 3.2.0
119
+ required_rubygems_version: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ requirements: []
125
+ rubygems_version: 3.6.9
126
+ specification_version: 4
127
+ summary: Parse Bash declare -p output into Ruby data structures.
128
+ test_files: []