pdict: Pretty print a dict

APN: Also see exhibit for a generalized package for pretty printing.

see also http://core.tcl.tk/tcllib/doc/trunk/embedded/www/tcllib/files/modules/dicttool/dicttool.html

tjk's implementation

I couldn't find a quick way to print a dict item when I was debugging some code so I wrote this pretty printer. Enjoy...tjk

# -- pdict
#
# Pretty print a dict similar to parray.
#
# USAGE:
#
#   pdict d [i [p [s]]]
#
# WHERE:
#  d - dict value or reference to be printed
#  i - indent level
#  p - prefix string for one level of indent
#  s - separator string between key and value
#
# EXAMPLE:
# % set d [dict create a {1 i 2 j 3 k} b {x y z} c {i m j {q w e r} k o}]
# a {1 i 2 j 3 k} b {x y z} c {i m j {q w e r} k o}
# % pdict $d
# a ->
#   1 -> 'i'
#   2 -> 'j'
#   3 -> 'k'
# b -> 'x y z'
# c ->
#   i -> 'm'
#   j ->
#     q -> 'w'
#     e -> 'r'
#   k -> 'o'
# % pdict d
# dict d
# a ->
# ...
proc pdict { d {i 0} {p "  "} {s " -> "} } {
    set fRepExist [expr {0 < [llength\
            [info commands tcl::unsupported::representation]]}]
    if { (![string is list $d] || [llength $d] == 1)
            && [uplevel 1 [list info exists $d]] } {
        set dictName $d
        unset d
        upvar 1 $dictName d
        puts "dict $dictName"
    }
    if { ! [string is list $d] || [llength $d] % 2 != 0 } {
        return -code error  "error: pdict - argument is not a dict"
    }
    set prefix [string repeat $p $i]
    set max 0
    foreach key [dict keys $d] {
        if { [string length $key] > $max } {
            set max [string length $key]
        }
    }
    dict for {key val} ${d} {
        puts -nonewline "${prefix}[format "%-${max}s" $key]$s"
        if {    $fRepExist && [string match "value is a dict*"\
                    [tcl::unsupported::representation $val]]
                || ! $fRepExist && [string is list $val]
                    && [llength $val] % 2 == 0 } {
            puts ""
            pdict $val [expr {$i+1}] $p $s
        } else {
            puts "'${val}'"
        }
    }
    return
}

RS's implementation

RS 2014-08-05 Here is my version, working similar to parray:

proc pdict {dict {pattern *}} {
   set longest 0
   set keys [dict keys $dict $pattern] 
   foreach key $keys {
      set l [string length $key]
      if {$l > $longest} {set longest $l}
   }
   foreach key $keys {
      puts [format "%-${longest}s = %s" $key [dict get $dict $key]]
   }
}

Testing:

 % pdict {a 1 b 2 verylongkey 3}
 a           = 1
 b           = 2
 verylongkey = 3
 % pdict {a 1 b 2 verylongkey 3} ? ;# show only one-character keys
 a = 1
 b = 2

DKF's implementation

DKF We might as well still use dict for:

proc pdict {dict {pattern *}} {
   set longest 0
   dict for {key -} $dict {
      if {[string match $pattern $key]} {
         set longest [expr {max($longest, [string length $key])}]
      }
   }
   dict for {key value} [dict filter $dict key $pattern] {
      puts [format "%-${longest}s = %s" $key $value]
   }
}

Or even:

proc pdict {dict {pattern *}} {
   set longest [tcl::mathfunc::max 0 {*}[lmap key [dict keys $dict $pattern] {string length $key}]]
   dict for {key value} [dict filter $dict key $pattern] {
      puts [format "%-${longest}s = %s" $key $value]
   }
}

It's probably not a good idea to sort the keys; the order semi-matters (usually not, but sometimes yes).

2014-08-07: added 0 to the invocation of tcl::mathfunc::max to handle empty dictionaries.

pdict's implementation

code

proc pdict {args} {
  set cmd pdict
  set usage "usage: $cmd ?maxlevel? dictionaryValue ?globPattern?..."
  if {[string is integer [lindex $args 0]} {
    set maxlvl lindex $args 0
    set args lrange $args 1 end
  } else {
    set maxlvl llength $args
  }
  foreach {dvar pat} $args break
  if {$dvar == ""} {error $usage}
  if {$pat  == ""} {set pat "*"}
  set args lrange $args 2 end
  upvar __pdict__level __pdict__level
  incr __pdict__level +1
  set sp [string repeat "  " [expr $__pdict__level-1]
  if {catch {dict keys $dvar $pat} keys} {
    error "$cmd error: 'dictionaryValue' is no 'dict'\n → $usage\n  → $keys"
  } elseif {llength $keys} {
    set size [::tcl::mathfunc::max {*}[lmap k $keys {string length $k}]
    foreach key $keys {
      set dsubvar dict get $dvar $key
      puts -nonewline format {%s%-*s} $sp $size $key
      if {$__pdict__level < $maxlvl} {
        set isVal   catch {dict keys $dsubvar} keys
        if {llength $keys == 0} {
          puts " = \{\}"
        } elseif {$isVal} {
          puts " = $dsubvar"
        } else {
          puts " \{"
          pdict $maxlvl $dsubvar {*}$args
          puts "$sp\}"
        }
      } else {
        puts " = $dsubvar"
      }
    }
  } else {
  }
  if {$__pdict__level == 1} {
    unset -nocomplain __pdict__level
  } else {
    incr __pdict__level -1
  }
}

example

the command

pdict 4 $::LP::analyze * 7

create the following output

…
166 {
  7 {
    IL {
      type      = EMPTY
      blocksize = 0
    }
  }
}
176 {
  7 {
    IL {
      type      = DS
      blocksize = 5
    }
  }
}
177 {
  7 {
    IL {
      type      = BN
      blocksize = 5
    }
  }
}
…

LV I wonder if something like this should be included in the same place that parray is located in the Tcl distribution...

Lars H, 2010-06-01: That reminds me… two years ago (I think it was) I wrote a package to a similar end that I was going to put in tcllib, but I got sidetracked somewhere late in the wrapping up process (probably something about the tests). Anyway, since the issue is up, I might as well put it on the wiki: exhibit.


kpv this fails for me when the value has a space in it. Try

pdict [dict create name "keith vetter"]

TK the result is

name -> 
  keith -> 'vetter'

which feels like an error but isn't. The problem here is that there isn't any way to distinguish between a value that is a dict and a value that is an even number of tcl words. In your example you provided an even number of tcl words as the value to name and the print code descend into any value that looks like a dict.


HaO Allowed me to do some enhancements I personally find handy:

  • May now be called by value or reference.
  • Restore ::errorInfo and ::errorCode. They might be changed inintentionally by the catch commands.
  • Return empty string

It would be nice to have a method to check if a value actually shimmers to a dict to eventually better display dicts in key values. Unfortunately, I did not find one.

AMG: Use [tcl::unsupported::representation].

HaO Ok, done.

Anyway, this is big fun due to internal optimisations. There are many "side-effects". Take the upper example and a fresh 8.6 wish:

% pdict [dict create name "keith vetter"]
name -> 'keith vetter'
% set d "keith vetter2"
keith vetter2
% dict keys $d
keith
% pdict [dict create name "keith vetter2"]
name ->
  keith -> 'vetter2'

AMG: Did I really say that? I'm having a hard time believing that I actually recommended using [representation] in code! Sorry, I gave bad advice. [representation] is only intended to produce an English-language message to present directly to the user for debugging, not to be used in program logic! The formatting of its output will change with no warning, apology, nor explanation. It doesn't conform to Tcl value semantics, so it shouldn't be allowed to impact the logic of a Tcl program. It exists outside EIAS, and it will produce surprising results.

Moving on. The problem you're dealing with is that Tcl's data structures are not completely self-describing. Type ambiguities exist, by design. Rather, the absolute structure of the data is embedded in the code that reads and writes said data. Type is in the eye of the beholder. Some examples:

  • "Mary had a little lamb,"
    • Is this a list? Technically, it could be a list; its formatting is consistent with the rules for lists. But really, when you look at it, you think it's a string, because you read it as the first line of a well-known nursery rhyme [L1 ]. That's what I mean by structure being embedded in the reader, not the data. See: duck typing. When your program looks at it, it could choose to view it as a string, a list, a binary string, a custom data type, or an opaque blob that is passed around but not parsed (e.g. a handle to something else). Heck, your program could even choose to see it as an invalid integer, if it was designed to key on [string is integer -strict]. Who's to say that "invalid integer" is not in and of itself a valid type? This is Tcl! We write our own rules.
    • Is this a dict? On that, we can answer "no" with certainty. Though it may be a valid list, it has an odd number of elements. It is inconsistent with the rules for dicts.
  • "little lamb, little lamb."
    • In one sense, this could be a valid dict. It's consistent with list formatting, and it has an even number of list elements. Those are the prerequisites needed for passing this string to the dict commands.
    • In another sense, this is not a valid dict. Seen as a dict, it has duplicate keys (little and little). Duplicate keys are retained in the string representation; are ignored by dict commands when they read; and are removed by dict commands when they write.
      • Your code uses [dict for], so it won't print the duplicates.
      • I'd suggest using [foreach] so that they do get printed, but that will cause the data's internal representation to change from dict to list. So if you call [pdict] twice, it won't recurse the second time around. This is exactly the sort of surprise that [representation] will cause. You could run [foreach] on a copy, but good luck finding a reliable way to make an unshared copy!
  • "whose fleece was white as snow."
    • This can be a list or a dict (even number of elements), as well a string or any of those other types I mentioned.

You know what? I take back what I said first. Using [representation] is okay in [pdict], because like [representation], [pdict] is only intended for presenting a debugging message directly to the user (it uses [puts]). What you are doing is making an extension to [representation]. You'll have to bear the burden of maintaining and updating it when [representation]'s output format changes.

HaO2013-01-16: TCL8.6.0 is out. I reworked the function and replaced all dict checks catch {dict keys $d} by [string is list $d] && [llength $d] % 2 == 0 to not pollute the errorstack. I hope, you like that.

AMG: Hmm, maybe we want [string is dict] which does the same as your code snippet. It might take a -strict or -canonical option to make it return 0 if the dict contains duplicate keys. -strict really should not fail on empty string which is a perfectly valid dict (or list, for that matter).