1: #! /usr/bin/env tclsh 2: 3: # Handle options 4: ## Set defaults 5: set options(DrawPorts) 1 6: set options(DrawPortsSideways) 0 7: set options(RankDir) RL 8: set options(PostScript) 0 9: set options(ClusterConnected) 1 10: set options(RankSep) 2.0 11: set options(DrawSameNodeEdges) 0 12: 13: # Procedures 14: proc __to_dot_node {str} { 15: if {[regexp {^[0-9]} $str]} { 16: set str "__$str" 17: } 18: 19: return [string map [list - _ / _ " " _ * _ . _] $str] 20: } 21: 22: proc __to_dot_port {str} { 23: set work [split $str /] 24: set subpart [lindex $work 1] 25: if {[string index $subpart 0] == "g" && [llength $work] == 2} { 26: set work [lreplace $work 1 1 [expr [string range $subpart 1 end] + 16384]] 27: set str [join $work /] 28: } 29: 30: set str [string map [list - _ / _ " " _ . _] $str] 31: return "port$str" 32: } 33: 34: proc __str_to_dot {str} { 35: return [string map [list "&" "&" "-" "-"] $str] 36: } 37: 38: proc __ports_to_color {sport dport} { 39: binary scan $sport H* snum 40: binary scan $dport H* dnum 41: 42: if {$::tcl_version == "8.4"} { 43: set snum [string range $snum 0 6] 44: set dnum [string range $dnum 0 6] 45: } 46: if {$snum == ""} { 47: set snum "0" 48: } 49: if {$dnum == ""} { 50: set dnum "0" 51: } 52: set snum "0x$snum" 53: set dnum "0x$dnum" 54: 55: expr srand($snum + $dnum) 56: 57: set edgecolor_part(r) [expr int(rand() * 192) + 32] 58: set edgecolor_part(g) [expr int(rand() * 192) + 32] 59: set edgecolor_part(b) [expr int(rand() * 192) + 32] 60: 61: return [format "#%02x%02x%02x" $edgecolor_part(r) $edgecolor_part(g) $edgecolor_part(b) ] 62: } 63: 64: proc get_all_nodes_connected_to {node nodemapping {retval ""}} { 65: array set node_mapping $nodemapping 66: 67: lappend retval $node 68: 69: if {[info exists node_mapping($node)]} { 70: foreach target [lsort -dictionary $node_mapping($node)] { 71: set dhost [lindex $target 1] 72: 73: # Exclude hosts we have already visited 74: if {[lsearch -exact $retval $dhost] != -1} { 75: continue 76: } 77: 78: lappend retval $dhost 79: 80: foreach childnode [get_all_nodes_connected_to $dhost $nodemapping $retval] { 81: if {[lsearch -exact $retval $childnode] != -1} { 82: continue 83: } 84: 85: lappend retval $childnode 86: } 87: } 88: 89: unset node_mapping($node) 90: } 91: 92: foreach tmpnode [array names node_mapping] { 93: foreach target [lsort -dictionary $node_mapping($tmpnode)] { 94: set dhost [lindex $target 1] 95: 96: if {$dhost == $node} { 97: if {[lsearch -exact $retval $tmpnode] != -1} { 98: continue 99: } 100: 101: lappend retval $tmpnode 102: 103: foreach childnode [get_all_nodes_connected_to $tmpnode $nodemapping $retval] { 104: if {[lsearch -exact $retval $childnode] != -1} { 105: continue 106: } 107: 108: lappend retval $childnode 109: } 110: } 111: } 112: 113: unset node_mapping($tmpnode) 114: } 115: 116: return $retval 117: } 118: 119: ## Process user actions 120: for {set idx 0} {$idx < [llength $argv]} {incr idx} { 121: set arg [lindex $argv $idx] 122: 123: if {[string index $arg 0] != "-"} { 124: break 125: } 126: 127: switch -- $arg { 128: "-p" - "--drawports" { 129: set options(DrawPorts) 1 130: } 131: "-n" - "--nodrawports" { 132: set options(DrawPorts) 0 133: } 134: "-r" - "--righttoleft" { 135: set options(RankDir) RL 136: } 137: "-t" - "--toptobottom" { 138: set options(RankDir) BT 139: } 140: "-R" { 141: set options(RankDir) [string reverse $options(RankDir)] 142: } 143: "-w" - "--widenodes" { 144: set options(DrawPortsSideways) 1 145: } 146: "-P" - "--postscript" { 147: set options(PostScript) 1 148: } 149: "-u" - "--unclusterconnected" { 150: set options(ClusterConnected) 0 151: } 152: default { 153: if {$arg == "-h" || $arg == "--help"} { 154: set outfd stdout 155: set retval 0 156: } else { 157: puts stderr "Unknown argument: \"$arg\"" 158: set outfd stderr 159: set retval 1 160: } 161: 162: puts $outfd "Usage: $argv0 \[-pnrtwPu\] \[--\[no\]drawports\] \[--righttoleft\] \[--toptobottom\] \[--widenodes\] \[--postscript\] \[--unclusterconnected\]" 163: 164: exit $retval 165: } 166: } 167: } 168: set argv [lrange $argv $idx end] 169: 170: # Set the title 171: set options(Title) [lindex $argv 0] 172: if {$options(Title) == ""} { 173: set options(Title) "Unnamed Graph [clock format [clock seconds]] [info host]" 174: } 175: 176: # Hard-coded definitions 177: set attr_info([list cluster *]) {cluster=$VALUE} 178: 179: # Read edges from standard input 180: set data [read stdin] 181: foreach line [split $data \n] { 182: # Check for pre-processor directives 183: if {[regexp {^# *define *} $line]} { 184: set preproc [split [regsub {^# *define *} $line {}]] 185: set attr_name [lindex $preproc 0] 186: set attr_val [lindex $preproc 1] 187: set newattrs [join [lrange $preproc 2 end]] 188: 189: lappend attr_info([list $attr_name $attr_val]) $newattrs 190: continue 191: } 192: 193: if {[regexp {^# *setdot *} $line]} { 194: set preproc [split [regsub {^# *setdot *} $line {}]] 195: set set_node [lindex $preproc 0] 196: set set_dot_attr [lindex $preproc 1] 197: set set_dot_attrval [join [lrange $preproc 2 end]] 198: lappend user_attr($set_node) $set_dot_attr $set_dot_attrval 199: } 200: 201: 202: if {[regexp {^# *set *} $line]} { 203: set preproc [split [regsub {^# *set *} $line {}]] 204: set set_node [lindex $preproc 0] 205: set set_dot_attr [lindex $preproc 1] 206: set set_dot_attrval [join [lrange $preproc 2 end]] 207: 208: set info [join [list "$set_node.$set_dot_attr" "$set_dot_attrval"] =] 209: set line [join [list "" "" $info] "|"] 210: } 211: 212: if {[regexp {^# *option *} $line]} { 213: set preproc [split [regsub {^# *option *} $line {}]] 214: set set_option_name [lindex $preproc 0] 215: set set_option_val [join [lrange $preproc 1 end]] 216: 217: set options($set_option_name) $set_option_val 218: } 219: 220: # Remove comments and blank lines 221: set line [regsub {#.*$} $line {}] 222: set line [string trim $line] 223: if {$line == ""} { 224: continue 225: } 226: 227: # Tokenize into source, destination, and informational sections 228: set work [split $line |] 229: 230: if {[string match "*=*" [lindex $work end]]} { 231: set info [string trim [lindex $work end]] 232: set work [lrange $work 0 end-1] 233: } else { 234: set info "" 235: } 236: 237: set pathnodes [list] 238: set pathedges [list] 239: for {set dstidx 1} {$dstidx < [llength $work]} {incr dstidx} { 240: set source [string trim [lindex $work [expr $dstidx - 1]]] 241: set dest [string trim [lindex $work $dstidx]] 242: 243: # Tokenize source and destination into node and port 244: set source_work [split $source :] 245: set dest_work [split $dest :] 246: 247: set source_node [lindex $source_work 0] 248: set source_port [lindex $source_work 1] 249: 250: set dest_node [lindex $dest_work 0] 251: set dest_port [lindex $dest_work 1] 252: 253: # Skip entries with no source or destination node 254: if {$source_node == "" || $dest_node == ""} { 255: continue 256: } 257: 258: lappend node_mapping($source_node) [list $source_port $dest_node $dest_port] 259: 260: lappend pathnodes $source_node 261: 262: if {$source_port != ""} { 263: lappend pathedges $source_node:$source_port 264: } 265: } 266: if {[info exists dest_node]} { 267: lappend pathnodes $dest_node 268: 269: if {$dest_port != ""} { 270: lappend pathedges $dest_node:$dest_port 271: } 272: } 273: 274: # Process informational section 275: foreach infoentry [split $info ,] { 276: set infoentry [string map [list "\\=" "||||"] $infoentry] 277: set infoentrywork [split $infoentry =] 278: set infoentrywork [string map [list "||||" "="] $infoentrywork] 279: set infoentry_targetattr [lindex $infoentrywork 0] 280: set infoentry_attrval [lindex $infoentrywork 1] 281: 282: set infoentry_targetattrwork [split $infoentry_targetattr .] 283: set infoentry_target [lindex $infoentry_targetattrwork 0] 284: set infoentry_attr [lindex $infoentry_targetattrwork 1] 285: 286: foreach entrychk [array names attr_info] { 287: set attr_name [lindex $entrychk 0] 288: set attr_val [lindex $entrychk 1] 289: 290: if {$infoentry_attr != $attr_name} { 291: continue 292: } 293: 294: if {[string match $attr_val $infoentry_attrval]} { 295: foreach dotattr $attr_info($entrychk) { 296: set VALUE $infoentry_attrval 297: 298: set dotattrwork [split $dotattr =] 299: set dotattr_name [lindex $dotattrwork 0] 300: set dotattr_val [subst [lindex $dotattrwork 1]] 301: 302: set targets [list] 303: switch -- $infoentry_target { 304: "__PATHNODES__" { 305: foreach pathnode $pathnodes { 306: lappend targets $pathnode 307: } 308: } 309: "__PATHEDGES__" { 310: foreach pathedge $pathedges { 311: lappend targets $pathedge 312: } 313: } 314: default { 315: lappend targets $infoentry_target 316: } 317: } 318: 319: foreach target $targets { 320: lappend user_attr($target) $dotattr_name $dotattr_val 321: } 322: } 323: } 324: } 325: } 326: } 327: 328: # Emit DOT header. 329: puts "digraph \"$options(Title)\" {" 330: puts " label = \"$options(Title)\";" 331: puts { normalize = true;} 332: puts " rankdir = $options(RankDir);" 333: puts " ranksep = $options(RankSep);" 334: puts { overlap = false;} 335: puts { remincross = true;} 336: puts { pack = true;} 337: puts { compound = true;} 338: puts { center = true;} 339: if {$options(PostScript)} { 340: puts { page = "8.5, 11.0";} 341: } 342: puts { outputorder = "nodesfirst";} 343: puts { node [shape=box,fontname="Helvetica",style="rounded,filled"];} 344: puts { edge [arrowhead=normal,arrowtail=normal,decorate=true];} 345: if {[info exists options(Splines)]} { 346: puts " splines = \"$options(Splines)\";" 347: } 348: 349: # Get list of all nodes 350: set allnodes [list] 351: foreach node [array names node_mapping] { 352: foreach target $node_mapping($node) { 353: set dhost [lindex $target 1] 354: 355: if {[lsearch -exact $allnodes $dhost] == -1} { 356: lappend allnodes $dhost 357: } 358: } 359: 360: if {[lsearch -exact $allnodes $node] == -1} { 361: lappend allnodes $node 362: } 363: } 364: 365: # Apply wildcard attributes to all nodes 366: foreach node $allnodes { 367: foreach attr_name [array names user_attr] { 368: if {[string match "$attr_name" $node] && $attr_name != $node} { 369: foreach attr_val $user_attr($attr_name) { 370: set NODENAME $node 371: lappend user_attr($node) [subst $attr_val] 372: } 373: } 374: } 375: } 376: 377: # Set group on all node from "cluster" Dot attribute 378: set node_clusters [list] 379: foreach node $allnodes { 380: unset -nocomplain node_attrs 381: if {[info exists user_attr($node)]} { 382: array set node_attrs $user_attr($node) 383: } 384: 385: if {[info exists node_attrs(cluster)]} { 386: set cluster $node_attrs(cluster) 387: 388: if {$options(ClusterConnected)} { 389: set connnodes [get_all_nodes_connected_to $node [array get node_mapping]] 390: } else { 391: set connnodes [list $node] 392: } 393: 394: foreach connnode $connnodes { 395: unset -nocomplain tmp_node_attrs 396: 397: array set tmp_node_attrs [list] 398: if {[info exists user_attr($connnode)]} { 399: array set tmp_node_attrs $user_attr($connnode) 400: } 401: 402: unset -nocomplain tmp_node_attrs(cluster) 403: set tmp_node_attrs(group) [__to_dot_node cluster_$cluster] 404: if {[lsearch -exact $node_clusters $cluster] == -1} { 405: lappend node_clusters $cluster 406: } 407: 408: set user_attr($connnode) [array get tmp_node_attrs] 409: } 410: } 411: } 412: unset -nocomplain node_attrs tmp_node_attrs connnode node cluster 413: lappend node_clusters __DEFAULT__ 414: 415: # Draw all nodes 416: foreach node [array names node_mapping] { 417: foreach target $node_mapping($node) { 418: set sport [lindex $target 0] 419: set dhost [lindex $target 1] 420: set dport [lindex $target 2] 421: 422: if {$sport != ""} { 423: lappend node_ports($node) $sport 424: } 425: if {$dport != "" && $dhost != ""} { 426: lappend node_ports($dhost) $dport 427: } 428: 429: } 430: } 431: 432: foreach cluster $node_clusters { 433: if {$cluster != "__DEFAULT__"} { 434: puts " subgraph \"[__to_dot_node cluster_$cluster]\" \{" 435: puts " label = \"$cluster\";" 436: set indent " " 437: } else { 438: set indent " " 439: } 440: 441: foreach node $allnodes { 442: unset -nocomplain node_attrs 443: if {[info exists user_attr($node)]} { 444: array set node_attrs $user_attr($node) 445: } 446: 447: if {$cluster == "__DEFAULT__"} { 448: if {[info exists node_attrs(group)]} { 449: continue 450: } 451: } else { 452: if {![info exists node_attrs(group)]} { 453: continue 454: } 455: 456: if {$node_attrs(group) != [__to_dot_node cluster_${cluster}]} { 457: continue 458: } 459: } 460: 461: set node_dot_attrs_list [list] 462: set node_label {<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="1">} 463: append node_label "<TR><TD TITLE=\"[__str_to_dot $node]\" BORDER=\"0\">$node</TD></TR>" 464: if {[info exists node_ports($node)]} { 465: 466: if {$options(DrawPorts)} { 467: if {$options(DrawPortsSideways)} { 468: append node_label "<TR>" 469: set portheader "" 470: set porttrailer "" 471: } else { 472: set portheader "<TR>" 473: set porttrailer "</TR>" 474: } 475: 476: set foundPorts [list] 477: foreach port [lsort -dictionary $node_ports($node)] { 478: if {[lsearch -exact $foundPorts $port] != -1} { 479: continue 480: } 481: 482: lappend foundPorts $port 483: 484: append node_label "${portheader}<TD PORT=\"[__to_dot_port $port]\" TITLE=\"[__str_to_dot $node:$port]\">$port</TD>${porttrailer}" 485: } 486: 487: if {$options(DrawPortsSideways)} { 488: append node_label "</TR>" 489: } 490: 491: } 492: 493: } 494: append node_label {</TABLE>} 495: 496: set node_attrs(label) "<$node_label>" 497: 498: foreach attrlookupnode [list $node __NODE__] { 499: if {[info exists user_attr($attrlookupnode)]} { 500: foreach {attr_name attr_val} $user_attr($attrlookupnode) { 501: set node_attrs($attr_name) $attr_val 502: } 503: } 504: } 505: 506: foreach {node_dot_attr_name node_dot_attr_val} [array get node_attrs] { 507: if {$node_dot_attr_name == "cluster"} { 508: continue 509: } 510: 511: lappend node_dot_attrs_list "$node_dot_attr_name=$node_dot_attr_val" 512: } 513: 514: puts "${indent}[__to_dot_node $node] \[[join $node_dot_attrs_list ,]\];" 515: } 516: 517: if {$cluster != "__DEFAULT__"} { 518: puts " \}" 519: } 520: } 521: 522: # Draw all edges 523: if {$options(DrawPortsSideways)} { 524: set compasssrc _ 525: set compassdst _ 526: } else { 527: set compasssrc _ 528: set compassdst _ 529: } 530: 531: foreach node [array names node_mapping] { 532: # Create edges from node 533: foreach target [lsort -dictionary $node_mapping($node)] { 534: set sport [lindex $target 0] 535: set dhost [lindex $target 1] 536: set dport [lindex $target 2] 537: 538: if {!$options(DrawSameNodeEdges)} { 539: if {$node == $dhost} { 540: continue 541: } 542: } 543: 544: unset -nocomplain edge_attrs 545: set edge_attrs(splines) "false" 546: set edge_attrs(color) [__ports_to_color $sport $dport] 547: 548: foreach attrlookupedge [list __EDGE__ $node-$dhost $node:$sport $dhost:$dport] { 549: if {[info exists user_attr($attrlookupedge)]} { 550: foreach {attr_name attr_val} $user_attr($attrlookupedge) { 551: set edge_attrs($attr_name) $attr_val 552: } 553: } 554: } 555: 556: set edge_attrs_list [list] 557: foreach attr [array names edge_attrs] { 558: lappend edge_attrs_list "$attr=\"[string trim $edge_attrs($attr) "\" "]\"" 559: } 560: 561: set edge_attrs_str [join $edge_attrs_list ,] 562: 563: puts " [__to_dot_node $node]:[__to_dot_port $sport]:${compasssrc} -> [__to_dot_node $dhost]:[__to_dot_port $dport]:${compassdst} \[$edge_attrs_str\];" 564: } 565: } 566: 567: 568: # Emit footer 569: puts "}" |