1 // Copyright 2019 The Go Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style
3 // license that can be found in the LICENSE file.
6 The digraph command performs queries over unlabelled directed graphs
7 represented in text form. It is intended to integrate nicely with
8 typical UNIX command pipelines.
12 your-application | digraph [command]
14 The support commands are:
19 the in-degree and out-degree of each node
21 the reverse of the input edges
23 the set of immediate predecessors of the specified nodes
25 the set of immediate successors of the specified nodes
27 the set of nodes transitively reachable from the specified nodes
29 the set of nodes that transitively reach the specified nodes
30 somepath <node> <node>
31 the list of nodes on some arbitrary path from the first node to the second
32 allpaths <node> <node>
33 the set of nodes on all paths from the first node to the second
35 all strongly connected components (one per line)
37 the set of nodes nodes strongly connected to the specified one
39 the subgraph containing all directed paths that pass through the specified node
43 Each line contains zero or more words. Words are separated by unquoted
44 whitespace; words may contain Go-style double-quoted portions, allowing spaces
45 and other characters to be expressed.
47 Each word declares a node, and if there are more than one, an edge from the
48 first to each subsequent one. The graph is provided on the standard input.
50 For instance, the following (acyclic) graph specifies a partial order among the
51 subtasks of getting dressed:
61 The line "shirt tie sweater" indicates the two edges shirt -> tie and
62 shirt -> sweater, not shirt -> tie -> sweater.
66 Using digraph with existing Go tools:
68 $ go mod graph | digraph nodes # Operate on the Go module graph.
69 $ go list -m all | digraph nodes # Operate on the Go package graph.
71 Show the transitive closure of imports of the digraph tool itself:
72 $ go list -f '{{.ImportPath}} {{join .Imports " "}}' ... | digraph forward golang.org/x/tools/cmd/digraph
74 Show which clothes (see above) must be donned before a jacket:
75 $ digraph reverse jacket
78 package main // import "golang.org/x/tools/cmd/digraph"
81 // - support input files other than stdin
82 // - support alternative formats (AT&T GraphViz, CSV, etc),
83 // a comment syntax, etc.
84 // - allow queries to nest, like Blaze query language.
102 fmt.Fprintf(os.Stderr, `Usage: your-application | digraph [command]
104 The support commands are:
108 the in-degree and out-degree of each node
110 the reverse of the input edges
112 the set of immediate predecessors of the specified nodes
114 the set of immediate successors of the specified nodes
116 the set of nodes transitively reachable from the specified nodes
118 the set of nodes that transitively reach the specified nodes
119 somepath <node> <node>
120 the list of nodes on some arbitrary path from the first node to the second
121 allpaths <node> <node>
122 the set of nodes on all paths from the first node to the second
124 all strongly connected components (one per line)
126 the set of nodes nodes strongly connected to the specified one
128 the subgraph containing all directed paths that pass through the specified node
142 if err := digraph(args[0], args[1:]); err != nil {
143 fmt.Fprintf(os.Stderr, "digraph: %s\n", err)
148 type nodelist []string
150 func (l nodelist) println(sep string) {
151 for i, node := range l {
153 fmt.Fprint(stdout, sep)
155 fmt.Fprint(stdout, node)
160 type nodeset map[string]bool // TODO(deklerk): change bool to struct to reduce memory footprint
162 func (s nodeset) sort() nodelist {
163 nodes := make(nodelist, len(s))
165 for node := range s {
173 func (s nodeset) addAll(x nodeset) {
174 for node := range x {
179 // A graph maps nodes to the non-nil set of their immediate successors.
180 type graph map[string]nodeset
182 func (g graph) addNode(node string) nodeset {
185 edges = make(nodeset)
191 func (g graph) addEdges(from string, to ...string) {
192 edges := g.addNode(from)
193 for _, to := range to {
199 func (g graph) reachableFrom(roots nodeset) nodeset {
200 seen := make(nodeset)
201 var visit func(node string)
202 visit = func(node string) {
205 for e := range g[node] {
210 for root := range roots {
216 func (g graph) transpose() graph {
218 for node, edges := range g {
220 for succ := range edges {
221 rev.addEdges(succ, node)
227 func (g graph) sccs() []nodeset {
228 // Kosaraju's algorithm---Tarjan is overkill here.
231 S := make(nodelist, 0, len(g)) // postorder stack
232 seen := make(nodeset)
233 var visit func(node string)
234 visit = func(node string) {
237 for e := range g[node] {
243 for node := range g {
251 var rvisit func(node string)
252 rvisit = func(node string) {
256 for e := range rev[node] {
264 S = S[:len(S)-1] // pop
268 sccs = append(sccs, scc)
274 func (g graph) allpaths(from, to string) error {
275 // Mark all nodes to "to".
276 seen := make(nodeset) // value of seen[x] indicates whether x is on some path to "to"
277 var visit func(node string) bool
278 visit = func(node string) bool {
279 reachesTo, ok := seen[node]
281 reachesTo = node == to
282 seen[node] = reachesTo
283 for e := range g[node] {
288 if reachesTo && node != to {
296 // For each marked node, collect its marked successors.
298 for n := range seen {
299 for succ := range g[n] {
301 edges = append(edges, n+" "+succ)
306 // Sort (so that this method is deterministic) and print edges.
308 for _, e := range edges {
309 fmt.Fprintln(stdout, e)
315 func (g graph) somepath(from, to string) error {
316 type edge struct{ from, to string }
317 seen := make(nodeset)
318 var dfs func(path []edge, from string) bool
319 dfs = func(path []edge, from string) bool {
323 // fmt.Println(path, len(path), cap(path))
325 for _, e := range path {
326 fmt.Fprintln(stdout, e.from+" "+e.to)
330 for e := range g[from] {
331 if dfs(append(path, edge{from: from, to: e}), e) {
338 maxEdgesInGraph := len(g) * (len(g) - 1)
339 if !dfs(make([]edge, 0, maxEdgesInGraph), from) {
340 return fmt.Errorf("no path from %q to %q", from, to)
345 func parse(rd io.Reader) (graph, error) {
349 in := bufio.NewScanner(rd)
352 // Split into words, honoring double-quotes per Go spec.
353 words, err := split(in.Text())
355 return nil, fmt.Errorf("at line %d: %v", linenum, err)
358 g.addEdges(words[0], words[1:]...)
361 if err := in.Err(); err != nil {
367 // Overridable for testing purposes.
368 var stdin io.Reader = os.Stdin
369 var stdout io.Writer = os.Stdout
371 func digraph(cmd string, args []string) error {
372 // Parse the input graph.
373 g, err := parse(stdin)
378 // Parse the command line.
382 return fmt.Errorf("usage: digraph nodes")
384 nodes := make(nodeset)
385 for node := range g {
388 nodes.sort().println("\n")
392 return fmt.Errorf("usage: digraph degree")
394 nodes := make(nodeset)
395 for node := range g {
399 for _, node := range nodes.sort() {
400 fmt.Fprintf(stdout, "%d\t%d\t%s\n", len(rev[node]), len(g[node]), node)
405 return fmt.Errorf("usage: digraph transpose")
407 var revEdges []string
408 for node, succs := range g.transpose() {
409 for succ := range succs {
410 revEdges = append(revEdges, fmt.Sprintf("%s %s", node, succ))
413 sort.Strings(revEdges) // make output deterministic
414 for _, e := range revEdges {
415 fmt.Fprintln(stdout, e)
418 case "succs", "preds":
420 return fmt.Errorf("usage: digraph %s <node> ... ", cmd)
426 result := make(nodeset)
427 for _, root := range args {
430 return fmt.Errorf("no such node %q", root)
434 result.sort().println("\n")
436 case "forward", "reverse":
438 return fmt.Errorf("usage: digraph %s <node> ... ", cmd)
440 roots := make(nodeset)
441 for _, root := range args {
443 return fmt.Errorf("no such node %q", root)
448 if cmd == "reverse" {
451 g.reachableFrom(roots).sort().println("\n")
455 return fmt.Errorf("usage: digraph somepath <from> <to>")
457 from, to := args[0], args[1]
459 return fmt.Errorf("no such 'from' node %q", from)
462 return fmt.Errorf("no such 'to' node %q", to)
464 if err := g.somepath(from, to); err != nil {
470 return fmt.Errorf("usage: digraph allpaths <from> <to>")
472 from, to := args[0], args[1]
474 return fmt.Errorf("no such 'from' node %q", from)
477 return fmt.Errorf("no such 'to' node %q", to)
479 if err := g.allpaths(from, to); err != nil {
485 return fmt.Errorf("usage: digraph sccs")
487 for _, scc := range g.sccs() {
488 scc.sort().println(" ")
493 return fmt.Errorf("usage: digraph scc <node>")
497 return fmt.Errorf("no such node %q", node)
499 for _, scc := range g.sccs() {
501 scc.sort().println("\n")
508 return fmt.Errorf("usage: digraph focus <node>")
512 return fmt.Errorf("no such node %q", node)
515 edges := make(map[string]struct{})
516 for from := range g.reachableFrom(nodeset{node: true}) {
517 for to := range g[from] {
518 edges[fmt.Sprintf("%s %s", from, to)] = struct{}{}
522 gtrans := g.transpose()
523 for from := range gtrans.reachableFrom(nodeset{node: true}) {
524 for to := range gtrans[from] {
525 edges[fmt.Sprintf("%s %s", to, from)] = struct{}{}
529 edgesSorted := make([]string, 0, len(edges))
530 for e := range edges {
531 edgesSorted = append(edgesSorted, e)
533 sort.Strings(edgesSorted)
534 fmt.Fprintln(stdout, strings.Join(edgesSorted, "\n"))
537 return fmt.Errorf("no such command %q", cmd)
543 // -- Utilities --------------------------------------------------------
545 // split splits a line into words, which are generally separated by
546 // spaces, but Go-style double-quoted string literals are also supported.
547 // (This approximates the behaviour of the Bourne shell.)
549 // `one "two three"` -> ["one" "two three"]
550 // `a"\n"b` -> ["a\nb"]
552 func split(line string) ([]string, error) {
560 r, size := utf8.DecodeRuneInString(line)
561 if unicode.IsSpace(r) {
563 words = append(words, current.String())
569 size, ok = quotedLength(line)
571 return nil, errors.New("invalid quotation")
573 s, err := strconv.Unquote(line[:size])
577 current.WriteString(s)
586 words = append(words, current.String())
591 // quotedLength returns the length in bytes of the prefix of input that
592 // contain a possibly-valid double-quoted Go string literal.
594 // On success, n is at least two (""); input[:n] may be passed to
595 // strconv.Unquote to interpret its value, and input[n:] contains the
596 // rest of the input.
598 // On failure, quotedLength returns false, and the entire input can be
599 // passed to strconv.Unquote if an informative error message is desired.
601 // quotedLength does not and need not detect all errors, such as
602 // invalid hex or octal escape sequences, since it assumes
603 // strconv.Unquote will be applied to the prefix. It guarantees only
604 // that if there is a prefix of input containing a valid string literal,
605 // its length is returned.
607 // TODO(adonovan): move this into a strconv-like utility package.
609 func quotedLength(input string) (n int, ok bool) {
612 // next returns the rune at offset, or -1 on EOF.
613 // offset advances to just after that rune.
614 next := func() rune {
615 if offset < len(input) {
616 r, size := utf8.DecodeRuneInString(input[offset:])
624 return // error: not a quotation
629 if r == '\n' || r < 0 {
630 return // error: string literal not terminated
633 return offset, true // success
638 case 'a', 'b', 'f', 'n', 'r', 't', 'v', '\\', '"':
640 case '0', '1', '2', '3', '4', '5', '6', '7':
649 return // error: invalid escape
652 for i := 0; i < skip; i++ {