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