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$
28#
29############################################################ INCLUDES
30
31# Prevent common.subr from auto initializing debugging (this is not an inter-
32# active utility so does not require 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			$BSDCFG_SHARE/script.subr \
253		; do
254			[ -e "$file" ] && echo $file
255		done | sort -u
256	)
257
258	#
259	# Build list of includes used by the above files
260	#
261	include_file_list=
262	for file in $file_list; do
263		include_file_list="$include_file_list $(
264			awk "$print_includes_awk" $file
265		)"
266	done
267
268	#
269	# Sort the list of includes and remove duplicate entries
270	#
271	include_file_list=$(
272		for include_file in $include_file_list; do
273			echo "$include_file"
274		done | sort -u
275	)
276
277	#
278	# Search previously-discovered include files for further includes
279	#
280	before="$include_file_list"
281	while :; do
282		for file in $include_file_list; do
283			include_file_list="$include_file_list $(
284				awk "$print_includes_awk" $BSDCFG_SHARE/$file
285			)"
286		done
287
288		#
289		# Sort list of includes and remove duplicate entries [again]
290		#
291		include_file_list=$(
292			for include_file in $include_file_list; do
293				echo "$include_file"
294			done | sort -u
295		)
296
297		[ "$include_file_list" = "$before" ] && break
298		before="$include_file_list"
299	done
300fi
301
302#
303# Start the directional-graph (digraph) output
304#
305printf 'strict digraph "" { // Empty name to prevent SVG Auto-Tooltip\n'
306label_format="$msg_graph_label_with_command"
307[ "$SHOW_GRAPH_LABEL_DATE" ] &&
308	label_format="$msg_graph_label_with_command_and_date"
309lang="${LANG:-$LC_ALL}"
310printf "\n\tlabel = \"$label_format\"\n" \
311       "${lang:+LANG=${lang%%[$IFS]*} }bsdconfig $pgm${ARGV:+ $ARGV}" \
312       "$( date +"%c %Z" )"
313
314#
315# Print graph-specific properties
316#
317printf '\n\t/*\n\t * Graph setup and orientation\n\t */\n'
318printf '\tlabelloc = top;\t\t// display above label at top of graph\n'
319printf '\trankdir = LR;\t\t// create ranks left-to-right\n'
320printf '\torientation = portrait;\t// default\n'
321printf '\tratio = fill;\t\t// approximate aspect ratio\n'
322printf '\tcenter = 1;\t\t// center drawing on page\n'
323
324#
325# Perform edge-concentration when displaying a lot of information
326#
327# NOTE: This is disabled because dot(1) version 2.28.0 (current) and older have
328#       a bug that causes a crash when rankdir = LR and concentrate = true
329#
330# NOTE: Do not re-enable until said bug is fixed in some future revision.
331#
332#[ "$SHOW_INCLUDES" -a "$SHOW_CMDLINE" ] &&
333#	printf '\tconcentrate = true;\t// enable edge concentrators\n'
334
335#
336# Print font details for graph/cluster label(s)
337#
338printf '\n\t/*\n\t * Font details for graph/cluster label(s)\n\t */\n'
339printf '\tfontname = "Times-Italic";\n'
340printf '\tfontsize = 14;\n'
341
342#
343# Print default node attributes
344#
345printf '\n\t/*\n\t * Default node attributes\n\t */\n'
346printf '\tnode [\n'
347printf '\t\tfontname = "Times-Roman",\n'
348printf '\t\tfontsize = 12,\n'
349printf '\t\twidth = 2.5, // arbitrary minimum width for all nodes\n'
350printf '\t\tfixedsize = true, // turn minimum width into exact width\n'
351printf '\t];\n'
352
353#
354# Print top-level item(s)
355#
356printf '\n\t/*\n\t * bsdconfig(8)\n\t */\n'
357shape=circle color=black fillcolor=yellow style=filled
358begin_nodelist "$shape" "$color" "$fillcolor" "$style"
359print_node "bsdconfig" "fontname = \"Times-Bold\"" "fontsize = 16"
360end_nodelist
361
362#
363# Print menus
364#
365printf '\n\t/*\n\t * Menu items\n\t */\n'
366shape=box color=black fillcolor=lightblue style=filled
367begin_nodelist "$shape" "$color" "$fillcolor" "$style"
368for menu_program in $menu_program_list; do
369	print_node "$menu_program" "label = \"${menu_program#*/}\""
370done
371end_nodelist
372
373#
374# Print sub-menus
375#
376printf '\n\t/*\n\t * Sub-menu items\n\t */\n'
377shape=box color=black fillcolor=lightblue style=filled
378begin_nodelist "$shape" "$color" "$fillcolor" "$style"
379for submenu_program in $submenu_program_list; do
380	print_node "$submenu_program" "label = \"${submenu_program#*/}\""
381done
382end_nodelist
383
384#
385# Print menu relationships
386#
387printf '\n\t/*\n\t * Menu item relationships\n\t */\n'
388shape=box color=black fillcolor=lightblue style=filled edge_color=blue
389begin_nodelist "$shape" "$color" "$fillcolor" "$style"
390print_node edge "penwidth = 5.0" "style = bold" "color = $edge_color"
391for menu_program in $menu_program_list; do
392	print_node2 "bsdconfig" "$menu_program"
393done
394end_nodelist
395
396#
397# Print sub-menu relationships
398#
399printf '\n\t/*\n\t * Sub-menu item relationships\n\t */\n'
400shape=box color=black fillcolor=lightblue style=filled edge_color=blue
401begin_nodelist "$shape" "$color" "$fillcolor" "$style"
402# Lock sub-menu headport to the West (unless `-c' was passed)
403[ "$SHOW_CMDLINE" -o ! "$SHOW_INCLUDES" ] && print_node edge "headport = w"
404print_node edge "style = bold" "color = $edge_color"
405for submenu_program in $submenu_program_list; do
406	for menu_program in $menu_program_list; do
407		case "$menu_program" in
408		[0-9][0-9][0-9].*/*) : fall-through ;;
409		*) continue # Not a menu item
410		esac
411
412		# Continue if program directories do not match
413		[ "${menu_program%%/*}" = "${submenu_program%%/*}" ] ||
414			continue
415
416		print_node2 "$menu_program" "$submenu_program"
417		break
418	done
419done
420end_nodelist
421
422#
423# [Optionally] Print include files
424#
425if [ "$SHOW_INCLUDES" ]; then
426	printf '\n\t/*\n\t * Include files\n\t */\n'
427	shape=oval color=black fillcolor=white style=filled
428	begin_nodelist "$shape" "$color" "$fillcolor" "$style"
429	printf '\t\tconstraint = false;\n'
430	for include_file in $include_file_list; do
431		print_node "$include_file" \
432		           "label = \"${include_file##*/}\""
433	done
434	end_nodelist
435fi
436
437#
438# [Optionally] Print f_include() usage/relationships
439#
440if [ "$SHOW_INCLUDES" ]; then
441	printf '\n\t/*\n\t * Include usage\n\t */\n'
442	shape=oval color=black fillcolor=white style=filled edge_color=grey
443	begin_nodelist "$shape" "$color" "$fillcolor" "$style"
444	print_node edge "style = dashed" "color = $edge_color"
445	#print_node edge "label = \"\\T\"" "fontsize = 9"
446		# NOTE: Edge labels are buggy on large graphs
447	file_list=$(
448		for file in \
449			$BSDCONFIG \
450			$menu_program_list \
451			$submenu_program_list \
452			$cmd_program_list \
453			$include_file_list \
454		; do
455			[ -f "$BSDCFG_SHARE/$file" ] &&
456				echo $BSDCFG_SHARE/$file
457			[ -e "$file" ] && echo $file
458		done | sort -u
459	)
460	for file in $file_list; do
461		# Skip binary files and text files that don't use f_include()
462		grep -qlI f_include $file || continue
463
464		awk \
465			-v file="${file#$BSDCFG_SHARE/}" \
466			-v bsdconfig="$BSDCONFIG" \
467		'
468			BEGIN { regex = "^f_include \\$BSDCFG_SHARE/" }
469			( $0 ~ regex ) {
470				sub(regex, "")
471				if ( file == bsdconfig ) sub(".*/", "", file)
472				printf "\t\t\"%s\" -> \"%s\";\n", $0, file
473			}
474		' $file
475	done | sort
476	end_nodelist
477fi
478
479#
480# Print command-line shortcuts
481#
482if [ "$SHOW_CMDLINE" ]; then
483	printf '\n\t/*\n\t * Command-line shortcuts\n\t */\n'
484	shape=parallelogram color=black fillcolor=lightseagreen style=filled
485	begin_nodelist "$shape" "$color" "$fillcolor" "$style"
486	for file in */INDEX; do
487		awk -v item="${file%%/*}" '
488			/^menu_selection="/ {
489				sub(/^.*="/, "")
490				sub(/\|.*/, "")
491				printf "\t\t\"bsdconfig %s\"", $0
492				printf " [ label = \"%s\" ];\n", $0
493			}
494		' $file
495	done
496	end_nodelist
497fi
498
499#
500# Print command-line shortcut relationships
501#
502if [ "$SHOW_CMDLINE" ]; then
503	printf '\n\t/*\n\t * Command-line shortcut relationships\n\t */\n'
504	shape=box color=black fillcolor=lightseagreen style=filled
505	begin_nodelist "$shape" "$color" "$fillcolor" "$style"
506	print_node edge "headport = w" "weight = 100.0"
507	print_node edge "style = bold" "color = $fillcolor"
508	for file in */INDEX; do
509		awk -v item="${file%%/*}" \
510		    -v node_fillcolor="$node_fillcolor" \
511		    -v edge_color="$edge_color" \
512		'
513			/^menu_selection="/ {
514				sub(/^.*="/, "")
515				sub(/"$/, "")
516
517				if ( ! $0 ) next
518
519				split($0, menusel, "|")
520				if ( menusel[2] !~ "^/" )
521					sub(/^/, item "/", menusel[2])
522
523				printf "\t\t\"bsdconfig %s\" -> \"%s\";\n",
524				       menusel[1], menusel[2]
525			}
526		' $file
527	done
528	end_nodelist
529fi
530
531#
532# Print clusters
533#
534bgcolor_bsdconfig="lightyellow"
535bgcolor_includes="gray98"
536bgcolor_menuitem="aliceblue"
537bgcolor_shortcuts="honeydew"
538printf '\n\t/*\n\t * Clusters\n\t */\n'
539printf '\tsubgraph "cluster_bsdconfig" {\n'
540printf '\t\tbgcolor = "%s";\n' "$bgcolor_bsdconfig"
541printf '\t\tlabel = "bsdconfig(8)";\n'
542printf '\t\ttooltip = "bsdconfig(8)";\n'
543print_node "bsdconfig"
544end_nodelist
545if [ "$SHOW_INCLUDES" ]; then
546	for include_file in $include_file_list; do
547		echo $include_file
548	done | awk \
549		-v bgcolor="$bgcolor_bsdconfig" \
550		-v msg_subroutines="$msg_subroutines" \
551	'
552		BEGIN { created = 0 }
553		function end_subgraph() { printf "\t};\n" }
554		( $0 !~ "/" ) {
555			if ( ! created )
556			{
557				printf "\tsubgraph \"%s\" {\n",
558				       "cluster_bsdconfig_includes"
559				printf "\t\tbgcolor = \"%s\";\n", bgcolor
560				printf "\t\tlabel = \"bsdconfig %s\";\n",
561				       msg_subroutines
562				created++
563			}
564			printf "\t\t\"%s\";\n", $1
565		}
566		END { created && end_subgraph() }
567	' # END-QUOTE
568
569	for include_file in $include_file_list; do
570		echo $include_file
571	done | awk -v msg_subroutines="$msg_subroutines" '
572	BEGIN { created = 0 }
573	function end_subgraph() { printf "\t};\n" }
574	( $0 ~ "/" ) {
575		include_dir_tmp = $1
576		sub("/[^/]*$", "", include_dir_tmp)
577		gsub(/[^[:alnum:]_]/, "_", include_dir_tmp)
578
579		if ( created && include_dir != include_dir_tmp )
580		{
581			end_subgraph()
582			created = 0
583		}
584
585		if ( ! created )
586		{
587			include_dir = include_dir_tmp
588			printf "\tsubgraph \"cluster_%s_includes\" {\n",
589			       include_dir
590			printf "\t\tbgcolor = \"thistle\";\n"
591			printf "\t\tlabel = \"%s %s\";\n", include_dir,
592			       msg_subroutines
593			created++
594		}
595
596		printf "\t\t\"%s\";\n", $1
597	}
598	END { created && end_subgraph() }'
599fi
600for INDEX in */INDEX; do
601	menu_title=
602	menu_help=
603	f_include_lang "$INDEX"
604
605	item="${INDEX%%/*}"
606	printf '\tsubgraph "cluster_%s" {\n' "$item"
607
608	case "$item" in
609	[0-9][0-9][0-9].*) bgcolor="$bgcolor_menuitem" ;;
610	*) bgcolor="$bgcolor_shortcuts"
611	esac
612	printf '\t\tbgcolor = "%s";\n' "$bgcolor"
613	if [ "$menu_title" ]; then
614		printf '\t\tlabel = "%s\\n\\"%s\\"";\n' "$item" "$menu_title"
615	else
616		printf '\t\tlabel = "%s";\n' "$item"
617	fi
618	printf '\t\ttooltip = "%s";\n' "${menu_help:-$item}"
619
620	program_list=$(
621		for program in \
622			$menu_program_list \
623			$submenu_program_list \
624			$cmd_program_list \
625		; do
626			echo "$program"
627		done | sort -u
628	)
629	for program in $program_list; do
630		case "$program" in "$item"/*)
631			print_node "$program" "label = \"${program#*/}\""
632		esac
633	done
634
635	if [ "$SHOW_INCLUDES" ]; then
636		item_include_list=
637		[ -d "$item/include" ] &&
638			item_include_list=$( find "$item/include" -type f )
639		item_include_list=$(
640			for item_include in $item_include_list; do
641				for include_file in $include_file_list; do
642					[ "$item_include" = "$include_file" ] ||
643						continue
644					echo "$item_include"; break
645				done
646			done
647		)
648		if [ "$item_include_list" ]; then
649			printf '\t\tsubgraph "cluster_%s_includes" {\n' "$item"
650			printf '\t\t\tbgcolor = "%s";\n' "$bgcolor_includes"
651			printf '\t\t\tlabel = "%s";\n' "$msg_includes"
652		fi
653		for item_include in $item_include_list; do
654			printf '\t\t\t"%s";\n' "$item_include"
655		done
656		[ "$item_include_list" ] && printf '\t\t};\n'
657	fi
658
659	if [ "$SHOW_CMDLINE" ]; then
660		printf '\t\tsubgraph "cluster_%s_shortcuts" {\n' "$item"
661		printf '\t\t\tbgcolor = "%s";\n' "$bgcolor_shortcuts"
662		printf '\t\t\tlabel = "%s";\n' "$msg_shortcuts"
663		awk '/^menu_selection="/ {
664			sub(/^.*="/, "")
665			sub(/\|.*/, "")
666			printf "\t\t\t\"bsdconfig %s\";\n", $0
667		}' "$INDEX"
668		printf '\t\t};\n'
669	fi
670
671	end_nodelist
672done
673
674printf '\n}\n'
675
676################################################################################
677# END
678################################################################################
679