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 += "0123456789abcdef|"
+ assert(result == dump)
+
+ def test_width(self):
+ # width=25
+ args = make_parser().parse_args(['--width', '25'])
+ hd = HexdumpEncoder(ansi_colored)
+ hd.append(bytes("foobar", "utf8"), "insert", args.width)
+ result = hd.final()
+
+ dump = '000000: '
+ dump += Ansi.insert + '66'
+ dump += '\n6f 6f 62 61 72' + Ansi.reset
+ dump += ' |'
+ dump += Ansi.insert + 'foobar' + Ansi.reset
+ dump += ' |'
+ assert(result == dump)
+
+ def test_width_max(self):
+ # width=max
+ 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 += "0123456789abcdef|"
+ assert(result == dump)
+
+if __name__ == '__main__':
+ unittest.main()