dot revision 260678
1#!/bin/sh
2#-
3# Copyright (c) 2012-2013 Devin Teske
4# All rights reserved.
5#
6# Redistribution and use in source and binary forms, with or without
7# modification, are permitted provided that the following conditions
8# are met:
9# 1. Redistributions of source code must retain the above copyright
10#    notice, this list of conditions and the following disclaimer.
11# 2. Redistributions in binary form must reproduce the above copyright
12#    notice, this list of conditions and the following disclaimer in the
13#    documentation and/or other materials provided with the distribution.
14#
15# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
16# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
19# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
21# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
22# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
23# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
24# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
25# SUCH DAMAGE.
26#
27# $FreeBSD: stable/10/usr.sbin/bsdconfig/dot/dot 260678 2014-01-15 07:49:17Z dteske $
28#
29############################################################ INCLUDES
30
31# Prevent common.subr from auto initializing debugging (this is not an inter-
32# active utility that requires debugging; also `-d' has been repurposed).
33#
34DEBUG_SELF_INITIALIZE=NO
35
36BSDCFG_SHARE="/usr/share/bsdconfig"
37. $BSDCFG_SHARE/common.subr || exit 1
38f_dprintf "%s: loading includes..." "$0"
39
40BSDCFG_LIBE="/usr/libexec/bsdconfig" APP_DIR="dot"
41f_include_lang $BSDCFG_LIBE/include/messages.subr
42f_include_lang $BSDCFG_LIBE/$APP_DIR/include/messages.subr
43
44f_index_menusel_keyword $BSDCFG_LIBE/$APP_DIR/INDEX "$pgm" ipgm &&
45	pgm="${ipgm:-$pgm}"
46
47############################################################ CONFIGURATION
48
49#
50# Location of bsdconfig(8)
51#
52BSDCONFIG=/usr/sbin/bsdconfig
53
54############################################################ GLOBALS
55
56#
57# Options
58#
59SHOW_GRAPH_LABEL_DATE=1
60SHOW_INCLUDES=1
61SHOW_CMDLINE=1
62
63############################################################ FUNCTIONS
64
65# begin_nodelist $shape $color $fillcolor $style
66#
67# Create a new multi-node list rendering nodes in a specific style described by
68# the arguments passed.
69#
70begin_nodelist()
71{
72	local shape="$1" color="$2" fillcolor="$3" style="$4"
73
74	printf "\tnode [\n"
75	[ "$shape" ] &&
76		printf '\t\tshape = "%s",\n' "$shape"
77	[ "$color" ] &&
78		printf '\t\tcolor = "%s",\n' "$color"
79	[ "$fillcolor" ] &&
80		printf '\t\tfillcolor = "%s",\n' "$fillcolor"
81	[ "$style" ] &&
82		printf '\t\tstyle = "%s",\n' "$style"
83	printf "\t] {\n"
84}
85
86# print_node $node [$attributes ...]
87#
88# Print a node within a multi-node list.
89#
90print_node()
91{
92	local node="$1"
93
94	shift 1 # node
95
96	case "$node" in
97	edge) printf '\t\t%s' "$node" ;;
98	   *) printf '\t\t"%s"' "$node" ;;
99	esac
100
101	if [ $# -gt 0 ]; then
102		echo -n ' ['
103		while [ $# -gt 0 ]; do
104			printf " %s" "$1"
105			shift 1
106			[ $# -gt 0 ] && echo -n ","
107		done
108		echo -n " ]"
109	fi
110
111	echo ";"
112}
113
114# print_node2 $node $node [$attributes ...]
115#
116# Print a directed node-node connection within a multi-node list.
117#
118print_node2()
119{
120	local node1="$1" node2="$2"
121
122	shift 2 # node1 node2
123
124	printf '\t\t"%s" -> "%s"' "$node1" "$node2"
125
126	if [ $# -gt 0 ]; then
127		echo -n ' ['
128		while [ $# -gt 0 ]; do
129			printf " %s" "$1"
130			shift 1
131			[ $# -gt 0 ] && echo -n ","
132		done
133		echo -n " ]"
134	fi
135
136	echo ";"
137}
138
139# end_nodelist
140#
141# Close a multi-node list.
142#
143end_nodelist()
144{
145	printf "\t};\n"
146}
147
148############################################################ MAIN
149
150# Incorporate rc-file if it exists
151[ -f "$HOME/.bsdconfigrc" ] && f_include "$HOME/.bsdconfigrc"
152
153#
154# Process command-line arguments
155#
156while getopts cdhi flag; do
157	case "$flag" in
158	i) SHOW_INCLUDES= ;;
159	d) SHOW_GRAPH_LABEL_DATE= ;;
160	c) SHOW_CMDLINE= ;;
161	h|\?) f_usage $BSDCFG_LIBE/$APP_DIR/USAGE "PROGRAM_NAME" "$pgm" ;;
162	esac
163done
164shift $(( $OPTIND - 1 ))
165
166cd $BSDCFG_LIBE || f_die # Pedantic
167
168#
169# Get a list of menu programs
170#
171menu_program_list=
172for file in [0-9][0-9][0-9].*/INDEX; do
173	menu_program_list="$menu_program_list $(
174		tail -r "$file" | awk -v item="${file%%/*}" '
175			/^[[:space:]]*menu_program="/ {
176				sub(/^.*="/, "")
177				sub(/"$/, "")
178				if ( ! $0 ) next
179				if ( $0 !~ "^/" ) sub(/^/, item "/")
180				print; exit
181			}'
182	)"
183done
184
185#
186# Get a list of submenu programs
187#
188submenu_program_list=
189for menu_program in $menu_program_list; do
190	case "$menu_program" in
191	[0-9][0-9][0-9].*/*) : fall-through ;;
192	*) continue # No sub-menus we can process
193	esac
194
195	submenu_program_list="$submenu_program_list $(
196		awk -v menu_program="$menu_program" \
197		    -v item="${menu_program%%/*}" \
198		'
199			/^menu_selection="/ {
200				sub(/.*\|/, "")
201				sub(/"$/, "")
202				if ( ! $0 ) next
203				if ( $0 !~ "^/" )
204					sub(/^/, item "/")
205				if ( $0 == menu_program ) next
206				print
207			}
208		' "${menu_program%%/*}/INDEX"
209	)"
210done
211
212#
213# Get a list of command-line programs
214#
215cmd_program_list=
216for file in */INDEX; do
217	cmd_program_list="$cmd_program_list $(
218		awk -v item="${file%%/*}" '
219			/^menu_selection="/ {
220				sub(/.*\|/, "")
221				sub(/"$/, "")
222
223				if ( ! $0 ) next
224
225				if ( $0 !~ "^/" )
226					sub(/^/, item "/")
227
228				print
229			}
230		' $file
231	)"
232done
233
234#
235# [Optionally] Calculate list of include files
236#
237if [ "$SHOW_INCLUDES" ]; then
238	print_includes_awk='
239		BEGIN { regex = "^f_include \\$BSDCFG_SHARE/" }
240		( $0 ~ regex ) { sub(regex, ""); print }
241	' # END-QUOTE
242
243	#
244	# Build list of files in which to search for includes
245	#
246	file_list=$(
247		for file in \
248			$BSDCONFIG \
249			$menu_program_list \
250			$submenu_program_list \
251			$cmd_program_list \
252		; do
253			[ -e "$file" ] && echo $file
254		done | sort -u
255	)
256
257	#
258	# Build list of includes used by the above files
259	#
260	include_file_list=
261	for file in $file_list; do
262		include_file_list="$include_file_list $(
263			awk "$print_includes_awk" $file
264		)"
265	done
266
267	#
268	# Sort the list of includes and remove duplicate entries
269	#
270	include_file_list=$(
271		for include_file in $include_file_list; do
272			echo "$include_file"
273		done | sort -u
274	)
275
276	#
277	# Search previously-discovered include files for further includes
278	#
279	before="$include_file_list"
280	while :; do
281		for file in $include_file_list; do
282			include_file_list="$include_file_list $(
283				awk "$print_includes_awk" $BSDCFG_SHARE/$file
284			)"
285		done
286
287		#
288		# Sort list of includes and remove duplicate entries [again]
289		#
290		include_file_list=$(
291			for include_file in $include_file_list; do
292				echo "$include_file"
293			done | sort -u
294		)
295
296		[ "$include_file_list" = "$before" ] && break
297		before="$include_file_list"
298	done
299fi
300
301#
302# Add script.subr to includes if it exists
303#
304[ -f $BSDCFG_SHARE/script.subr ] &&
305	include_file_list="$include_file_list script.subr"
306
307#
308# Start the directional-graph (digraph) output
309#
310printf 'strict digraph "" { // Empty name to prevent SVG Auto-Tooltip\n'
311label_format="$msg_graph_label_with_command"
312[ "$SHOW_GRAPH_LABEL_DATE" ] &&
313	label_format="$msg_graph_label_with_command_and_date"
314lang="${LANG:-$LC_ALL}"
315printf "\n\tlabel = \"$label_format\"\n" \
316       "${lang:+LANG=${lang%%[$IFS]*} }bsdconfig $pgm${ARGV:+ $ARGV}" \
317       "$( date +"%c %Z" )"
318
319#
320# Print graph-specific properties
321#
322printf '\n\t/*\n\t * Graph setup and orientation\n\t */\n'
323printf '\tlabelloc = top;\t\t// display above label at top of graph\n'
324printf '\trankdir = LR;\t\t// create ranks left-to-right\n'
325printf '\torientation = portrait;\t// default\n'
326printf '\tratio = fill;\t\t// approximate aspect ratio\n'
327printf '\tcenter = 1;\t\t// center drawing on page\n'
328
329#
330# Perform edge-concentration when displaying a lot of information
331#
332# NOTE: This is disabled because dot(1) version 2.28.0 (current) and older have
333#       a bug that causes a crash when rankdir = LR and concentrate = true
334#
335# NOTE: Do not re-enable until said bug is fixed in some future revision.
336#
337#[ "$SHOW_INCLUDES" -a "$SHOW_CMDLINE" ] &&
338#	printf '\tconcentrate = true;\t// enable edge concentrators\n'
339
340#
341# Print font details for graph/cluster label(s)
342#
343printf '\n\t/*\n\t * Font details for graph/cluster label(s)\n\t */\n'
344printf '\tfontname = "Times-Italic";\n'
345printf '\tfontsize = 14;\n'
346
347#
348# Print default node attributes
349#
350printf '\n\t/*\n\t * Default node attributes\n\t */\n'
351printf '\tnode [\n'
352printf '\t\tfontname = "Times-Roman",\n'
353printf '\t\tfontsize = 12,\n'
354printf '\t\twidth = 2.5, // arbitrary minimum width for all nodes\n'
355printf '\t\tfixedsize,   // turn minimum width into exact width\n'
356printf '\t];\n'
357
358#
359# Print top-level item(s)
360#
361printf '\n\t/*\n\t * bsdconfig(8)\n\t */\n'
362shape=circle color=black fillcolor=yellow style=filled
363begin_nodelist "$shape" "$color" "$fillcolor" "$style"
364print_node "bsdconfig" "fontname = \"Times-Bold\"" "fontsize = 16"
365end_nodelist
366
367#
368# Print menus
369#
370printf '\n\t/*\n\t * Menu items\n\t */\n'
371shape=box color=black fillcolor=lightblue style=filled
372begin_nodelist "$shape" "$color" "$fillcolor" "$style"
373for menu_program in $menu_program_list; do
374	print_node "$menu_program" "label = \"${menu_program#*/}\""
375done
376end_nodelist
377
378#
379# Print sub-menus
380#
381printf '\n\t/*\n\t * Sub-menu items\n\t */\n'
382shape=box color=black fillcolor=lightblue style=filled
383begin_nodelist "$shape" "$color" "$fillcolor" "$style"
384for submenu_program in $submenu_program_list; do
385	print_node "$submenu_program" "label = \"${submenu_program#*/}\""
386done
387end_nodelist
388
389#
390# Print menu relationships
391#
392printf '\n\t/*\n\t * Menu item relationships\n\t */\n'
393shape=box color=black fillcolor=lightblue style=filled edge_color=blue
394begin_nodelist "$shape" "$color" "$fillcolor" "$style"
395print_node edge "penwidth = 5.0" "style = bold" "color = $edge_color"
396for menu_program in $menu_program_list; do
397	print_node2 "bsdconfig" "$menu_program"
398done
399end_nodelist
400
401#
402# Print sub-menu relationships
403#
404printf '\n\t/*\n\t * Sub-menu item relationships\n\t */\n'
405shape=box color=black fillcolor=lightblue style=filled edge_color=blue
406begin_nodelist "$shape" "$color" "$fillcolor" "$style"
407# Lock sub-menu headport to the West (unless `-c' was passed)
408[ "$SHOW_CMDLINE" -o ! "$SHOW_INCLUDES" ] && print_node edge "headport = w"
409print_node edge "style = bold" "color = $edge_color"
410for submenu_program in $submenu_program_list; do
411	for menu_program in $menu_program_list; do
412		case "$menu_program" in
413		[0-9][0-9][0-9].*/*) : fall-through ;;
414		*) continue # Not a menu item
415		esac
416
417		# Continue if program directories do not match
418		[ "${menu_program%%/*}" = "${submenu_program%%/*}" ] ||
419			continue
420
421		print_node2 "$menu_program" "$submenu_program"
422		break
423	done
424done
425end_nodelist
426
427#
428# [Optionally] Print include files
429#
430if [ "$SHOW_INCLUDES" ]; then
431	printf '\n\t/*\n\t * Include files\n\t */\n'
432	shape=oval color=black fillcolor=white style=filled
433	begin_nodelist "$shape" "$color" "$fillcolor" "$style"
434	printf '\t\tconstraint = false;\n'
435	for include_file in $include_file_list; do
436		print_node "$include_file" \
437		           "label = \"${include_file##*/}\""
438	done
439	end_nodelist
440fi
441
442#
443# [Optionally] Print f_include() usage/relationships
444#
445if [ "$SHOW_INCLUDES" ]; then
446	printf '\n\t/*\n\t * Include usage\n\t */\n'
447	shape=oval color=black fillcolor=white style=filled edge_color=grey
448	begin_nodelist "$shape" "$color" "$fillcolor" "$style"
449	print_node edge "style = dashed" "color = $edge_color"
450	#print_node edge "label = \"\\T\"" "fontsize = 9"
451		# NOTE: Edge labels are buggy on large graphs
452	file_list=$(
453		for file in \
454			$BSDCONFIG \
455			$menu_program_list \
456			$submenu_program_list \
457			$cmd_program_list \
458			$include_file_list \
459		; do
460			[ -f "$BSDCFG_SHARE/$file" ] &&
461				echo $BSDCFG_SHARE/$file
462			[ -e "$file" ] && echo $file
463		done | sort -u
464	)
465	for file in $file_list; do
466		# Skip binary files and text files that don't use f_include()
467		grep -qlI f_include $file || continue
468
469		awk \
470			-v file="${file#$BSDCFG_SHARE/}" \
471			-v bsdconfig="$BSDCONFIG" \
472		'
473			BEGIN { regex = "^f_include \\$BSDCFG_SHARE/" }
474			( $0 ~ regex ) {
475				sub(regex, "")
476				if ( file == bsdconfig ) sub(".*/", "", file)
477				printf "\t\t\"%s\" -> \"%s\";\n", $0, file
478			}
479		' $file
480	done | sort
481	end_nodelist
482fi
483
484#
485# Print command-line shortcuts
486#
487if [ "$SHOW_CMDLINE" ]; then
488	printf '\n\t/*\n\t * Command-line shortcuts\n\t */\n'
489	shape=parallelogram color=black fillcolor=lightseagreen style=filled
490	begin_nodelist "$shape" "$color" "$fillcolor" "$style"
491	for file in */INDEX; do
492		awk -v item="${file%%/*}" '
493			/^menu_selection="/ {
494				sub(/^.*="/, "")
495				sub(/\|.*/, "")
496				printf "\t\t\"bsdconfig %s\"", $0
497				printf " [ label = \"%s\" ];\n", $0
498			}
499		' $file
500	done
501	end_nodelist
502fi
503
504#
505# Print command-line shortcut relationships
506#
507if [ "$SHOW_CMDLINE" ]; then
508	printf '\n\t/*\n\t * Command-line shortcut relationships\n\t */\n'
509	shape=box color=black fillcolor=lightseagreen style=filled
510	begin_nodelist "$shape" "$color" "$fillcolor" "$style"
511	print_node edge "headport = w" "weight = 100.0"
512	print_node edge "style = bold" "color = $fillcolor"
513	for file in */INDEX; do
514		awk -v item="${file%%/*}" \
515		    -v node_fillcolor="$node_fillcolor" \
516		    -v edge_color="$edge_color" \
517		'
518			/^menu_selection="/ {
519				sub(/^.*="/, "")
520				sub(/"$/, "")
521
522				if ( ! $0 ) next
523
524				split($0, menusel, "|")
525				if ( menusel[2] !~ "^/" )
526					sub(/^/, item "/", menusel[2])
527
528				printf "\t\t\"bsdconfig %s\" -> \"%s\";\n",
529				       menusel[1], menusel[2]
530			}
531		' $file
532	done
533	end_nodelist
534fi
535
536#
537# Print clusters
538#
539bgcolor_bsdconfig="lightyellow"
540bgcolor_includes="gray98"
541bgcolor_menuitem="aliceblue"
542bgcolor_shortcuts="honeydew"
543printf '\n\t/*\n\t * Clusters\n\t */\n'
544printf '\tsubgraph "cluster_bsdconfig" {\n'
545printf '\t\tbgcolor = "%s";\n' "$bgcolor_bsdconfig"
546printf '\t\tlabel = "bsdconfig(8)";\n'
547printf '\t\ttooltip = "bsdconfig(8)";\n'
548print_node "bsdconfig"
549if [ "$SHOW_INCLUDES" ]; then
550	printf '\t\tsubgraph "cluster_includes" {\n'
551	printf '\t\t\tbgcolor = "%s";\n' "$bgcolor_includes"
552	printf '\t\t\tlabel = "%s";\n' "$msg_includes"
553
554	for include_file in $include_file_list; do
555		echo $include_file
556	done | awk -v bgcolor="$bgcolor_bsdconfig" '
557	BEGIN { created = 0 }
558	function end_subgraph() { printf "\t\t\t};\n" }
559	( $0 !~ "/" ) {
560		if ( ! created )
561		{
562			printf "\t\t\tsubgraph \"%s\" {\n",
563			       "cluster_bsdconfig_includes"
564			printf "\t\t\t\tbgcolor = \"%s\";\n", bgcolor
565			printf "\t\t\t\tlabel = \"bsdconfig\";\n"
566			created++
567		}
568		printf "\t\t\t\t\"%s\";\n", $1
569	}
570	END { created && end_subgraph() }'
571
572	for include_file in $include_file_list; do
573		echo $include_file
574	done | awk '
575	BEGIN { created = 0 }
576	function end_subgraph() { printf "\t\t\t};\n" }
577	( $0 ~ "/" ) {
578		include_dir_tmp = $1
579		sub("/[^/]*$", "", include_dir_tmp)
580		gsub(/[^[:alnum:]_]/, "_", include_dir_tmp)
581
582		if ( created && include_dir != include_dir_tmp )
583		{
584			end_subgraph()
585			created = 0
586		}
587
588		if ( ! created )
589		{
590			include_dir = include_dir_tmp
591			printf "\t\t\tsubgraph \"cluster_%s_includes\" {\n",
592			       include_dir
593			printf "\t\t\t\tbgcolor = \"white\";\n"
594			printf "\t\t\t\tlabel = \"%s\";\n", include_dir
595			created++
596		}
597
598		printf "\t\t\t\t\"%s\";\n", $1
599	}
600	END { created && end_subgraph() }'
601
602	printf '\t\t};\n'
603fi
604end_nodelist
605for INDEX in */INDEX; do
606	menu_title=
607	menu_help=
608	f_include_lang "$INDEX"
609
610	item="${INDEX%%/*}"
611	printf '\tsubgraph "cluster_%s" {\n' "$item"
612
613	case "$item" in
614	[0-9][0-9][0-9].*) bgcolor="$bgcolor_menuitem" ;;
615	*) bgcolor="$bgcolor_shortcuts"
616	esac
617	printf '\t\tbgcolor = "%s";\n' "$bgcolor"
618	if [ "$menu_title" ]; then
619		printf '\t\tlabel = "%s\\n\\"%s\\"";\n' "$item" "$menu_title"
620	else
621		printf '\t\tlabel = "%s";\n' "$item"
622	fi
623	printf '\t\ttooltip = "%s";\n' "${menu_help:-$item}"
624
625	program_list=$(
626		for program in \
627			$menu_program_list \
628			$submenu_program_list \
629			$cmd_program_list \
630		; do
631			echo "$program"
632		done | sort -u
633	)
634	for program in $program_list; do
635		case "$program" in "$item"/*)
636			print_node "$program" "label = \"${program#*/}\""
637		esac
638	done
639
640	if [ "$SHOW_INCLUDES" ]; then
641		item_include_list=
642		[ -d "$item/include" ] &&
643			item_include_list=$( find "$item/include" -type f )
644		item_include_list=$(
645			for item_include in $item_include_list; do
646				for include_file in $include_file_list; do
647					[ "$item_include" = "$include_file" ] ||
648						continue
649					echo "$item_include"; break
650				done
651			done
652		)
653		if [ "$item_include_list" ]; then
654			printf '\t\tsubgraph "cluster_%s_includes" {\n' "$item"
655			printf '\t\t\tbgcolor = "%s";\n' "$bgcolor_includes"
656			printf '\t\t\tlabel = "%s";\n' "$msg_includes"
657		fi
658		for item_include in $item_include_list; do
659			printf '\t\t\t"%s";\n' "$item_include"
660		done
661		[ "$item_include_list" ] && printf '\t\t};\n'
662	fi
663
664	if [ "$SHOW_CMDLINE" ]; then
665		printf '\t\tsubgraph "cluster_%s_shortcuts" {\n' "$item"
666		printf '\t\t\tbgcolor = "%s";\n' "$bgcolor_shortcuts"
667		printf '\t\t\tlabel = "%s";\n' "$msg_shortcuts"
668		awk '/^menu_selection="/ {
669			sub(/^.*="/, "")
670			sub(/\|.*/, "")
671			printf "\t\t\t\"bsdconfig %s\";\n", $0
672		}' "$INDEX"
673		printf '\t\t};\n'
674	fi
675
676	end_nodelist
677done
678
679printf '\n};\n'
680
681################################################################################
682# END
683################################################################################
684