├── .gitignore
├── .github
└── screenshot.png
├── TODO
├── package.json
├── README.md
├── test.py
├── .vscode
└── launch.json
├── vt100.py
├── commands.py
└── red.py
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 |
--------------------------------------------------------------------------------
/.github/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reasonml/red/HEAD/.github/screenshot.png
--------------------------------------------------------------------------------
/TODO:
--------------------------------------------------------------------------------
1 | [ ] Breakpoints don't activate before module is loaded
2 | [x] Modules list command
3 | [x] Usage example when no arguments is given
4 | [ ] Check the binary had debug information
5 | [ ] Breakpoints: guess the module by substring, print similar if match fails
6 | [ ] Save and restore Breakpoints
7 | [ ] Toggle breakpoint at given line
8 | [ ] Highlight breakpoint added/removed confirmation
9 | [ ] Explore history depth settings
10 | [ ] Print-and-continue breakpoint
11 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "reason-red",
3 | "version": "0.0.0",
4 | "description": "Reason Debugger",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/frantic/red.git"
12 | },
13 | "keywords": [
14 | "reason",
15 | "ocaml",
16 | "debugger",
17 | "ocamldebug"
18 | ],
19 | "author": "Frantic",
20 | "license": "ISC",
21 | "bugs": {
22 | "url": "https://github.com/frantic/red/issues"
23 | },
24 | "homepage": "https://github.com/frantic/red#readme"
25 | }
26 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # RED
2 |
3 | Better UX for OCamlDebug. Works with Ocaml and [Reason](https://reasonml.github.io/) code.
4 |
5 |
6 |
7 | Watch Video
8 |
9 | ### Features:
10 |
11 | 1. Zero config, just prepend your command with `red`
12 | 2. Time traveling (can step back in time)
13 | 3. Printing arbitrary values and structs
14 | 4. Adding and removing breakpoints
15 | 5. More to come. See TODO file
16 |
17 |
18 |
19 | ### Usage:
20 |
21 | 1. Make sure your build target is `byte-code`, not `native`:
22 |
23 | ocamlbuild myapp.d.byte
24 |
25 | 2. Clone the repo and run RED:
26 |
27 | git clone https://github.com/frantic/red.git
28 | ./red/red.py /path/to/myapp.d.byte
29 |
30 | 3. Press `?` to see available options
31 |
--------------------------------------------------------------------------------
/test.py:
--------------------------------------------------------------------------------
1 | import re, arth
2 | import unittest
3 |
4 | class TestStringMethods(unittest.TestCase):
5 |
6 | def test_parse_normal(self):
7 | src = '\tOCaml Debugger version 4.02.3\n\n'
8 | self.assertEqual(arth.parse_output(src), (src, {}))
9 |
10 | def test_parse_time(self):
11 | src = 'Time: 53 - pc: 186180 - module Format\n'
12 | self.assertEqual(arth.parse_output(src), ('', {'time': '53', 'pc': '186180', 'module': 'Format'}))
13 |
14 | src = 'Loading program... Time: 1 - pc: 7344 - module Pervasives\n'
15 | self.assertEqual(arth.parse_output(src), ('Loading program... \n', {'time': '1', 'pc': '7344', 'module': 'Pervasives'}))
16 |
17 | src = 'Time: 101978\nProgram exit.\n'
18 | self.assertEqual(arth.parse_output(src), ('Program exit.\n', {'time': '101978', 'pc': None, 'module': None}))
19 |
20 | def test_parse_location(self):
21 | src = '\032\032M/Users/frantic/.opam/4.02.3/lib/ocaml/camlinternalFormat.ml:64903:65347:before\n'
22 | self.assertEqual(arth.parse_output(src), ('', {'file': '/Users/frantic/.opam/4.02.3/lib/ocaml/camlinternalFormat.ml',
23 | 'start': '64903', 'end': '65347', 'before_or_after': 'before'}))
24 |
25 | src = '\032\032H\n'
26 | self.assertEqual(arth.parse_output(src), ('', {'file': None, 'start': None, 'end': None, 'before_or_after': None}))
27 |
28 | def test_parse_breakpoints(self):
29 | src = 'No breakpoints.\n'
30 | self.assertEqual(arth.parse_breakpoints(src), [])
31 |
32 | src = '\n'.join([
33 | 'Num Address Where',
34 | ' 1 1646532 file src/flow.ml, line 62, characters 5-1272',
35 | ' 2 38632 file sys.ml, line 28, characters 31-41',
36 | '',
37 | ])
38 | self.assertEqual(arth.parse_breakpoints(src), [{'num': 1, 'pc': 1646532, 'file': 'src/flow.ml', 'line': 62},
39 | {'num': 2, 'pc': 38632, 'file': 'sys.ml', 'line': 28}])
40 |
41 |
42 | if __name__ == '__main__':
43 | unittest.main()
44 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "name": "Python",
6 | "type": "python",
7 | "request": "launch",
8 | "stopOnEntry": true,
9 | "pythonPath": "${config.python.pythonPath}",
10 | "program": "${file}",
11 | "debugOptions": [
12 | "WaitOnAbnormalExit",
13 | "WaitOnNormalExit",
14 | "RedirectOutput"
15 | ]
16 | },
17 | {
18 | "name": "Integrated Terminal/Console",
19 | "type": "python",
20 | "request": "launch",
21 | "stopOnEntry": true,
22 | "pythonPath": "${config.python.pythonPath}",
23 | "program": "${file}",
24 | "console": "integratedTerminal",
25 | "debugOptions": [
26 | "WaitOnAbnormalExit",
27 | "WaitOnNormalExit"
28 | ]
29 | },
30 | {
31 | "name": "External Terminal/Console",
32 | "type": "python",
33 | "request": "launch",
34 | "stopOnEntry": true,
35 | "pythonPath": "${config.python.pythonPath}",
36 | "program": "${file}",
37 | "console": "externalTerminal",
38 | "debugOptions": [
39 | "WaitOnAbnormalExit",
40 | "WaitOnNormalExit"
41 | ]
42 | },
43 | {
44 | "name": "Django",
45 | "type": "python",
46 | "request": "launch",
47 | "stopOnEntry": true,
48 | "pythonPath": "${config.python.pythonPath}",
49 | "program": "${workspaceRoot}/manage.py",
50 | "args": [
51 | "runserver",
52 | "--noreload"
53 | ],
54 | "debugOptions": [
55 | "WaitOnAbnormalExit",
56 | "WaitOnNormalExit",
57 | "RedirectOutput",
58 | "DjangoDebugging"
59 | ]
60 | },
61 | {
62 | "name": "Flask",
63 | "type": "python",
64 | "request": "launch",
65 | "stopOnEntry": true,
66 | "pythonPath": "${config.python.pythonPath}",
67 | "program": "${workspaceRoot}/run.py",
68 | "args": [],
69 | "debugOptions": [
70 | "WaitOnAbnormalExit",
71 | "WaitOnNormalExit",
72 | "RedirectOutput"
73 | ]
74 | },
75 | {
76 | "name": "Watson",
77 | "type": "python",
78 | "request": "launch",
79 | "stopOnEntry": true,
80 | "pythonPath": "${config.python.pythonPath}",
81 | "program": "${workspaceRoot}/console.py",
82 | "args": [
83 | "dev",
84 | "runserver",
85 | "--noreload=True"
86 | ],
87 | "debugOptions": [
88 | "WaitOnAbnormalExit",
89 | "WaitOnNormalExit",
90 | "RedirectOutput"
91 | ]
92 | },
93 | {
94 | "name": "Attach (Remote Debug)",
95 | "type": "python",
96 | "request": "attach",
97 | "localRoot": "${workspaceRoot}",
98 | "remoteRoot": "${workspaceRoot}",
99 | "port": 3000,
100 | "secret": "my_secret",
101 | "host": "localhost"
102 | }
103 | ]
104 | }
--------------------------------------------------------------------------------
/vt100.py:
--------------------------------------------------------------------------------
1 | import subprocess, sys, re
2 |
3 | # See https://github.com/chalk/ansi-styles
4 |
5 | def reset(text): return '\033[0m' + text + '\033[0m'
6 | def bold(text): return '\033[1m' + text + '\033[22m'
7 | def dim(text): return '\033[2m' + text + '\033[22m'
8 | def italic(text): return '\033[3m' + text + '\033[23m'
9 | def underline(text): return '\033[4m' + text + '\033[24m'
10 | def inverse(text): return '\033[7m' + text + '\033[27m'
11 | def hidden(text): return '\033[8m' + text + '\033[28m'
12 | def strikethrough(text): return '\033[9m' + text + '\033[29m'
13 | def black(text): return '\033[30m' + text + '\033[39m'
14 | def red(text): return '\033[31m' + text + '\033[39m'
15 | def green(text): return '\033[32m' + text + '\033[39m'
16 | def yellow(text): return '\033[33m' + text + '\033[39m'
17 | def blue(text): return '\033[34m' + text + '\033[39m'
18 | def magenta(text): return '\033[35m' + text + '\033[39m'
19 | def cyan(text): return '\033[36m' + text + '\033[39m'
20 | def white(text): return '\033[37m' + text + '\033[39m'
21 | def gray(text): return '\033[90m' + text + '\033[39m'
22 | def grey(text): return '\033[90m' + text + '\033[39m'
23 | def black_bg(text): return '\033[40m' + text + '\033[49m'
24 | def red_bg(text): return '\033[41m' + text + '\033[49m'
25 | def green_bg(text): return '\033[42m' + text + '\033[49m'
26 | def yellow_bg(text): return '\033[43m' + text + '\033[49m'
27 | def blue_bg(text): return '\033[44m' + text + '\033[49m'
28 | def magenta_bg(text): return '\033[45m' + text + '\033[49m'
29 | def cyan_bg(text): return '\033[46m' + text + '\033[49m'
30 | def white_bg(text): return '\033[47m' + text + '\033[49m'
31 |
32 | def tag_to_color(match):
33 | open_tag = match.group(1)
34 | text = match.group(2)
35 |
36 | color_fn = globals()[open_tag]
37 | return color_fn(from_tags_unsafe(text))
38 |
39 |
40 | def from_tags_unsafe(text):
41 | """
42 | Replaces "color" tags in text with actual colors. Example:
43 |
44 | Hello
45 |
46 | Don't call this function on user-provided input, it uses dark runtime
47 | magic to lookup color functions
48 | """
49 | return re.sub(r'<([_\w]+)>(.*)<\/\1>', tag_to_color, text)
50 |
51 | class Console:
52 | def __init__(self):
53 | self.out = sys.stdout
54 | self.lines_printed = 0
55 |
56 | def clear_last_render(self):
57 | for i in range(self.lines_printed):
58 | self.out.write('\033[1A\033[2K')
59 | self.lines_printed = 0
60 |
61 | def enable_line_wrap(self):
62 | self.out.write('\033[?7h')
63 |
64 | def disable_line_wrap(self):
65 | self.out.write('\033[?7l')
66 |
67 | def print_text(self, text):
68 | for line in text.splitlines():
69 | self.out.write(line + '\n')
70 | self.lines_printed += 1
71 |
72 | def safe_input(self, prompt=None):
73 | self.lines_printed += 1
74 | try:
75 | return raw_input(prompt)
76 | except:
77 | pass
78 |
79 | def push_state():
80 | sys.stdout.write('\0337')
81 |
82 | def pop_state():
83 | sys.stdout.write('\0338')
84 | sys.stdout.flush()
85 |
86 | def clear_to_eos():
87 | sys.stdout.write('\033[J')
88 | sys.stdout.flush()
89 |
90 | class _Getch:
91 | """
92 | Gets a single character from standard input.
93 | Does not echo to the screen.
94 | """
95 | def __init__(self):
96 | try:
97 | self.impl = _GetchWindows()
98 | except ImportError:
99 | self.impl = _GetchUnix()
100 |
101 | def __call__(self): return self.impl()
102 |
103 |
104 | class _GetchUnix:
105 | def __init__(self):
106 | import tty, sys
107 |
108 | def __call__(self):
109 | import sys, tty, termios
110 | fd = sys.stdin.fileno()
111 | old_settings = termios.tcgetattr(fd)
112 | try:
113 | tty.setraw(sys.stdin.fileno())
114 | ch = sys.stdin.read(1)
115 | finally:
116 | termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
117 | return ch
118 |
119 |
120 | class _GetchWindows:
121 | def __init__(self):
122 | import msvcrt
123 |
124 | def __call__(self):
125 | import msvcrt
126 | return msvcrt.getch()
127 |
128 |
129 | getch = _Getch()
130 |
131 |
--------------------------------------------------------------------------------
/commands.py:
--------------------------------------------------------------------------------
1 | import re
2 | import sys
3 | import vt100
4 | import inspect
5 | import functools
6 | import textwrap
7 |
8 | class Command:
9 | pass
10 |
11 |
12 | class Shortcut(Command):
13 | def run(self, execute, prompt, ctx):
14 | return execute(self.COMMAND)
15 |
16 |
17 | class Run(Shortcut):
18 | KEYS = ['>', 'r']
19 | HELP = 'Run the program forward'
20 | COMMAND = 'run'
21 |
22 |
23 | class Reverse(Shortcut):
24 | KEYS = ['<', 'R']
25 | HELP = 'Run the program backward'
26 | COMMAND = 'reverse'
27 |
28 |
29 | class Next(Shortcut):
30 | KEYS = ['j']
31 | HELP = 'Next line'
32 | COMMAND = 'next'
33 |
34 |
35 | class Prev(Shortcut):
36 | KEYS = ['k']
37 | HELP = 'Previous line'
38 | COMMAND = 'prev'
39 |
40 |
41 | class Step(Shortcut):
42 | KEYS = [']', 's']
43 | HELP = 'Step forward'
44 | COMMAND = 'step'
45 |
46 |
47 | class Backstep(Shortcut):
48 | KEYS = ['[', 'S']
49 | HELP = 'Step backward'
50 | COMMAND = 'backstep'
51 |
52 |
53 | class Yes(Shortcut):
54 | KEYS = ['y']
55 | HELP = 'Answer "y" to the question'
56 | COMMAND = 'y'
57 | HIDDEN = True
58 |
59 |
60 | class No(Shortcut):
61 | KEYS = ['n']
62 | HELP = 'Answer "n" to the question'
63 | COMMAND = 'n'
64 | HIDDEN = True
65 |
66 |
67 | class Timetravel(Shortcut):
68 | KEYS = ['t', 'g']
69 | HELP = 'Travel to specified time'
70 |
71 | def run(self, execute, prompt, ctx):
72 | banner = vt100.from_tags_unsafe(textwrap.dedent("""
73 | TIME TRAVEL
74 |
75 | 1337 - jump to specified time
76 | +100 - jump 100 time units forward
77 | -100 - jump 100 time units backwards
78 |
79 | (time travel) """))
80 |
81 | time = prompt(banner)
82 | if not len(time):
83 | return
84 |
85 | now = int(ctx['loc'].get('time') or 0)
86 | if time.startswith('-'):
87 | location = now - int(time[1:])
88 | elif time.startswith('+'):
89 | location = now + int(time[1:])
90 | elif time.isdigit():
91 | location = int(time)
92 | else:
93 | return
94 |
95 | return execute('goto ' + str(location))
96 |
97 |
98 | class Modules(Command):
99 | KEYS = ['m']
100 | HELP = 'Show available modules'
101 |
102 | std_modules = re.compile('(' + '\\b|\\b'.join([
103 | "Arg", "Arg_helper", "Arith_status", "Array", "ArrayLabels", "Ast_helper", "Ast_invariants",
104 | "Ast_iterator", "Ast_mapper", "Asttypes", "Attr_helper", "Big_int", "Bigarray", "Buffer",
105 | "Builtin_attributes", "Bytes", "BytesLabels", "Callback", "CamlinternalFormat",
106 | "CamlinternalFormatBasics", "CamlinternalLazy", "CamlinternalMod", "CamlinternalOO", "Ccomp",
107 | "Char", "Clflags", "Complex", "Condition", "Config", "Consistbl", "Depend", "Digest",
108 | "Docstrings", "Dynlink", "Ephemeron", "Event", "Filename", "Format", "Gc", "Genlex", "Graphics",
109 | "GraphicsX11", "Hashtbl", "Identifiable", "Int32", "Int64", "Lazy", "Lexer", "Lexing", "List",
110 | "ListLabels", "Location", "Longident", "Map", "Marshal", "Misc", "MoreLabels", "Mutex",
111 | "Nativeint", "Num", "Numbers", "Obj", "Oo", "Parse", "Parser", "Parsetree", "Parsing",
112 | "Pervasives", "Pprintast", "Printast", "Printexc", "Printf", "Queue", "Random", "Ratio",
113 | "Scanf", "Set", "Sort", "Spacetime", "Stack", "Std_exit", "StdLabels", "Str", "Stream", "String",
114 | "StringLabels", "Strongly_connected_components", "Syntaxerr", "Sys", "Tbl", "Terminfo", "Thread",
115 | "ThreadUnix", "Timings", "Uchar", "Unix", "UnixLabels", "Warnings", "Weak"
116 | ]) + ')')
117 |
118 | def run(self, execute, prompt, ctx):
119 | return re.sub(Modules.std_modules, vt100.dim('\\1'), execute('info modules'))
120 |
121 |
122 | class Print(Command):
123 | KEYS = ['p']
124 | HELP = 'Print variable value'
125 |
126 | def run(self, execute, prompt, ctx):
127 | var = prompt(vt100.magenta(vt100.dim('(print) ')))
128 | if not len(var):
129 | return
130 | output = execute('print ' + var)
131 | return vt100.magenta(output)
132 |
133 |
134 | class Quit(Shortcut):
135 | KEYS = ['q']
136 | HELP = 'Quit'
137 | COMMAND = 'quit'
138 |
139 |
140 | class Breakpoint(Command):
141 | KEYS = ['b']
142 | HELP = 'Add, remove and list breakpoints'
143 |
144 | def format_breakpoints(self, breakpoints):
145 | if len(breakpoints) == 0:
146 | return vt100.dim(' No breakpoins added yet')
147 |
148 | lines = []
149 | for b in breakpoints:
150 | lines.append(vt100.red(('#' + str(b.get('num'))).rjust(5)) + ' ' + (b.get('file') + ':' + str(b.get('line'))).ljust(30)
151 | + vt100.dim(' pc = ' + str(b.get('pc'))))
152 |
153 | return '\n'.join(lines)
154 |
155 | def command(self, prompt, ctx):
156 | loc = ctx['loc']
157 | breakpoints = ctx['breakpoints']
158 |
159 | banner = vt100.from_tags_unsafe(textwrap.dedent("""
160 | BREAKPOINTS
161 |
162 | {0}
163 |
164 | 42 - add breakpoint for current module ({1}) at line 42
165 | Module:42 - add breakpoint for specified module Module at line 42
166 | Module.foo - add breakpoint for Module.foo function
167 | -#2 - remove breakoint #2
168 | - do nothing
169 |
170 |
171 | (break) """)).format(self.format_breakpoints(breakpoints), loc.get('module'))
172 |
173 | command = prompt(banner)
174 |
175 | if command:
176 | if command.startswith('-#'):
177 | return 'delete ' + command[2:]
178 | elif command.isdigit():
179 | if loc.get('module'):
180 | return 'break @ ' + loc.get('module') + ' ' + command
181 | else:
182 | if ':' in command:
183 | return 'break @ ' + command.replace(':', ' ')
184 | else:
185 | return 'break ' + command
186 |
187 | def run(self, execute, prompt, ctx):
188 | command = self.command(prompt, ctx)
189 | # TODO: check if the breakpoint was really added
190 | # TODO: breakpoints don't work at the beginning/end of the program
191 | if command:
192 | return execute(command)
193 |
194 |
195 | class Custom(Command):
196 | KEYS = [':', ';']
197 | HELP = 'Custom OcamlDebug command'
198 |
199 | def run(self, execute, prompt, ctx):
200 | command = prompt(vt100.dim('(odb) '))
201 | if not len(command):
202 | return
203 | output = execute(command)
204 | return vt100.blue('>> {0}\n'.format(command)) + output
205 |
206 |
207 | def all_command_classes():
208 | # Suck hack, wow
209 | with open(__file__.replace('.pyc', '.py'), 'r') as f:
210 | content = f.read()
211 |
212 | class_pos = lambda pair: content.find('class ' + pair[0])
213 | cmds = sorted(inspect.getmembers(sys.modules[__name__], inspect.isclass), key=class_pos)
214 | return [cls for (_, cls) in cmds if hasattr(cls, 'HELP')]
215 |
--------------------------------------------------------------------------------
/red.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | from collections import namedtuple
4 | import commands
5 | import re
6 | import os
7 | import sys
8 | import subprocess
9 | import vt100
10 | import readline
11 | import atexit
12 | import textwrap
13 | import inspect
14 |
15 |
16 | debugger_log = open('/tmp/red.log', 'w')
17 | def trace(text):
18 | debugger_log.write(text)
19 | debugger_log.flush()
20 |
21 |
22 | def read_until(stream, terminators):
23 | chunk = ''
24 | while not stream.closed:
25 | byte = stream.read(1)
26 | if not byte:
27 | return chunk
28 |
29 | trace(byte)
30 |
31 | chunk += byte
32 | for terminator in terminators:
33 | if chunk.endswith(terminator):
34 | return chunk[:-len(terminator)]
35 |
36 |
37 | # Time: 53 - pc: 186180 - module Format
38 | TIME_RE = re.compile('^(.*)Time: (\d+)( - pc: (\d+) - module (.+))?\n', re.MULTILINE)
39 | # \032\032M/Users/frantic/.opam/4.02.3/lib/ocaml/camlinternalFormat.ml:64903:65347:before
40 | LOCATION_RE = re.compile('^\x1a\x1a(H|M(.+):(.+):(.+):(before|after))\n', re.MULTILINE)
41 |
42 | def parse_output(output):
43 | context = dict()
44 | def parse_time(match):
45 | context['time'] = match.group(2)
46 | context['pc'] = match.group(4)
47 | context['module'] = match.group(5)
48 | prefix = match.group(1)
49 | if prefix:
50 | return match.group(1) + '\n'
51 | return ''
52 |
53 | def parse_location(match):
54 | context['file'] = match.group(2)
55 | context['start'] = match.group(3)
56 | context['end'] = match.group(4)
57 | context['before_or_after'] = match.group(5)
58 |
59 | output = re.sub(TIME_RE, parse_time, output)
60 | output = re.sub(LOCATION_RE, parse_location, output)
61 | return output, context
62 |
63 |
64 | # 1 1646532 file src/flow.ml, line 62, characters 5-1272
65 | BREAKPOINT_RE = re.compile("^\s*(\d+)\s+(\d+)\s+file (\S+), line (\d+)", re.MULTILINE)
66 |
67 | def parse_breakpoints(output):
68 | breakpoints = []
69 | for match in BREAKPOINT_RE.finditer(output):
70 | breakpoints.append({'num': int(match.group(1)), 'pc': int(match.group(2)),
71 | 'file': match.group(3), 'line': int(match.group(4))})
72 |
73 | return breakpoints
74 |
75 | def breakpoint_lines_for_file(breakpoints, file_name):
76 | if not file_name:
77 | return []
78 |
79 | lines = []
80 | for b in breakpoints:
81 | if file_name.endswith(b.get('file')):
82 | lines.append(b.get('line'))
83 |
84 | return lines
85 |
86 |
87 | def debugger_command(dbgr, cmd):
88 | if cmd != '':
89 | trace(cmd + '\n')
90 | dbgr.stdin.write(cmd + '\n')
91 | dbgr.stdin.flush()
92 |
93 | output = read_until(dbgr.stdout, ['(ocd) ', '(y or n) '])
94 | return parse_output(output)
95 |
96 |
97 | # 950 let ppf = pp_make_formatter output
98 | LINE_RE = re.compile('(\d+) ?(.*)')
99 |
100 | def hl(src, breakpoint_lines):
101 | lines = []
102 | for line in src.split('\n'):
103 | match = LINE_RE.match(line)
104 | if not match:
105 | if line:
106 | lines.append(line + '\n')
107 | continue
108 |
109 | line_number = match.group(1)
110 | text = match.group(2)
111 | has_breakpoint = int(line_number) in breakpoint_lines
112 | is_current = '<|a|>' in text or '<|b|>' in text
113 | a_ptrn = re.compile("(\S?)<\|a\|>")
114 | b_ptrn = re.compile("<\|b\|>(\S?)")
115 |
116 | text = re.sub(a_ptrn, vt100.bold('\\1'), text)
117 | text = re.sub(b_ptrn, vt100.bold('\\1'), text)
118 |
119 | symbol = u'\u2022' if has_breakpoint else ' '
120 |
121 | # Can't use red twice, the closing color tag will mess the outputs
122 | if not is_current:
123 | symbol = vt100.red(symbol)
124 |
125 | result = ' ' + symbol + ' ' + vt100.dim(line_number.rjust(3)) + ' ' + text.ljust(80)
126 |
127 | if is_current:
128 | if has_breakpoint:
129 | result = vt100.red(result)
130 | result = vt100.inverse(result)
131 |
132 | lines.append(result + '\n')
133 |
134 | return ''.join(lines)
135 |
136 |
137 | def format_breakpoints(breakpoints):
138 | if len(breakpoints) == 0:
139 | return vt100.dim(' No breakpoins added yet')
140 |
141 | lines = []
142 | for b in breakpoints:
143 | lines.append(vt100.red(('#' + str(b.get('num'))).rjust(5)) + ' ' + (b.get('file') + ':' + str(b.get('line'))).ljust(30)
144 | + vt100.dim(' pc = ' + str(b.get('pc'))))
145 |
146 | return '\n'.join(lines)
147 |
148 |
149 | def main(args):
150 | histfile = os.path.join(os.path.expanduser("~"), ".red_history")
151 | try:
152 | readline.read_history_file(histfile)
153 | readline.set_history_length(1000)
154 | except IOError:
155 | pass
156 | atexit.register(readline.write_history_file, histfile)
157 | del histfile
158 |
159 |
160 | command_line = []
161 | breakpoints = []
162 | for arg in args:
163 | if arg.startswith('@'):
164 | breakpoints.append(arg[1:])
165 | else:
166 | command_line.append(arg)
167 |
168 | if len(command_line) == 0:
169 | print('Usage: red /path/to/your/target.byte')
170 | return 1
171 |
172 | dbgr = subprocess.Popen(['ocamldebug', '-emacs'] + command_line,
173 | stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
174 |
175 | print(debugger_command(dbgr, '')[0].replace('\tOCaml Debugger version ', vt100.red(u'\u2022 RED') + ' OCamlDebug v'))
176 | print(vt100.dim('Press ? for help'))
177 | print(debugger_command(dbgr, 'start')[0])
178 |
179 | auto_run = True
180 | for bp in breakpoints:
181 | if bp == '':
182 | auto_run = False
183 | else:
184 | print(debugger_command(dbgr, 'break @ ' + bp.replace(':', ' '))[0])
185 |
186 | console = vt100.Console()
187 | return repl(dbgr, console, auto_run)
188 |
189 |
190 | def repl(dbgr, console, auto_run):
191 | command_classes = commands.all_command_classes()
192 |
193 | loc = dict()
194 | breakpoints = list()
195 |
196 | def execute(cmd):
197 | output, context = debugger_command(dbgr, cmd)
198 | loc.update(context)
199 | return output
200 |
201 | def prompt(text):
202 | lines = text.split('\n')
203 | if len(lines) > 1:
204 | console.print_text('\n'.join(lines[:-1]))
205 | return console.safe_input(lines[-1])
206 |
207 | if auto_run:
208 | print(execute('run'))
209 |
210 | op = ''
211 | show_help = False
212 | while dbgr.poll() is None:
213 | breakpoints = parse_breakpoints(execute('info break'))
214 | console.disable_line_wrap()
215 | file_name = loc.get('file')
216 | listing = hl(execute('list'), breakpoint_lines_for_file(breakpoints, file_name))
217 | if listing:
218 | console.print_text((u'\u2500[ %s ]' % loc.get('file')) + u'\u2500' * 300)
219 | console.print_text(listing)
220 | console.print_text(u'\u2500' * 300)
221 | else:
222 | module = loc.get('module')
223 | if module:
224 | console.print_text(vt100.dim('(no source info for {0})'.format(module)))
225 | else:
226 | console.print_text(vt100.dim('(no source info)'))
227 | console.print_text(vt100.blue(vt100.bold('Time: {0} PC: {1}'.format(loc.get('time'), loc.get('pc')))))
228 | console.enable_line_wrap()
229 |
230 | if show_help:
231 | console.print_text(help(command_classes))
232 |
233 | op = vt100.getch()
234 | show_help = op == '?'
235 | cmd_cls = find_command_for_key(command_classes, op)
236 | if not cmd_cls:
237 | console.clear_last_render()
238 | continue
239 |
240 | console.print_text(vt100.bold(op) + ' ' + vt100.dim(cmd_cls.HELP))
241 | output = cmd_cls().run(execute, prompt, {'breakpoints': breakpoints, 'loc': loc})
242 | console.clear_last_render()
243 | if output and len(output):
244 | print(output)
245 |
246 | def help(command_classes):
247 | return '\n'.join([
248 | '{0}\t{1}'.format(vt100.bold(cls.KEYS[0]), cls.HELP)
249 | for cls in command_classes
250 | if not hasattr(command_classes, 'HIDDEN')])
251 |
252 | def find_command_for_key(command_classes, key):
253 | for cls in command_classes:
254 | if key in cls.KEYS:
255 | return cls
256 |
257 |
258 | if __name__ == '__main__':
259 | sys.exit(main(sys.argv[1:]))
260 |
261 |
--------------------------------------------------------------------------------