Checksumcolor

From Rosetta Code
Checksumcolor is a draft programming task. It is not yet considered ready to be promoted as a complete task, for reasons that should be found in its talk page.

Context: In December 2013 a patch was proposed to the coreutils list to add a --color option to the commands md5sum and shaXsum to display the checksums in color to make it easier to visually identify similarities in a list of printed checksums. The patch was not accepted for inclusion and instead it was suggested to create a command line utility which can be used to pipe the output of the md5sum and shaXsum commands similar to the utility colordiff.

Task: The task is to create this command line utility that we can use to pipe the output of the md5sum and shaXsum commands and that colors the checksum part of the output. Take each group of 3 or 6 hexadecimal characters and interpret it as if it was a color code and print it with the closest console color. Print with colors if the output is the terminal or print the input unchanged if the output of the utility is a pipe.

Example:

$ md5sum coreutils-* | checksumcolor
ab20d840e13adfebf2b6936a2dab071b  coreutils-8.29.tar.gz
b259b2936bb46009be3f5cc06a12c32d  coreutils-8.30.tar.gz
03cf26420de566c306d340df52f6ccd7  coreutils-8.31.tar.gz

BASIC

QBasic

Works with: QBasic
Works with: QuickBasic version 4.5
DECLARE SUB colourhex (s$)

CLS
colourhex ("#0123456789ABCDEF")
END

SUB colourhex (s$)
    FOR i = 1 TO LEN(s$)
        ch$ = MID$(s$, i, 1)
        k = INSTR("123456789ABCDEF", ch$)
        COLOR 15 - k, k
        PRINT ch$;
    NEXT
    COLOR 15, 0    'text_color, background_color
END SUB

FreeBASIC

Translation of: Phix
Sub colourhex(s As String)
    For i As Integer = 1 To Len(s)
        Dim As String*1 ch = Mid(s,i,1)
        Dim As Integer k = Instr("123456789ABCDEF", Ucase(ch))
        Color 15-k, k
        Print ch;
    Next
    Color Rgb(255,255,255), Rgb(0,0,0)
End Sub

colourhex("#0123456789ABCDEF")
Sleep

Go

Translation of: OCaml
package main

import (
    "bufio"
    "fmt"
    "golang.org/x/crypto/ssh/terminal"
    "log"
    "os"
    "regexp"
    "strconv"
)

type Color struct{ r, g, b int }

type ColorEx struct {
    color Color
    code  string
}

var colors = []ColorEx{
    {Color{15, 0, 0}, "31"},
    {Color{0, 15, 0}, "32"},
    {Color{15, 15, 0}, "33"},
    {Color{0, 0, 15}, "34"},
    {Color{15, 0, 15}, "35"},
    {Color{0, 15, 15}, "36"},
}

func squareDist(c1, c2 Color) int {
    xd := c2.r - c1.r
    yd := c2.g - c1.g
    zd := c2.b - c1.b
    return xd*xd + yd*yd + zd*zd
}

func printColor(s string) {
    n := len(s)
    k := 0
    for i := 0; i < n/3; i++ {
        j := i * 3
        c1 := s[j]
        c2 := s[j+1]
        c3 := s[j+2]
        k = j + 3
        r, err := strconv.ParseInt(fmt.Sprintf("0x%c", c1), 0, 64)
        check(err)
        g, err := strconv.ParseInt(fmt.Sprintf("0x%c", c2), 0, 64)
        check(err)
        b, err := strconv.ParseInt(fmt.Sprintf("0x%c", c3), 0, 64)
        check(err)
        rgb := Color{int(r), int(g), int(b)}
        m := 676
        colorCode := ""
        for _, cex := range colors {
            sqd := squareDist(cex.color, rgb)
            if sqd < m {
                colorCode = cex.code
                m = sqd
            }
        }
        fmt.Printf("\033[%s;1m%c%c%c\033[00m", colorCode, c1, c2, c3)
    }
    for j := k; j < n; j++ {
        c := s[j]
        fmt.Printf("\033[0;1m%c\033[00m", c)
    }
}

var (
    r       = regexp.MustCompile("^([A-Fa-f0-9]+)([ \t]+.+)$")
    scanner = bufio.NewScanner(os.Stdin)
    err     error
)

func colorChecksum() {
    for scanner.Scan() {
        line := scanner.Text()
        if r.MatchString(line) {
            submatches := r.FindStringSubmatch(line)
            s1 := submatches[1]
            s2 := submatches[2]
            printColor(s1)
            fmt.Println(s2)
        } else {
            fmt.Println(line)
        }
    }
    check(scanner.Err())
}

func cat() {
    for scanner.Scan() {
        line := scanner.Text()
        fmt.Println(line)
    }
    check(scanner.Err())
}

func check(err error) {
    if err != nil {
        log.Fatal(err)
    }
}

func main() {
    if terminal.IsTerminal(int(os.Stdout.Fd())) {
        colorChecksum()
    } else {
        cat()
    }
}
Output:
Same as OCaml entry.

Julia

Usage: md5sum *.* | julia thisfile.jl

while(occursin(r"^[\d\w]{32}", (begin s = readline() end)))
    (crc, restofline) = split(s, " ", limit=2)
    for i in 1:3:length(crc)-3
        print("\e[38;2", join([";$(16 * parse(Int, string(c), base=16))" 
            for c in crc[i:i+2]], ""), "m", crc[i:i+2])
    end
    println("\e[0m", crc[end-1:end], " ", restofline)
end
Output:

Same as the OCaml entry.

Nim

Translation of: Go

Using the functions provided by the terminal module which avoids manipulating ANSI codes.

Usage: md5sum *.* | ./colorchecksum

import re, terminal

const Colors = [((15,  0,  0), fgRed),
                (( 0, 15,  0), fgGreen),
                ((15, 15,  0), fgYellow),
                (( 0,  0, 15), fgBlue),
                ((15,  0, 15), fgMagenta),
                (( 0, 15, 15), fgCyan)]

let Re = re"^([A-Fa-f0-9]+)([ \t]+.+)$"

type RGB = tuple[r, g, b: int]

func squareDist(c1, c2: RGB): int =
  let xd = c1.r - c2.r
  let yd = c1.g - c2.g
  let zd = c1.b - c2.b
  result = xd * xd + yd * yd + zd * zd


func intValue(c: char): int =
  case c
  of 'a'..'f': ord(c) - ord('a') + 10
  of 'A'..'F': ord(c) - ord('A') + 10
  of '0'..'9': ord(c) - ord('0')
  else: raise newException(ValueError, "incorrect input")


proc printColor(s: string) =
  var k = 0
  for i in 0..<(s.len div 3):
    let j = i * 3
    let c1 = s[j]
    let c2 = s[j+1]
    let c3 = s[j+2]
    k = j + 3
    let rgb: RGB = (c1.intValue(), c2.intValue(), c3.intValue())
    var m = 676
    var color = fgDefault
    for cex in Colors:
      let sqd = squareDist(cex[0], rgb)
      if sqd < m:
        color = cex[1]
        m = sqd
    stdout.setForegroundColor(color)
    stdout.write c1, c2, c3

  setForegroundColor(fgDefault)
  for j in k..s.high: stdout.write s[j]


proc colorChecksum() =
  for line in stdin.lines:
    var s: array[2, string]
    if line.match(Re, s):
      printColor(s[0])
      echo s[1]
    else:
      echo line

proc cat() =
  for line in stdin.lines: echo line

if stdout.isatty: colorChecksum()
else: cat()
Output:

Same output as that of OCaml program.

OCaml

#load "unix.cma"
#load "str.cma"

let colors = [|
  ((15,  0,  0), "31");
  (( 0, 15,  0), "32");
  ((15, 15,  0), "33");
  (( 0,  0, 15), "34");
  ((15,  0, 15), "35");
  (( 0, 15, 15), "36");
|]

let square_dist (r1, g1, b1) (r2, g2, b2) =
  let xd = r2 - r1 in
  let yd = g2 - g1 in
  let zd = b2 - b1 in
  (xd * xd + yd * yd + zd * zd)

let print_color s =
  let n = String.length s in
  let k = ref 0 in
  for i = 0 to pred (n / 3) do
    let j = i * 3 in
    let c1 = s.[j]
    and c2 = s.[j+1]
    and c3 = s.[j+2] in
    k := j+3;
    let rgb =
      int_of_string (Printf.sprintf "0x%c" c1),
      int_of_string (Printf.sprintf "0x%c" c2),
      int_of_string (Printf.sprintf "0x%c" c3)
    in
    let m = ref 676 in
    let color_code = ref "" in
    Array.iter (fun (color, code) ->
      let sqd = square_dist color rgb in
      if sqd < !m then begin
        color_code := code;
        m := sqd;
      end
    ) colors;
    Printf.printf "\027[%s;1m%c%c%c\027[00m" !color_code c1 c2 c3;
  done;
  for j = !k to pred n do
    let c = s.[j] in
    Printf.printf "\027[0;1m%c\027[00m" c;
  done

let r = Str.regexp "^\\([A-Fa-f0-9]+\\)\\([ \t]+.+\\)$"

let color_checksum () =
  try while true do
    let line = input_line stdin in
    if Str.string_match r line 0
    then begin
      let s1 = Str.matched_group 1 line in
      let s2 = Str.matched_group 2 line in
      print_color s1;
      print_endline s2;
    end
    else print_endline line
  done with End_of_file -> ()

let cat () =
  try while true do
    let line = input_line stdin in
    print_endline line
  done with End_of_file -> ()

let () =
  if Unix.isatty Unix.stdout
  then color_checksum ()
  else cat ()
Output:
$ md5sum coreutils-* | ocaml checksumcolor.ml
ab20d840e13adfebf2b6936a2dab071b  coreutils-8.29.tar.gz
b259b2936bb46009be3f5cc06a12c32d  coreutils-8.30.tar.gz
03cf26420de566c306d340df52f6ccd7  coreutils-8.31.tar.gz

Perl

Translation of: Sidef
use strict;
use warnings;
use Term::ANSIColor qw<colored :constants256>;

while (<>) {
    my($cs,$fn) = /(^\S+)\s+(.*)/;
    print colored($_, 'ansi' . hex $_) for $cs =~ /(..)/g;
    print " $fn\n";
}
Output:

b2b3c0f48115985e0c8a406883d7fac5 ref/test/not-in-kansas.txt
57e969dd6797cb698ab44468dbf2b7a4 ref/test/reverse_words.txt
9031cf0ac7ffa96cd5b04aa80543a268 ref/test/sample.txt

Phix

Library: Phix/online

Since text_color() accepts 0..15 we may as well just do it digit-by-digit, but avoid (eg) black-on-black by using the inverse background colour as well.
You can run this online here.

with javascript_semantics
procedure colourhex(string s)
    for i=1 to length(s) do
        integer ch = s[i],
                k = find(upper(ch),"123456789ABCDEF")
        text_color(15-k)
        bk_color(k)
        puts(1,ch)
    end for
    text_color(BRIGHT_WHITE)
    bk_color(BLACK)
end procedure
colourhex("123456789ABCDEF\n")
Output:

Varies between windows and linux, but something a bit like this (which is a mock-up), or better yet just click on that link above:

123456789ABCDEF

PicoLisp

(in NIL
   (until (eof)
      (do 16
         (let C (pack (char) (char))
            (prin "^[[38;5;" (hex C) "m" C "^[[0m") ) )
      (prinl (line T)) ) )

Python

#!/usr/bin/env python
"""Colorize MD5 or SHA checksums read from stdin or files.

Tested with Python 2.7 and 3.8.
"""

from __future__ import unicode_literals

import argparse
import fileinput
import os
import sys

from functools import partial
from itertools import count
from itertools import takewhile


ANSI_RESET = "\u001b[0m"

RED = (255, 0, 0)
GREEN = (0, 255, 0)
YELLOW = (255, 255, 0)
BLUE = (0, 0, 255)
MAGENTA = (255, 0, 255)
CYAN = (0, 255, 255)

ANSI_PALETTE = {
    RED: "\u001b[31m",
    GREEN: "\u001b[32m",
    YELLOW: "\u001b[33m",
    BLUE: "\u001b[34m",
    MAGENTA: "\u001b[35m",
    CYAN: "\u001b[36m",
}

# Some alternative, 8-bit colors. This is just one row from the table at
# https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit
_8BIT_PALETTE = {
    (0xAF, 0x00, 0x00): "\u001b[38;5;124m",
    (0xAF, 0x00, 0x5F): "\u001b[38;5;125m",
    (0xAF, 0x00, 0x87): "\u001b[38;5;126m",
    (0xAF, 0x00, 0xAF): "\u001b[38;5;127m",
    (0xAF, 0x00, 0xD7): "\u001b[38;5;128m",
    (0xAF, 0x00, 0xFF): "\u001b[38;5;129m",
    (0xAF, 0x5F, 0x00): "\u001b[38;5;130m",
    (0xAF, 0x5F, 0x5F): "\u001b[38;5;131m",
    (0xAF, 0x5F, 0x87): "\u001b[38;5;132m",
    (0xAF, 0x5F, 0xAF): "\u001b[38;5;133m",
    (0xAF, 0x5F, 0xD7): "\u001b[38;5;134m",
    (0xAF, 0x5F, 0xFF): "\u001b[38;5;135m",
    (0xAF, 0x87, 0x00): "\u001b[38;5;136m",
    (0xAF, 0x87, 0x5F): "\u001b[38;5;137m",
    (0xAF, 0x87, 0x87): "\u001b[38;5;138m",
    (0xAF, 0x87, 0xAF): "\u001b[38;5;139m",
    (0xAF, 0x87, 0xD7): "\u001b[38;5;140m",
    (0xAF, 0x87, 0xFF): "\u001b[38;5;141m",
    (0xAF, 0xAF, 0x00): "\u001b[38;5;142m",
    (0xAF, 0xAF, 0x5F): "\u001b[38;5;143m",
    (0xAF, 0xAF, 0x87): "\u001b[38;5;144m",
    (0xAF, 0xAF, 0xAF): "\u001b[38;5;145m",
    (0xAF, 0xAF, 0xD7): "\u001b[38;5;146m",
    (0xAF, 0xAF, 0xFF): "\u001b[38;5;147m",
    (0xAF, 0xD7, 0x00): "\u001b[38;5;148m",
    (0xAF, 0xD7, 0x5F): "\u001b[38;5;149m",
    (0xAF, 0xD7, 0x87): "\u001b[38;5;150m",
    (0xAF, 0xD7, 0xAF): "\u001b[38;5;151m",
    (0xAF, 0xD7, 0xD7): "\u001b[38;5;152m",
    (0xAF, 0xD7, 0xFF): "\u001b[38;5;153m",
    (0xAF, 0xFF, 0x00): "\u001b[38;5;154m",
    (0xAF, 0xFF, 0x5F): "\u001b[38;5;155m",
    (0xAF, 0xFF, 0x87): "\u001b[38;5;156m",
    (0xAF, 0xFF, 0xAF): "\u001b[38;5;157m",
    (0xAF, 0xFF, 0xD7): "\u001b[38;5;158m",
    (0xAF, 0xFF, 0xFF): "\u001b[38;5;159m",
}


def error(msg):
    """Exit with an error message."""
    sys.stderr.write(msg)
    sys.stderr.write(os.linesep)
    sys.exit(1)


def rgb(group):
    """Derive an RGB color from a hexadecimal string."""
    nibbles_per_channel = len(group) // 3
    max_val = 16 ** nibbles_per_channel - 1
    nibbles = chunked(group, nibbles_per_channel)

    # Transform hex values into the range 0 to 255.
    return tuple((int(n, 16) * 255) // max_val for n in nibbles)


def distance(color, other):
    """Return the difference between two colors. Both ``color`` and ``other``
    are three-tuples of RGB values.

    Uses a simplfied Euclidean distance, as described at
    https://en.wikipedia.org/wiki/Color_difference#sRGB
    """
    return sum((o - s) ** 2 for s, o in zip(color, other))


def chunked(seq, n):
    """Split the given sequence into chunks of size `n`. The last item in the
    sequence could have a length less than `n`.
    """
    return takewhile(len, (seq[i : i + n] for i in count(0, n)))


def escape(group, palette):
    """Return the given checksum group wrapped with ANSI escape codes."""
    key = partial(distance, other=rgb(group.ljust(3, "0")))
    ansi_color = min(palette, key=key)
    return "".join([palette[ansi_color], group, ANSI_RESET])


def colorize(line, group_size=3, palette=ANSI_PALETTE):
    """Write a colorized version of the given checksum to stdout."""
    checksum, filename = line.split(None, 1)
    escaped = [escape(group, palette) for group in chunked(checksum, group_size)]
    sys.stdout.write("  ".join(["".join(escaped), filename]))


def html_colorize(checksum, group_size=3, palette=ANSI_PALETTE):
    """Helper function for generating colorized checksums suitable for display
    on RosettaCode."""

    def span(group):
        key = partial(distance, other=rgb(group.ljust(3, "0")))
        ansi_color = min(palette, key=key)
        int_val = int.from_bytes(ansi_color, byteorder="big")
        hex_val = hex(int_val)[2:].rjust(6, "0")
        return '<span style="color:#{}">{}</span>'.format(hex_val, group)

    checksum, filename = line.split(None, 1)
    escaped = [span(group) for group in chunked(checksum, group_size)]
    sys.stdout.write("  ".join(["".join(escaped), filename]))


if __name__ == "__main__":
    # Command line interface
    parser = argparse.ArgumentParser(description="Color checksum.")

    parser.add_argument(
        "-n",
        type=int,
        default=3,
        help="Color the checksum in groups of size N. Defaults to 3.",
    )

    parser.add_argument(
        "-e",
        "--extended-palette",
        action="store_true",
        help="Use the extended 8-bit palette. Defaults to False.",
    )

    parser.add_argument(
        "--html",
        action="store_true",
        help="Output checksum groups wrapped with 'span' tags instead of ANSI escape sequences.",
    )

    parser.add_argument("files", nargs="*", default="-", metavar="FILE")

    args = parser.parse_args()

    if sys.stdout.isatty():

        palette = ANSI_PALETTE
        if args.extended_palette:
            palette = _8BIT_PALETTE

        colorize_func = colorize
        if args.html:
            colorize_func = html_colorize

        for line in fileinput.input(files=args.files):
            colorize_func(line, group_size=args.n, palette=palette)
    else:
        # Piped or redirected. Don't colorize.
        for line in fileinput.input(files=args.files):
            sys.stdout.write(line)
Output:
$ md5sum tmp/coreutils-* | checksumcolor
ab06d68949758971fe744db66b572816  tmp/coreutils-8.30.tar.xz
0009a224d8e288e8ec406ef0161f9293  tmp/coreutils-8.31.tar.xz
f7b0e95946737ce907aac935a12bdf72  tmp/coreutils-8.32.tar.gz
022042695b7d5bcf1a93559a9735e668  tmp/coreutils-8.32.tar.xz

Some 8-bit colors with groups of six.

$ md5sum tmp/coreutils-* | checksumcolor -n 6 -e
ab06d68949758971fe744db66b572816  tmp/coreutils-8.30.tar.xz
0009a224d8e288e8ec406ef0161f9293  tmp/coreutils-8.31.tar.xz
f7b0e95946737ce907aac935a12bdf72  tmp/coreutils-8.32.tar.gz
022042695b7d5bcf1a93559a9735e668  tmp/coreutils-8.32.tar.xz

SHA256 with 8-bit colors and groups of five.

$ sha256sum tmp/coreutils-* | checksumcolor -n 5 -e --html
e831b3a86091496cdba720411f9748de81507798f6130adeaef872d206e1b057  tmp/coreutils-8.30.tar.xz
ff7a9c918edce6b4f4b2725e3f9b37b0c4d193531cac49a48b56c4d0d3a9e9fd  tmp/coreutils-8.31.tar.xz
d5ab07435a74058ab69a2007e838be4f6a90b5635d812c2e26671e3972fca1b8  tmp/coreutils-8.32.tar.gz
4458d8de7849df44ccab15e16b1548b285224dbba5f08fac070c1c0e0bcc4cfa  tmp/coreutils-8.32.tar.xz

No colors when piping or redirecting from checksumcolor

$ md5sum tmp/coreutils-* | checksumcolor | head
ab06d68949758971fe744db66b572816  tmp/coreutils-8.30.tar.xz
0009a224d8e288e8ec406ef0161f9293  tmp/coreutils-8.31.tar.xz
f7b0e95946737ce907aac935a12bdf72  tmp/coreutils-8.32.tar.gz
022042695b7d5bcf1a93559a9735e668  tmp/coreutils-8.32.tar.xz

Raku

(formerly Perl 6)

Works with: Rakudo version 2019.03

To determine the colors, rather than breaking the md5sum into groups of 3 characters, (which leaves two lonely characters at the end), I elected to replicate the first 5 characters onto the end, then for each character, used it and the 5 characters following as a true-color index. I also added an option to output as HTML code for ease of pasting in here.

unit sub MAIN ($mode = 'ANSI');

if $*OUT.t or $mode eq 'HTML' { # if OUT is a terminal or if in HTML $module

    say '<div style="background-color:black; font-size:125%; font-family: Monaco, monospace;">'
      if $mode eq 'HTML';

    while my $line = get() {
        my $cs  = $line.words[0];
        my $css = $cs ~ $cs.substr(0,5);
        given $mode {
            when 'ANSI' {
                print "\e[48;5;232m";
                .print for $css.comb.rotor(6 => -5)».map({ ($^a, $^b).join })\
                .map( { sprintf "\e[38;2;%d;%d;%dm", |$_».parse-base(16) } ) Z~ $cs.comb;
                say "\e[0m  {$line.words[1..*]}";
            }
            when 'HTML' {
                print "$_\</span>" for $css.comb.rotor(6 => -5)\
                .map( { "<span style=\"color:#{.join};\">" } ) Z~ $cs.comb;
                say " <span style=\"color:#ffffff\"> {$line.words[1..*]}</span>";
                say '<br>';
            }
            default { say $line; }
        }
    }

    say '</div>' if $mode eq 'HTML';
} else { # just pass the unaltered line through
    .say while $_ = get();
}

Can't really show the ANSI output directly so show the HTML output. Essentially identical.

Output:
md5sum *.p6 | raku checksum-color.p6 HTML > checksum-color.html

yields:

f09a3fc8551d8a703d64d8e918ece236 checksum-color (another copy).p6
f09a3fc8551d8a703d64d8e918ece236 checksum-color (copy).p6
f09a3fc8551d8a703d64d8e918ece236 checksum-color.p6
bbd8a92c326c8a35e80d2d71ab8902cd something-completely-different.p6

Ring

load "consolecolors.ring"

str   = "#0123456789ABCDEF"
color = [CC_FG_BLACK,CC_FG_DARK_RED,CC_FG_DARK_GREEN,CC_FG_DARK_YELLOW, CC_FG_DARK_BLUE, CC_FG_DARK_MAGENTA,
         CC_FG_DARK_CYAN,CC_FG_GRAY,CC_FG_DARK_GRAY,CC_FG_RED,CC_FG_GREEN,CC_FG_YELLOW,CC_FG_BLUE,
         CC_FG_MAGENTA,CC_FG_CYAN,CC_FG_WHITE,CC_BG_DARK_RED ]

for n = 1 to len(str)
    cc_print(color[n] | CC_FG_WHITE,str[n])
next
Output:
Same as the Phix entry.

Sidef

var ansi = frequire("Term::ANSIColor")

func colorhash(hash) {
    hash.split(2).map{|s| ansi.colored(s, "ansi" + s.hex) }.join
}

ARGF.each {|line|
    if (STDOUT.is_on_tty && (line =~ /^([[:xdigit:]]+)(.*)/)) {|m|
        say (colorhash(m[0]), m[1])
    }
    else {
        say line
    }
}
Output:

% md5sum *.sf | sf checksumcolor.sf
f8ac04c1857c109145e1bf0fe25550d2 checksumcolor (copy).sf
f8ac04c1857c109145e1bf0fe25550d2 checksumcolor.sf
af086941b6dc67001cd831bb1f22c3ed farey.sf
b585c25146e94df370ec48466911d9ae pell.sf

Wren

Translation of: OCaml
Library: Wren-dynamic
Library: Wren-fmt
Library: Wren-ioutil
Library: Wren-pattern

Translated via the Go entry. Currently, Wren-cli has no way to run external utilities. The md5sum's have therefore been pre-computed and saved to a file.

import "./dynamic" for Tuple
import "./fmt" for Conv, Fmt
import "./ioutil" for FileUtil, Stdout
import "./pattern" for Pattern

var Color = Tuple.create("Color", ["r", "g", "b"])

var ColorEx = Tuple.create("ColorEx", ["color", "code"])

var colors = [
    ColorEx.new(Color.new(15,  0,  0), "31"),
    ColorEx.new(Color.new( 0, 15,  0), "32"),
    ColorEx.new(Color.new(15, 15,  0), "33"),
    ColorEx.new(Color.new( 0,  0, 15), "34"),
    ColorEx.new(Color.new(15,  0, 15), "35"),
    ColorEx.new(Color.new( 0, 15, 15), "36")
]

var squareDist = Fn.new { |c1, c2|
    var xd = c2.r - c1.r
    var yd = c2.g - c1.g
    var zd = c2.b - c1.b
    return xd*xd + yd*yd + zd*zd
}

var printColor = Fn.new { |s|
    var n = s.count
    var k = 0
    for (i in 0...(n/3).floor){
        var j = i * 3
        var c1 = s[j]
        var c2 = s[j+1]
        var c3 = s[j+2]
        k = j + 3
        var r = Conv.atoi(c1, 16)
        var g = Conv.atoi(c2, 16)
        var b = Conv.atoi(c3, 16)
        var rgb = Color.new(r, g, b)
        var m = 676
        var colorCode = ""
        for (cex in colors) {
            var sqd = squareDist.call(cex.color, rgb)
            if (sqd < m) {
                colorCode = cex.code
                m = sqd
            }
        }
        Fmt.write("\e[$s;1m$s$s$s\e[00m", colorCode, c1, c2, c3)
    }
    var j = k
    while (j < n) {
        var c = s[j]
        Fmt.write("\e[0;1m$s\e[00m", c)
        j = j + 1
    }
    Stdout.flush()
}

var i = " \t"
var p = Pattern.new("[+1/w][+1/i+1/z]", Pattern.whole, i)

var colorChecksum = Fn.new { |fileName|
    for (line in FileUtil.readLines(fileName)) {
        if (!line) return
        if (p.isMatch(line)) {
            var m  = p.findAll(line)[0]
            var s1 = m.capsText[0]
            var s2 = m.capsText[1]
            printColor.call(s1)
            System.print(s2)
        } else {
            System.print(line)
        }
    }
}

colorChecksum.call("md5sums.txt")
Output:
Same as the OCaml entry

zkl

Translation of: OCaml
var [const] colorRGBs=T(T(15,  0,  0), T(0 ,15,  0), T(15, 15,  0),
			T( 0,  0, 15), T(15, 0, 15), T( 0, 15, 15) ),
            colorTxt =T("31","32","33","34","35","36");  // esc[<ab>m
fcn squareDist(rgb1,rgb2){ rgb2.zipWith('-,rgb1).apply("pow",2).sum(0) }
fcn colorize(chksum){   // "check sum" --> ansi color escape sequence
   k:=chksum.len()/3*3; // every three digits gets its own color
   chksum[0,k].pump(String,T(Void.Read,2), fcn(r,g,b){
      // find color closest to these three digits of check sum
      // minMaxNs returns indexes of min and max (in list of ints)
      vm.arglist.apply("toInt",16) : // f("a","b","c")-->(10,11,12)
        colorRGBs.apply(squareDist.fp(_)) : (0).minMaxNs(_)[0] : colorTxt[_] :
        "\e[%s;1m%c%c%c".fmt(_, r,g,b)
   })
   .append("\e[0m",chksum[k,*]); // reset color, rest of check sum
}

Fake "md5sum coreutils-* | zkl checksumcolor" for testing

re,lines := RegExp("([A-Fa-f0-9]+)([ \t]+.+)"),
#<<<
"ab20d840e13adfebf2b6936a2dab071b  coreutils-8.29.tar.gz
b259b2936bb46009be3f5cc06a12c32d  coreutils-8.30.tar.gz
03cf26420de566c306d340df52f6ccd7  coreutils-8.31.tar.gz"
#<<<
.split("\n");

foreach line in (lines){ 
   if(re.search(line)){
      chksum,txt := re.matched[1,*];
      println(colorize(chksum),txt);
   }
}
Output:
Same as the OCaml entry

This is what we would do to implement "md5sum chksum.zkl | zkl chksum" (instead of the above test code)

re:=RegExp("([A-Fa-f0-9]+)([ \t]+.+)");
foreach line in (File.stdin){ 
   if(re.search(line)){
      chksum,txt := re.matched[1,*];
      println(colorize(chksum),txt);
   } else print(line);
}