Erst einmal hier der aktuelle Stand meines Frameworks für objektorientierte Shell-Skripte:
Besonders stolz bin ich auf die Sache mit $caller.setvar. Das erlaubt es einer Methode Werte direkt in Variablen zurückzugeben. Dabei spielt es keine Rolle ob Variablen mit dem gleichen Namen in der Methode verwendet werden. Die Verwendung ist unter IMPLEMENTING METHODS beschrieben.
Kommen wir zum Größenwahn. Bisher verwendet pkg_upgrade Hintergrundprozesse für Downloads. Dabei kommunizieren alle Prozesse (der Hauptprozess, der Download Manager, die Downloadprozesse) über eine einzige Queue.
Nun will ich eine synchrone Variante von bsda_obj:createClass() erzeugen, die Klassen erzeugt, deren Objekte sich automatisch bei Zugriff über alle Prozesse synchronisieren.
Das soll komplett die Kommunikation über die Queue ersetzen und mir erlauben in allen Prozessen die gleichen objektorientierten Strukturen zu verwenden ohne mich dabei um die Synchronisierung der Daten zu kümmern. Damit kann ich auch einfacher andere Dinge parallelisieren, zum Beispiel die Integritätsprüfung.
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.3
# 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, constructor, destructor, a
# reset and a copy function.
#
# DEFINING CLASSES
#
# A detailed description on how to define classes can be found with the
# bsda_obj:createClass() function.
#
#
# 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.
#
# 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 functions to manipulate the caller context,
# which is the recommended way of returning data to the caller
#
# The following variable names may not be used in a method:
# _return
# _var
#
# 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 attribue, 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 later.
#
# 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.
#
#
# 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 function,
# if one was specified.
#
#
# 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 function if one was
# specified.
#
#
# 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 function if one was
# specified.
#
#
# 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
#
#
# 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.
#
#
# 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.
#
#
# TYPE CHECK
#
# This block documetns the use of the static type checking method created
# by the bsda_obj:createClass 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.
#
#
# 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
# The name of the variable to write to. If this is empty, the value
# is output to stdout instead.
# @param 2
# The reference to the variable.
#
bsda_obj:getVar() {
if [ -n "$1" ]; then
eval "$1=\"\$$2\""
else
eval "echo \"\$$2\""
fi
}
#
# Creates a new class, i.e. a constructor, destructor, resetter, getters
# and setters.
#
# So all that is left to be done are the methods.
#
# The following static variables are reserved:
# _nextId
#
# The following static methods are reserved:
# isInstance
#
# The following methods are reserved:
# copy
# delete
# reset
#
# @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.
#
# With these parameters a constructor and a destructor will be built.
# It is important that all used attributes are listed, or the copy
# and the delete methods will not work as expected.
#
# Everything that is not recognized as an identifier is treated as a
# comment.
#
# 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 an exact copy of an object, note
# that an init function must not break without parameters for copy to
# work.
#
# @return
# 0 on succes
# 1 if an identifier does not have a known prefix
# 2 if there is more than one init or clean method specified
#
bsda_obj:createClass() {
local IFS class methods attributes getters setters
local getter setter attribute reference init clean
IFS='
'
# There are some default methods.
methods="reset${IFS}delete${IFS}copy"
attributes=
getters=
setters=
init=
clean=
# Parse arguments.
for arg {
if [ -z "$class" ]; then
class="$arg"
continue
fi
case "$arg" in
x:*)
methods="$methods${methods:+$IFS}${arg#x:}"
;;
-:*)
attributes="$attributes${attributes:+$IFS}${arg#-:}"
;;
r:*)
attributes="$attributes${attributes:+$IFS}${arg#r:}"
getters="$getters${getters:+$IFS}${arg#r:}"
;;
w:*)
attributes="$attributes${attributes:+$IFS}${arg#w:}"
getters="$getters${getters:+$IFS}${arg#w:}"
setters="$setters${getters:+$IFS}${arg#w:}"
;;
i:*)
if [ -n "$init" ]; then
return 2
fi
methods="$methods${methods:+$IFS}${arg#i:}"
init="$class.${arg#i:} \"\$@\""
;;
c:*)
if [ -n "$clean"]; then
return 2
fi
methods="$methods${methods:+$IFS}${arg#c:}"
clean="$class.${arg#c:} \"\$@\""
;;
*)
# Assume everything else is a comment.
;;
esac
}
# Create getters.
for getter in $getters; {
attribute="$getter"
getter="get$(echo "${getter%%${getter#?}}" | tr '[:lower:]' '[:upper:]')${getter#?}"
# Add the getter to the list of methods.
methods="$methods${methods:+$IFS}$getter"
eval "
$class.$getter() {
if [ -n \"\$1\" ]; then
eval \"\$1=\\\"\\\$\${this}$attribute\\\"\"
else
eval \"echo \\\"\\\$\${this}$attribute\\\"\"
fi
}
"
}
# Create setters.
for setter in $setters; {
attribute="$setter"
setter="set$(echo "${setter%%${setter#?}}" | tr '[:lower:]' '[:upper:]')${setter#?}"
# Add the setter to the list of methods.
methods="$methods${methods:+$IFS}$setter"
eval "
$class.$setter() {
setvar \"\${this}$attribute\" \"\$1\"
}
"
}
# Create reference prefix.
reference="obj_$(echo "$class" | tr ':' '_')"
# Create object id counter.
setvar "${reference}_nextId" 0
# Create constructor.
eval "
$class() {
local this
# Create object reference.
this=\"${reference}_\${${reference}_nextId}_\"
# Increase the object id counter.
${reference}_nextId=\$((\$${reference}_nextId + 1))
# Create method instances.
bsda_obj:createMethods $class \$this \"$methods\"
# Return the object reference.
if [ -n \"\$1\" ]; then
setvar \"\$1\" \$this
else
echo \$this
fi
# Cast the reference variable from the parameters.
shift
$init
}
"
# Create a resetter.
eval "
$class.reset() {
$clean
# Delete attributes.
bsda_obj:deleteAttributes \$this \"$attributes\"
}
"
# Create destructor.
eval "
$class.delete() {
$clean
# Delete methods and attributes.
bsda_obj:deleteMethods \$this \"$methods\"
bsda_obj:deleteAttributes \$this \"$attributes\"
}
"
# Create copy method.
eval "
$class.copy() {
local IFS reference attribute
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 static type checker.
eval "
$class.isInstance() {
echo \"\$1\" | grep -xq '${reference}_[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 object reference.
# @param 3
# A list of method names.
#
bsda_obj:createMethods() {
local method
for method in $3; {
eval "
${2}_callStackCount=0
$2.$method() {
local class this caller _return
class=$1
this=$2
bsda_obj:callerSetup
$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; {
unset ${1}_callStackCount
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.
#
# Every method has a stack counter called _callStackCount (in the object
# context). This 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.
#
# @param caller
# Is set to the current stack count prefix.
# @param ${this}_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.
eval "
${this}_callStackCount=\$((\$${this}_callStackCount + 1))
caller=\"$this\${${this}_callStackCount}_\"
"
# Create a wrapper around bsda_obj:callerSetvar for access through
# the caller prefix.
eval "
$caller.setvar() {
bsda_obj:callerSetvar \"\$@\"
}
"
}
#
# 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
# Decrement the call stack counter.
eval "${this}_callStackCount=\$((\$${this}_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.
#
# 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() {
# Store value.
setvar $caller$1 "$2"
# Register variable.
eval "${caller}_setvars=\$${caller}_setvars\${${caller}_setvars:+ }$1"
}
Kommen wir zum Größenwahn. Bisher verwendet pkg_upgrade Hintergrundprozesse für Downloads. Dabei kommunizieren alle Prozesse (der Hauptprozess, der Download Manager, die Downloadprozesse) über eine einzige Queue.
Nun will ich eine synchrone Variante von bsda_obj:createClass() erzeugen, die Klassen erzeugt, deren Objekte sich automatisch bei Zugriff über alle Prozesse synchronisieren.
Das soll komplett die Kommunikation über die Queue ersetzen und mir erlauben in allen Prozessen die gleichen objektorientierten Strukturen zu verwenden ohne mich dabei um die Synchronisierung der Daten zu kümmern. Damit kann ich auch einfacher andere Dinge parallelisieren, zum Beispiel die Integritätsprüfung.
Zuletzt bearbeitet: