File: nh.py 1 #!/usr/bin/python3 2 3 # The MIT License (MIT) 4 # 5 # Copyright © 2020-2025 pacman64 6 # 7 # Permission is hereby granted, free of charge, to any person obtaining a copy 8 # of this software and associated documentation files (the “Software”), to deal 9 # in the Software without restriction, including without limitation the rights 10 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 # copies of the Software, and to permit persons to whom the Software is 12 # furnished to do so, subject to the following conditions: 13 # 14 # The above copyright notice and this permission notice shall be included in 15 # all copies or substantial portions of the Software. 16 # 17 # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 # SOFTWARE. 24 25 26 from os import fstat 27 from sys import argv, exit, stderr, stdin, stdout 28 29 30 info = ''' 31 nh [options...] [filepaths/URIs...] 32 33 34 Nice Hexadecimals is a byte-viewer which shows bytes as base-16 values, 35 using various ANSI styles to color-code output. 36 37 Output lines end with a panel showing all ASCII sequences detected along: 38 each such panel also includes all ASCII from the next row as well, since 39 not doing that would make grepping/matching whole strings less reliable, 40 as some matches may be missed simply due to the narrowness of the panel. 41 42 Options, where leading double-dashes are also allowed: 43 44 -h show this help message 45 -help same as -h 46 47 -n narrow output, which fits 80-column mode 48 -narrow same as -n 49 ''' 50 51 52 # bytes2styled_hex has `pre-rendered` strings for each possible byte 53 bytes2styled_hex = ( 54 '\x1b[38;2;135;175;255m00 ', 55 '\x1b[38;2;148;148;148m01 ', 56 '\x1b[38;2;148;148;148m02 ', 57 '\x1b[38;2;148;148;148m03 ', 58 '\x1b[38;2;148;148;148m04 ', 59 '\x1b[38;2;148;148;148m05 ', 60 '\x1b[38;2;148;148;148m06 ', 61 '\x1b[38;2;148;148;148m07 ', 62 '\x1b[38;2;148;148;148m08 ', 63 '\x1b[38;2;6;152;154m09\x1b[38;2;78;78;78m ', 64 '\x1b[38;2;6;152;154m0a\x1b[38;2;78;78;78m ', 65 '\x1b[38;2;148;148;148m0b ', 66 '\x1b[38;2;148;148;148m0c ', 67 '\x1b[38;2;6;152;154m0d\x1b[38;2;78;78;78m ', 68 '\x1b[38;2;148;148;148m0e ', 69 '\x1b[38;2;148;148;148m0f ', 70 '\x1b[38;2;148;148;148m10 ', 71 '\x1b[38;2;148;148;148m11 ', 72 '\x1b[38;2;148;148;148m12 ', 73 '\x1b[38;2;148;148;148m13 ', 74 '\x1b[38;2;148;148;148m14 ', 75 '\x1b[38;2;148;148;148m15 ', 76 '\x1b[38;2;148;148;148m16 ', 77 '\x1b[38;2;148;148;148m17 ', 78 '\x1b[38;2;148;148;148m18 ', 79 '\x1b[38;2;148;148;148m19 ', 80 '\x1b[38;2;148;148;148m1a ', 81 '\x1b[38;2;148;148;148m1b ', 82 '\x1b[38;2;148;148;148m1c ', 83 '\x1b[38;2;148;148;148m1d ', 84 '\x1b[38;2;148;148;148m1e ', 85 '\x1b[38;2;148;148;148m1f ', 86 '\x1b[38;2;6;152;154m20\x1b[38;2;78;78;78m ', 87 '\x1b[38;2;102;175;135m21\x1b[38;2;78;78;78m!', 88 '\x1b[38;2;102;175;135m22\x1b[38;2;78;78;78m"', 89 '\x1b[38;2;102;175;135m23\x1b[38;2;78;78;78m#', 90 '\x1b[38;2;102;175;135m24\x1b[38;2;78;78;78m$', 91 '\x1b[38;2;102;175;135m25\x1b[38;2;78;78;78m%', 92 '\x1b[38;2;102;175;135m26\x1b[38;2;78;78;78m&', 93 '\x1b[38;2;102;175;135m27\x1b[38;2;78;78;78m\'', 94 '\x1b[38;2;102;175;135m28\x1b[38;2;78;78;78m(', 95 '\x1b[38;2;102;175;135m29\x1b[38;2;78;78;78m)', 96 '\x1b[38;2;102;175;135m2a\x1b[38;2;78;78;78m*', 97 '\x1b[38;2;102;175;135m2b\x1b[38;2;78;78;78m+', 98 '\x1b[38;2;102;175;135m2c\x1b[38;2;78;78;78m,', 99 '\x1b[38;2;102;175;135m2d\x1b[38;2;78;78;78m-', 100 '\x1b[38;2;102;175;135m2e\x1b[38;2;78;78;78m.', 101 '\x1b[38;2;102;175;135m2f\x1b[38;2;78;78;78m/', 102 '\x1b[38;2;102;175;135m30\x1b[38;2;78;78;78m0', 103 '\x1b[38;2;102;175;135m31\x1b[38;2;78;78;78m1', 104 '\x1b[38;2;102;175;135m32\x1b[38;2;78;78;78m2', 105 '\x1b[38;2;102;175;135m33\x1b[38;2;78;78;78m3', 106 '\x1b[38;2;102;175;135m34\x1b[38;2;78;78;78m4', 107 '\x1b[38;2;102;175;135m35\x1b[38;2;78;78;78m5', 108 '\x1b[38;2;102;175;135m36\x1b[38;2;78;78;78m6', 109 '\x1b[38;2;102;175;135m37\x1b[38;2;78;78;78m7', 110 '\x1b[38;2;102;175;135m38\x1b[38;2;78;78;78m8', 111 '\x1b[38;2;102;175;135m39\x1b[38;2;78;78;78m9', 112 '\x1b[38;2;102;175;135m3a\x1b[38;2;78;78;78m:', 113 '\x1b[38;2;102;175;135m3b\x1b[38;2;78;78;78m;', 114 '\x1b[38;2;102;175;135m3c\x1b[38;2;78;78;78m<', 115 '\x1b[38;2;102;175;135m3d\x1b[38;2;78;78;78m=', 116 '\x1b[38;2;102;175;135m3e\x1b[38;2;78;78;78m>', 117 '\x1b[38;2;102;175;135m3f\x1b[38;2;78;78;78m?', 118 '\x1b[38;2;102;175;135m40\x1b[38;2;78;78;78m@', 119 '\x1b[38;2;102;175;135m41\x1b[38;2;78;78;78mA', 120 '\x1b[38;2;102;175;135m42\x1b[38;2;78;78;78mB', 121 '\x1b[38;2;102;175;135m43\x1b[38;2;78;78;78mC', 122 '\x1b[38;2;102;175;135m44\x1b[38;2;78;78;78mD', 123 '\x1b[38;2;102;175;135m45\x1b[38;2;78;78;78mE', 124 '\x1b[38;2;102;175;135m46\x1b[38;2;78;78;78mF', 125 '\x1b[38;2;102;175;135m47\x1b[38;2;78;78;78mG', 126 '\x1b[38;2;102;175;135m48\x1b[38;2;78;78;78mH', 127 '\x1b[38;2;102;175;135m49\x1b[38;2;78;78;78mI', 128 '\x1b[38;2;102;175;135m4a\x1b[38;2;78;78;78mJ', 129 '\x1b[38;2;102;175;135m4b\x1b[38;2;78;78;78mK', 130 '\x1b[38;2;102;175;135m4c\x1b[38;2;78;78;78mL', 131 '\x1b[38;2;102;175;135m4d\x1b[38;2;78;78;78mM', 132 '\x1b[38;2;102;175;135m4e\x1b[38;2;78;78;78mN', 133 '\x1b[38;2;102;175;135m4f\x1b[38;2;78;78;78mO', 134 '\x1b[38;2;102;175;135m50\x1b[38;2;78;78;78mP', 135 '\x1b[38;2;102;175;135m51\x1b[38;2;78;78;78mQ', 136 '\x1b[38;2;102;175;135m52\x1b[38;2;78;78;78mR', 137 '\x1b[38;2;102;175;135m53\x1b[38;2;78;78;78mS', 138 '\x1b[38;2;102;175;135m54\x1b[38;2;78;78;78mT', 139 '\x1b[38;2;102;175;135m55\x1b[38;2;78;78;78mU', 140 '\x1b[38;2;102;175;135m56\x1b[38;2;78;78;78mV', 141 '\x1b[38;2;102;175;135m57\x1b[38;2;78;78;78mW', 142 '\x1b[38;2;102;175;135m58\x1b[38;2;78;78;78mX', 143 '\x1b[38;2;102;175;135m59\x1b[38;2;78;78;78mY', 144 '\x1b[38;2;102;175;135m5a\x1b[38;2;78;78;78mZ', 145 '\x1b[38;2;102;175;135m5b\x1b[38;2;78;78;78m[', 146 '\x1b[38;2;102;175;135m5c\x1b[38;2;78;78;78m\\', 147 '\x1b[38;2;102;175;135m5d\x1b[38;2;78;78;78m]', 148 '\x1b[38;2;102;175;135m5e\x1b[38;2;78;78;78m^', 149 '\x1b[38;2;102;175;135m5f\x1b[38;2;78;78;78m_', 150 '\x1b[38;2;102;175;135m60\x1b[38;2;78;78;78m`', 151 '\x1b[38;2;102;175;135m61\x1b[38;2;78;78;78ma', 152 '\x1b[38;2;102;175;135m62\x1b[38;2;78;78;78mb', 153 '\x1b[38;2;102;175;135m63\x1b[38;2;78;78;78mc', 154 '\x1b[38;2;102;175;135m64\x1b[38;2;78;78;78md', 155 '\x1b[38;2;102;175;135m65\x1b[38;2;78;78;78me', 156 '\x1b[38;2;102;175;135m66\x1b[38;2;78;78;78mf', 157 '\x1b[38;2;102;175;135m67\x1b[38;2;78;78;78mg', 158 '\x1b[38;2;102;175;135m68\x1b[38;2;78;78;78mh', 159 '\x1b[38;2;102;175;135m69\x1b[38;2;78;78;78mi', 160 '\x1b[38;2;102;175;135m6a\x1b[38;2;78;78;78mj', 161 '\x1b[38;2;102;175;135m6b\x1b[38;2;78;78;78mk', 162 '\x1b[38;2;102;175;135m6c\x1b[38;2;78;78;78ml', 163 '\x1b[38;2;102;175;135m6d\x1b[38;2;78;78;78mm', 164 '\x1b[38;2;102;175;135m6e\x1b[38;2;78;78;78mn', 165 '\x1b[38;2;102;175;135m6f\x1b[38;2;78;78;78mo', 166 '\x1b[38;2;102;175;135m70\x1b[38;2;78;78;78mp', 167 '\x1b[38;2;102;175;135m71\x1b[38;2;78;78;78mq', 168 '\x1b[38;2;102;175;135m72\x1b[38;2;78;78;78mr', 169 '\x1b[38;2;102;175;135m73\x1b[38;2;78;78;78ms', 170 '\x1b[38;2;102;175;135m74\x1b[38;2;78;78;78mt', 171 '\x1b[38;2;102;175;135m75\x1b[38;2;78;78;78mu', 172 '\x1b[38;2;102;175;135m76\x1b[38;2;78;78;78mv', 173 '\x1b[38;2;102;175;135m77\x1b[38;2;78;78;78mw', 174 '\x1b[38;2;102;175;135m78\x1b[38;2;78;78;78mx', 175 '\x1b[38;2;102;175;135m79\x1b[38;2;78;78;78my', 176 '\x1b[38;2;102;175;135m7a\x1b[38;2;78;78;78mz', 177 '\x1b[38;2;102;175;135m7b\x1b[38;2;78;78;78m{', 178 '\x1b[38;2;102;175;135m7c\x1b[38;2;78;78;78m|', 179 '\x1b[38;2;102;175;135m7d\x1b[38;2;78;78;78m}', 180 '\x1b[38;2;102;175;135m7e\x1b[38;2;78;78;78m~', 181 '\x1b[38;2;148;148;148m7f ', 182 '\x1b[38;2;148;148;148m80 ', 183 '\x1b[38;2;148;148;148m81 ', 184 '\x1b[38;2;148;148;148m82 ', 185 '\x1b[38;2;148;148;148m83 ', 186 '\x1b[38;2;148;148;148m84 ', 187 '\x1b[38;2;148;148;148m85 ', 188 '\x1b[38;2;148;148;148m86 ', 189 '\x1b[38;2;148;148;148m87 ', 190 '\x1b[38;2;148;148;148m88 ', 191 '\x1b[38;2;148;148;148m89 ', 192 '\x1b[38;2;148;148;148m8a ', 193 '\x1b[38;2;148;148;148m8b ', 194 '\x1b[38;2;148;148;148m8c ', 195 '\x1b[38;2;148;148;148m8d ', 196 '\x1b[38;2;148;148;148m8e ', 197 '\x1b[38;2;148;148;148m8f ', 198 '\x1b[38;2;148;148;148m90 ', 199 '\x1b[38;2;148;148;148m91 ', 200 '\x1b[38;2;148;148;148m92 ', 201 '\x1b[38;2;148;148;148m93 ', 202 '\x1b[38;2;148;148;148m94 ', 203 '\x1b[38;2;148;148;148m95 ', 204 '\x1b[38;2;148;148;148m96 ', 205 '\x1b[38;2;148;148;148m97 ', 206 '\x1b[38;2;148;148;148m98 ', 207 '\x1b[38;2;148;148;148m99 ', 208 '\x1b[38;2;148;148;148m9a ', 209 '\x1b[38;2;148;148;148m9b ', 210 '\x1b[38;2;148;148;148m9c ', 211 '\x1b[38;2;148;148;148m9d ', 212 '\x1b[38;2;148;148;148m9e ', 213 '\x1b[38;2;148;148;148m9f ', 214 '\x1b[38;2;148;148;148ma0 ', 215 '\x1b[38;2;148;148;148ma1 ', 216 '\x1b[38;2;148;148;148ma2 ', 217 '\x1b[38;2;148;148;148ma3 ', 218 '\x1b[38;2;148;148;148ma4 ', 219 '\x1b[38;2;148;148;148ma5 ', 220 '\x1b[38;2;148;148;148ma6 ', 221 '\x1b[38;2;148;148;148ma7 ', 222 '\x1b[38;2;148;148;148ma8 ', 223 '\x1b[38;2;148;148;148ma9 ', 224 '\x1b[38;2;148;148;148maa ', 225 '\x1b[38;2;148;148;148mab ', 226 '\x1b[38;2;148;148;148mac ', 227 '\x1b[38;2;148;148;148mad ', 228 '\x1b[38;2;148;148;148mae ', 229 '\x1b[38;2;148;148;148maf ', 230 '\x1b[38;2;148;148;148mb0 ', 231 '\x1b[38;2;148;148;148mb1 ', 232 '\x1b[38;2;148;148;148mb2 ', 233 '\x1b[38;2;148;148;148mb3 ', 234 '\x1b[38;2;148;148;148mb4 ', 235 '\x1b[38;2;148;148;148mb5 ', 236 '\x1b[38;2;148;148;148mb6 ', 237 '\x1b[38;2;148;148;148mb7 ', 238 '\x1b[38;2;148;148;148mb8 ', 239 '\x1b[38;2;148;148;148mb9 ', 240 '\x1b[38;2;148;148;148mba ', 241 '\x1b[38;2;148;148;148mbb ', 242 '\x1b[38;2;148;148;148mbc ', 243 '\x1b[38;2;148;148;148mbd ', 244 '\x1b[38;2;148;148;148mbe ', 245 '\x1b[38;2;148;148;148mbf ', 246 '\x1b[38;2;148;148;148mc0 ', 247 '\x1b[38;2;148;148;148mc1 ', 248 '\x1b[38;2;148;148;148mc2 ', 249 '\x1b[38;2;148;148;148mc3 ', 250 '\x1b[38;2;148;148;148mc4 ', 251 '\x1b[38;2;148;148;148mc5 ', 252 '\x1b[38;2;148;148;148mc6 ', 253 '\x1b[38;2;148;148;148mc7 ', 254 '\x1b[38;2;148;148;148mc8 ', 255 '\x1b[38;2;148;148;148mc9 ', 256 '\x1b[38;2;148;148;148mca ', 257 '\x1b[38;2;148;148;148mcb ', 258 '\x1b[38;2;148;148;148mcc ', 259 '\x1b[38;2;148;148;148mcd ', 260 '\x1b[38;2;148;148;148mce ', 261 '\x1b[38;2;148;148;148mcf ', 262 '\x1b[38;2;148;148;148md0 ', 263 '\x1b[38;2;148;148;148md1 ', 264 '\x1b[38;2;148;148;148md2 ', 265 '\x1b[38;2;148;148;148md3 ', 266 '\x1b[38;2;148;148;148md4 ', 267 '\x1b[38;2;148;148;148md5 ', 268 '\x1b[38;2;148;148;148md6 ', 269 '\x1b[38;2;148;148;148md7 ', 270 '\x1b[38;2;148;148;148md8 ', 271 '\x1b[38;2;148;148;148md9 ', 272 '\x1b[38;2;148;148;148mda ', 273 '\x1b[38;2;148;148;148mdb ', 274 '\x1b[38;2;148;148;148mdc ', 275 '\x1b[38;2;148;148;148mdd ', 276 '\x1b[38;2;148;148;148mde ', 277 '\x1b[38;2;148;148;148mdf ', 278 '\x1b[38;2;148;148;148me0 ', 279 '\x1b[38;2;148;148;148me1 ', 280 '\x1b[38;2;148;148;148me2 ', 281 '\x1b[38;2;148;148;148me3 ', 282 '\x1b[38;2;148;148;148me4 ', 283 '\x1b[38;2;148;148;148me5 ', 284 '\x1b[38;2;148;148;148me6 ', 285 '\x1b[38;2;148;148;148me7 ', 286 '\x1b[38;2;148;148;148me8 ', 287 '\x1b[38;2;148;148;148me9 ', 288 '\x1b[38;2;148;148;148mea ', 289 '\x1b[38;2;148;148;148meb ', 290 '\x1b[38;2;148;148;148mec ', 291 '\x1b[38;2;148;148;148med ', 292 '\x1b[38;2;148;148;148mee ', 293 '\x1b[38;2;148;148;148mef ', 294 '\x1b[38;2;148;148;148mf0 ', 295 '\x1b[38;2;148;148;148mf1 ', 296 '\x1b[38;2;148;148;148mf2 ', 297 '\x1b[38;2;148;148;148mf3 ', 298 '\x1b[38;2;148;148;148mf4 ', 299 '\x1b[38;2;148;148;148mf5 ', 300 '\x1b[38;2;148;148;148mf6 ', 301 '\x1b[38;2;148;148;148mf7 ', 302 '\x1b[38;2;148;148;148mf8 ', 303 '\x1b[38;2;148;148;148mf9 ', 304 '\x1b[38;2;148;148;148mfa ', 305 '\x1b[38;2;148;148;148mfb ', 306 '\x1b[38;2;148;148;148mfc ', 307 '\x1b[38;2;148;148;148mfd ', 308 '\x1b[38;2;148;148;148mfe ', 309 '\x1b[38;5;209mff ', 310 ) 311 312 # int2ascii slightly speeds up func show_ascii 313 int2ascii = tuple(chr(i) if 32 <= i < 127 else ' ' for i in range(256)) 314 315 # visible noticeably speeds up func show_ascii; notice how spaces (code 32) 316 # aren't considered visible symbols, which makes sense in func show_ascii 317 visible = tuple(32 < i < 127 for i in range(256)) 318 319 320 def show_hex(w, src, chunk_size: int = 16) -> None: 321 'Handle all input from the source given, emitting styled output.' 322 323 # make the ruler/line-breather, which shows up every 5 hex-output lines 324 pre = 8 * ' ' 325 pat = ' ·' 326 pat = int(3 * chunk_size / len(pat)) * pat 327 sep_line = f'{pre} \x1b[38;5;245m{pat}\x1b[0m\n' 328 329 # n is the current byte offset shown at the start of each display line 330 n = 0 331 332 # lines keeps track of the main output line/row count, to figure out 333 # when to put `breather` lines 334 lines = 0 335 336 # prev remembers the previous chunk, as showing ASCII content for 337 # 2 output-lines worth of bytes requires staying 1 step behind, so 338 # to speak 339 prev = src.read(chunk_size) 340 if len(prev) == 0: 341 return 342 343 while True: 344 chunk = src.read(chunk_size) 345 if len(chunk) == 0: 346 break 347 348 if lines % 5 == 0 and lines > 0: 349 w.write(sep_line) 350 show_line(w, n, prev, chunk, chunk_size) 351 352 n += len(prev) 353 prev = chunk 354 lines += 1 355 356 # don't forget the last output line 357 if len(prev) > 0: 358 if lines % 5 == 0 and lines > 0: 359 w.write(sep_line) 360 show_line(w, n, prev, bytes(), chunk_size) 361 362 363 def show_line(w, n: int, prev, chunk, chunk_size: int) -> None: 364 'Help func show_hex do its job, simplifying its control flow.' 365 366 # w.write(f'{n:8} \x1b[48;5;254m') 367 show_restyled_uint(w, n, 8) 368 w.write(' \x1b[48;5;254m') 369 for e in prev: 370 w.write(bytes2styled_hex[e]) 371 w.write('\x1b[0m') 372 show_ascii(w, prev, chunk, 3 * (chunk_size - len(prev)) + 2) 373 w.write('\n') 374 375 376 def show_restyled_uint(w, n: int, width: int) -> None: 377 'Alternate styles on 3-item chunks of digits from the integer given.' 378 379 digits = str(n) 380 l = len(digits) 381 382 # left-pad digits with spaces to fill the output-width given 383 write_spaces(w, width - l) 384 385 # it's quicker to just emit short-enough digit-runs verbatim 386 if l < 4: 387 w.write(digits) 388 return 389 390 # emit leading chunk of digits, which is the only one which 391 # can have fewer than 3 items 392 lead = l % 3 393 w.write(digits[:lead]) 394 395 # the rest of the string now has a multiple of 3 items left 396 start = lead 397 398 # start by styling the next digit-group only if there was a 399 # non-empty leading group at the start of the full digit-run 400 use_style = lead > 0 401 402 # alternate styles until the string is over 403 while start < l: 404 # the digits left are always a multiple of 3 405 stop = start + 3 406 407 if use_style: 408 w.write('\x1b[38;5;248m') 409 w.write(digits[start:stop]) 410 w.write('\x1b[0m') 411 else: 412 w.write(digits[start:stop]) 413 414 # switch style and advance to the next 3-digit chunk 415 use_style = not use_style 416 start = stop 417 418 419 def show_ascii(w, first, second: bytes, pre: int) -> None: 420 'Emit the ASCII side-panel for func show_hex.' 421 422 # prev_vis keeps track of the previous byte's `visibility`, so spaces 423 # are added when bytes change from non-visible-ASCII to visible-ASCII 424 prev_vis = False 425 426 is_vis = False 427 spaces = pre 428 429 # show ASCII symbols from the first `line` in the pair 430 for e in first: 431 is_vis = visible[e] 432 if is_vis: 433 if not prev_vis: 434 write_spaces(w, spaces) 435 spaces = 1 436 w.write(int2ascii[e]) 437 prev_vis = is_vis 438 439 # do the same for the second `line` in the pair 440 for e in second: 441 is_vis = visible[e] 442 if is_vis: 443 if not prev_vis: 444 write_spaces(w, spaces) 445 spaces = 1 446 w.write(int2ascii[e]) 447 prev_vis = is_vis 448 449 450 def write_spaces(w, n: int) -> None: 451 'Emit the number of spaces given, minimizing `write` calls.' 452 453 if n < 1: 454 return 455 456 if n < len(spaces): 457 w.write(spaces[n]) 458 return 459 460 while n >= len(spaces): 461 w.write(spaces[-1]) 462 n -= len(spaces) 463 w.write(spaces[n]) 464 465 466 def seems_url(s: str) -> bool: 467 protocols = ('https://', 'http://', 'file://', 'ftp://', 'data:') 468 return any(s.startswith(p) for p in protocols) 469 470 471 # args is the `proper` list of arguments given to the script 472 args = argv[1:] 473 474 # a leading help-option arg means show the help message and quit 475 if len(args) > 0 and args[0] in ('-h', '--h', '-help', '--help'): 476 print(info.strip(), file=stderr) 477 exit(0) 478 479 # narrow-output is to fit results in 80-column mode 480 bytes_per_line = 16 481 if len(args) > 0 and args[0] in ('-n', '--n', '-narrow', '--narrow'): 482 bytes_per_line = 12 483 args = args[1:] 484 elif len(args) > 0: 485 # allow a leading integer argument to set exactly how many bytes per 486 # line to show in the styled output, before the ASCII-panel contents 487 try: 488 # try to parse an integer number, after turning double-dashes 489 # into single ones, which may lead to parsed negative integers 490 n = int(args[0].lstrip('-')) 491 # negative integers are a result of option-style leading dashes 492 n = int(abs(n)) 493 494 if n > 0: 495 # only change the width-setting if leading number isn't zero 496 bytes_per_line = n 497 # don't treat a leading integer as a filepath, no matter what 498 args = args[1:] 499 except Exception: 500 # avoid exceptions if leading arg isn't a valid integer 501 pass 502 503 # spaces lets func write_spaces minimize `write` operations, resulting in 504 # noticeable speed-ups when the script deals with megabytes of data 505 spaces = tuple(i * ' ' for i in range(3 * bytes_per_line + 4)) 506 507 try: 508 if args.count('-') > 1: 509 msg = 'reading from `-` (standard input) more than once not allowed' 510 raise ValueError(msg) 511 512 if any(seems_url(e) for e in args): 513 from urllib.request import urlopen 514 515 for i, path in enumerate(args): 516 if i > 0: 517 stdout.write('\n') 518 stdout.write('\n') 519 520 if path == '-': 521 stdout.write('• - (<stdin>)\n') 522 stdout.write('\n') 523 show_hex(stdout, stdin.buffer, bytes_per_line) 524 continue 525 526 if seems_url(path): 527 with urlopen(path) as inp: 528 stdout.write(f'• {path}\n') 529 stdout.write('\n') 530 show_hex(stdout, inp, bytes_per_line) 531 continue 532 533 with open(path, mode='rb', buffering=4_960) as inp: 534 n = fstat(inp.fileno()).st_size 535 stdout.write(f'• {path} \x1b[38;5;245m({n:,} bytes)\x1b[0m\n') 536 stdout.write('\n') 537 show_hex(stdout, inp, bytes_per_line) 538 539 if len(args) == 0: 540 stdout.write('• <stdin>\n') 541 stdout.write('\n') 542 show_hex(stdout, stdin.buffer, bytes_per_line) 543 except BrokenPipeError: 544 # quit quietly, instead of showing a confusing error message 545 stderr.close() 546 except KeyboardInterrupt: 547 exit(2) 548 except Exception as e: 549 print(f'\x1b[31m{e}\x1b[0m', file=stderr) 550 exit(1)