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 +7 -0
- data/.fasterer.yml +3 -0
- data/.github/workflows/gem-push.yml +23 -0
- data/.github/workflows/ruby.yml +36 -0
- data/.gitignore +8 -0
- data/.rubocop.yml +37 -0
- data/Gemfile +7 -0
- data/LICENSE +21 -0
- data/README.md +88 -0
- data/Rakefile +8 -0
- data/bashvar.gemspec +33 -0
- data/bashvar.md +490 -0
- data/lib/bashvar/version.rb +5 -0
- data/lib/bashvar.rb +102 -0
- metadata +128 -0
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,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
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
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
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.
|
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: []
|