toolbox 0.1.4 → 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 +7 -0
- checksums.yaml.gz.sig +0 -0
- data/bake/ruby/gdb.rb +135 -0
- data/bake/toolbox/gdb.rb +137 -0
- data/bake/toolbox/lldb.rb +137 -0
- data/context/fiber-debugging.md +171 -0
- data/context/getting-started.md +200 -0
- data/context/heap-debugging.md +351 -0
- data/context/index.yaml +28 -0
- data/context/object-inspection.md +208 -0
- data/context/stack-inspection.md +188 -0
- data/data/toolbox/command.py +479 -0
- data/data/toolbox/constants.py +200 -0
- data/data/toolbox/context.py +371 -0
- data/data/toolbox/debugger/__init__.py +101 -0
- data/data/toolbox/debugger/gdb_backend.py +664 -0
- data/data/toolbox/debugger/lldb_backend.py +986 -0
- data/data/toolbox/fiber.py +877 -0
- data/data/toolbox/format.py +205 -0
- data/data/toolbox/heap.py +679 -0
- data/data/toolbox/init.py +89 -0
- data/data/toolbox/print.py +79 -0
- data/data/toolbox/rarray.py +116 -0
- data/data/toolbox/rbasic.py +99 -0
- data/data/toolbox/rbignum.py +48 -0
- data/data/toolbox/rclass.py +136 -0
- data/data/toolbox/readme.md +214 -0
- data/data/toolbox/rexception.py +150 -0
- data/data/toolbox/rfloat.py +88 -0
- data/data/toolbox/rhash.py +151 -0
- data/data/toolbox/rstring.py +230 -0
- data/data/toolbox/rstruct.py +149 -0
- data/data/toolbox/rsymbol.py +278 -0
- data/data/toolbox/rvalue.py +183 -0
- data/data/toolbox/stack.py +620 -0
- data/lib/toolbox/gdb.rb +21 -0
- data/lib/toolbox/lldb.rb +21 -0
- data/lib/toolbox/version.rb +7 -1
- data/lib/toolbox.rb +9 -24
- data/license.md +21 -0
- data/readme.md +64 -0
- data/releases.md +9 -0
- data.tar.gz.sig +0 -0
- metadata +95 -165
- metadata.gz.sig +0 -0
- data/Rakefile +0 -61
- data/lib/dirs.rb +0 -9
- data/lib/toolbox/config.rb +0 -211
- data/lib/toolbox/default_controller.rb +0 -393
- data/lib/toolbox/helpers.rb +0 -11
- data/lib/toolbox/rendering.rb +0 -413
- data/lib/toolbox/searching.rb +0 -85
- data/lib/toolbox/session_params.rb +0 -63
- data/lib/toolbox/sorting.rb +0 -74
- data/locale/de/LC_MESSAGES/toolbox.mo +0 -0
- data/public/images/add.png +0 -0
- data/public/images/arrow_down.gif +0 -0
- data/public/images/arrow_up.gif +0 -0
- data/public/images/close.png +0 -0
- data/public/images/edit.gif +0 -0
- data/public/images/email.png +0 -0
- data/public/images/page.png +0 -0
- data/public/images/page_acrobat.png +0 -0
- data/public/images/page_add.png +0 -0
- data/public/images/page_copy.png +0 -0
- data/public/images/page_delete.png +0 -0
- data/public/images/page_edit.png +0 -0
- data/public/images/page_excel.png +0 -0
- data/public/images/page_list.png +0 -0
- data/public/images/page_save.png +0 -0
- data/public/images/page_word.png +0 -0
- data/public/images/remove.png +0 -0
- data/public/images/show.gif +0 -0
- data/public/images/spinner.gif +0 -0
- data/public/javascripts/popup.js +0 -498
- data/public/javascripts/toolbox.js +0 -18
- data/public/stylesheets/context_menu.css +0 -168
- data/public/stylesheets/popup.css +0 -30
- data/public/stylesheets/toolbox.css +0 -107
- data/view/toolbox/_collection.html.erb +0 -24
- data/view/toolbox/_collection_header.html.erb +0 -7
- data/view/toolbox/_context_menu.html.erb +0 -17
- data/view/toolbox/_dialogs.html.erb +0 -6
- data/view/toolbox/_form.html.erb +0 -30
- data/view/toolbox/_form_collection_row.html.erb +0 -18
- data/view/toolbox/_form_fieldset.html.erb +0 -30
- data/view/toolbox/_form_fieldset_row.html.erb +0 -19
- data/view/toolbox/_list.html.erb +0 -25
- data/view/toolbox/_list_row.html.erb +0 -10
- data/view/toolbox/_menu.html.erb +0 -7
- data/view/toolbox/_search_field.html.erb +0 -8
- data/view/toolbox/_show.html.erb +0 -12
- data/view/toolbox/_show_collection_row.html.erb +0 -6
- data/view/toolbox/_show_fieldset.html.erb +0 -21
- data/view/toolbox/edit.html.erb +0 -5
- data/view/toolbox/index.html.erb +0 -3
- data/view/toolbox/new.html.erb +0 -9
- data/view/toolbox/show.html.erb +0 -39
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
class Arguments:
|
|
2
|
+
"""Structured result from parsing GDB command arguments.
|
|
3
|
+
|
|
4
|
+
Attributes:
|
|
5
|
+
expressions: List of expressions to evaluate (e.g., ["$var", "$ec->cfp->sp[-1]"])
|
|
6
|
+
flags: Set of boolean flags (e.g., {'debug'})
|
|
7
|
+
options: Dict of options with values (e.g., {'depth': 3})
|
|
8
|
+
"""
|
|
9
|
+
def __init__(self, expressions, flags, options):
|
|
10
|
+
self.expressions = expressions
|
|
11
|
+
self.flags = flags
|
|
12
|
+
self.options = options
|
|
13
|
+
|
|
14
|
+
def has_flag(self, flag_name):
|
|
15
|
+
"""Check if a boolean flag is present"""
|
|
16
|
+
return flag_name in self.flags
|
|
17
|
+
|
|
18
|
+
def get_option(self, option_name, default=None):
|
|
19
|
+
"""Get an option value with optional default"""
|
|
20
|
+
return self.options.get(option_name, default)
|
|
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
|
+
|
|
247
|
+
class ArgumentParser:
|
|
248
|
+
"""Parse GDB command arguments handling nested brackets, quotes, and flags.
|
|
249
|
+
|
|
250
|
+
This parser correctly handles:
|
|
251
|
+
- Nested brackets: $ec->cfp->sp[-1]
|
|
252
|
+
- Nested parentheses: ((struct foo*)bar)->baz
|
|
253
|
+
- Quotes: "string value"
|
|
254
|
+
- Flags: --debug, --depth 3
|
|
255
|
+
- Complex expressions: foo + 10, bar->ptr[idx * 2]
|
|
256
|
+
"""
|
|
257
|
+
|
|
258
|
+
def __init__(self, argument_string):
|
|
259
|
+
self.argument_string = argument_string.strip()
|
|
260
|
+
self.position = 0
|
|
261
|
+
self.length = len(self.argument_string)
|
|
262
|
+
|
|
263
|
+
def parse(self):
|
|
264
|
+
"""Parse the argument string and return (expressions, flags, options)
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
tuple: (expressions: list[str], flags: set, options: dict)
|
|
268
|
+
- expressions: List of expressions to evaluate
|
|
269
|
+
- flags: Set of boolean flags (e.g., {'debug'})
|
|
270
|
+
- options: Dict of options with values (e.g., {'depth': 3})
|
|
271
|
+
"""
|
|
272
|
+
flags = set()
|
|
273
|
+
options = {}
|
|
274
|
+
expressions = []
|
|
275
|
+
|
|
276
|
+
while self.position < self.length:
|
|
277
|
+
self.skip_whitespace()
|
|
278
|
+
if self.position >= self.length:
|
|
279
|
+
break
|
|
280
|
+
|
|
281
|
+
if self.peek() == '-' and self.peek(1) == '-':
|
|
282
|
+
# Parse a flag or option
|
|
283
|
+
flag_name, flag_value = self.parse_flag()
|
|
284
|
+
if flag_value is None:
|
|
285
|
+
flags.add(flag_name)
|
|
286
|
+
else:
|
|
287
|
+
options[flag_name] = flag_value
|
|
288
|
+
else:
|
|
289
|
+
# Parse an expression
|
|
290
|
+
expression = self.parse_expression()
|
|
291
|
+
if expression:
|
|
292
|
+
expressions.append(expression)
|
|
293
|
+
|
|
294
|
+
return expressions, flags, options
|
|
295
|
+
|
|
296
|
+
def peek(self, offset=0):
|
|
297
|
+
"""Peek at character at current position + offset"""
|
|
298
|
+
position = self.position + offset
|
|
299
|
+
if position < self.length:
|
|
300
|
+
return self.argument_string[position]
|
|
301
|
+
return None
|
|
302
|
+
|
|
303
|
+
def consume(self, count=1):
|
|
304
|
+
"""Consume and return count characters"""
|
|
305
|
+
result = self.argument_string[self.position:self.position + count]
|
|
306
|
+
self.position += count
|
|
307
|
+
return result
|
|
308
|
+
|
|
309
|
+
def skip_whitespace(self):
|
|
310
|
+
"""Skip whitespace characters"""
|
|
311
|
+
while self.position < self.length and self.argument_string[self.position].isspace():
|
|
312
|
+
self.position += 1
|
|
313
|
+
|
|
314
|
+
def parse_flag(self):
|
|
315
|
+
"""Parse a flag starting with --
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
tuple: (flag_name: str, value: str|None)
|
|
319
|
+
- For boolean flags: ('debug', None)
|
|
320
|
+
- For valued flags: ('depth', '3')
|
|
321
|
+
"""
|
|
322
|
+
# Consume '--'
|
|
323
|
+
self.consume(2)
|
|
324
|
+
|
|
325
|
+
# Read flag name
|
|
326
|
+
flag_name = ''
|
|
327
|
+
while self.position < self.length and self.argument_string[self.position].isalnum():
|
|
328
|
+
flag_name += self.consume()
|
|
329
|
+
|
|
330
|
+
# Check if flag has a value
|
|
331
|
+
self.skip_whitespace()
|
|
332
|
+
|
|
333
|
+
# If next character is not another flag and not end of string, it might be a value
|
|
334
|
+
if self.position < self.length and not (self.peek() == '-' and self.peek(1) == '-'):
|
|
335
|
+
# Try to parse a value - it could start with $, digit, or letter
|
|
336
|
+
if self.peek() and not self.peek().isspace():
|
|
337
|
+
value = ''
|
|
338
|
+
while self.position < self.length and not self.argument_string[self.position].isspace():
|
|
339
|
+
if self.peek() == '-' and self.peek(1) == '-':
|
|
340
|
+
break
|
|
341
|
+
value += self.consume()
|
|
342
|
+
|
|
343
|
+
# Try to convert to int if it's a number
|
|
344
|
+
try:
|
|
345
|
+
return flag_name, int(value)
|
|
346
|
+
except ValueError:
|
|
347
|
+
return flag_name, value
|
|
348
|
+
|
|
349
|
+
return flag_name, None
|
|
350
|
+
|
|
351
|
+
def parse_expression(self):
|
|
352
|
+
"""Parse a single expression, stopping at whitespace (unless nested) or flags.
|
|
353
|
+
|
|
354
|
+
An expression can be:
|
|
355
|
+
- A quoted string: "foo" or 'bar'
|
|
356
|
+
- A parenthesized expression: (x + y)
|
|
357
|
+
- A variable with accessors: $ec->cfp->sp[-1]
|
|
358
|
+
- Any combination that doesn't contain unquoted/unnested whitespace
|
|
359
|
+
"""
|
|
360
|
+
expression = ''
|
|
361
|
+
|
|
362
|
+
while self.position < self.length:
|
|
363
|
+
self.skip_whitespace()
|
|
364
|
+
if self.position >= self.length:
|
|
365
|
+
break
|
|
366
|
+
|
|
367
|
+
character = self.peek()
|
|
368
|
+
|
|
369
|
+
# Stop at flags
|
|
370
|
+
if character == '-' and self.peek(1) == '-':
|
|
371
|
+
break
|
|
372
|
+
|
|
373
|
+
# Handle quoted strings - these are complete expressions
|
|
374
|
+
if character in ('"', "'"):
|
|
375
|
+
quoted = self.parse_quoted_string(character)
|
|
376
|
+
expression += quoted
|
|
377
|
+
# After a quoted string, we're done with this expression
|
|
378
|
+
break
|
|
379
|
+
|
|
380
|
+
# Handle parentheses - collect the whole balanced expression
|
|
381
|
+
if character == '(':
|
|
382
|
+
expression += self.parse_balanced('(', ')')
|
|
383
|
+
continue
|
|
384
|
+
|
|
385
|
+
# Handle brackets
|
|
386
|
+
if character == '[':
|
|
387
|
+
expression += self.parse_balanced('[', ']')
|
|
388
|
+
continue
|
|
389
|
+
|
|
390
|
+
# Handle braces
|
|
391
|
+
if character == '{':
|
|
392
|
+
expression += self.parse_balanced('{', '}')
|
|
393
|
+
continue
|
|
394
|
+
|
|
395
|
+
# Stop at whitespace (this separates expressions)
|
|
396
|
+
if character.isspace():
|
|
397
|
+
break
|
|
398
|
+
|
|
399
|
+
# Regular character - part of a variable name, operator, etc.
|
|
400
|
+
expression += self.consume()
|
|
401
|
+
|
|
402
|
+
return expression.strip()
|
|
403
|
+
|
|
404
|
+
def parse_quoted_string(self, quote_character):
|
|
405
|
+
"""Parse a quoted string, handling escapes"""
|
|
406
|
+
result = self.consume() # Opening quote
|
|
407
|
+
|
|
408
|
+
while self.position < self.length:
|
|
409
|
+
character = self.peek()
|
|
410
|
+
|
|
411
|
+
if character == '\\':
|
|
412
|
+
# Escape sequence
|
|
413
|
+
result += self.consume()
|
|
414
|
+
if self.position < self.length:
|
|
415
|
+
result += self.consume()
|
|
416
|
+
elif character == quote_character:
|
|
417
|
+
# Closing quote
|
|
418
|
+
result += self.consume()
|
|
419
|
+
break
|
|
420
|
+
else:
|
|
421
|
+
result += self.consume()
|
|
422
|
+
|
|
423
|
+
return result
|
|
424
|
+
|
|
425
|
+
def parse_balanced(self, open_character, close_character):
|
|
426
|
+
"""Parse a balanced pair of delimiters (e.g., parentheses, brackets)"""
|
|
427
|
+
result = self.consume() # Opening delimiter
|
|
428
|
+
depth = 1
|
|
429
|
+
|
|
430
|
+
while self.position < self.length and depth > 0:
|
|
431
|
+
character = self.peek()
|
|
432
|
+
|
|
433
|
+
# Handle quotes inside balanced delimiters
|
|
434
|
+
if character in ('"', "'"):
|
|
435
|
+
result += self.parse_quoted_string(character)
|
|
436
|
+
continue
|
|
437
|
+
|
|
438
|
+
if character == open_character:
|
|
439
|
+
depth += 1
|
|
440
|
+
elif character == close_character:
|
|
441
|
+
depth -= 1
|
|
442
|
+
|
|
443
|
+
result += self.consume()
|
|
444
|
+
|
|
445
|
+
return result
|
|
446
|
+
|
|
447
|
+
def parse_arguments(input):
|
|
448
|
+
"""Convenience function to parse argument string.
|
|
449
|
+
|
|
450
|
+
Arguments:
|
|
451
|
+
input: The raw argument string from GDB command
|
|
452
|
+
|
|
453
|
+
Returns:
|
|
454
|
+
Arguments: Structured object with expressions, flags, and options
|
|
455
|
+
|
|
456
|
+
Examples:
|
|
457
|
+
>>> arguments = parse_arguments("$var --debug")
|
|
458
|
+
>>> arguments.expressions
|
|
459
|
+
['$var']
|
|
460
|
+
>>> arguments.has_flag('debug')
|
|
461
|
+
True
|
|
462
|
+
|
|
463
|
+
>>> arguments = parse_arguments('"foo" "bar" --depth 3')
|
|
464
|
+
>>> arguments.expressions
|
|
465
|
+
['"foo"', '"bar"']
|
|
466
|
+
>>> arguments.get_option('depth')
|
|
467
|
+
3
|
|
468
|
+
|
|
469
|
+
>>> arguments = parse_arguments("$ec->cfp->sp[-1] --debug --depth 2")
|
|
470
|
+
>>> arguments.expressions
|
|
471
|
+
['$ec->cfp->sp[-1]']
|
|
472
|
+
>>> arguments.has_flag('debug')
|
|
473
|
+
True
|
|
474
|
+
>>> arguments.get_option('depth')
|
|
475
|
+
2
|
|
476
|
+
"""
|
|
477
|
+
parser = ArgumentParser(input)
|
|
478
|
+
expressions, flags, options = parser.parse()
|
|
479
|
+
return Arguments(expressions, flags, options)
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import debugger
|
|
2
|
+
|
|
3
|
+
# Global shared cache for Ruby constants
|
|
4
|
+
_CACHE = {}
|
|
5
|
+
|
|
6
|
+
# Global shared cache for debugger type lookups
|
|
7
|
+
_TYPE_CACHE = {}
|
|
8
|
+
|
|
9
|
+
# Default values for Ruby type constants (ruby_value_type enum)
|
|
10
|
+
_TYPE_DEFAULTS = {
|
|
11
|
+
'RUBY_T_NONE': 0x00,
|
|
12
|
+
'RUBY_T_OBJECT': 0x01,
|
|
13
|
+
'RUBY_T_CLASS': 0x02,
|
|
14
|
+
'RUBY_T_MODULE': 0x03,
|
|
15
|
+
'RUBY_T_FLOAT': 0x04,
|
|
16
|
+
'RUBY_T_STRING': 0x05,
|
|
17
|
+
'RUBY_T_REGEXP': 0x06,
|
|
18
|
+
'RUBY_T_ARRAY': 0x07,
|
|
19
|
+
'RUBY_T_HASH': 0x08,
|
|
20
|
+
'RUBY_T_STRUCT': 0x09,
|
|
21
|
+
'RUBY_T_BIGNUM': 0x0a,
|
|
22
|
+
'RUBY_T_FILE': 0x0b,
|
|
23
|
+
'RUBY_T_DATA': 0x0c,
|
|
24
|
+
'RUBY_T_MATCH': 0x0d,
|
|
25
|
+
'RUBY_T_COMPLEX': 0x0e,
|
|
26
|
+
'RUBY_T_RATIONAL': 0x0f,
|
|
27
|
+
'RUBY_T_NIL': 0x11,
|
|
28
|
+
'RUBY_T_TRUE': 0x12,
|
|
29
|
+
'RUBY_T_FALSE': 0x13,
|
|
30
|
+
'RUBY_T_SYMBOL': 0x14,
|
|
31
|
+
'RUBY_T_FIXNUM': 0x15,
|
|
32
|
+
'RUBY_T_UNDEF': 0x16,
|
|
33
|
+
'RUBY_T_IMEMO': 0x1a,
|
|
34
|
+
'RUBY_T_NODE': 0x1b,
|
|
35
|
+
'RUBY_T_ICLASS': 0x1c,
|
|
36
|
+
'RUBY_T_ZOMBIE': 0x1d,
|
|
37
|
+
'RUBY_T_MOVED': 0x1e,
|
|
38
|
+
'RUBY_T_MASK': 0x1f,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# Default values for Ruby flag constants (ruby_fl_type enum)
|
|
42
|
+
_FLAG_DEFAULTS = {
|
|
43
|
+
'RUBY_FL_USHIFT': 12,
|
|
44
|
+
'RUBY_FL_USER1': 1 << 13,
|
|
45
|
+
'RUBY_FL_USER2': 1 << 14,
|
|
46
|
+
'RUBY_FL_USER3': 1 << 15,
|
|
47
|
+
'RUBY_FL_USER4': 1 << 16,
|
|
48
|
+
'RUBY_FL_USER5': 1 << 17,
|
|
49
|
+
'RUBY_FL_USER6': 1 << 18,
|
|
50
|
+
'RUBY_FL_USER7': 1 << 19,
|
|
51
|
+
'RUBY_FL_USER8': 1 << 20,
|
|
52
|
+
'RUBY_FL_USER9': 1 << 21,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
def get(name, default=None):
|
|
56
|
+
"""Get a Ruby constant value, with caching.
|
|
57
|
+
|
|
58
|
+
Arguments:
|
|
59
|
+
name: The constant name (e.g., 'RUBY_T_STRING', 'RUBY_FL_USER1')
|
|
60
|
+
default: Default value if constant cannot be found
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
The integer value of the constant, or default if not found
|
|
64
|
+
|
|
65
|
+
Raises:
|
|
66
|
+
Exception if constant not found and no default provided
|
|
67
|
+
"""
|
|
68
|
+
if name in _CACHE:
|
|
69
|
+
return _CACHE[name]
|
|
70
|
+
|
|
71
|
+
# Try direct evaluation (works in GDB with debug info, LLDB with macros/variables)
|
|
72
|
+
try:
|
|
73
|
+
val = int(debugger.parse_and_eval(name))
|
|
74
|
+
# Only cache if we got a non-zero value or if it's a known zero constant
|
|
75
|
+
if val != 0 or name in ['Qfalse', 'RUBY_Qfalse', 'RUBY_T_NONE']:
|
|
76
|
+
_CACHE[name] = val
|
|
77
|
+
return val
|
|
78
|
+
except Exception:
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
# Couldn't find the constant
|
|
82
|
+
if default is None:
|
|
83
|
+
raise Exception(f"Constant {name} not found and no default provided")
|
|
84
|
+
|
|
85
|
+
# Return default but don't cache (might be available later with a process)
|
|
86
|
+
return default
|
|
87
|
+
|
|
88
|
+
def get_enum(enum_name, member_name, default=None):
|
|
89
|
+
"""Get an enum member value from a specific enum.
|
|
90
|
+
|
|
91
|
+
This is more explicit than get() and works better in LLDB.
|
|
92
|
+
|
|
93
|
+
Arguments:
|
|
94
|
+
enum_name: The enum type name (e.g., 'ruby_value_type')
|
|
95
|
+
member_name: The member name (e.g., 'RUBY_T_STRING')
|
|
96
|
+
default: Default value if enum member cannot be found
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
The integer value of the enum member, or default if not found
|
|
100
|
+
|
|
101
|
+
Raises:
|
|
102
|
+
Exception if member not found and no default provided
|
|
103
|
+
|
|
104
|
+
Examples:
|
|
105
|
+
>>> constants.get_enum('ruby_value_type', 'RUBY_T_STRING', 0x05)
|
|
106
|
+
5
|
|
107
|
+
"""
|
|
108
|
+
cache_key = f"{enum_name}::{member_name}"
|
|
109
|
+
|
|
110
|
+
if cache_key in _CACHE:
|
|
111
|
+
return _CACHE[cache_key]
|
|
112
|
+
|
|
113
|
+
# Use the debugger abstraction (handles GDB vs LLDB differences)
|
|
114
|
+
try:
|
|
115
|
+
val = debugger.get_enum_value(enum_name, member_name)
|
|
116
|
+
_CACHE[cache_key] = val
|
|
117
|
+
return val
|
|
118
|
+
except Exception:
|
|
119
|
+
pass
|
|
120
|
+
|
|
121
|
+
# Couldn't find the enum member
|
|
122
|
+
if default is None:
|
|
123
|
+
raise Exception(f"Enum member {enum_name}::{member_name} not found and no default provided")
|
|
124
|
+
|
|
125
|
+
return default
|
|
126
|
+
|
|
127
|
+
def type_struct(type_name):
|
|
128
|
+
"""Get a C struct/type from the debugger, with caching.
|
|
129
|
+
|
|
130
|
+
Arguments:
|
|
131
|
+
type_name: The type name (e.g., 'struct RString', 'struct RArray', 'VALUE')
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
The debugger type object
|
|
135
|
+
|
|
136
|
+
Raises:
|
|
137
|
+
Exception if type cannot be found
|
|
138
|
+
|
|
139
|
+
Examples:
|
|
140
|
+
>>> rbasic_type = constants.type_struct('struct RBasic')
|
|
141
|
+
>>> value_type = constants.type_struct('VALUE')
|
|
142
|
+
"""
|
|
143
|
+
if type_name in _TYPE_CACHE:
|
|
144
|
+
return _TYPE_CACHE[type_name]
|
|
145
|
+
|
|
146
|
+
dbg_type = debugger.lookup_type(type_name)
|
|
147
|
+
_TYPE_CACHE[type_name] = dbg_type
|
|
148
|
+
return dbg_type
|
|
149
|
+
|
|
150
|
+
def type(name):
|
|
151
|
+
"""Get a Ruby type constant (RUBY_T_*) value.
|
|
152
|
+
|
|
153
|
+
This is a convenience wrapper around get_enum() for ruby_value_type enum.
|
|
154
|
+
Uses built-in defaults for all standard Ruby type constants.
|
|
155
|
+
|
|
156
|
+
Arguments:
|
|
157
|
+
name: The type constant name (e.g., 'RUBY_T_STRING', 'RUBY_T_ARRAY')
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
The integer value of the type constant
|
|
161
|
+
|
|
162
|
+
Raises:
|
|
163
|
+
Exception if constant is not found and not in default table
|
|
164
|
+
|
|
165
|
+
Examples:
|
|
166
|
+
>>> constants.type('RUBY_T_STRING')
|
|
167
|
+
5
|
|
168
|
+
"""
|
|
169
|
+
default = _TYPE_DEFAULTS.get(name)
|
|
170
|
+
return get_enum('ruby_value_type', name, default)
|
|
171
|
+
|
|
172
|
+
def flag(name):
|
|
173
|
+
"""Get a Ruby flag constant (RUBY_FL_*) value.
|
|
174
|
+
|
|
175
|
+
This is a convenience wrapper around get_enum() for ruby_fl_type enum.
|
|
176
|
+
Uses built-in defaults for all standard Ruby flag constants.
|
|
177
|
+
|
|
178
|
+
Arguments:
|
|
179
|
+
name: The flag constant name (e.g., 'RUBY_FL_USER1', 'RUBY_FL_USHIFT')
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
The integer value of the flag constant
|
|
183
|
+
|
|
184
|
+
Raises:
|
|
185
|
+
Exception if constant is not found and not in default table
|
|
186
|
+
|
|
187
|
+
Examples:
|
|
188
|
+
>>> constants.flag('RUBY_FL_USER1')
|
|
189
|
+
8192
|
|
190
|
+
"""
|
|
191
|
+
default = _FLAG_DEFAULTS.get(name)
|
|
192
|
+
return get_enum('ruby_fl_type', name, default)
|
|
193
|
+
|
|
194
|
+
def clear():
|
|
195
|
+
"""Clear the constants and type caches.
|
|
196
|
+
|
|
197
|
+
Useful when switching between different Ruby processes or versions.
|
|
198
|
+
"""
|
|
199
|
+
_CACHE.clear()
|
|
200
|
+
_TYPE_CACHE.clear()
|