#!/usr/bin/env python3 # # Copyright (c) 2021 Paul Serice # # This software is provided 'as-is', without any express or implied # warranty. In no event will the author or authors be held liable for # any damages arising from the use of this software. # # Permission is granted to anyone to use this software for any purpose, # including commercial applications, and to alter it and redistribute it # freely, subject to the following restrictions: # # 1. The origin of this software must not be misrepresented; you # must not claim that you wrote the original software. If you use # this software in a product, an acknowledgment in the product # documentation would be appreciated but is not required. # # 2. Altered source versions must be plainly marked as such and # must not be misrepresented as being the original software. # # 3. This notice may not be removed or altered from any source # distribution. # """Print files as hexadecimal. """ import argparse import os import sys __version__ = "1.0.0" # Set of printable characters. PRINTABLE = "0123456789" + \ "abcdefghijklmnopqrstuvwxyz" + \ "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + \ "!\"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ " def hex_dump_file(src_file, dst_file, width): """Print the source file as hexadecimal to the destination file. Both files should be open in binary mode. The width determines the number of bytes to print per line. :param src_file: source file :type fname: file :param dst_file: destination file :type dst_file: file :param width: number of bytes to print per line :type width: int """ total_count = 0 line_count = 0 # Arrays that will be joined to create the left and right parts of # the output. left = [] right = [] while True: # Get the next block. block = src_file.read(16384) if not block: break # Iterate over each byte in the block. for i in range(len(block)): # Insert the hyphen that demarks the middle of both the # left and right sides. if (width > 5) and (line_count == width // 2): left += "- " right += " - " # Update the left side with the hexadecimal value of the # byte. left += "%02x " % (block[i],) # Determine the character to print on the right side. If # the character is not printable use a "." instead. ch = chr(block[i]) if ch not in PRINTABLE: ch = "." # Update the right side. right += ch # Increment line_count but not total_count. The # total_count will be incremented only after the current # line is printed. line_count += 1 # Print this line for the hex dump. if line_count >= width: print("%08x: %s%s" % (total_count, ''.join(left), ''.join(right))) total_count += line_count line_count = 0 left.clear() right.clear() # Print the remainder if any. if left: # Because this last line does not have display as many # bytes as the other lines, we have to add extra padding # between the left and right parts. pad = " " * (3 * (width + 1) - len(''.join(left)) - 1) print("%08x: %s%s%s" % (total_count, ''.join(left), pad, ''.join(right))) def hex_dump_fname(src_fname, dst_file, width): """Print the file as hexadecimal to the destination file which should be open in binary mode. The width determines the number of bytes to print per line. :param src_fname: source file name :type fname: str :param dst_file: destination file :type dst_file: file :param width: number of bytes to print per line :type width: int """ with open(src_fname, "rb") as src_file: hex_dump_file(src_file, dst_file, width) def main(): # Create a new ArgrumentParser instance that shows the default # values for options that have them as part of the help message. parser = argparse.ArgumentParser( formatter_class=argparse.ArgumentDefaultsHelpFormatter, description="Print files as hexadecimal.") # Number of bytes to print per line. parser.add_argument("-w", "--width", type=int, required=False, default=16, dest="width", help="number of bytes to print per line") # Print the version. This has to be handled with a special # action; otherwise, the user also would have to provide all the # required options above just to print out the version. parser.add_argument("-v", "--version", action="version", version="%s %s" % (os.path.basename(sys.argv[0]), __version__), help="version infomation") # Everything else on the command-line is a file name. parser.add_argument(dest="fnames", metavar="file", nargs="*") # Parse command-line arguments. args = parser.parse_args() # Sanity check. if args.width <= 0: raise ValueError(f"Invalid width: {args.width}") # Portably set up references to the binary standard I/O streams. if sys.version_info[0] >= 3: myin = sys.stdin.buffer myout = sys.stdout.buffer myerr = sys.stderr.buffer else: myin = sys.stdin myout = sys.stdout myerr = sys.stderr # Iterate over the file names passed in on the command line. if not args.fnames: hex_dump_file(myin, myout, args.width) else: for fname in args.fnames: hex_dump_fname(fname, myout, args.width) if __name__ == "__main__": main()