So, inzwischen ist auch noch Mehrfachvererbung und Zugriffskontrolle (private, protected, public) dazugekommen:
Code:
#!/bin/sh -f
#
# Copyright (c) 2009
# Dominic Fandrey <kamikaze@bsdforen.de>
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
# version 1.9
# Include once.
test -n "$bsda_obj" && return 0
bsda_obj=1
#
# This file contains helper functions for creating object oriented
# shell scripts.
#
# The most significant function is bsda:obj:createClass(), which basically
# creates a class, including getters, setters, a constructor, a destructor,
# a reset and a copy function. It also creates serialization methods.
#
#
# TABLE OF CONTENTS
#
# 1) DEFINING CLASSES
# 1.1) Basic Class Creation
# 1.2) Inheritance
# 1.3) Access Scope
# 1.4) Interfaces
# 2) IMPLEMENTING METHODS
# 2.1) Regular Methods
# 2.2) Special Methods
# 3) CONSTRUCTOR
# 4) RESET
# 5) DESTRUCTOR
# 6) COPY
# 7) GET
# 8) SET
# 9) TYPE CHECKS
# 9.1) Object Type Checks
# 9.2) Primitive Type Checks
# 10) SERIALIZE
# 10.1) Serializing
# 10.2) Deserializing
# bsda:obj:createClass()
# bsda:obj:createInterface()
# bsda:obj:getVar()
# bsda:obj:getSerializedId()
# bsda:obj:deserialize()
# bsda:obj:isObject()
# bsda:obj:isInt()
# bsda:obj:isFloat()
# bsda:obj:isSimpleFloat()
# bsda:obj:createMethods()
# bsda:obj:deleteMethods()
# bsda:obj:deleteAttributes()
# bsda:obj:callerSetup()
# bsda:obj:callerFinish()
# bsda:obj:callerSetvar()
#
#
# 1) DEFINING CLASSES
#
# This section describes the creation of classes.
#
# NOTE: The details of creating classes are listed in front of the
# bsda:obj:createClass() function.
#
# Creating a class consists of two steps, the first step is to call
# the bsda:obj:createClass() function, the second one is to implement the
# methods. This section describes the first step.
#
# In order to create classes this framework has to be loaded:
#
# . bsda_obj.sh
#
# 1.1) Basic Class Creation
#
# Creating a class does not require more than a class name:
#
# bsda:obj:createClass MyClass
#
# After the previous line the class can be used to create objects,
# however useless, and all the reserved methods are available:
#
# MyClass myObject
# $myObject.delete
#
# It is possible to create classes as simple data structures, that do not
# require the programmer to write any methods to function:
#
# bsda:obj:createClass MyPoint2D \
# w:name \
# w:x \
# w:y
#
# Instances of the MyPoint2D class now offer the getName() and setName(),
# getX() and setX(), getY() and setY() methods:
#
# MyPoint2D point
# $point.setName "upper left corner"
# $point.setX 0
# $point.setY 0
#
# It might be a good idea to add an init method to the class in order to
# assign values:
#
# bsda:obj:createClass MyConstPoint2D \
# i:init \
# r:name \
# r:x \
# r:y
#
# MyConstPoint2D.init() {
# [ ... assign values, maybe even check types ... ]
# }
#
# NOTE: The init method can have an arbitrary name.
#
# Note that the attributes were now created with "r:", this means they only
# have get methods, no set methods. All the values are now assigned during
# object creation by the init method:
#
# MyConstPoint2D corner "upper right corner" 640 0
#
# 1.2) Inheritance
#
# If a similar class is required there is no reason to start anew, the
# previous class can be extended:
#
# bsda:obj:createClass MyConstPoint3D extends:MyConstPoint2D \
# i:init \
# r:z
#
# MyConstPoint3D.init() {
# # Call the init method of the parent class.
# $class.superInit "$1" "$2" "$3" || return 1
# # Check whether the given coordinate is an integer.
# bsda:obj:isInt "$4" || return 1
# setvar ${this}z "$4"
# }
#
# This example shows that to overload methods they have to be stated in the
# class declaration. If init had not been stated, the init method of the
# parent class MyConstPoint2D would have been used.
#
# NOTE: If the init method does not return 0 the object is instantly
# destroyed and the return value forwarded to the caller.
# The caller then has a reference to a no longer existing object
# and does not know about it, unless the return value of the
# constructor is checked.
#
# Multiple inheritance is possible, but should be used with great care,
# because there are several limits. If several extended classes provide
# the same method, the method of the first class has the priority.
#
# The super init and cleanup methods are those of the first class providing
# an init or cleanup method.
# The remaining init and cleanup methods might continue to exist as regular
# methods, if their names do not conflict.
#
# 1.3) Access Scope
#
# You might want to limit access to certain methods, for this you can
# add the scope operators private, protected and public. If no scope
# operator is given, public is assumed.
#
# public:
# This scope allows access from anywhere.
# protected:
# The protected scope only allows classes that are derived from the
# current class or reside within the same namespace access.
# private:
# Only instances of the same class have access.
#
# Namespaces are colon (the character ":") seperated. E.g. the class
# bsda:pkg:Index has the namespace "bsd:pkg".
#
# The scope operator is added after the identifier type prefix. Only
# prefixes that declare methods can have a scope operator.
#
# bsda:obj:createClass myNs:Person \
# i:private:init \
# w:private:familyName \
# w:private:firstName
#
# NOTE: The constructor is always public. Declaring a scope for an init method
# only affects direct calls of the method.
#
# Now the getters and setters for both familyName and firstName are private.
# It is possible to widen the scope of a method by redeclaring it.
#
# bsda:obj:createClass myNs:Person \
# i:private:init \
# w:private:familyName \
# x:public:getFamilyName \
# w:private:firstName \
# x:public:getFirstName
#
# NOTE: When methods are inherited the widest declared scope always wins, no
# matter from which class it originates.
#
# 1.4) Interfaces
#
# Implementations of generic solutions normally require the classes using them
# to conform to a certain interface (e.g. in a listener and notify pattern).
#
# Technically this can be realized with inheritance, but this is often a dirty
# solution, especially when conformance to several interfaces is required.
#
# To circumvent the consistency problems imposed by multiple inheritance the
# bsda:obj:createInterface() method allows the creation of interfaces:
#
# bsda:obj:createInterface Listener \
# x:notify
#
# NOTE: Methods defined by an interface are always public, so there is not
# scope operator.
#
# NOTE: Interfaces cannot be used to define attributes.
#
# Every class conforming to the interface has to implement the methods defined
# by the interface:
#
# bsda:obj:createClass Display implements:Listener \
# [ ... additional method and attribute definitions ... ]
#
# Display.notify() {
# [ ... ]
# }
#
# Interfaces can also extend other interfaces.
#
# To check whether an object is derived from a class conforming to an
# interface the static isInstance method can be use:
#
# if ! Listener.isInstance $object; then
# [ ... ]
# fi
#
#
# 2) IMPLEMENTING METHODS
#
# All that requires to be done to get a functional class after defining it,
# is to implement the required methods.
# Methods are really just functions that the constructor
# creates a wrapper for that forwards the object reference to them.
#
# 2.1) Regular Methods
#
# The following special variables are available:
# this A reference to the current object
# class The name of the class this object is an instance of
# caller Provides access to methods to manipulate the caller context,
# which is the recommended way of returning data to the caller
#
# The following methods are offered by the caller:
# setvar Sets a variable in the caller context.
# getObject
# Returns a reference to the calling object.
# getClass
# Returns the name of the calling class.
#
# The following variable names may not be used in a method:
# _return
# _var
# _setvars
#
# A method must always be named "<class>.<method>". So a valid implementation
# for a method named "bar" and a class named "foo" would look like this:
#
# foo.bar() {
# }
#
# The object reference is always available in the variable "this", which
# performs the same function as "self" in python or "this" in Java.
#
# Attributes are resolved as "<objectId><attribute>", the following example
# shows how to read an attribute, manipulate it and write the new value.
# Directly operating on attributes is not possible.
#
# foo.bar() {
# local count
# # Get counter value.
# bsda:obj:getVar count ${this}count
# # Increase counter value copy.
# count=$(($count + 1))
# # Store the counter value.
# setvar ${this}count $count
# }
#
# The following example does the same with getters and setters. Getters and
# setters are documented in chapter 7 and 8.
#
# foo.bar() {
# local count
# # Get counter value.
# $this.getCount count
# # Increase counter value copy.
# count=$(($count + 1))
# # Store the counter value.
# $this.setCount $count
# }
#
# To return data into the calling context $caller.setvar is used. It
# provides the possibility to overwrite variables in the caller context
# even when there is a local variable using the same name.
# Note that it has to be assumed that the names of variables used within
# a method are unknown to the caller, so this can always be the case.
#
# The name of the variable to store something in the caller context is
# normally given by the caller itself as a parameter to the method call.
#
# The following method illustrates this, the attribute count is fetched
# and returned to the caller through the variable named in $1.
# Afterwards the attribute is incremented:
#
# foo.countInc() {
# local count
# # Get counter value.
# $this.getCount count
# $caller.setvar $1 $count
# # Increase counter value copy.
# count=$(($count + 1))
# # Store the counter value.
# $this.setCount $count
# }
#
# This is how a call could look like:
#
# local count
# $obj.countInc count
# echo "The current count is $count."
#
# Note that both the method and the caller use the local variable count, yet by
# using $caller.setvar the method is still able to overwrite count in the
# caller context.
#
# If a method uses no local variables (which is only sensible in very rare
# cases), the regular shell builtin setvar can be used to overwrite variables
# in the caller context to reduce overhead.
#
# 2.2) Special Methods
#
# There are two special kinds of methods available, init and cleanup methods.
# These methods are special, because they are called implicitely, the first
# when an object is created, the second when it is reset or deleted.
#
# The init method is special because the $caller.setvar() method is not
# available. It is called by the constructor with all values apart from the
# first one, which is the variable the constructor stores the object
# reference in. It can also be called directly (e.g. after a call to the
# reset() method).
#
# The purpose of an init method is to initialize attributes during class
# creation. If the current class is derived from another class it might
# be a good idea to call the init method of the parent class. This is
# done by calling $class.superInit().
#
# If the init method fails (returns a value > 0) the constructor immediately
# destroys the object.
#
# The cleanup method is called implicitely by the delete() and reset()
# methods. Unlike the init method it has all the posibilities of an
# ordinary method. Both the delete() and reset() methods ignore the return
# values of the the cleanup method.
#
#
# 3) CONSTRUCTOR
#
# This block documents the use of a constructor created by the
# bsda:obj:createClass() function below.
#
# The name of the class acts as the name of the constructor. The first
# parameter is the name of a variable to store the object reference in.
# An object reference is a unique id that allows the accessing of all methods
# belonging to an object.
#
# The object id is well suited for "grep -F", which is nice to have when
# implementing lists.
#
# The following example shows how to create an object of the type "foo:bar",
# by calling the "foo:bar" constructor:
#
# foo:bar foobar
#
# The following example shows how to use a method belonging to the object:
#
# $foobar.copy foobarCopy
#
# @param 1
# The name of the variable to store the reference to the new object in.
# @param @
# The remaining parameters are forwarded to an init method,
# if one was specified.
# @return
# Returns 0 for success and higher values for failure. If the
# init method has a fail case, this should be checked, because
# the object is not created in case of a failure.
#
#
# 4) RESET
#
# This block documents the use of a resetter created by the
# bsda:obj:createClass() function below.
#
# The resetter first calls the cleanup method with all parameters, if one
# has been defined. Afterwards it simply removes all attributes from memory.
#
# The resetter does not call the init method afterwards, because it would
# not be possible to provide different parameters to the init and cleanup
# methods in that case.
#
# The following example shows how to reset an object referenced by "foobar".
#
# $foobar.reset
#
# @param @
# The parameters are forwareded to a cleanup() method if one was
# specified.
#
#
# 5) DESTRUCTOR
#
# This block documents the use of a destructor created by the
# bsda:obj:createClass() function below.
#
# The destructor calls a cleanup method with all parameters, if
# one was specified. Afterwards it simply removes all method wrappers and
# attributes from memory.
#
# The following example illustrates the use of the destructor on an object
# that is referenced by the variable "foobar".
#
# $foobar.delete
#
# @param @
# The parameters are forwareded to a cleanup() method if one was
# specified.
#
#
# 6) COPY
#
# This block documents the use of a copy method created by the
# bsda:obj:createClass() function below.
#
# The copy method creates a new object of the same type and copies all
# attributes over to the new object.
#
# The following exampe depicts the copying of an object referenced by the
# variable "foobar". The new object will be referenced by the variable
# "foobarCopy".
#
# $foobar.copy foobarCopy
#
#
# 7) GET
#
# This block documents the use of a getter method created by the
# bsda:obj:createClass() function below.
#
# A getter method either outputs an attribute value to stdout or stores it
# in a variable, named by the first parameter.
#
# The following example shows how to get the attribute "value" from the object
# referenced by "foobar" and store it in the variable "value".
#
# $foobar.getValue value
#
# @param 1
# The optional name of the variable to store the attribute value in.
# If ommitted the value is output to stdout.
#
#
# 8) SET
#
# This block documents the use of a setter method created by the
# bsda:obj:createClass() function below.
#
# A setter method stores a value in an attribute.
#
# This example shows how to store the value 5 in the attribute "value" of
# the object referenced by "foobar".
#
# $foobar.setValue 5
#
# @param 1
# The value to write to an attribute.
#
#
# 9) TYPE CHECKS
#
# This framework supplies basic type checking facilities.
#
# 9.1) Object Type Checks
#
# This block documents the use of the static type checking method created
# by the bsda:obj:createClass() and bsda:obj:createInterface() function below.
#
# The type checking method isInstance() takes an argument string and checks
# whether it is a reference to an object of this class.
#
# This example shows how to check whether the object "foobar" is an instance
# of the class "foo:bar".
#
# if foo:bar.isInstance $foobar; then
# ...
# else
# ...
# fi
#
# @param 1
# Any string that might be a reference.
#
# 9.2) Primitive Type Checks
#
# The following primitive type checking functions are available and documented
# below:
# bsda:obj:isObject()
# bsda:obj:isInt()
# bsda:obj:isFloat()
# bsda:obj:isSimpleFloat()
#
#
# 10) SERIALIZE
#
# This documents the process of serialization and deserialization.
# Serialization is the process of turning data structures into string
# representations. Serialized objects can be stored in a file and reloaded
# at a later time. They can be passed on to other processess, through a file
# or a pipe. They can even be transmitted over a network through nc(1).
#
# NOTE: Static attributes are not subject to serialization.
#
# 10.1) Serializing
#
# The following example serializes the object $foobar and stores the string
# the variable serialized.
#
# $foobar.serialize serialized
#
# The next example saves the object $configuration in a file.
#
# $configuration.serialize > ~/.myconfig
#
# If $configuration references other objects it will fail to access them
# if deserialized in a new context.
# This is what the serializeDeep() method is good for. It serializes entire
# data structures recursively and is the right choice in many use cases.
# It is used in exactly the same way as the serialize method.
#
# $configuration.serializeDeep > ~/.myconfig
#
# @param 1
# If given it is used as the variable name to store the serialized string
# in, otherwise the serialized string is output to stdout.
#
# 10.2) Deserializing
#
# This example loads the object $configuration from a file and restores it.
#
# # Deserialize the data and get the object reference.
# bsda:obj:deserialize configuration - < ~/.myconfig
#
# After the last line the $configuration object can be used exactly like
# in the previous session.
#
# @param 1
# The name of the variable to store the object ID (reference) of the
# serialized object in.
# @param 2
# This is expected to be a serialized string. In the special case that
# this parameter is a dash (the charachter "-"), the serialized string
# will be read from stdin.
#
#
# The stack counter that holds the number of methods that currently
# use the return stack.
#
bsda_obj_callStackCount=0
#
# This session ID becomes part of every newly created object and ensures
# that there are no object id collisions.
#
# Multi processing safety is ensured by adding the PID in the constructor.
# However it might still be possible that a program might load a serialized
# object (e.g. stored in a file) and have the same PID.
# This is why this object ID has a time stamp.
#
# However, there is still the chance an object might be sent over a network
# and received by a process with the same ID and started at the same time.
# This is what the first part of this ID takes care of, by adding a hex
# encoded 64bit random number.
#
readonly bsda_obj_sessionId=$(dd bs=8 count=1 < /dev/random 2> /dev/null | xxd -p)_$(date -u '+%s')
#
# This is a prefix to every object ID and should be the same among all
# compatible frameworks to ensure that deep serialization works.
#
readonly bsda_obj_frameworkPrefix=BSDA_OBJ_
#
# This is used as a buffer during deep serialization.
#
#bsda_obj_serialized=
#
# During deep serialization this holds a list of objects to prevent circular
# recursion.
#
#bsda_obj_serializeBlacklist=
#
# The copy method sets this temporarily to tell the constructor not to call
# an init method.
#
#bsda_obj_doCopy
#
# Creates a new class, i.e. a constructor, destructor, resetter, getters,
# setters and so forth.
#
# So all that is left to be done are the methods.
#
# The following static methods are reserved:
# superInit()
# superClean()
# deserialize()
# isInstance()
# isClass()
# isInterface()
# getAttributes()
# getMethods()
# getPrefix()
# getInit()
# getClean()
# getParents()
# getInterfaces()
#
# The following methods are reserved:
# copy()
# delete()
# reset()
# serialize()
# serializeDeep()
#
# The following class prefix bound static attributes are reserved:
# instancePatterns
# private
# protected
# public
#
# The following session and process bound static attributes are reserved:
# nextId
#
# @param 1
# The first parameter is the name of the class.
# @param @
# A description of the class to create.
#
# All parameters following the class name make up a list of identifiers
# for attributes and methods. Every identifier has a prefix, the
# following prefixes are supported:
#
# -: A plain attribute.
# r: An attribute with a get method. The identifier
# "r:foo" results in a method called "getFoo".
# w: An attribute with a get and set method. "w:foo" results
# in "getFoo" and "setFoo".
# x: A method, this has to be user implemented as
# "<class>.<method>()".
# i: An init method that is called with the remaining parameters
# given to the constructor.
# c: A cleanup method that is called before the reset or delete
# command, with the parameters given to them.
# extends:
# This prefix is followed by the name of another class
# this class inherits methods and attributes from.
# Classes have to be given in the order of priority.
#
# The init and clean methods are inherited from the first
# class having them if no own init or clean method is
# supplied.
#
# The superInit() and superClean() methods also call
# the the first encountered init and clean methods.
# implements:
# This prefix is followed by the name of an interfaces.
# Interfaces define public methods that need to be implemented
# by a class to conform to the interface.
#
# With these parameters a constructor and a destructor will be built.
# It is important that all used attributes are listed, or the copy,
# delete and serialization methods will not work as expected.
#
# Everything that is not recognized as an identifier is treated as a
# comment.
#
# The prefixes r, w, x, i and c can be followed by a scope operator
# public, protected or private.
#
# The constructor can be called in the following way:
# <class> <refname>
# The class name acts as the name of the constructor, <refname> is the
# name of the variable to store the reference to the new object in.
#
# The resetter deletes all attributes, this can be used to replace
# an object. The resetter is called this way:
# $reference.reset
#
# The destructor can be called in the following way:
# $reference.delete
# This will destroy all methods and attributes.
#
# A getter takes the name of the variable to store the value in as the
# first argument, if this is ommitted, the value is written to stdout.
#
# The copy method can be used to create a shallow copy of an object.
#
# @param bsda_obj_namespace
# The frameowrk namespace to use when building a class. The impact is on
# the use of helper functions.
# @return
# 0 on succes
# 1 if there is more than one init method (i:) specified
# 2 if there is more than one cleanup method (c:) specified
# 3 if there was an unknown scope operator
# 4 for an attempt to extend something that is not a class
# 5 for an attempt to implement something that is not an interface
#
bsda:obj:createClass() {
local IFS class methods method attributes getters setters arg
local getter setter attribute reference init clean serialize extends
local implements bsda_obj_namespace
local namespacePrefix classPrefix prefix
local superInit superClean
local inheritedAttributes inheritedMethods parent parents
local previousMethod scope interface
# Default framework namespace.
: ${bsda_obj_namespace='bsda:obj'}
# Get the class name and shift it off the parameter list.
class="$1"
shift
IFS='
'
# There are some default methods.
methods="reset${IFS}delete${IFS}copy${IFS}serialize${IFS}serializeDeep"
attributes=
getters=
setters=
init=
clean=
extends=
implements=
superInit=
superClean=
# Parse arguments.
for arg {
case "$arg" in
x:*)
methods="$methods${methods:+$IFS}${arg#x:}"
;;
-:*)
attributes="$attributes${attributes:+$IFS}${arg#-:}"
;;
r:*)
attributes="$attributes${attributes:+$IFS}${arg##*:}"
getters="$getters${getters:+$IFS}${arg#r:}"
;;
w:*)
attributes="$attributes${attributes:+$IFS}${arg##*:}"
getters="$getters${getters:+$IFS}${arg#w:}"
setters="$setters${getters:+$IFS}${arg#w:}"
;;
i:*)
if [ -n "$init" ]; then
echo "bsda:obj:createClasss: ERROR: More than one init method was supplied!" 1>&2
return 1
fi
methods="$methods${methods:+$IFS}${arg#i:}"
init="$class.${arg##*:}"
;;
c:*)
if [ -n "$clean" ]; then
echo "bsda:obj:createClasss: ERROR: More than one cleanup method was supplied!" 1>&2
return 2
fi
methods="$methods${methods:+$IFS}${arg#c:}"
clean="$class.${arg##*:}"
;;
extends:*)
extends="$extends${extends:+$IFS}${arg#extends:}"
;;
implements:*)
implements="$implements${implements:+$IFS}${arg#implements:}"
;;
*)
# Assume everything else is a comment.
;;
esac
}
# Create reference prefix. The Process id is added to the prefix when
# an object is created.
namespacePrefix="${bsda_obj_frameworkPrefix}$(echo "$bsda_obj_namespace" | tr ':' '_')_"
classPrefix="${namespacePrefix}$(echo "$class" | tr ':' '_')_"
prefix="${classPrefix}${bsda_obj_sessionId}_"
# Set the instance match pattern.
setvar ${classPrefix}instancePatterns "${classPrefix}[0-9a-f]+_[0-9]+_[0-9]+_[0-9]+_"
# Create getters.
for method in $getters; {
getter="${method##*:}"
attribute="$getter"
getter="get$(echo "${getter%%${getter#?}}" | tr '[:lower:]' '[:upper:]')${getter#?}"
eval "
$class.$getter() {
if [ -n \"\$1\" ]; then
eval \"\$1=\\\"\\\$\${this}$attribute\\\"\"
else
eval \"echo \\\"\\\$\${this}$attribute\\\"\"
fi
}
"
# Check for scope operator.
if [ "${method%:*}" != "$method" ]; then
# Add scope operator to the getter name.
getter="${method%:*}:$getter"
fi
# Add the getter to the list of methods.
methods="$methods${methods:+$IFS}${getter}"
}
# Create setters.
for method in $setters; {
setter="${method##*:}"
attribute="$setter"
setter="set$(echo "${setter%%${setter#?}}" | tr '[:lower:]' '[:upper:]')${setter#?}"
eval "
$class.$setter() {
setvar \"\${this}$attribute\" \"\$1\"
}
"
# Check for scope operator.
if [ "${method%:*}" != "$method" ]; then
# Add scope operator to the getter name.
setter="${method%:*}:$setter"
fi
# Add the setter to the list of methods.
methods="$methods${methods:+$IFS}$setter"
}
# Add implicit public scope to methods.
for method in $methods$(methods=); {
# Check the scope.
case "${method%:*}" in
$method)
# There is no scope operator, add public.
methods="${methods:+$methods$IFS}public:$method"
;;
public | protected | private)
# The accepted scope operators.
methods="${methods:+$methods$IFS}$method"
;;
*)
# Everything else is not accepted.
echo "bsda:obj:createClasss: ERROR: Unknown scope operator \"${method%:*}\"!" 1>&2
return 3
;;
esac
}
# Manage inheritance.
superInit=
superClean=
for parent in $extends; {
if ! $parent.isClass; then
echo "bsda:obj:createClasss: ERROR: Extending \"$parent\" failed, not a class!" 1>&2
return 4
fi
# Get the interfaces implemented by the class.
# Filter already registered interfaces.
parents="$($parent.getInterfaces | grep -vFx "$implements")"
# Append the detected interfaces to the list of implemented
# interfaces.
implements="$implements${implements:+${parents:+$IFS}}$parents"
# Get the parents of this class.
# Filter already registered parents.
parents="$($parent.getParents | grep -vFx "$extends")"
# Append the detected parents to the list of extended classes.
extends="$extends${parents:+$IFS$parents}"
# Get the super methods, first class wins.
test -z "$superInit" && $parent.getInit superInit
test -z "$superClean" && $parent.getClean superClean
# Get inherited methods and attributes.
inheritedMethods="$($parent.getMethods | grep -vFx "$methods")"
inheritedAttributes="$($parent.getAttributes | grep -vFx "$attributes")"
# Update the list of attributes.
attributes="$inheritedAttributes${inheritedAttributes:+${attributes:+$IFS}}$attributes"
# Create aliases for methods.
for method in $inheritedMethods; {
# Check whether this method already exists with a
# different scope.
if echo "$methods" | grep -qx ".*:${method##*:}"; then
# Skip ahead.
continue
fi
# Inherit method.
alias $class.${method##*:}=$parent.${method##*:}
}
# Update the list of methods.
methods="$inheritedMethods${inheritedMethods:+${methods:+$IFS}}$methods"
# Update the instance match patterns of parents.
for parent in $parent${parents:+$IFS$parents}; {
$parent.getPrefix parent
eval "${parent}instancePatterns=\"\${${parent}instancePatterns}|\${${classPrefix}instancePatterns}\""
}
}
# Manage implements.
for interface in $implements; {
if ! $interface.isInterface; then
echo "bsda:obj:createClasss: ERROR: Implementing \"$interface\" failed, not an interface!" 1>&2
return 5
fi
# Get the parents of this interface.
# Filter already registered parents.
parents="$($interface.getParents | grep -vFx "$implements")"
# Append the detected parents to the list of extended classes.
implements="$implements${parents:+$IFS$parents}"
# Get inherited public methods.
inheritedMethods="$($interface.getMethods | grep -vFx "$methods")"
# Update the list of methods.
methods="$inheritedMethods${inheritedMethods:+${methods:+$IFS}}$methods"
# Update the instance match patterns of parents.
for parent in $interface${parents:+$IFS$parents}; {
$interface.getPrefix parent
eval "${parent}instancePatterns=\"\${${parent}instancePatterns:+\${${parent}instancePatterns}|}\${${classPrefix}instancePatterns}\""
}
}
# If a method is defined more than once, the widest scope wins.
# Go through the methods sorted by method name.
previousMethod=
scope=
for method in $(methods=; echo "$methods" | sort -t: -k2); {
# Check whether the previous and the current method were the
# same.
if [ "$previousMethod" != "${method##*:}" ]; then
# If all scopes of this method have been found,
# store it in the final list.
methods="${methods:+$methods${previousMethod:+$IFS$scope:$previousMethod}}"
scope="${method%:*}"
else
# Widen the scope if needed.
case "${method%:*}" in
public)
scope=public
;;
protected)
if [ "$scope" == "private" ]; then
scope=protected
fi
;;
esac
fi
previousMethod="${method##*:}"
}
# Add the last method (this never happens in the loop).
methods="${methods:+$methods${previousMethod:+$IFS$scope:$previousMethod}}"
# Store access scope checks for each scope in the class context.
# Note that at the time this is run the variables class and this
# still belong to the the caller.
setvar ${classPrefix}private "
if [ \\\"\\\$class\\\" != \\\"$class\\\" ]; then
echo \\\"$class.\${method##*:}(): Terminated because of access attempt to a private method\\\${class:+ by \\\$class}!\\\" 1>&2
return 255
fi
"
setvar ${classPrefix}protected "
if (! $class.isInstance \\\$this) && [ \\\"\\\${class#${class%:*}}\\\" = \\\"\\\$class\\\" ]; then
echo \\\"$class.\${method##*:}(): Terminated because of access attempt to a protected method\\\${class:+ by \\\$class}!\\\" 1>&2
return 255
fi
"
setvar ${classPrefix}public ''
# Create constructor.
eval "
$class() {
local _return this class
class=$class
eval \"
# Create object reference.
this=\\\"${prefix}\$\$_\\\${${prefix}\$\$_nextId:-0}_\\\"
# Increase the object id counter.
${prefix}\$\$_nextId=\\\$((\\\$${prefix}\$\$_nextId + 1))
\"
# Create method instances.
$bsda_obj_namespace:createMethods $class $classPrefix \$this \"$methods\"
# Return the object reference.
if [ -n \"\$1\" ]; then
setvar \"\$1\" \$this
else
echo \$this
fi
# If this object construction is part of a copy() call,
# this constructor is done.
test -n \"$bsda_obj_doCopy\" && return 0
${init:+
# Cast the reference variable from the parameters.
shift
# Call the init method.
$init \"\$@\"
_return=\$?
# Destroy the object on failure.
test \$_return -ne 0 && \$this.delete
return \$_return
}
}
"
# Create a resetter.
eval "
$class.reset() {
${clean:+$clean \"\$@\"}
# Delete attributes.
$bsda_obj_namespace:deleteAttributes \$this \"$attributes\"
}
"
# Create destructor.
eval "
$class.delete() {
${clean:+$clean \"\$@\"}
# Delete methods and attributes.
$bsda_obj_namespace:deleteMethods \$this \"$methods\"
$bsda_obj_namespace:deleteAttributes \$this \"$attributes\"
}
"
# Create copy method.
eval "
$class.copy() {
local IFS bsda_obj_doCopy reference attribute
bsda_obj_doCopy=1
IFS='
'
# Create a new empty object.
$class reference
# Store the new object reference in the target variable.
if [ -n \"\$1\" ]; then
setvar \"\$1\" \$reference
else
echo \$reference
fi
# For each attribute copy the value over to the
# new object.
for attribute in \$(echo \"$attributes\"); {
eval \"\$reference\$attribute=\\\"\\\$\${this}\$attribute\\\"\"
}
}
"
# A serialize method.
eval "
$class.serialize() {
local IFS attribute serialized
IFS='
'
serialized=
for attribute in \$(echo '$attributes'); {
serialized=\"\${serialized:+\$serialized;}\${this}\$attribute='\$(
eval \"printf '%s' \\\"\\\${\${this}\$attribute}\\\"\" | xxd -p | rs -T
)'\"
}
serialized=\"\$serialized;$class.deserialize \$this\"
\$caller.setvar \"\$1\" \"\$serialized\"
}
"
# A recursive serialize method.
eval "
$class.serializeDeep() {
local IFS rootCall objects object serialized attribute
serialized=
rootCall=
IFS='
'
# Check whether this has already been serialized.
if echo \"\$this\" | grep -qFx \"\$bsda_obj_serializeBlacklist\"; then
# Already serialized, return.
return 0
fi
# Check whether this is the root call.
if [ -z \"\$bsda_obj_serializeBlacklist\" ]; then
rootCall=1
fi
# Add this to the blacklist to prevent circular
# recursion.
bsda_obj_serializeBlacklist=\"\${bsda_obj_serializeBlacklist:+\$bsda_obj_serializeBlacklist;}\$this\"
# Create a list of all referenced objects.
objects=\"\$(
# Echo each attribute.
for attribute in \$(echo '$attributes'); {
eval \"echo \\\"\\\${\$this\$attribute}\\\"\"
} | grep -Eo '$bsda_obj_frameworkPrefix[_[:alnum:]]+_[0-9a-f]+_[0-9]+_[0-9]+_[0-9]+_' | sort -u
)\"
# Serialize all required objects.
for object in \$objects; {
\$object.serializeDeep
}
# Serialize this.
\$this.serialize serialized
# Append this to the recursive serialization list.
bsda_obj_serialized=\"\${bsda_obj_serialized:+\$bsda_obj_serialized$IFS}\$serialized\"
# Root call only.
if [ -n \"\$rootCall\" ]; then
# Return serialized string.
\$caller.setvar \"\$1\" \"\$bsda_obj_serialized\"
# Wipe static serialization variables.
unset bsda_obj_serialized
unset bsda_obj_serializeBlacklist
fi
}
"
# A static super method, which calls the init method of the
# parent class.
eval "
$class.superInit() {
${superInit:+local class}
${superInit:+class=$extends}
${superInit:+$superInit \"\$@\"}
}
"
# A static super method, which calls the cleanup method of the
# parent class.
eval "
$class.superClean() {
${superClean:+local class}
${superClean:+class=$extends}
${superClean:+$superClean \"\$@\"}
}
"
# A static deserialize method.
eval "
$class.deserialize() {
local IFS attribute
IFS='
'
# Create method instances.
$bsda_obj_namespace:createMethods $class $classPrefix \$1 \"$methods\"
# Deserialize attributes.
for attribute in \$(echo '$attributes'); {
setvar \"\$1\$attribute\" \"\$(
eval \"echo \\\"\\\${\$1\$attribute}\\\"\" | rs -T | xxd -r -p
)\"
}
}
"
# A static type checker.
eval "
$class.isInstance() {
echo \"\$1\" | grep -xEq \"\${${classPrefix}instancePatterns}\"
}
"
# Return whether this is a class.
eval "
$class.isClass() {
return 0
}
"
# Return whether this is an interface.
eval "
$class.isInterface() {
return 1
}
"
# A static method that returns the attributes of a class.
eval "
$class.getAttributes() {
if [ -n \"\$1\" ]; then
setvar \"\$1\" '$attributes'
else
echo '$attributes'
fi
}
"
# A static method that returns the methods of a class.
eval "
$class.getMethods() {
if [ -n \"\$1\" ]; then
setvar \"\$1\" '$methods'
else
echo '$methods'
fi
}
"
# A static method that returns the class prefix.
eval "
$class.getPrefix() {
if [ -n \"\$1\" ]; then
setvar \"\$1\" '$classPrefix'
else
echo '$classPrefix'
fi
}
"
# A static method that returns the parentage of this class.
eval "
$class.getInterfaces() {
if [ -n \"\$1\" ]; then
setvar \"\$1\" '$implements'
else
echo '$implements'
fi
}
"
# A static method that returns the parentage of this class.
eval "
$class.getParents() {
if [ -n \"\$1\" ]; then
setvar \"\$1\" '$extends'
else
echo '$extends'
fi
}
"
# A static method that returns the name of the init method.
eval "
$class.getInit() {
if [ -n \"\$1\" ]; then
setvar \"\$1\" '$init'
else
echo '$init'
fi
}
"
# A static method that returns the name of the cleanup method.
eval "
$class.getClean() {
if [ -n \"\$1\" ]; then
setvar \"\$1\" '$clean'
else
echo '$clean'
fi
}
"
}
#
# This function creates an interface that can be implemented by a class.
#
# It is similar to the bsda:obj:createClass() function, but a lot less complex.
#
# The following static methods are reserved:
# isInstance()
# isClass()
# isInterface()
# getMethods()
# getPrefix()
# getParents()
#
# The following class prefix bound static attributes are reserved:
# instancePatterns
#
# @param 1
# The first parameter is the name of the class.
# @param @
# A description of the interface to create.
#
# All parameters following the interface name make up a list of
# identifiers, the different types of identifiers are distinguished by
# the following prefixes:
#
# x: Defines a public method.
# extends:
# This prefix is followed by the name of another interface
# from which method definitions are inherited.
#
# Everything that is not recognized as an identifier is treated as a
# comment.
#
# @param bsda_obj_namespace
# The frameowrk namespace to use when building a class. The impact is on
# the use of helper functions.
# @return
# 0 on succes
# 1 for an attempt to extend something that is not an interface
#
bsda:obj:createInterface() {
local IFS interface bsda_obj_namespace methods extends
local interfacePrefix namespacePrefix parent parents
local inheritedMethods
# Default framework namespace.
: ${bsda_obj_namespace='bsda:obj'}
# Get the interface name and shift it off the parameter list.
interface="$1"
shift
IFS='
'
methods=
extends=
# Parse arguments.
for arg {
case "$arg" in
x:*)
methods="$methods${methods:+$IFS}public:${arg#x:}"
;;
extends:*)
extends="$extends${extends:+$IFS}${arg#extends:}"
;;
*)
# Assume everything else is a comment.
;;
esac
}
# Create an interface prefix, this is required to access the instance
# matching patterns.
namespacePrefix="${bsda_obj_frameworkPrefix}$(echo "$bsda_obj_namespace" | tr ':' '_')_"
interfacePrefix="${namespacePrefix}$(echo "$interface" | tr ':' '_')_"
# Manage inheritance.
for parent in $extends; {
if ! $parent.isInterface; then
echo "bsda:obj:createInterface: ERROR: Extending \"$interface\" failed, not an interface!" 1>&2
return 1
fi
# Get the parents of this interface.
# Filter already registered parents.
parents="$($parent.getParents | grep -vFx "$extends")"
# Append the detected parents to the list of extended interfaces.
extends="$extends${parents:+$IFS$parents}"
# Get inherited public methods.
inheritedMethods="$($parent.getMethods | grep -vFx "$methods")"
# Update the list of methods.
methods="$inheritedMethods${inheritedMethods:+${methods:+$IFS}}$methods"
}
# A static type checker.
eval "
$interface.isInstance() {
echo \"\${1:-dummy}\$1\" | grep -xEq \"\${${interfacePrefix}instancePatterns}\"
}
"
# Return whether this is a class.
eval "
$interface.isClass() {
return 1
}
"
# Return whether this is an interface.
eval "
$interface.isInterface() {
return 0
}
"
# A static method that returns the methods declared in this interace.
eval "
$interface.getMethods() {
if [ -n \"\$1\" ]; then
setvar \"\$1\" '$methods'
else
echo '$methods'
fi
}
"
# A static method that returns the interface prefix.
eval "
$interface.getPrefix() {
if [ -n \"\$1\" ]; then
setvar \"\$1\" '$interfacePrefix'
else
echo '$interfacePrefix'
fi
}
"
# A static method that returns the parentage of this interface.
eval "
$interface.getParents() {
if [ -n \"\$1\" ]; then
setvar \"\$1\" '$extends'
else
echo '$extends'
fi
}
"
}
#
# Returns a variable from a given reference. The variable is either written
# to a named variable, or in absence of one, output to stdout.
#
# @param 1
# If this is the sole parameter it is a reference to the variable
# to output to stdout. If a second parameter exists, it is the name of
# the variable to write to.
# @param 2
# The reference to the variable to return.
#
bsda:obj:getVar() {
if [ -n "$2" ]; then
eval "$1=\"\$$2\""
else
eval "echo \"\$$1\""
fi
}
#
# Returns an object reference to a serialized object.
#
# @param 1
# If this is the sole parameter, this is a serialized string of which
# the object reference should be output. In case of a second parameter
# this is the name of the variable to return the reference to the
# serialized object to.
# @param 2
# The serialized string of which the reference should be returned.
#
bsda:obj:getSerializedId() {
if [ -n "$2" ]; then
setvar "$1" "${2##* }"
else
echo "${1##* }"
fi
}
#
# Deserializes a serialized object and returns or outputs a reference to
# said object.
#
# @param 1
# If there is a second parameter this is the name of the variable to
# store the object reference in. Otherwise this is the serialized string
# and the reference is be output to stdout.
# @param 2
# If given this is the serialized string and the object reference is
# saved in the variable named with the first parameter.
#
bsda:obj:deserialize() {
if [ "$2" = "-" ]; then
local stream
stream="$(cat)"
eval "$stream"
setvar "$1" "${stream##* }"
elif [ -n "$2" ]; then
eval "$2"
setvar "$1" "${2##* }"
else
eval "$1"
echo "${1##* }"
fi
}
#
# Checks whether the given parameter is an object.
#
# @param 1
# The parameter to check.
# @return
# 0 for objects, 1 for everything else.
#
bsda:obj:isObject() {
echo "$1" | grep -qExe "$bsda_obj_frameworkPrefix[_[:alnum:]]+_[0-9a-f]+_[0-9]+_[0-9]+_[0-9]+_"
}
#
# Checks whether the given parameter is an integer.
# Integers may be signed, but there must not be any spaces.
#
# @param 1
# The parameter to check.
# @return
# 0 for integers, 1 for everything else.
#
bsda:obj:isInt() {
echo "$1" | grep -qExe "(-|\+)?[0-9]+"
}
#
# Checks whether the given parameter is a floating point value.
# Floats may be signed, but there must not be any spaces.
# This function does not obey the locale.
#
# The following are examples for valid floats:
# 1
# 1.0
# -1.5
# 1000
# 1e3 = 1000
# 1e-3 = 0.001
# -1e-1 = -0.1
# +1e+2 = 100
#
# @param 1
# The parameter to check.
# @return
# 0 for floats, 1 for everything else.
#
bsda:obj:isFloat() {
echo "$1" | grep -qExe "(-|\+)?[0-9]+(\.[0-9]+)?(e(-|\+)?[0-9]+)?"
}
#
# Checks whether the given parameter is a simple floating point value.
# Simple floats may be signed, but there must not be any spaces.
# This function does not obey the locale.
#
# The following are examples for valid floats:
# 1
# 1.0
# -1.5
# 1000
#
# @param 1
# The parameter to check.
# @return
# 0 for simple floats, 1 for everything else.
#
bsda:obj:isSimpleFloat() {
echo "$1" | grep -qExe "(-|\+)?[0-9]+(\.[0-9]+)?"
}
#
# Creates the methods to a new object from a class.
#
# This is achieved by creating a method wrapper that provides the
# context variables this, class and caller.
#
# It works under the assumption, that methods are defined as:
# <class>.<method>()
#
# @param 1
# The class name.
# @param 2
# The class prefix where the scope checks are stored.
# @param 3
# The object reference.
# @param 4
# A list of method names.
#
bsda:obj:createMethods() {
local method scope
for method in $4; {
scope=${method%:*}
eval "scope=\"\$$2$scope\""
# Add method name to scope.
eval "scope=\"$scope\""
method=${method##*:}
eval "
$3.$method() {
local class this caller _return
# Check access permission.
$scope
bsda:obj:callerSetup
class=$1
this=$3
$1.$method \"\$@\"
_return=\$?
bsda:obj:callerFinish
return \$_return
}
"
}
}
#
# Deletes methods from an object. This is intended to be used in a destructor.
#
# @param 1
# The object reference.
# @param 2
# A list of method names.
#
bsda:obj:deleteMethods() {
local method
for method in $2; {
method=${method##*:}
unset -f "$1.$method"
}
}
#
# Deletes attributes from an object. This is intended to be used in a
# destructor.
#
# This works under the assumption, that attributes are defined as:
# <reference>_<attribute>
#
# @param 1
# The object reference.
# @param 2
# A list of attribute names.
#
bsda:obj:deleteAttributes() {
local attribute
for attribute in $2; {
unset "${1}$attribute"
}
}
#
# Setup the caller stack to store variables that should be overwritten
# in the caller context upon exiting the method.
#
# This function is called by the wrapper around class instance methods.
#
# The bsda_obj_callStackCount counter is increased and and a stack count prefix
# is created, which is used by bsda:obj:callerSetvar() to store variables
# for functions in the caller context until bsda:obj:callerFinish() is
# called.
#
# The call stack prefix is in the format 'bsda_obj_callStack_[0-9]+_'.
#
# @param caller
# Is set to the current stack count prefix.
# @param bsda_obj_callStackCount
# Is incremented by 1 and used to create the caller variable.
#
bsda:obj:callerSetup() {
# Increment the call stack counter and create the caller prefix.
caller="bsda_obj_callStack_${bsda_obj_callStackCount}_"
bsda_obj_callStackCount=$(($bsda_obj_callStackCount + 1))
# Create functions to interact with the caller.
eval "
# Create a wrapper around bsda:obj:callerSetvar for access
# through the caller prefix. I do not have the slightest idea
# why alias does not work for this.
$caller.setvar() {
bsda:obj:callerSetvar \"\$@\"
}
# Create a function that returns the object ID of the caller.
$caller.getObject() {
if [ -n \"\$1\" ]; then
setvar \"\$1\" '$this'
else
echo '$this'
fi
}
# Create a function that returns the class of the caller.
$caller.getClass() {
if [ -n \"\$1\" ]; then
setvar \"\$1\" '$class'
else
echo '$class'
fi
}
"
}
#
# Copy variables from the caller stack into the caller context and clean
# the stack up.
#
# This function is called by the wrapper around class instance methods
# after the actual method has terminated.
#
# @param caller
# The caller context prefix.
# @param ${caller}_setvars
# The list of variables to copy into the caller context.
# @param ${this}_callStackCount
# Is decremented by 1.
#
bsda:obj:callerFinish() {
# Remove the bsda:obj:callerSetvar() wrapper.
unset -f $caller.setvar $caller.getObject $caller.getClass
# Decrement the call stack counter and delete if no longer required.
bsda_obj_callStackCount=$(($bsda_obj_callStackCount - 1))
# Copy variables to the caller context.
local _var IFS
IFS=' '
eval "_var=\"\$${caller}_setvars\""
for _var in $_var; {
# Copy variable.
eval "setvar $_var \"\$$caller$_var\""
# Delete variable from stack.
unset $caller$_var
}
# Delete list of variables from stack.
unset ${caller}_setvars
}
#
# This function stores a variables for overwriting variables in the context
# of the caller. If no storing variable has been specified (i.e. the first
# parameter is empty), the value is printed instead.
#
# This function is accessable in methods by calling:
# $caller.setvar
#
# The stored variables are processed by the bsda:obj:callerFinish() function.
#
# @param 1
# The name of the variable to store.
# @param 2
# The value to store.
# @param caller
# The context to store variables in.
# @param ${caller}_setvars
# A list of all the stored variables for the caller context.
#
bsda:obj:callerSetvar() {
# Print if no return variable was specified.
test -z "$1" && echo "$2" && return
# Store value.
setvar $caller$1 "$2"
# Register variable.
eval "${caller}_setvars=\$${caller}_setvars\${${caller}_setvars:+ }$1"
}
So langsam brauche ich länger für das Dokumentieren als für das Implementieren. Ich denke jetzt ist endlich Schluss und ich konzentriere mich ab sofort voll auf die nächste Version von pkg_upgrade.
Update zu Version 1.9 - Über $caller.getObject und $caller.getClass kann man jetzt erfahren, von wem man eigentlich aufgerufen wurde