1test -n "$_bsda_obj_" && return 0
2readonly _bsda_obj_=1
3set -f
4
5#
6# This file contains helper functions for creating object oriented
7# shell scripts.
8#
9# The most significant function is bsda:obj:createClass(), which basically
10# creates a class, including getters, setters, a constructor, a destructor
11# and a copy function. It also creates serialisation methods.
12#
13
14#
15# The stack counter that holds the number of methods that currently
16# use the return stack.
17#
18bsda_obj_callStackCount=0
19
20#
21# This is a prefix to every object ID.
22#
23readonly bsda_obj_frameworkPrefix=BSDA_OBJ_
24
25#
26# The UID to use for creating new objects. When forking a session use the
27# bsda:obj:fork() function to update this value in the forked process.
28#
29#bsda_obj_uid
30
31#
32# The copy method sets this temporarily to tell the constructor not to call
33# an init method.
34#
35#bsda_obj_doCopy
36
37#
38# A list of objects with a cleanup function, used by the EXIT trap handler
39# to allow objects the destruction of acquired resources.
40#
41#bsda_obj_freeOnExit
42
43#
44# A list of available file descriptors for bsda:obj:getDesc().
45#
46bsda_obj_desc=3,4,5,6,7,8,9,
47
48#
49# Creates a new class, i.e. a constructor, destructor, getters, setters
50# and so forth.
51#
52# So all that is left to be done are the methods.
53#
54# The following static methods are reserved:
55# - deserialise()
56# - isInstance()
57# - getAttributes()
58# - getMethods()
59#
60# The following methods are reserved:
61# - copy()
62# - delete()
63# - dump()
64# - serialise()
65#
66# The following class prefix bound static attributes are reserved:
67# - private
68# - public
69#
70# The following session, class and process bound static attributes are
71# reserved:
72#	nextId
73#
74# @param 1
75#	The first parameter is the name of the class.
76# @param @
77#	A description of the class to create.
78#
79#	All parameters following the class name make up a list of identifiers
80#	for attributes and methods. Every identifier has a prefix, the
81#	following formats are supported:
82#
83#	- `-:<name>`:
84#	  A plain attribute.
85#	- `r:[<scope>:]<name>`
86#	  An attribute with a get method named `$obj.getName()`.
87#	- `w:[<scope>:]<name>`
88#	  An attribute with a getter and setter named `$obj.getName()`
89#	  and `$obj.setName()`.
90#	- `x:[<scope>:]<name>`
91#	  A user implemented method named `$obj.name()`, needs to
92#	  be implemented as 'class.name()`.
93#	- `i:[<scope>:]<name>`
94#	  An initialisation method called by the constructor. The
95#	  scope only affects a direct call of the method, the constructor
96#	  is always public.
97#	- `c:[<scope>:]<name>`
98#	  A cleanup method called by the destructor. The scope only
99#	  affects direct calls of the method.
100#	- `a:[<scope>:]<name>[=<class>]
101#	  An aggregation. Aggregations are special attributes,
102#	  referencing other objects, that are automatically
103#	  deleted, copied and serialised along with an instance
104#	  of a class.
105#	  An aggregation attribute has a getter named `$obj.name()`.
106#	  An optional class name is used to determine if the default
107#	  `$obj.copy()` and `$obj.serialise()` methods can be created.
108#	  If the class is omitted, they never are.
109#
110#	With these parameters a constructor and a destructor will be built.
111#	It is important that all used attributes are listed, or the copy,
112#	delete and serialisation methods will not work as expected.
113#
114#	Everything that is not recognized as an identifier is treated as a
115#	comment.
116#
117#	The prefixes r, w, x, i, c and a can be followed by a scope operator
118#	public or private.
119#
120#	The constructor can be called in the following way:
121#		<class> <refname>
122#	The class name acts as the name of the constructor, <refname> is the
123#	name of the variable to store the reference to the new object in.
124#
125#	The destructor can be called in the following way:
126#		$reference.delete
127#	This will destroy all methods and attributes.
128#
129#	A getter takes the name of the variable to store the value in as the
130#	first argument, if this is ommitted, the value is written to stdout.
131#
132#	The copy method can be used to create a shallow copy of an object.
133#
134# @param bsda_obj_namespace
135#	The framework namespace to use when building a class. The impact is on
136#	the use of helper functions.
137# @retval 0
138#	On success
139# @retval 1
140#	If there is more than one init method (i:) specified
141# @retval 2
142#	If there is more than one cleanup method (c:) specified
143# @retval 3
144#	If there was an unknown scope operator
145# @retval 4
146#	If an aggregation with an undefined class occurred
147# @retval 5
148#	No class name was given
149# @retval 6
150#	Forbidden characters in attribute name, [a-zA-Z0-9_] are allowed
151#
152bsda:obj:createClass() {
153	local IFS class methods method attributes getters setters arg
154	local getter setter attribute init clean
155	local namespacePrefix classPrefix instancePattern
156	local previousMethod scope
157	local aggregations aggregation classname
158	local amethods has_copy has_serialise
159
160	# Default framework namespace.
161	: ${bsda_obj_namespace='bsda:obj'}
162
163	# Get the class name and shift it off the parameter list.
164	if [ -z "$1" ]; then
165		echo "bsda:obj:createClass: ERROR: No class name supplied!" 1>&2
166		return 5
167	fi
168	class="$1"
169	shift
170
171	IFS=$'\n'
172
173	# There are some default methods.
174	methods="delete${IFS}dump${IFS}"
175	attributes=
176	getters=
177	setters=
178	init=
179	clean=
180	has_copy=1
181	has_serialise=1
182
183	# Parse arguments.
184	for arg in "$@"; do
185		case "$arg" in
186			x:*)
187				methods="$methods${arg#x:}$IFS"
188			;;
189			-:*)
190				attributes="$attributes${arg#-:}$IFS"
191			;;
192			r:*)
193				attributes="$attributes${arg##*:}$IFS"
194				getters="$getters${arg#r:}$IFS"
195			;;
196			w:*)
197				attributes="$attributes${arg##*:}$IFS"
198				getters="$getters${arg#w:}$IFS"
199				setters="$setters${arg#w:}$IFS"
200			;;
201			i:*)
202				if [ -n "$init" ]; then
203					echo "bsda:obj:createClass: ERROR: $class: More than one init method was supplied!" 1>&2
204					return 1
205				fi
206				methods="$methods${arg#i:}$IFS"
207				init="$class.${arg##*:}"
208			;;
209			c:*)
210				if [ -n "$clean" ]; then
211					echo "bsda:obj:createClass: ERROR: $class: More than one cleanup method was supplied!" 1>&2
212					return 2
213				fi
214				methods="$methods${arg#c:}$IFS"
215				clean="$class.${arg##*:}"
216			;;
217			a:*)
218				aggregations="$aggregations${arg#a:}$IFS"
219			;;
220			*)
221				# Assume everything else is a comment.
222			;;
223		esac
224	done
225
226	# Create aggregations.
227	aggregation="$aggregations"
228	aggregations=
229	for aggregation in $aggregation; do
230		# Get class
231		case "$aggregation" in
232		*=*)
233			classname="${aggregation#*=}"
234			aggregation="${aggregation%%=*}"
235		;;
236		*)
237			classname=
238		;;
239		esac
240
241		# Get scope of the getter method
242		case "$aggregation" in
243		*:*)
244			scope="${aggregation%%:*}"
245			aggregation="${aggregation#*:}"
246		;;
247		*)
248			scope=public
249		;;
250		esac
251
252		aggregations="$aggregations$aggregation$IFS"
253		attributes="$attributes$aggregation$IFS"
254		methods="$methods$scope:$aggregation$IFS"
255
256		eval "$class.$aggregation() {
257			if [ -n \"\$1\" ]; then
258				eval \"\$1=\\\"\\\$\${this}$aggregation\\\"\"
259			else
260				eval \"echo \\\"\\\$\${this}$aggregation\\\"\"
261			fi
262		}"
263
264		if [ -z "$classname" ]; then
265			has_copy=
266			has_serialise=
267		elif [ "$classname" != "$class" ]; then
268			if ! $classname.getMethods amethods 2>&-; then
269				echo "bsda:obj:createClass: ERROR: $class: Aggregation with undefined class: $classname" 1>&2
270				return 4
271			fi
272			$classname.getMethods \
273			| /usr/bin/grep -qFx public:copy || has_copy=
274			$classname.getMethods \
275			| /usr/bin/grep -qFx public:serialise || has_serialise=
276		fi
277	done
278
279	# Remove duplicated attributes.
280	attribute="$attributes"
281	attributes=
282	for attribute in $(echo "$attribute" | /usr/bin/awk '!a[$0]++'); do
283		attributes="$attributes$attribute$IFS"
284		# Verify attribute names.
285		if ! echo "$attribute" | /usr/bin/grep -qx '[a-zA-Z0-9_]*'; then
286			echo "bsda:obj:createClass: ERROR: $class: Attributes must only contain the characters [a-zA-Z0-9_]: $attribute" 1>&2
287			return 6
288		fi
289	done
290
291	# Only classes without a custom destructor get copy() and
292	# serialise() members.
293	if [ -z "$clean" ] && [ -n "$has_copy" ]; then
294		methods="${methods}copy${IFS}"
295	fi
296	if [ -z "$clean" ] && [ -n "$has_serialise" ]; then
297		methods="${methods}serialise${IFS}"
298	fi
299
300	# Create reference prefix. The Process id is added to the prefix when
301	# an object is created.
302	namespacePrefix="${bsda_obj_frameworkPrefix}$(echo "$bsda_obj_namespace" | /usr/bin/tr ':' '_')_"
303	classPrefix="${namespacePrefix}$(echo "$class" | /usr/bin/tr ':' '_')_"
304
305	# Set the instance match pattern.
306	instancePattern="${classPrefix}[0-9a-f]*[0-9]_"
307
308	# Create getters.
309	for method in $getters; do
310		getter="${method##*:}"
311		attribute="$getter"
312		getter="get$(echo "${getter%%${getter#?}}" | /usr/bin/tr '[:lower:]' '[:upper:]')${getter#?}"
313
314		eval "$class.$getter() {
315			if [ -n \"\$1\" ]; then
316				eval \"\$1=\\\"\\\$\${this}$attribute\\\"\"
317			else
318				eval \"echo \\\"\\\$\${this}$attribute\\\"\"
319			fi
320		}"
321
322		# Check for scope operator.
323		if [ "${method%:*}" != "$method" ]; then
324			# Add scope operator to the getter name.
325			getter="${method%:*}:$getter"
326		fi
327		# Add the getter to the list of methods.
328		methods="$methods$getter$IFS"
329	done
330
331	# Create setters.
332	for method in $setters; do
333		setter="${method##*:}"
334		attribute="$setter"
335		setter="set$(echo "${setter%%${setter#?}}" | /usr/bin/tr '[:lower:]' '[:upper:]')${setter#?}"
336
337		eval "$class.$setter() {
338			setvar \"\${this}$attribute\" \"\$1\"
339		}"
340
341		# Check for scope operator.
342		if [ "${method%:*}" != "$method" ]; then
343			# Add scope operator to the getter name.
344			setter="${method%:*}:$setter"
345		fi
346		# Add the setter to the list of methods.
347		methods="$methods$setter$IFS"
348	done
349
350	# Add implicit public scope to methods.
351	method="$methods"
352	methods=
353	for method in $method; do
354		# Check the scope.
355		case "${method%:*}" in
356			$method)
357				# There is no scope operator, add public.
358				methods="${methods}public:$method$IFS"
359			;;
360			public | private)
361				# The accepted scope operators.
362				methods="$methods$method$IFS"
363			;;
364			*)
365				# Everything else is not accepted.
366				echo "bsda:obj:createClass: ERROR: $class: Unknown scope operator: ${method%:*}" 1>&2
367				return 3
368			;;
369		esac
370	done
371
372
373	# If a method is defined more than once, the widest scope wins.
374	# Go through the methods sorted by method name.
375	previousMethod=
376	method="$methods"
377	methods=
378	scope=
379	for method in $(echo "$method" | /usr/bin/sort -t: -k2); do
380		# Check whether the previous and the current method were the
381		# same.
382		if [ "$previousMethod" != "${method##*:}" ]; then
383			# If all scopes of this method have been found,
384			# store it in the final list.
385			methods="$methods${previousMethod:+$scope:$previousMethod$IFS}"
386			scope="${method%:*}"
387		else
388			# Widen the scope if needed.
389			case "${method%:*}" in
390			public)
391				scope=public
392			;;
393			esac
394		fi
395
396		previousMethod="${method##*:}"
397	done
398	# Add the last method (this never happens in the loop).
399	methods="$methods${previousMethod:+$scope:$previousMethod$IFS}"
400
401	#
402	# Store access scope checks for each scope in the class context.
403	# Note that at the time this is run the variables class and this
404	# still belong to the the caller.
405	# These definitions are repeatedly subject to eval calls, hence
406	# the different escape depth which makes sure the variables
407	# are resolved at the right stage.
408	#
409
410	# Private methods allow the following kinds of access:
411	# - Same class
412	#   Access is allowed by all objects with the same class.
413	setvar ${classPrefix}private "
414		if [ \\\"\\\$class\\\" != \\\"$class\\\" ]; then
415			echo \\\"$class.\${method##*:}: Terminated because of access attempt to a private method\\\${class:+ by \\\$class}!\\\" 1>&2
416			return 255
417		fi
418	"
419	# Public methods allow unchecked access.
420	setvar ${classPrefix}public ''
421
422	# Create constructor.
423	eval "$class() {
424		local this class
425		class=$class
426
427		# Create object reference.
428		this=\"${classPrefix}${bsda_obj_uid}_\${${classPrefix}${bsda_obj_uid}_nextId:-0}_\"
429
430		# Increase the object id counter.
431		${classPrefix}${bsda_obj_uid}_nextId=\$((\$${classPrefix}${bsda_obj_uid}_nextId + 1))
432
433		# Create method instances.
434		$bsda_obj_namespace:createMethods $class $classPrefix \"\$this\" '$methods'
435
436		${clean:+bsda_obj_freeOnExit=\"\$bsda_obj_freeOnExit\$this$IFS\"}
437
438		# If this object construction is part of a copy() call,
439		# this constructor is done.
440		if [ -n \"\$bsda_obj_doCopy\" ]; then
441			# Return the object reference.
442			if [ -n \"\$1\" ]; then
443				setvar \"\$1\" \"\$this\"
444			else
445				echo \"\$this\"
446			fi
447			return 0
448		fi
449
450		local _return _var
451		_var=\"\$1\"
452		${init:+
453		# Cast the reference variable from the parameters.
454		shift
455		local caller
456		bsda:obj:callerSetup
457		# Call the init method.
458		$init \"\$@\"
459		_return=\$?
460		bsda:obj:callerFinish
461		# Destroy the object on failure.
462		if [ \$_return -ne 0 ]; then
463			\"\$this\".delete
464			return \$_return
465		fi
466		}
467
468		# Return the object reference.
469		if [ -n \"\$_var\" ]; then
470			setvar \"\$_var\" \"\$this\"
471		else
472			echo \"\$this\"
473		fi
474		return 0
475	}"
476
477	# Create destructor.
478	eval "$class.delete() {
479		${clean:+
480		$clean \"\$@\" || return \$?
481		# Unregister cleanup function from EXIT trap
482		local nl
483		nl='$IFS'
484		bsda_obj_freeOnExit=\"\${bsda_obj_freeOnExit%%\$this*\}\${bsda_obj_freeOnExit#*\$this\$nl\}\"
485		}
486
487		${aggregations:+eval \"$(
488		for aggregation in $aggregations; do
489			echo \\\"\\\$\${this}$aggregation\\\".delete
490		done
491		)\"}
492
493		# Delete methods and attributes.
494		$bsda_obj_namespace:deleteMethods \"\$this\" '$methods'
495		$bsda_obj_namespace:deleteAttributes \"\$this\" '$attributes'
496	}"
497
498	# Prints an object in a human readable format.
499	eval "$class.dump() {
500		local result
501		result=\"$class@\$this {${attributes:+$IFS\$( (
502			$(for aggregation in $aggregations; do
503				echo "eval \"\\\"\\\$\${this}$aggregation\\\".dump var\""
504				echo "echo \"$aggregation=\$var\""
505			done)
506			$(for attribute in $attributes; do
507				for aggregation in $aggregations; do
508					test "$aggregation" = "$attribute" && continue 2
509				done
510				echo "getvar var \"\${this}$attribute\""
511				echo "echo \"$attribute='\$var'\""
512			done)
513		) | /usr/bin/sed 's/^/  /')$IFS}}\"
514		\"\$caller\".setvar \"\$1\" \"\$result\"
515	}"
516
517	# Create copy method.
518	eval "$class.copy() {
519		local bsda_obj_doCopy reference
520
521		bsda_obj_doCopy=1
522
523		# Create a new empty object.
524		$class reference
525
526		# Store the new object reference in the target variable.
527		\"\$caller\".setvar \"\$1\" \"\$reference\"
528
529		# For each attribute copy the value over to the
530		# new object.
531		${attributes:+eval \"$(
532		for attribute in $attributes; do
533			echo "\${reference}$attribute=\\\"\\\$\${this}$attribute\\\""
534		done
535		)\"}
536
537		${aggregations:+eval \"$(
538		for aggregation in $aggregations; do
539			echo "\\\"\\\$\${this}$aggregation\\\".copy \${reference}$aggregation"
540		done
541		)\"}
542	}"
543
544	# A serialise method.
545	eval "$class.serialise() {
546		local serialised svar
547
548		serialised=
549		$(for attribute in $attributes; do
550			echo "bsda:obj:serialiseVar svar \"\${this}$attribute\""
551			echo "serialised=\"\${serialised:+\$serialised;}\$svar\""
552		done)
553		serialised=\"\$serialised;$class.deserialise \$this\"
554
555		$(for aggregation in $aggregations; do
556			echo eval \"\\\"\\\$\${this}$aggregation\\\".serialise svar\"
557			echo 'serialised="$svar;$serialised"'
558		done)
559
560		\"\$caller\".setvar \"\$1\" \"\$serialised\"
561	}"
562
563	# A static deserialise method.
564	eval "$class.deserialise() {
565		# Create method instances.
566		$bsda_obj_namespace:createMethods $class $classPrefix \"\$1\" '$methods'
567		${clean:+
568		if [ -z \"\$bsda_obj_freeOnExit\" ] || \
569		   [ -n \"\${bsda_obj_freeOnExit%%*\$1*\}\" ]; then
570			bsda_obj_freeOnExit=\"\$bsda_obj_freeOnExit\$1$IFS\"
571		fi
572		}
573	}"
574
575	# A static type checker.
576	eval "$class.isInstance() {
577		case \"\$1\" in
578		$instancePattern)
579			return 0
580		;;
581		esac
582		return 1
583	}"
584
585	# A static method that returns the attributes of a class.
586	eval "$class.getAttributes() {
587		if [ -n \"\$1\" ]; then
588			setvar \"\$1\" '$attributes'
589		else
590			echo '$attributes'
591		fi
592	}"
593
594	# A static method that returns the methods of a class.
595	eval "$class.getMethods() {
596		if [ -n \"\$1\" ]; then
597			setvar \"\$1\" '$methods'
598		else
599			echo '$methods'
600		fi
601	}"
602}
603
604#
605# Delete all objects in a list of objects.
606#
607# If deleting an object fails, it has no influence on the subsequent
608# deletion of the following objects.
609#
610# @param @
611#	The list of objects
612# @return
613#	The bitwise OR product of all destructor return values
614#
615bsda:obj:delete[]() {
616	local obj ret
617	ret=0
618	for obj in "$@"; do
619		$obj.delete
620		ret=$((ret | $?))
621	done
622	return $ret
623}
624
625#
626# Returns an object reference to a serialised object.
627#
628# @param 1
629#	If this is the sole parameter, this is a serialised string of which
630#	the object reference should be output. In case of a second parameter
631#	this is the name of the variable to return the reference to the
632#	serialised object to.
633# @param 2
634#	The serialised string of which the reference should be returned.
635#
636bsda:obj:getSerializedId() {
637	if [ -n "$2" ]; then
638		setvar "$1" "${2##* }"
639	else
640		echo "${1##* }"
641	fi
642}
643
644#
645# Deserialises a serialised object and returns or outputs a reference to
646# said object.
647#
648# @param &1
649#	The name of the variable to store the deserialised object reference
650#	in. If empty the reference will be output to stdout.
651# @param 2
652#	If given this is the string to be serialised, otherwise it will be
653#	expected on stdin.
654#
655bsda:obj:deserialise() {
656	if [ $# -lt 2 ]; then
657		set -- "$1" "$(/bin/cat)"
658	fi
659
660	if [ -n "$1" ]; then
661		setvar "$1" "${2##* }"
662	else
663		echo "${2##* }"
664	fi
665
666	local IFS
667	IFS=$'\n'
668	eval "$2"
669}
670
671#
672# Filters the given string of serialised data to only contain the last
673# representation of each object.
674#
675# The order of objects may be subject to change.
676#
677# @param &1
678#	The name of the variable to store the resulting string in.
679#	If empty the string is output to stdout.
680# @param 2
681#	If given this is the serialised data, otherwise it will be
682#	expected on stdin.
683#
684bsda:obj:serialisedUniq() {
685	if [ -n "$1" ]; then
686		if [ -n "$2" ]; then
687			setvar "$1" "$(echo "$2" | /usr/bin/awk '{lines[$NF] = $0} END {for (line in lines) print lines[line]}')"
688		else
689			setvar "$1" "$(/usr/bin/awk '{lines[$NF] = $0} END {for (line in lines) print lines[line]}')"
690		fi
691	else
692		if [ -n "$2" ]; then
693			echo "$2" | /usr/bin/awk '{lines[$NF] = $0} END {for (line in lines) print lines[line]}'
694		else
695			/usr/bin/awk '{lines[$NF] = $0} END {for (line in lines) print lines[line]}'
696		fi
697	fi
698}
699
700#
701# Creates the methods to a new object from a class.
702#
703# This is achieved by creating a method wrapper that provides the
704# context variables this, class and caller.
705#
706# It works under the assumption, that methods are defined as:
707#	<class>.<method>()
708#
709# @param 1
710#	The class name.
711# @param 2
712#	The class prefix where the scope checks are stored.
713# @param 3
714#	The object reference.
715# @param 4
716#	A list of method names.
717#
718bsda:obj:createMethods() {
719	local IFS method scope
720	IFS=$'\n'
721	for method in $4; do
722		scope=${method%:*}
723		# Get scope check from class.
724		eval "scope=\"\$$2$scope\""
725		# Add method name to scope.
726		eval "scope=\"$scope\""
727		method=${method##*:}
728		eval "$3.$method() {
729			$scope
730			local caller
731			bsda:obj:callerSetup
732			local class this _return
733			class=$1
734			this=$3
735			$1.$method \"\$@\"
736			_return=\$?
737			bsda:obj:callerFinish
738			return \$_return
739		}"
740	done
741}
742
743#
744# Deletes methods from an object. This is intended to be used in a destructor.
745#
746# @param 1
747#	The object reference.
748# @param 2
749#	A list of method names.
750#
751bsda:obj:deleteMethods() {
752	local IFS method
753	IFS=$'\n'
754	for method in $2; do
755		method=${method##*:}
756		unset -f "$1.$method"
757	done
758}
759
760#
761# Deletes attributes from an object. This is intended to be used in a
762# destructor.
763#
764# This works under the assumption, that attributes are defined as:
765#	<reference>_<attribute>
766#
767# @param 1
768#	The object reference.
769# @param 2
770#	A list of attribute names.
771#
772bsda:obj:deleteAttributes() {
773	local IFS attribute
774	IFS=$'\n'
775	for attribute in $2; do
776		unset "${1}$attribute"
777	done
778}
779
780#
781# Setup the caller stack to store variables that should be overwritten
782# in the caller context upon exiting the method.
783#
784# This function is called by the wrapper around class instance methods.
785#
786# The bsda_obj_callStackCount counter is increased and and a stack count prefix
787# is created, which is used by bsda:obj:callerSetvar() to store variables
788# for functions in the caller context until bsda:obj:callerFinish() is
789# called.
790#
791# The call stack prefix is in the format 'bsda_obj_callStack_[0-9]+_'.
792#
793# @param caller
794#	Is set to the current stack count prefix.
795# @param bsda_obj_callStackCount
796#	Is incremented by 1 and used to create the caller variable.
797#
798bsda:obj:callerSetup() {
799	# Increment the call stack counter and create the caller prefix.
800	caller="bsda_obj_callStack_${bsda_obj_callStackCount}_"
801	bsda_obj_callStackCount=$(($bsda_obj_callStackCount + 1))
802
803	# Create a wrapper around bsda:obj:callerSetvar for access
804	# through the caller prefix.
805	eval "$caller.setvar() {
806		bsda:obj:callerSetvar \"\$@\"
807	}"
808
809	# Delete the given object when returning to the caller.
810	eval "$caller.delete() {
811		delete_${caller}=\"\$1.delete;\${delete_${caller}}\"
812	}"
813}
814
815#
816# Copy variables from the caller stack into the caller context and clean
817# the stack up.
818#
819# This function is called by the wrapper around class instance methods
820# after the actual method has terminated.
821#
822# @param caller
823#	The caller context prefix.
824# @param delete_${caller}
825#	The list of objects to delete when returning to the caller.
826# @param setvars_${caller}
827#	The list of variables to copy into the caller context.
828# @param bsda_obj_callStackCount
829#	Is decremented by 1.
830#
831bsda:obj:callerFinish() {
832	# Delete objects
833	eval "eval \"\${delete_${caller}}\""
834	unset "delete_${caller}"
835
836	# Remove the bsda:obj:callerSetvar() wrapper.
837	unset -f "$caller.setvar" "$caller.delete"
838	# Decrement the call stack counter.
839	bsda_obj_callStackCount=$(($bsda_obj_callStackCount - 1))
840
841	# Copy variables to the caller context.
842	local _var IFS
843	IFS=' '
844	eval "_var=\"\$setvars_${caller}\""
845	for _var in $_var; do
846		# Copy variable.
847		eval "setvar $_var \"\$$caller$_var\""
848		# Delete variable from stack.
849		unset $caller$_var
850	done
851	# Delete list of variables from stack.
852	unset setvars_${caller}
853}
854
855#
856# This function stores a variables for overwriting variables in the context
857# of the caller. If no storing variable has been specified (i.e. the first
858# parameter is empty), the value is printed instead.
859#
860# This function is accessable in methods by calling:
861#	$caller.setvar
862#
863# The stored variables are processed by the bsda:obj:callerFinish() function.
864#
865# @param 1
866#	The name of the variable to store.
867# @param 2
868#	The value to store.
869# @param caller
870#	The context to store variables in.
871# @param setvars_${caller}
872#	A list of all the stored variables for the caller context.
873#
874bsda:obj:callerSetvar() {
875	# Print if no return variable was specified.
876	test -z "$1" && echo "$2" && return
877
878	# Store value.
879	setvar "$caller$1" "$2"
880	# Register variable.
881	eval "setvars_${caller}=\"\$setvars_${caller}\${setvars_${caller}:+ }$1\""
882}
883
884#
885# Serialises a single variable by a given name.
886#
887# @param 1
888#	The name of the variable to return the string to.
889# @param 2
890#	The name of the variable to serialise.
891#
892bsda:obj:serialiseVar() {
893	if [ -n "$1" ]; then
894		setvar "$1" "$2=\"\$(printf '$(eval "echo -n \"\$$2\"" | bsda:obj:escape)')\""
895	else
896		echo "$2=\"\$(printf '$(eval "echo -n \"\$$2\"" | bsda:obj:escape)')\""
897	fi
898}
899
900#
901# Escapes strings on stdin for serialisation.
902#
903# The printf command can be used for deserialisation.
904#
905bsda:obj:escape() {
906	/usr/bin/vis -woe"'%\$\"-"
907}
908
909#
910# Install traps for garbage collection upon termination of the process.
911#
912bsda:obj:trap() {
913	trap bsda:obj:exit EXIT
914	trap "trap '' HUP INT TERM;exit 1" HUP INT TERM
915}
916
917#
918# This function can be used to update bsda_obj_uid in forked processes.
919#
920# This is necessary when both processes exchange objects (commonly in
921# serialised form) and thus need to be able to create objects with unique
922# IDs.
923#
924# The function should be called within the forked process without parameters.
925#
926# @param bsda_obj_uid
927#	Is set to a new uid
928# @param bsda_obj_freeOnExit
929#	The list of objects to garbage collect when terminating
930# @param bsda_obj_callStackCount
931#	The current call stack depth
932#
933bsda:obj:fork() {
934	# Reset resource collection
935	bsda_obj_freeOnExit=
936	bsda:obj:trap
937
938	# Clear the record of temp objects on the stack below, so
939	# they do not get deleted in the forked process
940	local caller i
941	i=$((bsda_obj_callStackCount))
942	while [ $i -gt 0 ]; do
943		caller="bsda_obj_callStack_$((i -= 1))_"
944		unset "delete_${caller}"
945	done
946
947	# Update UID
948	bsda_obj_uid="$(/bin/uuidgen | /usr/bin/tr '-' '_')"
949}
950
951#
952# This function can be used to detach the script from its execution context.
953#
954# I.e. it forks and the forked process inherits the responsibilities of
955# the main process, while the main process dies.
956#
957# @warning
958#	Detaching the script means loosing the guarantee that resources
959#	are freed in order upon termination. It becomes the programmers
960#	responsibility to make sure that processes die in the right order.
961# @param @
962#	The command to detach, treat this like an argument to eval
963# @return
964#	This function does not return
965#
966bsda:obj:detach() {
967	eval "bsda:obj:trap;" "$@" &
968	trap - EXIT
969	exit 0
970}
971
972#
973# This function calls all delete functions of objects having a cleanup
974# method.
975#
976# Objects spawning processes are responsible for killing them in their
977# destructor.
978#
979# @param bsda_obj_callStackCount
980#	The stack depth for unwiding
981# @param bsda_obj_freeOnExit
982#	The list of objects to call
983#
984bsda:obj:exit() {
985	local nl obj caller
986	nl=$'\n'
987	# Stack unwinding, just remove temp objects
988	while [ $((bsda_obj_callStackCount)) -gt 0 ]; do
989		caller="bsda_obj_callStack_$((bsda_obj_callStackCount - 1))_"
990		eval "eval \"\${delete_${caller}}\""
991		unset "delete_${caller}"
992		: $((bsda_obj_callStackCount -= 1))
993	done
994
995	# Garbage collection
996	while [ -n "$bsda_obj_freeOnExit" ]; do
997		obj="${bsda_obj_freeOnExit%%$nl*}"
998		if ! "$obj".delete; then
999			echo "bsda:obj:exit: WARNING: Delete of $obj failed!" 1>&2
1000			bsda_obj_freeOnExit="${bsda_obj_freeOnExit#$obj$nl}"
1001		fi
1002	done
1003	# Wait if any children stick around.
1004	trap - HUP INT TERM
1005	wait
1006}
1007
1008#
1009# Returns an exclusive file descriptor number for use.
1010#
1011# Note that FreeBSD sh only supports up to 9 file descriptors, so only the 7
1012# descriptors [3; 9] are available.
1013#
1014# @param bsda_obj_desc
1015#	The list of available file descriptors
1016# @param &1
1017#	A reference to the variable that should contain the descriptor number
1018# @retval 0
1019#	The descriptor was returned successfully
1020# @retval 1
1021#	No more descriptors were available
1022#
1023bsda:obj:getDesc() {
1024	if [ -z "$bsda_obj_desc" ]; then
1025		return 1
1026	fi
1027	# Return first available file descriptor
1028	setvar $1 ${bsda_obj_desc%%,*}
1029	# Remove descriptor from the store of available pipes
1030	bsda_obj_desc=${bsda_obj_desc#*,}
1031}
1032
1033#
1034# Releases an exclusive file descriptor.
1035#
1036# Returns a file descriptor back into the pool of usable descriptors.
1037#
1038# @param bsda_obj_desc
1039#	The list of available file descriptors
1040# @param 1
1041#	The file descriptor to release
1042#
1043bsda:obj:releaseDesc() {
1044	test -z "$1" && return
1045	bsda_obj_desc="$bsda_obj_desc$1,"
1046}
1047
1048#
1049# Initialise session UID and garbage collection.
1050#
1051bsda:obj:fork
1052
1053#
1054# Hacks.
1055#
1056
1057#
1058# Ignore nullptr delete.
1059#
1060.delete() {
1061	: # bash does not allow empty functions
1062}
1063
1064#
1065# Perform nullptr dump.
1066#
1067# @param &1
1068#	The dump string destination variable, set to empty
1069#
1070.dump() {
1071	setvar "$1"
1072}
1073
1074#
1075# Perform nullptr copy.
1076#
1077# @param &1
1078#	The copy reference destination variable, set to empty
1079#
1080.copy() {
1081	setvar "$1"
1082}
1083
1084#
1085# Perform nullptr serialise.
1086#
1087# @param &1
1088#	The serialise string destination variable, set to empty
1089#
1090.serialise() {
1091	setvar "$1"
1092}
1093
1094#
1095# Compatibility hacks.
1096#
1097
1098# Emulate setvar for shells that don't have it, i.e. bash.
1099if ! setvar 2>&-; then
1100	setvar() {
1101		eval "$1=\"\$2\""
1102	}
1103fi
1104
1105# Setup getvar for symmetry with setvar
1106if ! getvar 2>&-; then
1107	#
1108	# Returns a variable from a given reference.
1109	#
1110	# The variable is either written to a named variable, or in
1111	# absence of one, output to stdout.
1112	#
1113	# @param &1
1114	#	The name of the variable to write to
1115	# @param 2
1116	#	The reference to the variable to return
1117	#
1118	getvar() {
1119		if [ -n "$1" ]; then
1120			eval "$1=\"\$$2\""
1121		else
1122			eval "echo \"\$$2\""
1123		fi
1124	}
1125fi
1126