From 1d82f23484c64cf49b3c97834867e4bd99479a78 Mon Sep 17 00:00:00 2001 From: Utkarsh Sharma Date: Sat, 1 Jun 2019 03:55:15 -0700 Subject: [PATCH] Adds width attribute Adds width attribute which allows user to specify number of bytes printed per line --- .travis.yml | 2 +- multidiff/Render.py | 74 +++++++++++++++++++----- multidiff/StreamView.py | 5 +- multidiff/command_line_interface.py | 48 ++++++++++++---- test/bin_file1 | 1 + test/bin_file2 | 1 + test/cli_test.py | 59 +++++++++++++++++++ test/width_test.py | 88 +++++++++++++++++++++++++++++ 8 files changed, 248 insertions(+), 30 deletions(-) create mode 100644 test/bin_file1 create mode 100644 test/bin_file2 create mode 100644 test/cli_test.py create mode 100644 test/width_test.py diff --git a/.travis.yml b/.travis.yml index 983ecab..feaa795 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,6 @@ python: - 3.5 - 3.4.2 install: - - pip install git+https://github.com/juhakivekas/multidiff + - pip install . script: - python -m pytest diff --git a/multidiff/Render.py b/multidiff/Render.py index eb8d538..79bdb5c 100644 --- a/multidiff/Render.py +++ b/multidiff/Render.py @@ -1,9 +1,11 @@ from multidiff.Ansi import Ansi import binascii import html +import textwrap +import re class Render(): - def __init__(self, encoder='hexdump', color='ansi'): + def __init__(self, encoder='hexdump', color='ansi', bytes=16, width=None): '''Configure the output encoding and coloring method of this rendering object''' if color == 'ansi': self.highligther = ansi_colored @@ -16,7 +18,10 @@ def __init__(self, encoder='hexdump', color='ansi'): self.encoder = HexEncoder elif encoder == 'utf8': self.encoder = Utf8Encoder - + + self.width = width + self.bytes = bytes + def render(self, model, diff): '''Render the diff in the given model into a UTF-8 String''' result = self.encoder(self.highligther) @@ -24,9 +29,11 @@ def render(self, model, diff): for op in diff.opcodes: data = obj.data[op[3]:op[4]] if type(data) == bytes: - result.append(data, op[0]) + result.append(data, op[0], self.width, self.bytes) elif type(data) == str: - result.append(bytes(data, "utf8"), op[0]) + result.append(bytes(data, "utf8"), op[0], self.width, self.bytes) + if self.bytes != 16: + return result.reformat(result.final(), int(self.bytes)) return result.final() def dumps(self, model): @@ -42,9 +49,12 @@ def __init__(self, highligther): self.highligther = highligther self.output = '' - def append(self, data, color): + def append(self, data, color, width=None, bytes=16): self.output += self.highligther(str(data, 'utf8'), color) - + if width: + if len(self.output) > int(width): + self.output = textwrap.fill(self.output, int(width)) + def final(self): return self.output @@ -53,10 +63,14 @@ class HexEncoder(): def __init__(self, highligther): self.highligther = highligther self.output = '' - def append(self, data, color): + + def append(self, data, color, width=None, bytes=16): data = str(binascii.hexlify(data),'utf8') self.output += self.highligther(data, color) - + if width: + if len(self.output) > int(width): + self.output = textwrap.fill(self.output, int(width)) + def final(self): return self.output @@ -71,21 +85,21 @@ def __init__(self, highligther): self.skipspace = False self.asciirow = '' - def append(self, data, color): + def append(self, data, color, width=None, bytes=16): if len(data) == 0: - self._append(data, color) + self._append(data, color, width) while len(data) > 0: if self.rowlen == 16: self._newrow() - consumed = self._append(data[:16 - self.rowlen], color) + consumed = self._append(data[:16 - self.rowlen], color, width) data = data[consumed:] - def _append(self, data, color): + def _append(self, data, color, width): if len(data) == 0: #in the case of highlightig a deletion in a target or an #addition in the source, print a highlighted space and mark #it skippanble for the next append - hexs = ' ' + hexs = ' ' self.skipspace = True else: self._add_hex_space() @@ -101,7 +115,10 @@ def _append(self, data, color): asciis += '.' self.asciirow += self.highligther(asciis, color) - self.hexrow += self.highligther(hexs, color) + self.hexrow += self.highligther(hexs, color) + if width: + if len(self.hexrow) > int(width): + self.hexrow = textwrap.fill(self.hexrow, int(width)) self.rowlen += len(data) return len(data) @@ -121,7 +138,7 @@ def _add_hex_space(self): self.skipspace = False else: self.hexrow += ' ' - + def final(self): self.hexrow += 3*(16 - self.rowlen) * ' ' @@ -129,6 +146,33 @@ def final(self): self._newrow() return self.body + def reformat(self, body, n=16): + instring = '' + foo = body.split('\n') + for line in foo: + line.rstrip() + line = line[line.find(':')+1:line.find('|')] + instring += line + instring += '\n' + outstring = '' + # Remove line numbers and newlines. + clean_string = instring.replace(r'\d+:|\n', '') + # Split on spaces that are not in tags. + elements = re.split(r'\s+(?![^<]+>)', clean_string) + # Omit first tag so that everything else can be chunked by n. + clean_elements = elements[1:] + # Chunk by n. + chunks = [' '.join(clean_elements[i:i+n]) + for i in range(0, len(clean_elements), n)] + # Concatenate the chunks as a line in outstring, with a line number. + for i, chunk in enumerate(chunks): + line = '{:06x}'.format(i*16) + if i == 0: + outstring += '{}:{} {}\n'.format(line, elements[0], chunk) + else: + outstring += '{}:{}\n'.format(line, chunk) + return outstring + def ansi_colored(string, op): if op == 'equal': return string diff --git a/multidiff/StreamView.py b/multidiff/StreamView.py index a196878..6912b26 100644 --- a/multidiff/StreamView.py +++ b/multidiff/StreamView.py @@ -4,11 +4,12 @@ class StreamView(): '''A class for building UIs. Has some pretty serious side effects. Use Render instead if you're not making a long-running UI''' - def __init__(self, model, encoding='hexdump', mode='sequence', color='ansi'): + def __init__(self, model, encoding='hexdump', mode='sequence', color='ansi', bytes=16, width=None): self.color = color - self.render = Render(color=color, encoder=encoding) + self.render = Render(color=color, encoder=encoding, bytes=bytes, width=width) self.mode = mode self.model = model + self.bytes = bytes model.add_listener(self) def object_added(self, index): diff --git a/multidiff/command_line_interface.py b/multidiff/command_line_interface.py index 020538a..d4ab680 100755 --- a/multidiff/command_line_interface.py +++ b/multidiff/command_line_interface.py @@ -1,12 +1,21 @@ #!/usr/bin/python3 import argparse from multidiff import MultidiffModel, StreamView, SocketController, FileController, StdinController +import shutil +import sys -def main(): - args = make_parser().parse_args() +def main(args=None): + + if args is None: + args = sys.argv[1:] + args = make_parser().parse_args(args) m = MultidiffModel() - v = StreamView(m, encoding=args.outformat, mode=args.mode, color=args.color) - + + if args.width == 'max': + args.width = get_max_width(args) + + v = StreamView(m, encoding=args.outformat, mode=args.mode, color=args.color, bytes=args.bytes, width=args.width) + if len(args.file) > 0: informat = args.informat if args.informat else 'raw' files = FileController(m, informat) @@ -20,6 +29,11 @@ def main(): server = SocketController(('127.0.0.1', args.port), m, informat) server.serve_forever() +def get_max_width(args): + columns = int(shutil.get_terminal_size((120,30)).columns) + args.width = columns + return args.width + def make_parser(): parser = argparse.ArgumentParser( formatter_class=argparse.RawTextHelpFormatter, @@ -32,17 +46,17 @@ def make_parser(): │ sensor module │ └───────────────┘ """) - + parser.add_argument('file', type=str, nargs='*', help='file or directory to include in multidiff') - + parser.add_argument('-p','--port', dest='port', type=int, help='start a local socket server on the given port') - + parser.add_argument('-s','--stdin', dest='stdin', action='store_true', @@ -52,7 +66,7 @@ def make_parser(): dest='mode', default='sequence', help='mode of operation, either "baseline" or "sequence"') - + parser.add_argument('-i','--informat', dest='informat', help='input data format:\n' + @@ -60,13 +74,13 @@ def make_parser(): ' raw (file and server default)\n' + ' hex\n'+ ' json') - + parser.add_argument('-o','--outformat', dest='outformat', default='hexdump', - help='output data format:\n' + - ' utf8\n' + - ' hex\n' + + help='output data format:\n' + + ' utf8\n' + + ' hex\n' + ' hexdump (default)') parser.add_argument('--html', @@ -75,6 +89,16 @@ def make_parser(): const='html', default='ansi', help='use html for colors instead of ansi codes') + + parser.add_argument('-w', '--width', + dest='width', + default='82', + help='number of bytes printed per line, either an integer or max(width of console)') + + parser.add_argument('-b', '--bytes', + dest='bytes', + default=16, + help='number of hexs printed per line, either an integer or max(width of console)') return parser if __name__ == '__main__': diff --git a/test/bin_file1 b/test/bin_file1 new file mode 100644 index 0000000..7cd027a --- /dev/null +++ b/test/bin_file1 @@ -0,0 +1 @@ +0123456789abcdef012345678 \ No newline at end of file diff --git a/test/bin_file2 b/test/bin_file2 new file mode 100644 index 0000000..454f6b3 --- /dev/null +++ b/test/bin_file2 @@ -0,0 +1 @@ +0123456789abcdef \ No newline at end of file diff --git a/test/cli_test.py b/test/cli_test.py new file mode 100644 index 0000000..f83119c --- /dev/null +++ b/test/cli_test.py @@ -0,0 +1,59 @@ +from multidiff.Render import * +from multidiff import Ansi + +import unittest +from pathlib import Path +import subprocess + + +class MainCLITests(unittest.TestCase): + + def call_run(self, expected_stdout, args): + got_stdout = '(no stdout)' + cmd = ['multidiff'] + args + try: + got_stdout = subprocess.check_output(cmd, universal_newlines=True) + except subprocess.CalledProcessError as err: + print('Got stderr: `{err_message}`'.format(err_message=err)) + finally: + print('Got stdout: `{stdout}`'.format(stdout=got_stdout)) + + self.assertEqual(expected_stdout, got_stdout) + + def test_diff_cli_no_args(self): + expected_output = '' + self.call_run(expected_output, []) + + def test_diff_cli_simple(self): + p = Path(".") + res = list(p.glob("**/bin_file*")) + res = [str(x) for x in res] + + dump = Ansi.bold + res[1] + Ansi.reset + dump += "\n000000: " + dump += "30 31 32 33 34 35 36 37 38 39 61 62 63 64 65 66" + dump += Ansi.delete + " " + Ansi.reset + dump += "|0123456789abcdef|\n" + + expected_output = dump + self.call_run(expected_output, res) + + def test_diff_cli_with_width_flag(self): + p = Path('.') + res = res = list(p.glob('**/bin_file*')) + res = [str(x) for x in res] + res += ['--width', '25'] + + dump = Ansi.bold + res[1] + Ansi.reset + dump += "\n000000: " + dump += "30 31 32 33 34 35 36 37" + dump += "\n38 39 61 62 63 64 65" + dump += "\n66" + dump += Ansi.delete + " " + Ansi.reset + dump += "|0123456789abcdef|\n" + + expected_output = dump + self.call_run(expected_output, res) + +if __name__ == '__main__': + unittest.main() diff --git a/test/width_test.py b/test/width_test.py new file mode 100644 index 0000000..53bc687 --- /dev/null +++ b/test/width_test.py @@ -0,0 +1,88 @@ +from multidiff.Render import * +from multidiff import Ansi +from multidiff.command_line_interface import make_parser, get_max_width + +import shutil +from unittest import TestCase +import argparse + + +class WidthTests(TestCase): + + def setUp(self): + pass + + def test_parser(self): + args = make_parser().parse_args([]) + res = argparse.Namespace( + bytes=16, color='ansi', file=[], informat=None, + mode='sequence', outformat='hexdump', port=None, stdin=False, + width='82' + ) + assert(args == res) + + def test_get_max_width(self): + args = make_parser().parse_args(['--width', 'max']) + args.width = get_max_width(args) + columns = int(shutil.get_terminal_size((120, 30)).columns) + assert(args.width == columns) + + def test_width_default(self): + # width = 82(default) + args = make_parser().parse_args([]) + args.width = get_max_width(args) + hd = HexdumpEncoder(html_colored) + hd.append(bytes("012", "utf8"), "replace", args.width) + hd.append(bytes("3456789ab", "utf8"), "equal", args.width) + hd.append(bytes("", "utf8"), "delete", args.width) + hd.append(bytes("cdef", "utf8"), "equal", args.width) + result = hd.final() + + dump = "000000: " + dump += "30 31 32 " + dump += "33 34 35 36 37 38 39 61 62" + dump += " " + dump += "63 64 65 66" + dump += " |" + dump += "