[Sidefx-houdini-list] Re: perl in Houdini

Ammon Riley ammon at rhythm.com
Mon Aug 29 22:57:02 EDT 2005

Andrew D Lyons wrote:
> Today I've been using openport and hcommand to try and get perl to talk 
> to Houdini. I'm hoping to make some kind of an interface to the pipeline 
> here for Houdini.

Perhaps I can save you some troubles, then...

> To do so I need to source a hscript command that starts a shell (unix 
> command), so the shell can run a perl script, which then runs hcommand 
> on the original Houdini process.

Yep. Sounds familiar.

> Two problems:
> 1./ STDOUT from hcommand is ending up in Houdini's parent shell and not 
> perl.

That doesn't sound familiar. Are you just using a system() command?
If so, you may be confusing perl's system() with Houdini's system().
The former is more akin to Houdini's systemES().

    perldoc -q output.\*system

> 2./ Is it just me - or does this seem like a tenuous foundation for an 
> important pipeline interface?

Kind of, but not really, once you get it working. It's actually 
extremely flexible, which is what Houdini is all about.

Take a look at the two attached scripts. One's a "perl" hscript,
which you can call from within Houdini. The other is a perl module,
which takes care of the bi-directional communication. Between the
two, they should do fairly well.

Caveat emptor: If you notice, I wrote this a long time ago, back when
Houdini was at v4.1. It is now quite out of date, in terms of the 
exportable tags. However, it should give you a pretty solid basis
to start from, if you want to build on it.


  Ammon Riley || Pipeline Setup || Rhythm & Hues || LA, CA || ammon at rhythm.com
-------------- next part --------------
# Find out if a port has already been opened.
set portnum = `execute("openport")`

if (`index($portnum, "None") >= 0`) then
    # Open up a port. I like 3241874. :)
    set portnum = 3241874
    openport $portnum
    # Find the existing port number.
    set portnum = `substr($portnum, index($portnum, ":") + 2, strlen($portnum))`    if (`index($portnum, " ") > 0`) then
        set portnum = `substr($portnum, 0, index($portnum, " "))`

# If you've got more args than this, then you need to do something
# a little different. 
unix perl $arg1 $portnum $arg2 $arg3 $arg4 $arg5 $arg6 $arg7 $arg8 $arg9
-------------- next part --------------
package Houdini;

# Uncomment when we get a reasonable install of perl.
# use warnings;
use strict;

use IPC::Open2;
use FileHandle;
use Carp;

    use Exporter   ();
    use vars       qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);

    # set the version for version checking
    $VERSION     = 0.01;

    @ISA         = qw(Exporter);
    @EXPORT      = qw();	# Don't export anything by default!
    @EXPORT_OK   = qw();

    # The following hscript commands are *not* implemented/exported,
    # either because they have a perl equivalent (such as for), are
    # deprecated in hscript (such as animview), or are meaningless in 
    # a perl context (such as shift).
    #		animview   break   continue   echo      editor   else
    #		endif      exit    for        foreach   if       quit
    #		read       shift   time       unix      while

    %EXPORT_TAGS = (
	':ch'	=> [qw( chadd      chcommit  chgadd   chgls    chgop
			chgrm      chkey     chls     chread   chrename
			chreverse  chrm      chrmkey  chround  chscope
			chstretch  chwrite
	':gui'	=> [qw( audiopanel      chaneditor      chanlist
			chlayout        chopscope       desk
			dsreload        neteditor       opcolor
			oplayout        oplocate        opscale
			pane            toolbar         viewbackground
			viewcamera      viewcopy        viewdisplay
			viewerformat    vieweroption    viewerstow
			viewlayout      viewls          viewportformat
			viewprojection  viewtransform   viewtype
	':ex'	=> [qw(	excat  exedit  exhelp  exls  exread  exrm )],
	':mat'	=> [qw( matrman  matupdateref  matvex  matvexparm )],
	':op'	=> [qw( opadd       opcf     opchange  opcomment  opcook
			opcopy      opcp     opdepend  opfind     opgadd
			opgetinput  opglob   opgls     opgop      opgrm
			ophelp      opinfo   opls      opname     oporder
			opparm      oppaste  oppwf     opramp     opread
			oprm        opsave   opscript  opset      opunwire
			opupdate    opwire   opwrite   opcd
	':tm'	=> [qw(	tmgadd  tmgls  tmgname  tmgop  tmgrm  tmgshift )],
	':var'	=> [qw(	hip  job  set  setenv )],
		=> [qw( cmdread  mread  mwrite  source  tcl  tk )],
		=> [qw( render rkill rps )],
	':misc'	=> [qw( alias        bonemoveend   bookmark    closeport
			compfree     doublebuffer  dsoinfo     fcur
			fplayback    fps           frange      fset
			ftimecode    help          history     kinconvert
			memory       nextkey       objparent   openport
			paramset     performance   play        prompt
			shopconvert  tcur          texcache    timeslice
			tset         varchange     version     vexinfo
	':aux'	=> [qw( echo hopen hclose importVars hex )],
		=> [qw( :ch  :mat  :op  :tm  :var  :render  :aux  :extern )],
	':all'	=> [qw( :standard  :gui  :ex  :extern  :misc )],

    foreach (keys %EXPORT_TAGS)

use vars qw( @EXPORT_OK %EXPORT_TAGS $DEBUG %HENV $ReadWait );

# Print out some debug info.
$DEBUG = 0;

# Just a bit of a pause to ensure Houdini has had time to write.
$ReadWait = 0.15;

# Houdini environment. This can be overridden by the user. It's
# currently stuffed with defaults so that hopefully the user 
# won't need to do anything with this.
%HENV = ( 'HB' => '/usr/hfs4.1/bin' );

# I like the way CGI.pm makes use of the %EXPORT_TAGS, so I've
# shamelessly stolen the following two subs from CGI.pm. Note,
# however, that they've been modified slightly to suit my purpose.
# Away, ye of the Cargo Cult! ;)
sub import {
    my $self = shift;  
    my ($callpack, $callfile, $callline) = caller;

    no strict qw( refs );

    foreach (@_) {
        foreach my $sym (&expand_tags($_)) {
            tr/a-zA-Z0-9_//cd;  # don't allow weird function names
	    *{"${callpack}::$sym"} = \&{$sym};

sub expand_tags {
    my($tag) = @_;
    return ($tag) unless $EXPORT_TAGS{$tag};
    foreach (@{$EXPORT_TAGS{$tag}}) {
    return @r;

# This is what will be used if everything is being called as 
# class methods. 
my $Default = undef;

sub new
    my $class = shift;
    my $self = {};
    bless $self, $class;

    print "Creating new Houdini object.\n" if $DEBUG;

    # We don't have anything open.

    return $self;

sub connected
    my $self = shift;
    return $self->{'connected'} = shift if (@_);
    return $self->{'connected'};

    my $self = shift;

    return $self->{'TOHOUDINI'} = shift if (@_);
    return $self->{'TOHOUDINI'};

    my $self = shift;

    return $self->{'FROMHOUDINI'} = shift if (@_);
    return $self->{'FROMHOUDINI'};

sub isaHoudini()
    return 1;

sub self_or_Default
    return @_ if (defined($_[0]) && !ref($_[0]) &&
		    ($_[0] eq 'Houdini')); # This is a default.

    unless (defined($_[0]) && ref($_[0]) &&
		(ref($_[0]) eq 'Houdini' || eval "\$_[0]->isaHoudini()"))
    { # optimize for the common case
	$Default = new Houdini unless defined($Default);
    return @_;

sub write
    my ($self, @params) = self_or_Default(@_);

    print "Sending command: ", join(" ", @params), "\n" if $DEBUG;

    print { $self->TOHOUDINI } join " ", @params;

sub read
    my ($self) = self_or_Default(@_);

    my $rin = '';
    vec($rin,fileno($self->FROMHOUDINI),1) = 1;

    # Wait for houdini to finish.
    select(my $rout=$rin, undef, undef, undef);

    my @data = ();
    my $buf = undef;

    # Now read something as long as there's something to read.
    while( select($rout=$rin, undef, undef, $ReadWait) &&
	sysread($self->FROMHOUDINI, $buf, 80))
	print "got: ", $buf, "\n" if $DEBUG;
	push @data, $buf;
    @data = split "\n", join "", @data;

    return \@data;

sub hopen
    my ($self, $file) = self_or_Default(@_);

    # Close the old filehandles, if they're open.
    if ($self->connected)
	print "Closing old filehandles.\n" if $DEBUG;
	print "Creating filehandles.\n" if $DEBUG;
	# We're going to need a few filehandles. One for in, one for out.
	$self->TOHOUDINI( new FileHandle );
	$self->FROMHOUDINI( new FileHandle );

    # Open up an hscript (and possibly with a filename). Set up 
    # the appropriate pipes to enable reading and writing to
    # the hscript process.
    if (!defined $file || $file eq '')
	print "Opening new hscript.\n" if $DEBUG;

	# Warning: open2 may not work on WinBloze!
	return 0 unless open2($self->FROMHOUDINI, $self->TOHOUDINI,
    if ($file =~ /\.hip$/)
	print "Opening hscript ($file).\n" if $DEBUG;

	# Catch the non-existant file.
	return 0 unless (-e $file);

	# Warning: open2 may not work on WinBloze!
	return 0 unless open2($self->FROMHOUDINI, $self->TOHOUDINI,
				    "$HENV{'HB'}/hscript", $file);

    if ($self->connected)
	print "Reading preamble from hscript.\n" if $DEBUG;
	#read the pre-amble from hscript.
	print join("\n", @{$self->read()}), "\n";
	# Change the prompt to absolutely nothing. This will keep
	# Us from needing to read after every write. Hopefully.
	$self->write("prompt ''\n");

    # Set up to read and write from hcommand.
    if ($file =~ /^\d+$/)
	print "Opening houdini port $file.\n" if $DEBUG;

	# Test that we can connect to the specified port.
	chomp(my $test = `$HENV{'HB'}/hcommand $file echo OK`);
	return 0 unless ($test eq 'OK');

	# Warning: open2 may not work on WinBloze!
	return 0 unless open2($self->FROMHOUDINI, $self->TOHOUDINI,
				    "$HENV{'HB'}/hcommand", $file);

    return $self->connected;

sub hclose
    my ($self) = self_or_Default(@_);

    print "Closing connection.\n" if $DEBUG;

    # Close the old filehandles, if they're open.
    if ($self->connected)

# Import variables set in Houdini into the namespace.
sub importVars(@)
    my ($self, $namespace) = self_or_Default(@_);  

    # We'll default to the 'H' namespace.
    $namespace = 'H' unless defined($namespace);
    croak "Can't import names into 'main'\n"
	if $namespace eq 'main';

    no strict qw( refs );

    foreach ($self->set('-s')) {
	my ($var, $val) = m|^\s*set\s+-.\s+([^=]+?)\s+=\s+'(.*)'$|;
	${"${namespace}::$var"} = $val;

    my ($self) = self_or_Default(@_);

    if ($self->connected)

# This command provides access to the Houdini expressions.
sub hex
    my ($self, @parms) = self_or_Default(@_);

    # The value of an expression is only printed if it is echoed.
    # This function can be used as a cheap hack for the echo 
    # functionality. Try hex('1` hee hee hee `1');
    $self->write(q{echo `}, @parms, qq{`\n});
    my $response = $self->read;
    # Get rid of blank lines in the response. If there was a 
    # problem with the expression, then there will be a blank line
    # or more before the error message.
    shift @{$response} while (scalar(@{$response}) &&
				    $response->[0] =~ /^\s*$/);

    # If there is no output, then return undef (though I don't know why
    # there wouldn't be output.
    return undef unless scalar(@{$response});

    # If there was an error, carp and return undef.
    if ($response->[0] =~ /Expression error:/)
	croak $response->[0];
	return undef;

    return $response->[0];

    my ($self, @parms) = self_or_Default(@_);

    my $command = undef;
	no strict qw( vars );
	$command = $AUTOLOAD;

    $command =~ s/^.*:://;
    # Some Houdini commands will create output. Some won't. However,
    # I have no idea which commands will do this, and which don't,
    # since a command's behaviour varies with its parameters. To keep
    # from blocking on commands that *don't* output anything, we'll
    # tack on a command that's guaranteed to output something.
    my $term_str = '--done--';

    # Note that for some commands that return a single line (such as
    # opglob), $term_str is appended onto the single line. Using a
    # double echo here should fix that.
    my $term = qq(; echo; echo "$term_str");

    $self->write($command, @parms, $term, "\n");
    my $response = $self->read;

    # Get rid of the echoed signal, as well as any blank bits at the
    # end of the list.
    while (scalar(@{$response}) &&
	    ($response->[-1] =~ /^\s*$term_str\s*$/ ||
		$response->[-1] =~/^\s*$/))
	pop @{$response};

    # For some things

    my ($error) = grep { /Unknown command:/ } @{$response};
    croak $error if $error;

    ($error) = grep { /$command:  Invalid usage/ } @{$response};
    croak join("\n", @{$response}), "\n" if $error;

    return @{$response};

END { }       # module clean-up code here (global destructor)



=head1 NAME

Houdini - An interface to hcommand and hscript


use Houdini;

The rest is a little too involved for a synopsis. Read on.


This library uses perl5 objects to provide a more powerful scripting
layer on top of Side Effects Software's Houdini hscript language.
It abstracts away the pipes into hscript and hcommand, and brings
the hscript commands and Houdini expressions into the perl level.


I haven't written a Makefile.PL yet. I don't know if I will. For
now you will have to copy this file into your @INC path somewhere.
If you prefer not to do that, or don't have the appropriate permissions,

    use lib '/path/to/module';
    use Houdini;

There are other alternatives, but there is other documentation
specifically on that.



    $h = new Houdini;

This will create a new Houdini object. At this point, it is not
connected to anything. The reason for this is to allow the user
to modify various settings (such as the environment) before
opening a session.


To connect your Houdini object to a session, use the B<hopen()>
method. This method can be called in one of two ways:

=over 4

=item $h->hopen($filename)

This starts hscript, and loads $filename. If $filename is undef
or B<hopen()> is called without a parameter, hscript is started
without a file. Note that it is not possible to connect to an
already running hscript process.

=item $h->hopen($port)

This connects the Houdini object to $port of an already running
Houdini process. The $port must be a port number that was 
used as an argument to B<openport> in the Houdini textport.


Needless to say, as with the regular B<open()> call, you should
always check the return value!


Once you have connected your Houdini object to a session, you can
interact with it by using the same commands you would in the
textport. All of the operators return a list of the output from
the Houdini session, as if you had done "@a = <>;". This list
may be empty, depending on what is output from the Houdini session.

    @objects = $h->opls;	# Get a list of the objects

As usual, there is always an exception to the rule (see the 

For commands that require arguments (or for which arguments are
optional), you can supply them as a list, as a string, or any
combination of the two.

    @objects = $h->opls;
    $h->opset(qq{-d off -p on -e off @objects});

    @flags = ('-d off', '-p on', '-e off');
    $h->opset(@flags, @objects);

Because hashes get flattened to an array when passed as an argument, 
you can also do:

    %flags = (
	'-d'	=>	'off',
	'-p'	=>	'on',
	'-e'	=>	'off'
    $h->opset(%flags, @objects);

Not all hscript commands have a Houdini object method. Specifically,
the loop constructs such as B<for>, B<foreach>, and B<while>, as well
as the B<if> statement have equivalents in perl that are much easier
to use. It would not make sense to use the Houdini equivalents when
the perl constructs are available. To see the complete list of commands
that are not duplicated, see the comments in Houdini.pm, above where
B<%EXPORT_TAGS> is defined.


In Houdini, expression functions are called through backticks.
Since perl uses backticks for another purpose, a Houdini object
calls expression functions through the B<hex> method. The return
value is either a scalar, or undef. As with the regular methods, 
arguments are passed as either a string or a list. 

    # Normalize a vector.
    $v = q([1,2,3]);
    $v_n = $h->hex(qq{normalize(vector("$v"))});


You will note, in the previous example, the "" around the $v.
This is a necessity because Houdini has no concept of perl's
variables. Consider the code

    $v_n = $h->hex("normalize(vector($v))");

perl interpolates the $v before the string gets passed to the
Houdini session:


Houdini doesn't realize that the $v is a string, so you must force
the $v to be a string within Houdini. The other option would be to
set a variable withing Houdini, then use that variable:

    # Normalize a vector.
    $v = q([1,2,3]);
    $h->set("-l V = $v");
    # Now use the Houdini variable.
    $v_n = $h->hex('normalize(vector($V))');

I suggest that, unless you need that variable to remain in Houdini
after your script finishes, you use the other method. Adding a
few "" here and there as necessary is bound to be less typing 
than the above. (This is Laziness in action.)

Also of note, because perl's data types consist of scalar and 
lists, Houdini variable types are not preserved as you go back
and forth between Houdini and perl, no matter what the return
value of a Houdini expression might be.

For example, the B<vector()> expression returns a vector in

    $v = $h->hex('vector("[1,2,3]");
    $h->hex("normalize($v)");		# Fails. $v is B<not> a vector.

Thus, if a perl variable is interpolated in a string to be used
as a vector or matrix, it must first be wrapped in the appropriate
B<vector()> or B<matrix()> expression to convert the string into
a Houdini type. The necessity of wrapping strings in "" or '' 
depends on the instance.

    $v = q([1,2,3]);
    $h->set("-l V = $v");		# No quotes needed.
    $h->hex(qq{vector("$v")});		# Need quotes.

One of the worst parts of embedding hscript in other languages
such as B<csh> is the nightmare of escaping quotes. As with perl,
the type of quotation mark used has significance to Houdini. A
string using B<""> interpolates variables, while one using B<''>
doesn't. Thankfully, perl provides other "quotation marks" for
strings. Compare:

    # Need to interpolate $v in perl, so "" are required.

    # But who wants to do all that escaping!?

While this module makes life easier in many regards, it doesn't do
away with the difficulty arising from using variables in the Houdini
world within your scripts. In order to access a variable in Houdini
(say $F), it needs to be passed through from perl. This can be done
by either escaping it, "\$F", or by enclosing it in single quotes,
'$F'. Now we're really glad to have alternate quoting methods in

    $renderer = '/out/mantra1';

    # Don't interpolate the $F variable in Houdini.
    $h->opparm("$renderer picture('test.\$F4.pic')");
    $h->opls($renderer, 'picture(\'test.$F4.pic\')');

    # Or the easy way
    $h->opparm($renderer, q[ picture('test.$F4.pic') ]);


There is one problem with calling perl scripts written with the
Houdini module from the textport. If that script tries to write
to a Houdini port in the process which called the script, you 
will run into a problem called deadlock. When a command is running
in the textport, the texport is busy. Any further commands sent to
the textport must wait until the texport is "unbusy" before they
can be processed. When the command being waited on to finish is
the command waiting to send commands, you've now created a situation
where your script is waiting for itself to finish before it can

To get around this problem, you can either write the backgrounding
into the perl script:

    #!/usr/bin/perl -s

    use Houdini;

    my $Usage = "Use the -tp switch if running from the textport.\n";

    fork && exit if ($tp);

    # The rest of your script.

or, you can force the script to run in the background from the textport
command line:

    unix "your_script.pl &"

You can write a cmd script to open a port and background the process,
and call all your scripts from it:

    # Find out if a port has already been opened.
    set portnum = `execute("openport")`

    if (`index($portnum, "None") >= 0`) then
	# Open up a port. I like 3241874. :)
	set portnum = 3241874
	openport $portnum
	# Find the existing port number.
	set start = `index($portnum, ":") + 2`
	set portnum = `substr($portnum, $start, strlen($portnum))`
	if (`index($portnum, " ") > 0`) then
	    set portnum = `substr($portnum, 0, index($portnum, " "))`

    # If you've got more args than this, then you need to do something
    # a little different. 
    unix "perl $arg1 $portnum $arg2 $arg3 $arg4 $arg5 $arg6 $arg7 &"



To set a perl variable to have the same value as a variable set
in Houdini, you must use:

    $var = $h->echo('$var');

Optionally, if there are a good many variables you would like access
to, you can load variables set in a Houdini session into a given namespace.
The syntax to do that is:


(If a namespace is not provided, the variables are imported into
the H:: namespace.)

You can then access those variables as follows:

    # Supposing a normal hip file

and so on.


=over 4

=item B<$Houdini::ReadWait>

In order to ensure that hscript or hcommand has time to write its
output, Houdini objects pause momentarily before attempting to read
the output. If you find that you are not getting all an expected
return value, the Houdini object may think that the Houdini session
has finished writing output if the Houdini session has an abnormally
long pause while writing data. Increasing this value will cause all
Houdini objects to wait longer between successive checks for output.
The current value is 0.15 seconds. See B<perldoc -f select> for
more info.

=item B<%Houdini::HENV>

As the name implies, this variable supplies an environment to
the Houdini session. Currently there must be a single environment
common to all Houdini objects. That may change. The only key in
the hash by default is HB, which is needed to find the hscript or
hcommand binarly. Note that for Houdini objects connecting via
a port to an already open Houdini process, this environment
(with the exception of HB) is moot.

=item B<$Houdini::DEBUG>

Setting this to a non-zero value spits out various information that
might help in debugging.



For convenience, you can import the Houdini methods into your 
namespace. The syntax for this is:

    use Houdini <list of methods>

The listed methods will be imported into the current package; you can
call them directly without creating a Houdini object first.  This
example shows how to import the B<hopen()>, B<hclose()> and B<opls()>
methods, and then use them directly:

    use Houdini qw(hopen hclose opls);
    hopen() || die;
    print join("\n", opls), "\n";

You can import groups of methods by referring to a number of special

=over 4

=item B<op> 

Import all op commands such as B<opls()>, B<opcd()>, and B<opscript()>.

=item B<ch>   

Import channel related commands such as B<chadd()>, B<chls()>.

=item B<gui>    

Import functions to manipulate the state of the Houdini GUI. These
functions include B<chlayout()>, B<desk()>, etc.

=item B<mat>    

Import functions to work with materials and shops.

=item B<ex>     

Import functions to work with custom expressions. These functions
will likely be seldom used.

=item B<tm>   

Import timegroup functions. 

=item B<var>  

Import functions that manipulate Houdini variables. These include not
only B<set()> and B<setenv()>, but B<hip()> and B<job()>, as well.

=item B<extern> 

Import functions that provide hooks to files and scripts outside of
Houdini. These functions include B<mread()> and B<mwrite()>, B<source()>
and B<tk()>.

=item B<render> 

Import functions that kick off, list, and kill renders.

=item B<misc>

Import functions that don't fit into the any of the above. These
functions include the likes of B<alias()>, B<history()>, and functions
to control playback (which may move into their own group).

=item B<aux>  

Import functions that are not hscript commands, but which make
life easier for you. These include B<hopen()>, B<hclose()>,
B<importVars()>, and B<hex()> (which provides access to the
Houdini expressions.

=item B<standard>

Import the most useful commands. This is the same as specifiying
    use Houdini qw( :ch :mat :op :tm :var :render :aux :extern );

=item B<all> 

Import everything under the sun. For the full list, look at the source
to Houdini.pm where %EXPORT_TAGS is defined.



    #!/usr/bin/perl -sw

    use strict;
    use Houdini qw(:standard);

    $0 =~ s|^.*/||;
    my $usage = <<EOU;
    Usage: $0 [-tp] <renderer> <start> <end> <file|port#>
           Use the -tp switch if $0 is called from the texport.

    die $usage unless (scalar(@ARGV) == 4);

    my ($renderer, $start, $end, $hipfile) = @ARGV;

    # Prevent deadlock if called from the textport.
    fork && exit if ($::tp);

    hopen($hipfile) || die "Could not open $hipfile.\n";


    # Make sure the renderer exists.
    die "$renderer not found.\n"
	    unless ((opls($renderer))[0] =~ /^$renderer/);

    my @parms = (
	'trange ( on )',
	"f ( $start $end )",
	'background ( off )',

    print "Setting parameters.\n";
    opparm($renderer, @parms);

	local $| = 1;
	print "Rendering $renderer...";
    print " done.\n";


=head1 BUGS

=item 1.
Using a non-existing port does not fail until trying to write to
the pipe causes a SIGPIPE. Similarly, if hscript starts, but closes
immediately because it couldn't get a license, things do not die


=head1 TODO

=item 1.
Add a signal handler for SIGPIPE. (See B<BUGS>)

=item 2.
Make use of the B<%HENV> variable. Maybe even move it into a
per object variable.

=item 3.
Write explicit subs for methods like set/setenv, instead of using 
the Autoloader. This will allow them to call them differently
(such as set($var, $val)).



This module was written by Ammon Riley. All rights reserved. This module
may be used and modified under the same terms as the Perl license. This
copyright notice must remain attached to the file. If you redistribute
a modified version, please attach a note listing the modifications you
have made.

This module has no guarantees of working, and is supported only to the
extent of my mood on the day you ask a question. For all intents and
purposes, presume it unsupported. Especially by the support staff at
Side Effects -- this module was not written by Side Effects.

Send bug reports, questions and comments to ammon at rhythm.com.

=head1 CREDITS

=item Side Effects Software, for creating the Houdini software.

=item Mishka Gorodnitzky, for occasionally acting as the perl manpage.

=item John Coldrick, Lucio Ismael Flores, Antoine Durr, Jeff Wagner,
and the support staff at Side Effects for helping alpha test.

=item Lincoln D. Stein, from whose CGI.pm some of the ideas, code
and documentation were stolen.

More information about the Sidefx-houdini-list mailing list