├── .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 | --------------------------------------------------------------------------------