toolbox 0.2.0 → 0.3.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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/context/fiber-debugging.md +1 -1
- data/context/getting-started.md +36 -14
- data/context/index.yaml +1 -1
- data/context/object-inspection.md +25 -25
- data/context/stack-inspection.md +3 -3
- data/data/toolbox/command.py +225 -0
- data/data/toolbox/context.py +358 -282
- data/data/toolbox/debugger/__init__.py +2 -0
- data/data/toolbox/debugger/gdb_backend.py +70 -1
- data/data/toolbox/debugger/lldb_backend.py +110 -9
- data/data/toolbox/fiber.py +837 -845
- data/data/toolbox/format.py +70 -65
- data/data/toolbox/heap.py +66 -56
- data/data/toolbox/init.py +9 -5
- data/data/toolbox/print.py +79 -0
- data/data/toolbox/rarray.py +4 -12
- data/data/toolbox/rbasic.py +31 -35
- data/data/toolbox/rbignum.py +3 -7
- data/data/toolbox/rclass.py +5 -5
- data/data/toolbox/readme.md +8 -8
- data/data/toolbox/rexception.py +3 -3
- data/data/toolbox/rfloat.py +8 -18
- data/data/toolbox/rhash.py +4 -12
- data/data/toolbox/rstring.py +4 -8
- data/data/toolbox/rstruct.py +6 -14
- data/data/toolbox/rsymbol.py +9 -33
- data/data/toolbox/{value.py → rvalue.py} +9 -9
- data/data/toolbox/stack.py +595 -605
- data/lib/toolbox/version.rb +1 -1
- data/readme.md +1 -1
- data.tar.gz.sig +0 -0
- metadata +3 -3
- metadata.gz.sig +0 -0
- data/data/toolbox/object.py +0 -84
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f8bc49b89f83b1baa5270a63e16df4c63dfc3de61fa98e8c6da8fabbcde66e87
|
|
4
|
+
data.tar.gz: cd5b34b044e80e8aa2b278a67fa138cd5230829da83aa9710b04b8b77478b8ff
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 891326e75355c9a42cbed524b63345f7b47e6ee8ed29f5c08ee96a5c8a40cb495ad1913bfeb0043b2479f1e08ff4c763d93b4f8231a30f5b958a356eabc0ae46
|
|
7
|
+
data.tar.gz: c3edb1db9689d4b399c96a112876d8b8ba3f3b4754903d61e2fafb999d2913409509f4ee2a4964096a4ed5b30ccec0647deee5843a31d77b8700de0ef62ee9b9
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/context/fiber-debugging.md
CHANGED
|
@@ -161,7 +161,7 @@ After switching to a fiber with `rb-fiber-scan-switch`, you can use standard GDB
|
|
|
161
161
|
(gdb) bt # Show C backtrace
|
|
162
162
|
(gdb) frame <n> # Switch to specific frame
|
|
163
163
|
(gdb) info locals # Show local variables
|
|
164
|
-
(gdb) rb-
|
|
164
|
+
(gdb) rb-print $errinfo # Print exception if present
|
|
165
165
|
~~~
|
|
166
166
|
|
|
167
167
|
The fiber switch command sets up several convenience variables:
|
data/context/getting-started.md
CHANGED
|
@@ -15,7 +15,7 @@ $ gem install toolbox
|
|
|
15
15
|
Install the GDB extensions (automatically adds to `~/.gdbinit`):
|
|
16
16
|
|
|
17
17
|
~~~ bash
|
|
18
|
-
$ bake toolbox:gdb:install
|
|
18
|
+
$ bake -g toolbox toolbox:gdb:install
|
|
19
19
|
~~~
|
|
20
20
|
|
|
21
21
|
This adds a single line to your `~/.gdbinit` that sources the extensions from the gem's data directory. The extensions will then load automatically every time you start GDB.
|
|
@@ -23,7 +23,7 @@ This adds a single line to your `~/.gdbinit` that sources the extensions from th
|
|
|
23
23
|
To install to a custom `.gdbinit` location:
|
|
24
24
|
|
|
25
25
|
~~~ bash
|
|
26
|
-
$ bake toolbox:gdb:install gdbinit=/path/to/custom/gdbinit
|
|
26
|
+
$ bake -g toolbox toolbox:gdb:install gdbinit=/path/to/custom/gdbinit
|
|
27
27
|
~~~
|
|
28
28
|
|
|
29
29
|
### Installing LLDB Extensions
|
|
@@ -31,7 +31,7 @@ $ bake toolbox:gdb:install gdbinit=/path/to/custom/gdbinit
|
|
|
31
31
|
Install the LLDB extensions (automatically adds to `~/.lldbinit`):
|
|
32
32
|
|
|
33
33
|
~~~ bash
|
|
34
|
-
$ bake toolbox:lldb:install
|
|
34
|
+
$ bake -g toolbox toolbox:lldb:install
|
|
35
35
|
~~~
|
|
36
36
|
|
|
37
37
|
This adds a command script import line to your `~/.lldbinit` that loads the extensions from the gem's data directory. The extensions will then load automatically every time you start LLDB.
|
|
@@ -39,7 +39,7 @@ This adds a command script import line to your `~/.lldbinit` that loads the exte
|
|
|
39
39
|
To install to a custom `.lldbinit` location:
|
|
40
40
|
|
|
41
41
|
~~~ bash
|
|
42
|
-
$ bake toolbox:lldb:install lldbinit=/path/to/custom/lldbinit
|
|
42
|
+
$ bake -g toolbox toolbox:lldb:install lldbinit=/path/to/custom/lldbinit
|
|
43
43
|
~~~
|
|
44
44
|
|
|
45
45
|
### Verifying Installation
|
|
@@ -47,7 +47,7 @@ $ bake toolbox:lldb:install lldbinit=/path/to/custom/lldbinit
|
|
|
47
47
|
Check GDB installation status:
|
|
48
48
|
|
|
49
49
|
~~~ bash
|
|
50
|
-
$ bake toolbox:gdb:info
|
|
50
|
+
$ bake -g toolbox toolbox:gdb:info
|
|
51
51
|
Ruby Toolbox GDB Extensions v0.1.0
|
|
52
52
|
Status: ✓ Installed
|
|
53
53
|
~~~
|
|
@@ -55,7 +55,7 @@ Status: ✓ Installed
|
|
|
55
55
|
Check LLDB installation status:
|
|
56
56
|
|
|
57
57
|
~~~ bash
|
|
58
|
-
$ bake toolbox:lldb:info
|
|
58
|
+
$ bake -g toolbox toolbox:lldb:info
|
|
59
59
|
Ruby Toolbox LLDB Extensions v0.1.0
|
|
60
60
|
Status: ✓ Installed
|
|
61
61
|
~~~
|
|
@@ -63,7 +63,7 @@ Status: ✓ Installed
|
|
|
63
63
|
Test that extensions load automatically:
|
|
64
64
|
|
|
65
65
|
~~~ bash
|
|
66
|
-
$ gdb --batch -ex "help rb-
|
|
66
|
+
$ gdb --batch -ex "help rb-print"
|
|
67
67
|
Recursively print Ruby hash and array structures...
|
|
68
68
|
~~~
|
|
69
69
|
|
|
@@ -72,23 +72,23 @@ Recursively print Ruby hash and array structures...
|
|
|
72
72
|
To remove the GDB extensions:
|
|
73
73
|
|
|
74
74
|
~~~ bash
|
|
75
|
-
$ bake toolbox:gdb:uninstall
|
|
75
|
+
$ bake -g toolbox toolbox:gdb:uninstall
|
|
76
76
|
~~~
|
|
77
77
|
|
|
78
78
|
To remove the LLDB extensions:
|
|
79
79
|
|
|
80
80
|
~~~ bash
|
|
81
|
-
$ bake toolbox:lldb:uninstall
|
|
81
|
+
$ bake -g toolbox toolbox:lldb:uninstall
|
|
82
82
|
~~~
|
|
83
83
|
|
|
84
|
-
This removes the source line from your `~/.gdbinit`.
|
|
84
|
+
This removes the source line from your `~/.gdbinit` or `~/.lldbinit`.
|
|
85
85
|
|
|
86
86
|
## Core Concepts
|
|
87
87
|
|
|
88
88
|
Ruby GDB provides specialized commands for debugging Ruby at multiple levels:
|
|
89
89
|
|
|
90
90
|
- **Context Setup** (`rb-context`) - Get current execution context and set up convenience variables
|
|
91
|
-
- **Object Inspection** (`rb-
|
|
91
|
+
- **Object Inspection** (`rb-print`) - View Ruby objects, hashes, arrays, and structs with proper formatting
|
|
92
92
|
- **Fiber Debugging** (`rb-fiber-*`) - Scan heap for fibers, inspect state, and switch contexts
|
|
93
93
|
- **Stack Analysis** (`rb-stack-trace`) - Examine combined VM (Ruby) and C (native) stack frames
|
|
94
94
|
- **Heap Navigation** (`rb-heap-scan`) - Scan the Ruby heap to find objects by type
|
|
@@ -132,7 +132,7 @@ Diagnose the issue (extensions load automatically if installed):
|
|
|
132
132
|
(gdb) rb-fiber-scan-heap # Scan heap for all fibers
|
|
133
133
|
(gdb) rb-fiber-scan-stack-trace-all # Show backtraces for all fibers
|
|
134
134
|
(gdb) rb-fiber-scan-switch 0 # Switch to main fiber
|
|
135
|
-
(gdb) rb-
|
|
135
|
+
(gdb) rb-print $errinfo --depth 2 # Print exception (now $errinfo is set)
|
|
136
136
|
(gdb) rb-heap-scan --type RUBY_T_HASH --limit 10 # Find hashes
|
|
137
137
|
~~~
|
|
138
138
|
|
|
@@ -146,7 +146,7 @@ When a Ruby exception occurs, you can inspect it in detail:
|
|
|
146
146
|
(gdb) break rb_exc_raise
|
|
147
147
|
(gdb) run
|
|
148
148
|
(gdb) rb-context
|
|
149
|
-
(gdb) rb-
|
|
149
|
+
(gdb) rb-print $errinfo --depth 2
|
|
150
150
|
~~~
|
|
151
151
|
|
|
152
152
|
This shows the exception class, message, and any nested structures. The `rb-context` command displays the current execution context and sets up `$ec`, `$cfp`, and `$errinfo` convenience variables.
|
|
@@ -167,7 +167,7 @@ When working with fibers, you often need to see what each fiber is doing:
|
|
|
167
167
|
Ruby hashes and arrays can contain nested structures:
|
|
168
168
|
|
|
169
169
|
~~~
|
|
170
|
-
(gdb) rb-
|
|
170
|
+
(gdb) rb-print $some_hash --depth 2
|
|
171
171
|
<T_HASH@...>
|
|
172
172
|
[ 0] K: <T_SYMBOL> :name
|
|
173
173
|
V: <T_STRING@...> "Alice"
|
|
@@ -176,3 +176,25 @@ Ruby hashes and arrays can contain nested structures:
|
|
|
176
176
|
~~~
|
|
177
177
|
|
|
178
178
|
The `--depth` flag controls how deep to recurse into nested objects.
|
|
179
|
+
|
|
180
|
+
## Requirements
|
|
181
|
+
|
|
182
|
+
- GDB with Python support (GDB 7.0+) or LLDB with Python support.
|
|
183
|
+
- Ruby 3.3+ recommended.
|
|
184
|
+
|
|
185
|
+
### Platform Support
|
|
186
|
+
|
|
187
|
+
- **Linux**: Full support with all features (GDB or LLDB).
|
|
188
|
+
- **macOS**:
|
|
189
|
+
- Ruby head: Full support.
|
|
190
|
+
- Ruby 3.4.x: Limited support (see below).
|
|
191
|
+
- **BSD**: Should work similar to Linux (untested).
|
|
192
|
+
|
|
193
|
+
### macOS + Ruby 3.4.x Limitation
|
|
194
|
+
|
|
195
|
+
On macOS with LLDB and Ruby <= 3.4.x, some commands including `rb-fiber-scan-heap` will not work due to a `dsymutil` bug that drops `struct RTypedData` from debug symbols. This appears fixed in `ruby-head`.
|
|
196
|
+
|
|
197
|
+
**Workarounds:**
|
|
198
|
+
- Use Ruby head: `ruby-install ruby-head -- CFLAGS="-g -O0"`
|
|
199
|
+
- Use GDB instead of LLDB (works with Ruby 3.4.x)
|
|
200
|
+
- Other commands like `rb-print`, `rb-stack-trace`, `rb-context` work fine
|
data/context/index.yaml
CHANGED
|
@@ -12,7 +12,7 @@ files:
|
|
|
12
12
|
programs and core dumps with GDB or LLDB.
|
|
13
13
|
- path: object-inspection.md
|
|
14
14
|
title: Object Inspection
|
|
15
|
-
description: This guide explains how to use `rb-
|
|
15
|
+
description: This guide explains how to use `rb-print` to inspect Ruby objects,
|
|
16
16
|
hashes, arrays, and structs in GDB.
|
|
17
17
|
- path: stack-inspection.md
|
|
18
18
|
title: Stack Inspection
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# Object Inspection
|
|
2
2
|
|
|
3
|
-
This guide explains how to use `rb-
|
|
3
|
+
This guide explains how to use `rb-print` to inspect Ruby objects, hashes, arrays, and structs in GDB.
|
|
4
4
|
|
|
5
5
|
## Why Object Inspection Matters
|
|
6
6
|
|
|
7
7
|
When debugging Ruby programs or analyzing core dumps, you often need to inspect complex data structures that are difficult to read in their raw memory representation. Standard GDB commands show pointer addresses and raw memory, but not the logical structure of Ruby objects.
|
|
8
8
|
|
|
9
|
-
Use `rb-
|
|
9
|
+
Use `rb-print` when you need:
|
|
10
10
|
|
|
11
11
|
- **Understand exception objects**: See the full exception hierarchy, message, and backtrace data
|
|
12
12
|
- **Inspect fiber storage**: View thread-local data and fiber-specific variables
|
|
@@ -15,12 +15,12 @@ Use `rb-object-print` when you need:
|
|
|
15
15
|
|
|
16
16
|
## Basic Usage
|
|
17
17
|
|
|
18
|
-
The `rb-
|
|
18
|
+
The `rb-print` command recursively prints Ruby objects in a human-readable format.
|
|
19
19
|
|
|
20
20
|
### Syntax
|
|
21
21
|
|
|
22
22
|
~~~
|
|
23
|
-
rb-
|
|
23
|
+
rb-print <expression> [--depth N] [--debug]
|
|
24
24
|
~~~
|
|
25
25
|
|
|
26
26
|
Where:
|
|
@@ -33,10 +33,10 @@ Where:
|
|
|
33
33
|
Print immediate values and special constants:
|
|
34
34
|
|
|
35
35
|
~~~
|
|
36
|
-
(gdb) rb-
|
|
37
|
-
(gdb) rb-
|
|
38
|
-
(gdb) rb-
|
|
39
|
-
(gdb) rb-
|
|
36
|
+
(gdb) rb-print 0 # <T_FALSE>
|
|
37
|
+
(gdb) rb-print 8 # <T_NIL>
|
|
38
|
+
(gdb) rb-print 20 # <T_TRUE>
|
|
39
|
+
(gdb) rb-print 85 # <T_FIXNUM> 42
|
|
40
40
|
~~~
|
|
41
41
|
|
|
42
42
|
These work without any Ruby process running, making them useful for learning and testing.
|
|
@@ -46,10 +46,10 @@ These work without any Ruby process running, making them useful for learning and
|
|
|
46
46
|
Use any valid GDB expression:
|
|
47
47
|
|
|
48
48
|
~~~
|
|
49
|
-
(gdb) rb-
|
|
50
|
-
(gdb) rb-
|
|
51
|
-
(gdb) rb-
|
|
52
|
-
(gdb) rb-
|
|
49
|
+
(gdb) rb-print $ec->errinfo # Exception object
|
|
50
|
+
(gdb) rb-print $ec->cfp->sp[-1] # Top of VM stack
|
|
51
|
+
(gdb) rb-print $ec->storage # Fiber storage hash
|
|
52
|
+
(gdb) rb-print (VALUE)0x00007f8a12345678 # Object at specific address
|
|
53
53
|
~~~
|
|
54
54
|
|
|
55
55
|
## Inspecting Hashes
|
|
@@ -61,7 +61,7 @@ Ruby hashes have two internal representations (ST table and AR table). The comma
|
|
|
61
61
|
For hashes with fewer than 8 entries, Ruby uses an array-based implementation:
|
|
62
62
|
|
|
63
63
|
~~~
|
|
64
|
-
(gdb) rb-
|
|
64
|
+
(gdb) rb-print $some_hash
|
|
65
65
|
<T_HASH@...>
|
|
66
66
|
[ 0] K: <T_SYMBOL> :name
|
|
67
67
|
V: <T_STRING@...> "Alice"
|
|
@@ -76,7 +76,7 @@ For hashes with fewer than 8 entries, Ruby uses an array-based implementation:
|
|
|
76
76
|
For larger hashes, Ruby uses a hash table (output format is similar):
|
|
77
77
|
|
|
78
78
|
~~~
|
|
79
|
-
(gdb) rb-
|
|
79
|
+
(gdb) rb-print $large_hash
|
|
80
80
|
<T_HASH@...>
|
|
81
81
|
[ 0] K: <T_SYMBOL> :user_id
|
|
82
82
|
V: <T_FIXNUM> 12345
|
|
@@ -90,12 +90,12 @@ For larger hashes, Ruby uses a hash table (output format is similar):
|
|
|
90
90
|
Prevent overwhelming output from deeply nested structures:
|
|
91
91
|
|
|
92
92
|
~~~
|
|
93
|
-
(gdb) rb-
|
|
93
|
+
(gdb) rb-print $nested_hash --depth 1 # Only top level
|
|
94
94
|
<T_HASH@...>
|
|
95
95
|
[ 0] K: <T_SYMBOL> :data
|
|
96
96
|
V: <T_HASH@...> # Nested hash not expanded
|
|
97
97
|
|
|
98
|
-
(gdb) rb-
|
|
98
|
+
(gdb) rb-print $nested_hash --depth 2 # Expand one level
|
|
99
99
|
<T_HASH@...>
|
|
100
100
|
[ 0] K: <T_SYMBOL> :data
|
|
101
101
|
V: <T_HASH@...>
|
|
@@ -114,7 +114,7 @@ Arrays also have two representations based on size:
|
|
|
114
114
|
Arrays display their elements with type information:
|
|
115
115
|
|
|
116
116
|
~~~
|
|
117
|
-
(gdb) rb-
|
|
117
|
+
(gdb) rb-print $array
|
|
118
118
|
<T_ARRAY@...>
|
|
119
119
|
[ 0] <T_FIXNUM> 1
|
|
120
120
|
[ 1] <T_FIXNUM> 2
|
|
@@ -124,7 +124,7 @@ Arrays display their elements with type information:
|
|
|
124
124
|
For arrays with nested objects:
|
|
125
125
|
|
|
126
126
|
~~~
|
|
127
|
-
(gdb) rb-
|
|
127
|
+
(gdb) rb-print $array --depth 2
|
|
128
128
|
<T_ARRAY@...>
|
|
129
129
|
[ 0] <T_STRING@...> "first item"
|
|
130
130
|
[ 1] <T_HASH@...>
|
|
@@ -138,7 +138,7 @@ For arrays with nested objects:
|
|
|
138
138
|
Ruby Struct objects work similarly to arrays:
|
|
139
139
|
|
|
140
140
|
~~~
|
|
141
|
-
(gdb) rb-
|
|
141
|
+
(gdb) rb-print $struct_instance
|
|
142
142
|
<T_STRUCT@...>
|
|
143
143
|
[ 0] <T_STRING@...> "John"
|
|
144
144
|
[ 1] <T_FIXNUM> 25
|
|
@@ -155,7 +155,7 @@ When a fiber has an exception, inspect it:
|
|
|
155
155
|
~~~
|
|
156
156
|
(gdb) rb-fiber-scan-heap
|
|
157
157
|
(gdb) rb-fiber-scan-switch 5 # Switch to fiber #5
|
|
158
|
-
(gdb) rb-
|
|
158
|
+
(gdb) rb-print $errinfo --depth 3
|
|
159
159
|
~~~
|
|
160
160
|
|
|
161
161
|
This reveals the full exception structure including any nested causes. After switching to a fiber, `$errinfo` and `$ec` convenience variables are automatically set.
|
|
@@ -167,8 +167,8 @@ Break at a method and examine arguments on the stack:
|
|
|
167
167
|
~~~
|
|
168
168
|
(gdb) break some_method
|
|
169
169
|
(gdb) run
|
|
170
|
-
(gdb) rb-
|
|
171
|
-
(gdb) rb-
|
|
170
|
+
(gdb) rb-print $ec->cfp->sp[-1] # Last argument
|
|
171
|
+
(gdb) rb-print $ec->cfp->sp[-2] # Second-to-last argument
|
|
172
172
|
~~~
|
|
173
173
|
|
|
174
174
|
### Examining Fiber Storage
|
|
@@ -178,17 +178,17 @@ Thread-local variables are stored in fiber storage:
|
|
|
178
178
|
~~~
|
|
179
179
|
(gdb) rb-fiber-scan-heap
|
|
180
180
|
(gdb) rb-fiber-scan-switch 0
|
|
181
|
-
(gdb) rb-
|
|
181
|
+
(gdb) rb-print $ec->storage --depth 2
|
|
182
182
|
~~~
|
|
183
183
|
|
|
184
184
|
This shows all thread-local variables and their values.
|
|
185
185
|
|
|
186
186
|
## Debugging with --debug Flag
|
|
187
187
|
|
|
188
|
-
When `rb-
|
|
188
|
+
When `rb-print` doesn't show what you expect, use `--debug`:
|
|
189
189
|
|
|
190
190
|
~~~
|
|
191
|
-
(gdb) rb-
|
|
191
|
+
(gdb) rb-print $suspicious_value --debug
|
|
192
192
|
DEBUG: Evaluated '$suspicious_value' to 0x7f8a1c567890
|
|
193
193
|
DEBUG: Loaded constant RUBY_T_MASK = 31
|
|
194
194
|
DEBUG: Object at 0x7f8a1c567890 with flags=0x20040005, type=0x5
|
data/context/stack-inspection.md
CHANGED
|
@@ -140,9 +140,9 @@ See what values are on the current frame's stack:
|
|
|
140
140
|
(gdb) set $sp = $ec->cfp->sp
|
|
141
141
|
|
|
142
142
|
# Print values on stack
|
|
143
|
-
(gdb) rb-
|
|
144
|
-
(gdb) rb-
|
|
145
|
-
(gdb) rb-
|
|
143
|
+
(gdb) rb-print *(VALUE*)($sp - 1) # Top of stack
|
|
144
|
+
(gdb) rb-print *(VALUE*)($sp - 2) # Second value
|
|
145
|
+
(gdb) rb-print *(VALUE*)($sp - 3) # Third value
|
|
146
146
|
~~~
|
|
147
147
|
|
|
148
148
|
### Tracking Fiber Switches
|
data/data/toolbox/command.py
CHANGED
|
@@ -19,6 +19,231 @@ class Arguments:
|
|
|
19
19
|
"""Get an option value with optional default"""
|
|
20
20
|
return self.options.get(option_name, default)
|
|
21
21
|
|
|
22
|
+
|
|
23
|
+
class Usage:
|
|
24
|
+
"""Command specification DSL for declarative command interfaces.
|
|
25
|
+
|
|
26
|
+
Defines what parameters, options, and flags a command accepts,
|
|
27
|
+
enabling validation and help text generation.
|
|
28
|
+
|
|
29
|
+
Example:
|
|
30
|
+
usage = Usage(
|
|
31
|
+
summary="Print Ruby objects with recursion",
|
|
32
|
+
parameters=['value'],
|
|
33
|
+
options={'depth': (int, 1), 'limit': (int, None)},
|
|
34
|
+
flags=['debug', 'verbose']
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
arguments = usage.parse("$var --depth 3 --debug")
|
|
38
|
+
# arguments.expressions = ['$var']
|
|
39
|
+
# arguments.options = {'depth': 3}
|
|
40
|
+
# arguments.flags = {'debug'}
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(self, summary, parameters=None, options=None, flags=None, examples=None):
|
|
44
|
+
"""Define command interface.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
summary: One-line command description
|
|
48
|
+
parameters: List of parameter names or tuples (name, description)
|
|
49
|
+
options: Dict of {name: (type, default, description)}
|
|
50
|
+
flags: List of flag names or tuples (name, description)
|
|
51
|
+
examples: List of example command strings with descriptions
|
|
52
|
+
|
|
53
|
+
Example:
|
|
54
|
+
Usage(
|
|
55
|
+
summary="Scan heap for objects",
|
|
56
|
+
parameters=[('type_name', 'Ruby type to search for')],
|
|
57
|
+
options={
|
|
58
|
+
'limit': (int, None, 'Maximum objects to find'),
|
|
59
|
+
'depth': (int, 1, 'Recursion depth')
|
|
60
|
+
},
|
|
61
|
+
flags=[
|
|
62
|
+
('terminated', 'Include terminated fibers'),
|
|
63
|
+
('cache', 'Use cached results')
|
|
64
|
+
],
|
|
65
|
+
examples=[
|
|
66
|
+
("rb-heap-scan --type RUBY_T_STRING", "Find all strings"),
|
|
67
|
+
("rb-heap-scan --type RUBY_T_HASH --limit 10", "Find first 10 hashes")
|
|
68
|
+
]
|
|
69
|
+
)
|
|
70
|
+
"""
|
|
71
|
+
self.summary = summary
|
|
72
|
+
self.parameters = self._normalize_params(parameters or [])
|
|
73
|
+
self.options = options or {}
|
|
74
|
+
self.flags = self._normalize_flags(flags or [])
|
|
75
|
+
self.examples = examples or []
|
|
76
|
+
|
|
77
|
+
def _normalize_params(self, params):
|
|
78
|
+
"""Normalize parameters to list of (name, description) tuples."""
|
|
79
|
+
normalized = []
|
|
80
|
+
for param in params:
|
|
81
|
+
if isinstance(param, tuple):
|
|
82
|
+
normalized.append(param)
|
|
83
|
+
else:
|
|
84
|
+
normalized.append((param, None))
|
|
85
|
+
return normalized
|
|
86
|
+
|
|
87
|
+
def _normalize_flags(self, flags):
|
|
88
|
+
"""Normalize flags to list of (name, description) tuples."""
|
|
89
|
+
normalized = []
|
|
90
|
+
for flag in flags:
|
|
91
|
+
if isinstance(flag, tuple):
|
|
92
|
+
normalized.append(flag)
|
|
93
|
+
else:
|
|
94
|
+
normalized.append((flag, None))
|
|
95
|
+
return normalized
|
|
96
|
+
|
|
97
|
+
def parse(self, argument_string):
|
|
98
|
+
"""Parse and validate command arguments.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
argument_string: Raw argument string from debugger
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Arguments object with validated and type-converted values
|
|
105
|
+
|
|
106
|
+
Raises:
|
|
107
|
+
ValueError: If validation fails (wrong parameter count, invalid types, etc.)
|
|
108
|
+
"""
|
|
109
|
+
# Use existing parser to extract raw arguments
|
|
110
|
+
arguments = parse_arguments(argument_string)
|
|
111
|
+
|
|
112
|
+
# Validate parameter count
|
|
113
|
+
if len(arguments.expressions) != len(self.parameters):
|
|
114
|
+
if len(self.parameters) == 0 and len(arguments.expressions) > 0:
|
|
115
|
+
raise ValueError(f"Command takes no parameters, got {len(arguments.expressions)}")
|
|
116
|
+
elif len(self.parameters) == 1:
|
|
117
|
+
raise ValueError(f"Command requires 1 parameter, got {len(arguments.expressions)}")
|
|
118
|
+
else:
|
|
119
|
+
raise ValueError(f"Command requires {len(self.parameters)} parameters, got {len(arguments.expressions)}")
|
|
120
|
+
|
|
121
|
+
# Validate and convert option types
|
|
122
|
+
converted_options = {}
|
|
123
|
+
for option_name, option_value in arguments.options.items():
|
|
124
|
+
if option_name not in self.options:
|
|
125
|
+
raise ValueError(f"Unknown option: --{option_name}")
|
|
126
|
+
|
|
127
|
+
# Unpack option spec (handle 2-tuple or 3-tuple)
|
|
128
|
+
opt_spec = self.options[option_name]
|
|
129
|
+
option_type = opt_spec[0]
|
|
130
|
+
option_default = opt_spec[1]
|
|
131
|
+
|
|
132
|
+
# Convert to specified type
|
|
133
|
+
try:
|
|
134
|
+
if option_type == int:
|
|
135
|
+
converted_options[option_name] = int(option_value)
|
|
136
|
+
elif option_type == str:
|
|
137
|
+
converted_options[option_name] = str(option_value)
|
|
138
|
+
elif option_type == bool:
|
|
139
|
+
converted_options[option_name] = bool(option_value)
|
|
140
|
+
else:
|
|
141
|
+
# Custom type converter
|
|
142
|
+
converted_options[option_name] = option_type(option_value)
|
|
143
|
+
except (ValueError, TypeError) as e:
|
|
144
|
+
raise ValueError(f"Invalid value for --{option_name}: {option_value} (expected {option_type.__name__})")
|
|
145
|
+
|
|
146
|
+
# Add defaults for missing options
|
|
147
|
+
for option_name, opt_spec in self.options.items():
|
|
148
|
+
option_default = opt_spec[1]
|
|
149
|
+
if option_name not in converted_options and option_default is not None:
|
|
150
|
+
converted_options[option_name] = option_default
|
|
151
|
+
|
|
152
|
+
# Validate flags
|
|
153
|
+
flag_names = {flag[0] for flag in self.flags}
|
|
154
|
+
for flag_name in arguments.flags:
|
|
155
|
+
if flag_name not in flag_names:
|
|
156
|
+
raise ValueError(f"Unknown flag: --{flag_name}")
|
|
157
|
+
|
|
158
|
+
# Return new Arguments with converted options
|
|
159
|
+
return Arguments(arguments.expressions, arguments.flags, converted_options)
|
|
160
|
+
|
|
161
|
+
def print_to(self, terminal, command_name):
|
|
162
|
+
"""Print help text from usage specification.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
terminal: Terminal for colored output
|
|
166
|
+
command_name: Name of the command (e.g., "rb-print")
|
|
167
|
+
"""
|
|
168
|
+
import format as fmt
|
|
169
|
+
|
|
170
|
+
# Summary with color
|
|
171
|
+
terminal.print(fmt.bold, self.summary, fmt.reset)
|
|
172
|
+
terminal.print()
|
|
173
|
+
|
|
174
|
+
# Print usage line
|
|
175
|
+
terminal.print("Usage: ", end='')
|
|
176
|
+
terminal.print(fmt.bold, command_name, fmt.reset, end='')
|
|
177
|
+
|
|
178
|
+
for param_name, _ in self.parameters:
|
|
179
|
+
terminal.print(' ', end='')
|
|
180
|
+
terminal.print(fmt.placeholder, f"<{param_name}>", fmt.reset, end='')
|
|
181
|
+
|
|
182
|
+
# Add option placeholders
|
|
183
|
+
for option_name in self.options.keys():
|
|
184
|
+
terminal.print(' ', end='')
|
|
185
|
+
terminal.print(fmt.placeholder, f"[--{option_name} N]", fmt.reset, end='')
|
|
186
|
+
|
|
187
|
+
# Add flag placeholders
|
|
188
|
+
for flag_name, _ in self.flags:
|
|
189
|
+
terminal.print(' ', end='')
|
|
190
|
+
terminal.print(fmt.placeholder, f"[--{flag_name}]", fmt.reset, end='')
|
|
191
|
+
|
|
192
|
+
terminal.print()
|
|
193
|
+
terminal.print()
|
|
194
|
+
|
|
195
|
+
# Parameter descriptions
|
|
196
|
+
if self.parameters:
|
|
197
|
+
terminal.print(fmt.title, "Parameters:", fmt.reset)
|
|
198
|
+
|
|
199
|
+
for param_name, param_desc in self.parameters:
|
|
200
|
+
terminal.print(" ", fmt.symbol, param_name, fmt.reset, end='')
|
|
201
|
+
if param_desc:
|
|
202
|
+
terminal.print(f" - {param_desc}")
|
|
203
|
+
else:
|
|
204
|
+
terminal.print()
|
|
205
|
+
terminal.print()
|
|
206
|
+
|
|
207
|
+
# Option descriptions
|
|
208
|
+
if self.options:
|
|
209
|
+
terminal.print(fmt.title, "Options:", fmt.reset)
|
|
210
|
+
|
|
211
|
+
for option_name, opt_spec in self.options.items():
|
|
212
|
+
opt_type, opt_default = opt_spec[0], opt_spec[1]
|
|
213
|
+
opt_desc = opt_spec[2] if len(opt_spec) > 2 else None
|
|
214
|
+
|
|
215
|
+
type_str = opt_type.__name__ if hasattr(opt_type, '__name__') else str(opt_type)
|
|
216
|
+
default_str = f" (default: {opt_default})" if opt_default is not None else ""
|
|
217
|
+
|
|
218
|
+
terminal.print(" ", fmt.symbol, f"--{option_name}", fmt.reset, end='')
|
|
219
|
+
terminal.print(fmt.placeholder, f" <{type_str}>", fmt.reset, end='')
|
|
220
|
+
terminal.print(default_str)
|
|
221
|
+
|
|
222
|
+
if opt_desc:
|
|
223
|
+
terminal.print(f" {opt_desc}")
|
|
224
|
+
terminal.print()
|
|
225
|
+
|
|
226
|
+
# Flag descriptions
|
|
227
|
+
if self.flags:
|
|
228
|
+
terminal.print(fmt.title, "Flags:", fmt.reset)
|
|
229
|
+
|
|
230
|
+
for flag_name, flag_desc in self.flags:
|
|
231
|
+
terminal.print(" ", fmt.symbol, f"--{flag_name}", fmt.reset, end='')
|
|
232
|
+
if flag_desc:
|
|
233
|
+
terminal.print(f" - {flag_desc}")
|
|
234
|
+
else:
|
|
235
|
+
terminal.print()
|
|
236
|
+
terminal.print()
|
|
237
|
+
|
|
238
|
+
# Examples section
|
|
239
|
+
if self.examples:
|
|
240
|
+
terminal.print(fmt.title, "Examples:", fmt.reset)
|
|
241
|
+
|
|
242
|
+
for example_cmd, example_desc in self.examples:
|
|
243
|
+
terminal.print(fmt.example, f" {example_cmd}", fmt.reset)
|
|
244
|
+
if example_desc:
|
|
245
|
+
terminal.print(f" {example_desc}")
|
|
246
|
+
|
|
22
247
|
class ArgumentParser:
|
|
23
248
|
"""Parse GDB command arguments handling nested brackets, quotes, and flags.
|
|
24
249
|
|