Shell-Skripte objektorientiert

Kamikaze

Warrior of Sunlight
Teammitglied
Das folgende ist das erste Ergebnis meiner Bemühungen zum objektorientierten Shell-Scripting. Es handelt sich um ein Framework dass es einem erleichtert Klassen anzulegen.

Die Klassen bekommen einen Konstruktor, Destruktor eine Kopiermethode und auf Wunsch werden auch automatisch Getter und Setter für Attribute erzeugt.

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.0

# 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 and
# a copy function.
#
# All that requires to be done to get a functional class 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.
#
# 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 first parameter to a method is always the reference to the object
# instance it was called for. So "$1" is the same 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 $1_count
#		# Increase counter value copy.
#		count=$(($count + 1))
#		# Store the counter value.
#		setvar $1_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.
#		$1.getCount count
#		# Increase counter value copy.
#		count=$(($count + 1))
#		# Store the counter value.
#		$1.setCount $count
#	}
#

#
# 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":
#
#	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.
#

#
# RESET
#
# This block documents the use of a resetter created by the
# bsda_obj:createClass function below.
#
# The resetter takes no arguments. It simply removes all attributes from memory.
#
# The following example shows how to reset an object referenced by "foobar".
#
#	$foobar.reset
#

#
# DESTRUCTOR
#
# This block documents the use of a destructor created by the
# bsda_obj:createClass function below.
#
# The destructor takes no arguments. 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
#

#
# 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
#	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.
#

#
# 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
		setvar "$1" "$(eval "echo \"\$$2\"")"
	else
		echo "$(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.
#
# @param @
#	A description of the class to create.
#	
#	The first parameter is the name of the class.
#	
#	The following is 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>()".
#
#	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.
#	
#	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 as the first argument the name of the variable to store
#	the value in, if this is ommitted, the value is written to stdout.
#
#	Additionally to the constructor and destructor the following methods
#	will be created:
#		copy	This method creates a copy of the object, it is used
#			in the same way as the constructor.
# @return
#	0 on succes, 1 if an identifier does not have a known prefix.
#
bsda_obj:createClass() {
	local IFS class methods attributes getters setters
	local getter setter attribute reference

	IFS='
'

	# There are some default methods.
	methods="reset${IFS}delete${IFS}copy"

	# 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:}"
			;;
			*)
				return 1
			;;
		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() {
				local value
				value=\"\${1}_$attribute\"
				value=\"\$(eval \"echo \\\"\\\$\$value\\\"\")\"
				if [ -n \"\$2\" ]; then
					setvar \"\$2\" \"\$value\"
				else
					echo \"\$value\"
				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 \"\$1_$attribute\" \"\$2\"
			}
		"
	}

	# Create reference prefix.
	reference="obj_$(echo "$class" | tr ':' '_')"

	# Create object id counter.
	setvar "${reference}_nextId" 0

	# Create constructor.
	eval "
		$class() {
			local self
	
			# Create object reference.
			self=\"${reference}_\${${reference}_nextId}_\"
	
			# Increase the object id counter.
			${reference}_nextId=\$((\$${reference}_nextId + 1))
	
			# Create method instances.
			bsda_obj:createMethods $class \$self \"$methods\"
	
			# Return the object reference.
			setvar \"\$1\" \$self
		}
	"

	# Create a resetter.
	eval "
		$class.reset() {
			# Delete attributes.
			bsda_obj:deleteAttributes \$1 \"$attributes\"
		}
	"

	# Create destructor.
	eval "
		$class.delete() {
			# Delete methods and attributes.
			bsda_obj:deleteMethods \$1 \"$methods\"
			bsda_obj:deleteAttributes \$1 \"$attributes\"
		}
	"

	# Create copy method.
	eval "
		$class.copy() {
			local IFS reference attribute value

			IFS='
'

			# Create a new empty object.
			$class reference

			# Store the new object reference in the target variable.
			setvar \"\$2\" \$reference

			# For each attribute copy the value over to the
			# new object.
			for attribute in \$(echo \"$attributes\"); {
				value=\"\${1}_\$attribute\"
				value=\"\$(eval \"echo \\\"\\\$\$value\\\"\")\"
				setvar \"\${reference}_\$attribute\" \"\$value\"
			}
		}
	"
}

#
# Creates the methods to a new object from a class. A method really is just
# a wrapper around a function that tekes the object reference as the first
# parameter. This is inteded to be used in a constructor.
#
# 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.$method() {
				$1.$method $2 \"\$@\"
			}
		"
	}
}

#
# 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 -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"
	}
}

Hier ist mal ein kleines Beispiel zum Veranschaulichen wie es verwendet wird:
Code:
> . bsda_obj.sh
> bsda_obj:createClass keksdose w:anzahlKekse
> keksdose omasKekse
> $omasKekse.setAnzahlKekse 25
> $omasKekse.getAnzahlKekse 
25
> $omasKekse.copy meineKekse
> $meineKekse.getAnzahlKekse 
25
> $omasKekse.delete
> $omasKekse.getAnzahlKekse
obj_keksdose_0_.getAnzahlKekse: not found
> $meineKekse.getAnzahlKekse
25
 
Zuletzt bearbeitet:
Ist das reine Spielerei oder verfolgst du damit auch einen produktiven Sinn?
Würde mich nämlich mal interessieren, wo/wie man so eine oo-Lösung nutzen kann.

Momentan entzieht sich mir die Praxis. Wobei das natürlich an den frühen Morgenstunden liegen kann. Oder aber auch an meiner Unerfahrenheit in Sachen OOP (nur durch den Java-Zwang) und Shellscripting :D



Schönen Tag zusammen!

//edit:
achso, sieht auf jeden Fall Klasse aus ;)
 
Die Komplexität (im Sinne von Verzweigungstiefe, ausnahme-Regelungen, usw.) von pkg_upgrade wächst mir über den Kopf.

Ich habe vor mit entsprechenden Klassen diese Dinge in Objekte zu verlagern, damit sie nicht mehr mit der Ablauflogik durchgemischt werden.

Außerdem kann man prima mit grep -Fx über Listen schrubben, wovon ich mir Performance-Zuwächse verspreche, die hoffentlich den zusätzlichen Overhead schlucken.
 
Das kann man nur mit einem Wort beschreiben: Geil! :cool:

Das muss ich mir auf jeden Fall mal etwas genauer ansehen. Endlich mal Datenstrukturen jenseits einfacher Listen in einem Shell-Skript - da tun sich ganz neue Möglichkeiten auf.

Danke!

P.S.:
Quickfix für ksh(1) auf OpenBSD: s/setvar/set -A/g
 
Zurück
Oben