#!/usr/bin/perl
# 99aeabc9ec7fe80b1b39f5e53dc7e49e <- self-modifying Perl magic
# This is a self-modifying Perl file. I'm sorry you're viewing the source (it's
# really gnarly). If you're curious what it's made of, I recommend reading
# http://github.com/spencertipping/writing-self-modifying-perl.
#
# If you got one of these from someone and don't know what to do with it, send
# it to spencer@spencertipping.com and I'll see if I can figure out what it
# does.
# For the benefit of HTML viewers (this is hack):
#
$|++;
my %data;
my %transient;
my %externalized_functions;
my %datatypes;
my %locations; # Maps eval-numbers to attribute names
sub meta::define_form {
my ($namespace, $delegate) = @_;
$datatypes{$namespace} = $delegate;
*{"meta::${namespace}::implementation"} = $delegate;
*{"meta::$namespace"} = sub {
my ($name, $value, %options) = @_;
chomp $value;
$data{"${namespace}::$name"} = $value unless $options{no_binding};
&$delegate($name, $value) unless $options{no_delegate}}}
sub meta::eval_in {
my ($what, $where) = @_;
# Obtain next eval-number and alias it to the designated location
@locations{eval('__FILE__') =~ /\(eval (\d+)\)/} = ($where);
my $result = eval $what;
$@ =~ s/\(eval \d+\)/$where/ if $@;
warn $@ if $@;
$result}
meta::define_form 'meta', sub {
my ($name, $value) = @_;
meta::eval_in($value, "meta::$name")};
meta::meta('configure', <<'__');
# A function to configure transients. Transients can be used to store any number of
# different things, but one of the more common usages is type descriptors.
sub meta::configure {
my ($datatype, %options) = @_;
$transient{$_}{$datatype} = $options{$_} for keys %options;
}
__
meta::meta('externalize', <<'__');
# Function externalization. Data types should call this method when defining a function
# that has an external interface.
sub meta::externalize {
my ($name, $attribute, $implementation) = @_;
my $escaped = $name;
$escaped =~ s/[^A-Za-z0-9:]/_/go;
$externalized_functions{$name} = $externalized_functions{$escaped} = $attribute;
*{"::$name"} = *{"::$escaped"} = $implementation || $attribute;
}
__
meta::meta('externalize_template', <<'__');
sub meta::externalize_template {
my ($name, $implementation) = @_;
meta::externalize "template::$name", "template::$name", $implementation;
}
__
meta::meta('functor::code-templates', <<'__');
package code;
# Templates for generating named source files.
sub template {
my ($name, $implementation) = @_;
$implementation ||= sub {
my ($line, $block) = @_;
my $append = $line =~ s/^\h*>>\h*//o;
$line =~ s/\s+.*$//o;
file::write(&{'::source-directory'}() . "/$line", ($append && "\n") . $block, mkpath => 1, append => $append);
"\\lstset{caption={$line" . ($append && ' (continued)') . "},name={$line}}\\begin{${name}code}\n$block \\end{${name}code}";
};
meta::externalize_template $name, $implementation;
}
__
meta::meta('functor::editable', <<'__');
# An editable type. This creates a type whose default action is to open an editor
# on whichever value is mentioned. This can be changed using different flags.
sub meta::functor::editable {
my ($typename, %options) = @_;
meta::configure $typename, %options;
meta::define_form $typename, sub {
my ($name, $value) = @_;
$options{on_bind} && &{$options{on_bind}}($name, $value);
meta::externalize $options{prefix} . $name, "${typename}::$name", sub {
my $attribute = "${typename}::$name";
my ($command, @new_value) = @_;
return &{$options{default}}(retrieve($attribute)) if ref $options{default} eq 'CODE' and not defined $command;
return edit($attribute) if $command eq 'edit' or $options{default} eq 'edit' and not defined $command;
return associate($attribute, @new_value ? join(' ', @new_value) : join('', )) if $command eq '=' or $command eq 'import' or $options{default} eq 'import' and not defined $command;
return retrieve($attribute)}}}
__
meta::meta('functor::html-templates', <<'__');
my @html_elements = qw/html head title meta script style link body div/; # Very incomplete list
for my $e (@html_elements) {
meta::externalize "template::$e", "template::$e", sub {
my ($line, $block) = @_;
"<$e $line>\n$block\n$e>";
};
}
__
meta::meta('functor::tex-templates', <<'__');
package tex;
# A wrapper for TeX templates. The idea is always the same, so I'm abstracting out the
# common externalization logic here.
sub template_for(&) {
my ($implementation) = @_;
sub {
my %names = @_;
for my $name (keys %names) {
::meta::externalize_template $name, sub {
&$implementation($names{$name}, @_);
};
}
};
}
sub id {
map {$_ => $_} @_;
}
# Creates a one-line or multiline template based on normal TeX syntax. It's a straight
# transfer into TeX with no preprocessing.
*template = template_for {
my ($name, $line, $block) = @_;
$block ? "\\begin{$name}\n$block\n\\end{$name}" : "\\$name\{$line\}";
};
# Creates a labeled one-line template. This is just like normal TeX, but assumes the
# specification of a label name after a pipe character.
*labeled_template = template_for {
my ($name, $line, undef) = @_;
my ($real_stuff, $label) = split /\h*\|\h*/, $line;
"\\$name\{$real_stuff\}" . ($label && "\\label{$label}");
};
__
meta::meta('template::beamer', <<'__');
tex::template tex::id(qw/frame pause item block alertblock exampleblock/);
my $fframe_template = tex::template_for {
my ($name, $line, $block) = @_;
"\\begin{frame}[fragile]\n$block\n\\end{frame}\n";
};
&$fframe_template(tex::id('fframe'));
__
meta::meta('template::code', <<'__');
code::template $_ for qw/java cpp asm javascript html resource perl ruby/;
__
meta::meta('template::document', 'tex::template tex::id(qw/document tableofcontents maketitle title author date abstract documentclass verbatim/);');
meta::meta('template::enumeration', 'tex::template tex::id(qw/enumerate itemize description item/);');
meta::meta('template::math', <<'__');
tex::template align => 'align*', nalign => 'align';
tex::template tex::id(qw/theorem proof lemma corollary conjecture definition proposition/);
__
meta::meta('template::quotations', 'tex::template tex::id(qw/quotation quote/);');
meta::meta('template::sections', <<'__');
tex::labeled_template(s1 => 'section', s2 => 'subsection', s3 => 'subsubsection', s4 => 'paragraph', s5 => 'subparagraph');
tex::labeled_template(sc => 'chapter', sp => 'part');
__
meta::meta('type::alias', <<'__');
meta::configure 'alias', inherit => 0;
meta::define_form 'alias', sub {
my ($name, $value) = @_;
meta::externalize $name, "alias::$name", sub {
# Can't pre-tokenize because shell::tokenize doesn't exist until the library::
# namespace has been evaluated (which will be after alias::).
shell::run(shell::tokenize($value), shell::tokenize(@_));
};
};
__
meta::meta('type::bootstrap', <<'__');
# Bootstrap attributes don't get executed. The reason for this is that because
# they are serialized directly into the header of the file (and later duplicated
# as regular data attributes), they will have already been executed when the
# file is loaded.
meta::configure 'bootstrap', extension => '.pl', inherit => 1;
meta::define_form 'bootstrap', sub {};
__
meta::meta('type::cache', <<'__');
meta::configure 'cache', inherit => 0;
meta::define_form 'cache', \&meta::bootstrap::implementation;
__
meta::meta('type::cached_dependency', <<'__');
meta::configure 'cached_dependency', inherit => 0, extension => '';
meta::define_form 'cached_dependency', \&meta::bootstrap::implementation;
__
meta::meta('type::configuration', <<'__');
meta::functor::editable 'configuration', inherit => 0, extension => '.conf', default => sub {
# Any lines starting with #, with or without leading whitespace, are treated as comments.
# Comments are not parsed in option text; that is, you could specify an option that contained
# a # and the # and following text would be considered part of that option.
my ($data) = @_;
my @options = grep /:\h+/o && ! /^\h*#/o && ! /^\h*$/o, split(/\v+/o, $data);
s/^\h+//o for @options;
my @key_values = map split(/\h*:\h+/o, $_, 2), @options;
$key_values[$_ << 1] and $key_values[$_ << 1] =~ s/\s/_/go for 0 .. @key_values >> 1;
$key_values[$_ << 1] and $key_values[$_ << 1] = lc $key_values[$_ << 1] for 0 .. @key_values >> 1;
@key_values;
};
__
meta::meta('type::data', 'meta::functor::editable \'data\', extension => \'\', inherit => 0, default => \'cat\';');
meta::meta('type::function', <<'__');
meta::configure 'function', extension => '.pl', inherit => 1;
meta::define_form 'function', sub {
my ($name, $value) = @_;
meta::externalize $name, "function::$name", meta::eval_in("sub {\n$value\n}", "function::$name");
};
__
meta::meta('type::hook', <<'__');
meta::configure 'hook', extension => '.pl', inherit => 0;
meta::define_form 'hook', sub {
my ($name, $value) = @_;
*{"hook::$name"} = meta::eval_in("sub {\n$value\n}", "hook::$name");
};
__
meta::meta('type::inc', <<'__');
meta::configure 'inc', inherit => 1, extension => '.pl';
meta::define_form 'inc', sub {
use File::Path 'mkpath';
use File::Basename qw/basename dirname/;
my ($name, $value) = @_;
my $tmpdir = basename($0) . '-' . $$;
my $filename = "/tmp/$tmpdir/$name";
push @INC, "/tmp/$tmpdir" unless grep /^\/tmp\/$tmpdir$/, @INC;
mkpath(dirname($filename));
unless (-e $filename) {
open my $fh, '>', $filename;
print $fh $value;
close $fh;
}
};
__
meta::meta('type::indicator', <<'__');
# Shell indicator function. The output of each of these is automatically
# appended to the shell prompt.
meta::configure 'indicator', inherit => 1, extension => '.pl';
meta::define_form 'indicator', sub {
my ($name, $value) = @_;
*{"indicator::$name"} = meta::eval_in("sub {\n$value\n}", "indicator::$name");
};
__
meta::meta('type::internal_function', <<'__');
meta::configure 'internal_function', extension => '.pl', inherit => 1;
meta::define_form 'internal_function', sub {
my ($name, $value) = @_;
*{$name} = meta::eval_in("sub {\n$value\n}", "internal_function::$name");
};
__
meta::meta('type::library', <<'__');
meta::configure 'library', extension => '.pl', inherit => 1;
meta::define_form 'library', sub {
my ($name, $value) = @_;
meta::eval_in($value, "library::$name");
};
__
meta::meta('type::message_color', <<'__');
meta::configure 'message_color', extension => '', inherit => 1;
meta::define_form 'message_color', sub {
my ($name, $value) = @_;
terminal::color($name, $value);
};
__
meta::meta('type::meta', <<'__');
# This doesn't define a new type. It customizes the existing 'meta' type
# defined in bootstrap::initialization. Note that horrible things will
# happen if you redefine it using the editable functor.
meta::configure 'meta', extension => '.pl', inherit => 1;
__
meta::meta('type::note', 'meta::functor::editable \'note\', extension => \'.sdoc\', inherit => 0, default => \'edit\';');
meta::meta('type::parent', <<'__');
meta::define_form 'parent', \&meta::bootstrap::implementation;
meta::configure 'parent', extension => '', inherit => 1;
__
meta::meta('type::resource', 'meta::functor::editable \'resource\', extension => \'.cltex\', inherit => 1, default => \'edit\';');
meta::meta('type::retriever', <<'__');
meta::configure 'retriever', extension => '.pl', inherit => 1;
meta::define_form 'retriever', sub {
my ($name, $value) = @_;
$transient{retrievers}{$name} = meta::eval_in("sub {\n$value\n}", "retriever::$name");
};
__
meta::meta('type::section', 'meta::functor::editable \'section\', extension => \'.cltex\', inherit => 0, default => \'edit\';');
meta::meta('type::state', <<'__');
# Allows temporary or long-term storage of states. Nothing particularly insightful
# is done about compression, so storing alternative states will cause a large
# increase in size. Also, states don't contain other states -- otherwise the size
# increase would be exponential.
# States are created with the save-state function.
meta::configure 'state', inherit => 0, extension => '.pl';
meta::define_form 'state', \&meta::bootstrap::implementation;
__
meta::meta('type::template', <<'__');
meta::configure 'template', extension => '.pl', inherit => 1;
meta::define_form 'template', sub {
my ($name, $value) = @_;
meta::externalize "template::$name", "template::$name", meta::eval_in("sub {\n$value\n}", "template::$name");
};
__
meta::meta('type::vim_highlighter', <<'__');
meta::configure 'vim_highlighter', extension => '.vim', inherit => 1;
meta::define_form 'vim_highlighter', \&meta::bootstrap::implementation;
__
meta::bootstrap('html', <<'__');
__
meta::bootstrap('initialization', <<'__');
#!/usr/bin/perl
# 99aeabc9ec7fe80b1b39f5e53dc7e49e <- self-modifying Perl magic
# This is a self-modifying Perl file. I'm sorry you're viewing the source (it's
# really gnarly). If you're curious what it's made of, I recommend reading
# http://github.com/spencertipping/writing-self-modifying-perl.
#
# If you got one of these from someone and don't know what to do with it, send
# it to spencer@spencertipping.com and I'll see if I can figure out what it
# does.
# For the benefit of HTML viewers (this is hack):
#
$|++;
my %data;
my %transient;
my %externalized_functions;
my %datatypes;
my %locations; # Maps eval-numbers to attribute names
sub meta::define_form {
my ($namespace, $delegate) = @_;
$datatypes{$namespace} = $delegate;
*{"meta::${namespace}::implementation"} = $delegate;
*{"meta::$namespace"} = sub {
my ($name, $value, %options) = @_;
chomp $value;
$data{"${namespace}::$name"} = $value unless $options{no_binding};
&$delegate($name, $value) unless $options{no_delegate}}}
sub meta::eval_in {
my ($what, $where) = @_;
# Obtain next eval-number and alias it to the designated location
@locations{eval('__FILE__') =~ /\(eval (\d+)\)/} = ($where);
my $result = eval $what;
$@ =~ s/\(eval \d+\)/$where/ if $@;
warn $@ if $@;
$result}
meta::define_form 'meta', sub {
my ($name, $value) = @_;
meta::eval_in($value, "meta::$name")};
__
meta::bootstrap('perldoc', <<'__');
=head1 Self-modifying Perl script
=head2 Original implementation by Spencer Tipping L
The prototype for this script is licensed under the terms of the MIT source code license.
However, this script in particular may be under different licensing terms. To find out how
this script is licensed, please contact whoever sent it to you. Alternatively, you may
run it with the 'license' argument if they have specified a license that way.
You should not edit this file directly. For information about how it was constructed, go
to L. For quick usage guidelines,
run this script with the 'usage' argument.
=cut
__
meta::cache('parent-identification', <<'__');
./sdoc
/home/spencertipping/bin/configuration aa772900bb5b925cb84346bd72a4249d
/home/spencertipping/bin/literate-project cf9c87bde4f2e9aed1ea468d85b2ddad
/home/spencertipping/bin/object 99aeabc9ec7fe80b1b39f5e53dc7e49e
configuration aa772900bb5b925cb84346bd72a4249d
development 3d94eaf9719db17882d02c1d1fe18718
notes a9e5975593ed5d90d943ad98405c71e5
object 99aeabc9ec7fe80b1b39f5e53dc7e49e
preprocessor 70dae4b46eb4e06798ec6f38d17d4c7b
repository 05bc3036c343fdb8aec5b0be12a9b19e
vim-highlighters 902333a0bd6ed90ff919fe8477cb4e69
__
meta::data('author', 'Spencer Tipping');
meta::data('default-action', 'shell');
meta::data('license', <<'__');
MIT License
Copyright (c) 2010 Spencer Tipping
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
__
meta::data('output-dir', '/tmp');
meta::data('pdf-output-file', '/tmp/writing-self-modifying-perl.9Yr3/document.pdf');
meta::data('pdf-reader', '/usr/bin/evince');
meta::data('pdftex-command', 'pdflatex -output-directory=__TEMPORARY_DIRECTORY__ __INPUT_FILE__');
meta::data('permanent-identity', '2572d38bedc92761b662fe6b546a01cf');
meta::data('source-directory', '.');
meta::data('table-of-contents', '1');
meta::data('tex-command', 'latex -output-directory=__TEMPORARY_DIRECTORY__ __INPUT_FILE__');
meta::data('watching', '0');
meta::function('ad', <<'__');
return @{$transient{path}} = () unless @_;
push @{$transient{path}}, @_;
__
meta::function('alias', <<'__');
my ($name, @stuff) = @_;
@_ ? @stuff ? around_hook('alias', @_, sub {associate("alias::$name", join(' ', @stuff), execute => 1)})
: retrieve("alias::$name") // "Undefined alias $name"
: table_display([select_keys('--namespace' => 'alias')], [map retrieve($_), select_keys('--namespace' => 'alias')]);
__
meta::function('cat', 'join "\\n", retrieve(@_);');
meta::function('cc', <<'__');
# Stashes a quick one-line continuation. (Used to remind me what I was doing.)
@_ ? associate('data::current-continuation', hook('set-cc', join(' ', @_))) : retrieve('data::current-continuation');
__
meta::function('ccc', 'rm(\'data::current-continuation\');');
meta::function('child', <<'__');
around_hook('child', @_, sub {
my ($child_name) = @_;
clone($child_name);
enable();
qx($child_name update-from $0 -n);
disable()});
__
meta::function('clean', <<'__');
my $output_directory = pdf_output_file();
$output_directory =~ s+/.*++g;
unlink <$output_directory/*>;
rmdir $output_directory;
__
meta::function('clone', <<'__');
for (grep length, @_) {
around_hook('clone', $_, sub {
hypothetically(sub {
rm('data::permanent-identity');
file::write($_, serialize(), noclobber => 1);
chmod(0700, $_)})})}
__
meta::function('compile', <<'__');
my $tex_command = tex_command();
my $pdftex_command = pdftex_command();
my $filename = 'document';
my $contents = tex();
my $output_directory = output_dir();
chomp $output_directory;
my $temporary_directory = temporary_name();
$temporary_directory =~ s+^.*/++;
$temporary_directory = "$output_directory/$temporary_directory";
$tex_command =~ s/__TEMPORARY_DIRECTORY__/$temporary_directory/g;
$tex_command =~ s+__INPUT_FILE__+$temporary_directory/$filename.tex+g;
$pdftex_command =~ s/__TEMPORARY_DIRECTORY__/$temporary_directory/g;
$pdftex_command =~ s+__INPUT_FILE__+$temporary_directory/$filename.tex+g;
mkdir $temporary_directory;
file::write("$temporary_directory/$filename.tex", $contents);
my $result = table_of_contents() ? system($tex_command) || system($tex_command) || system($pdftex_command) : system($pdftex_command);
$transient{pdf_output_file} = "$temporary_directory/$filename.pdf";
$result;
__
meta::function('cp', <<'__');
my $from = shift @_;
my $value = retrieve($from);
associate($_, $value) for @_;
__
meta::function('create', <<'__');
my ($name, $value) = @_;
around_hook('create', $name, $value, sub {
return edit($name) if exists $data{$name};
associate($name, defined $value ? $value : '');
edit($name) unless defined $value});
__
meta::function('current-state', 'serialize(\'-pS\');');
meta::function('disable', 'hook(\'disable\', chmod_self(sub {$_[0] & 0666}));');
meta::function('dupdate', <<'__');
# Update the repository based on the dependencies it lists. These dependencies
# can be anything that's retrievable.
# If you have any ::s in the local name of the dependency, then the cached_dependency::
# prefix won't be added. This lets you import slices of other objects and have
# those slices merge into any namespace you'd like.
rm(grep /^cached_dependency::/, keys %data);
my %dependencies = map &{attribute($_)}(), select_keys('--criteria' => "^configuration::depend.*");
for (keys %dependencies) {
terminal::info("Retrieving $dependencies{$_} as $_");
my $prefix = /::/ ? '' : 'cached_dependency::';
associate("${prefix}$_", retrieve($dependencies{$_}))}
reload();
__
meta::function('edit', <<'__');
my ($name, %options) = @_;
my $extension = extension_for($name);
die "$name is virtual or does not exist" unless exists $data{$name};
die "$name is inherited; use 'edit $name -f' to edit anyway" unless is($name, '-u') || is($name, '-d') || exists $options{'-f'};
around_hook('edit', @_, sub {
associate($name, invoke_editor_on($data{$name} // '', %options, attribute => $name, extension => $extension), execute => 1)});
save() unless $data{'data::edit::no-save'};
'';
__
meta::function('enable', 'hook(\'enable\', chmod_self(sub {$_[0] | $_[0] >> 2}));');
meta::function('export', <<'__');
# Exports data into a text file.
# export attr1 attr2 attr3 ... file.txt
my $name = pop @_;
@_ or die 'Expected filename';
file::write($name, join "\n", retrieve(@_));
__
meta::function('extern', '&{$_[0]}(retrieve(@_[1 .. $#_]));');
meta::function('grep', <<'__');
# Looks through attributes for a pattern. Usage is grep pattern [options], where
# [options] is the format as provided to select_keys.
my ($pattern, @args) = @_;
my ($options, @criteria) = separate_options(@args);
my @attributes = select_keys(%$options, '--criteria' => join('|', @criteria));
$pattern = qr/$pattern/;
my @m_attributes;
my @m_line_numbers;
my @m_lines;
for my $k (@attributes) {
next unless length $k;
my @lines = split /\n/, retrieve($k);
for (0 .. $#lines) {
next unless $lines[$_] =~ $pattern;
push @m_attributes, $k;
push @m_line_numbers, $_ + 1;
push @m_lines, '' . ($lines[$_] // '')}}
unless ($$options{'-C'}) {
s/($pattern)/\033[1;31m\1\033[0;0m/g for @m_lines;
s/^/\033[1;34m/o for @m_attributes;
s/^/\033[1;32m/o && s/$/\033[0;0m/o for @m_line_numbers}
table_display([@m_attributes], [@m_line_numbers], [@m_lines]);
__
meta::function('hash', 'fast_hash(@_);');
meta::function('hook', <<'__');
my ($hook, @args) = @_;
$transient{active_hooks}{$hook} = 1;
dangerous('', sub {&$_(@args)}) for grep /^hook::${hook}::/, sort keys %data;
@args;
__
meta::function('hooks', 'join "\\n", sort keys %{$transient{active_hooks}};');
meta::function('identity', 'retrieve(\'data::permanent-identity\') || associate(\'data::permanent-identity\', fast_hash(rand() . name() . serialize()));');
meta::function('import', <<'__');
my $name = pop @_;
associate($name, @_ ? join('', map(file::read($_), @_)) : join('', ));
__
meta::function('initial-state', '$transient{initial};');
meta::function('is', <<'__');
my ($attribute, @criteria) = @_;
my ($options, @stuff) = separate_options(@criteria);
exists $data{$attribute} and attribute_is($attribute, %$options);
__
meta::function('load-state', <<'__');
around_hook('load-state', @_, sub {
my ($state_name) = @_;
my $state = retrieve("state::$state_name");
terminal::state('saving current state into _...');
save_state('_');
delete $data{$_} for grep ! /^state::/, keys %data;
%externalized_functions = ();
terminal::state("restoring state $state_name...");
meta::eval_in($state, "state::$state_name");
terminal::error(hook('load-state-failed', $@)) if $@;
reload();
verify()});
__
meta::function('lock', 'hook(\'lock\', chmod_self(sub {$_[0] & 0555}));');
meta::function('ls', <<'__');
my ($options, @criteria) = separate_options(@_);
my ($external, $shadows, $sizes, $flags, $long, $hashes, $parent_hashes) = @$options{qw(-e -s -z -f -l -h -p)};
$sizes = $flags = $hashes = $parent_hashes = 1 if $long;
return table_display([grep ! exists $data{$externalized_functions{$_}}, sort keys %externalized_functions]) if $shadows;
my $criteria = join('|', @criteria);
my @definitions = select_keys('--criteria' => $criteria, '--path' => $transient{path}, %$options);
my %inverses = map {$externalized_functions{$_} => $_} keys %externalized_functions;
my @externals = map $inverses{$_}, grep length, @definitions;
my @internals = grep length $inverses{$_}, @definitions;
my @sizes = map sprintf('%6d %6d', length(serialize_single($_)), length(retrieve($_))), @{$external ? \@internals : \@definitions} if $sizes;
my @flags = map {my $k = $_; join '', map(is($k, "-$_") ? $_ : '-', qw(d i m u))} @definitions if $flags;
my @hashes = map fast_hash(retrieve($_)), @definitions if $hashes;
my %inherited = parent_attributes(grep /^parent::/o, keys %data) if $parent_hashes;
my @parent_hashes = map $inherited{$_} || '-', @definitions if $parent_hashes;
join "\n", map strip($_), split /\n/, table_display($external ? [grep length, @externals] : [@definitions],
$sizes ? ([@sizes]) : (), $flags ? ([@flags]) : (), $hashes ? ([@hashes]) : (), $parent_hashes ? ([@parent_hashes]) : ());
__
meta::function('make', <<'__');
compile() || view();
clean();
__
meta::function('mv', <<'__');
my ($from, $to) = @_;
die "'$from' does not exist" unless exists $data{$from};
associate($to, retrieve($from));
rm($from);
__
meta::function('name', <<'__');
my $name = $0;
$name =~ s/^.*\///;
$name;
__
meta::function('note', <<'__');
# Creates a note with a given name, useful for jotting things down.
my $name = join('-', @_);
create("note::$name");
__
meta::function('notes', 'ls(\'-a\', \'^note::\');');
meta::function('parents', 'join "\\n", grep s/^parent:://o, sort keys %data;');
meta::function('perl', <<'__');
my $result = eval(join ' ', @_);
$@ ? terminal::error($@) : $result;
__
meta::function('preprocess', <<'__');
# Implements a simple preprocessing language.
# Syntax follows two forms. One is the 'line form', which gives you a way to specify arguments inline
# but not spanning multiple lines. The other is 'block form', which gives you access to both one-line
# arguments and a block of lines. The line parameters are passed in verbatim, and the block is
# indentation-adjusted and then passed in as a second parameter. (Indentation is adjusted to align
# with the name of the command.)
#
# Here are the forms:
#
# - line arguments to function
#
# - block line arguments << eof
# block contents
# block contents
# ...
# - eof
my ($string, %options) = @_;
my $expansions = 0;
my $old_string = '';
my $limit = $options{expansion_limit} || 100;
my @pieces = ();
sub adjust_spaces {
my ($spaces, $string) = @_;
$string =~ s/^$spaces //mg;
chomp $string;
$string;
}
while ($old_string ne $string and $expansions++ < $limit) {
$old_string = $string;
while ((my @pieces = split /(^(\h*)-\h \S+ \h* \V* <<\h*(\w+)$ \n .*? ^\2-\h\3$)/xms, $string) > 1 and $expansions++ < $limit) {
$pieces[1 + ($_ << 2)] =~ /^ (\h*)-\h(\S+)\h*(\V*)<<\h*(\w+)$ \n(.*?) ^\1-\h\4 $/xms && $externalized_functions{"template::$2"} and
$pieces[1 + ($_ << 2)] = &{"template::$2"}($3, adjust_spaces($1, $5))
for 0 .. $#pieces / 4;
@pieces[2 + ($_ << 2), 3 + ($_ << 2)] = '' for 0 .. $#pieces / 4;
$string = join '', @pieces;
}
if ((my @pieces = split /^(\h*-\h \S+ \h* .*)$/xom, $string) > 1) {
$pieces[1 + ($_ << 1)] =~ /^ \h*-\h(\S+)\h*(.*)$/xom && $externalized_functions{"template::$1"} and
$pieces[1 + ($_ << 1)] = &{"template::$1"}($2)
for 0 .. $#pieces >> 1;
$string = join '', @pieces;
}
}
$string;
__
meta::function('rd', <<'__');
if (@_) {my $pattern = join '|', @_;
@{$transient{path}} = grep $_ !~ /^$pattern$/, @{$transient{path}}}
else {pop @{$transient{path}}}
__
meta::function('reload', 'around_hook(\'reload\', sub {execute($_) for grep ! /^bootstrap::/, keys %data});');
meta::function('render', <<'__');
use File::Copy 'copy';
compile();
my $pdf = $transient{pdf_output_file};
my $tex = $pdf;
my $directory = source_directory();
my $name = name();
$tex =~ s/\.pdf$/.tex/;
copy($pdf, "$directory/$name.pdf") or die $!;
copy($tex, "$directory/$name.tex") or die $!;
__
meta::function('rm', <<'__');
around_hook('rm', @_, sub {
exists $data{$_} or terminal::warning("$_ does not exist") for @_;
delete @data{@_}});
__
meta::function('rmparent', <<'__');
# Removes one or more parents.
my ($options, @parents) = separate_options(@_);
my $clobber_divergent = $$options{'-D'} || $$options{'--clobber-divergent'};
my %parents = map {$_ => 1} @parents;
my @other_parents = grep !$parents{$_}, grep s/^parent:://, select_keys('--namespace' => 'parent');
my %kept_by_another_parent;
$kept_by_another_parent{$_} = 1 for grep s/^(\S+)\s.*$/\1/, split /\n/o, cat(@other_parents);
for my $parent (@parents) {
my $keep_parent_around = 0;
for my $line (split /\n/, retrieve("parent::$parent")) {
my ($name, $hash) = split /\s+/, $line;
next unless exists $data{$name};
my $local_hash = fast_hash(retrieve($name));
if ($clobber_divergent or $hash eq $local_hash or ! defined $hash) {rm($name) unless $kept_by_another_parent{$name}}
else {terminal::info("local attribute $name exists and is divergent; use rmparent -D $parent to delete it");
$keep_parent_around = 1}}
$keep_parent_around ? terminal::info("not deleting parent::$parent so that you can run", "rmparent -D $parent if you want to nuke divergent attributes too")
: rm("parent::$parent")}
__
meta::function('save', 'around_hook(\'save\', sub {dangerous(\'\', sub {file::write($0, serialize()); $transient{initial} = state()}) if verify()});');
meta::function('save-state', <<'__');
# Creates a named copy of the current state and stores it.
my ($state_name) = @_;
around_hook('save-state', $state_name, sub {
associate("state::$state_name", current_state(), execute => 1)});
__
meta::function('serialize', <<'__');
my ($options, @criteria) = separate_options(@_);
my $partial = $$options{'-p'};
my $criteria = join '|', @criteria;
my @attributes = map serialize_single($_), select_keys(%$options, '-m' => 1, '--criteria' => $criteria), select_keys(%$options, '-M' => 1, '--criteria' => $criteria);
my @final_array = @{$partial ? \@attributes : [retrieve('bootstrap::initialization'), @attributes, 'internal::main();', '', '__END__']};
join "\n", @final_array;
__
meta::function('serialize-single', <<'__');
# Serializes a single attribute and optimizes for content.
my $name = $_[0] || $_;
my $contents = $data{$name};
my $meta_function = 'meta::' . namespace($name);
my $invocation = attribute($name);
my $escaped = $contents;
$escaped =~ s/\\/\\\\/go;
$escaped =~ s/'/\\'/go;
return "$meta_function('$invocation', '$escaped');" unless $escaped =~ /\v/;
my $delimiter = '__' . fast_hash($contents);
my $chars = 2;
++$chars until $chars >= length($delimiter) || index("\n$contents", "\n" . substr($delimiter, 0, $chars)) == -1;
$delimiter = substr($delimiter, 0, $chars);
"$meta_function('$invocation', <<'$delimiter');\n$contents\n$delimiter";
__
meta::function('serialize_single', <<'__');
# Serializes a single attribute and optimizes for content.
my $name = $_[0] || $_;
my $contents = $data{$name};
my $meta_function = 'meta::' . namespace($name);
my $invocation = attribute($name);
my $escaped = $contents;
$escaped =~ s/\\/\\\\/go;
$escaped =~ s/'/\\'/go;
return "$meta_function('$invocation', '$escaped');" unless $escaped =~ /\v/;
my $delimiter = '__' . fast_hash($contents);
my $chars = 2;
++$chars until $chars >= length($delimiter) || index("\n$contents", "\n" . substr($delimiter, 0, $chars)) == -1;
$delimiter = substr($delimiter, 0, $chars);
"$meta_function('$invocation', <<'$delimiter');\n$contents\n$delimiter";
__
meta::function('sh', 'system(@_);');
meta::function('shb', <<'__');
# Backgrounded shell job.
exec(@_) unless fork;
__
meta::function('shell', <<'__');
my ($options, @arguments) = separate_options(@_);
$transient{repl_prefix} = $$options{'--repl-prefix'};
terminal::cc(retrieve('data::current-continuation')) if length $data{'data::current-continuation'};
around_hook('shell', sub {shell::repl(%$options)});
__
meta::function('size', <<'__');
my $size = 0;
$size += length $data{$_} for keys %data;
sprintf "% 7d % 7d % 7d", length(serialize()), $size, length(serialize('-up'));
__
meta::function('snapshot', <<'__');
my ($name) = @_;
file::write(my $finalname = temporary_name($name), serialize(), noclobber => 1);
chmod 0700, $finalname;
hook('snapshot', $finalname);
__
meta::function('state', <<'__');
my @keys = sort keys %data;
my $hash = fast_hash(fast_hash(scalar @keys) . join '|', @keys);
$hash = fast_hash("$data{$_}|$hash") for @keys;
$hash;
__
meta::function('tex', <<'__');
my ($document) = @_;
$document ||= 'main';
preprocess(retrieve("section::$document"));
__
meta::function('touch', 'associate($_, \'\') for @_;');
meta::function('unlock', 'hook(\'unlock\', chmod_self(sub {$_[0] | 0200}));');
meta::function('update', <<'__');
update_from(@_, grep s/^parent:://o, sort keys %data);
__
meta::function('update-from', <<'__');
# Upgrade all attributes that aren't customized. Customization is defined when the data type is created,
# and we determine it here by checking for $transient{inherit}{$type}.
# Note that this assumes you trust the remote script. If you don't, then you shouldn't update from it.
around_hook('update-from-invocation', separate_options(@_), sub {
my ($options, @targets) = @_;
my %parent_id_cache = cache('parent-identification');
my %already_seen;
@targets or return;
my @known_targets = grep s/^parent:://, parent_ordering(map "parent::$_", grep exists $data{"parent::$_"}, @targets);
my @unknown_targets = grep ! exists $data{"parent::$_"}, @targets;
@targets = (@known_targets, @unknown_targets);
my $save_state = ! ($$options{'-n'} || $$options{'--no-save'});
my $no_parents = $$options{'-P'} || $$options{'--no-parent'} || $$options{'--no-parents'};
my $force = $$options{'-f'} || $$options{'--force'};
my $clobber_divergent = $$options{'-D'} || $$options{'--clobber-divergent'};
save_state('before-update') if $save_state;
for my $target (@targets) {
dangerous("updating from $target", sub {
around_hook('update-from', $target, sub {
my $identity = $parent_id_cache{$target} ||= join '', qx($target identity);
next if $already_seen{$identity};
$already_seen{$identity} = 1;
my $attributes = join '', qx($target ls -ahiu);
my %divergent;
die "skipping unreachable $target" unless $attributes;
for my $to_rm (split /\n/, retrieve("parent::$target")) {
my ($name, $hash) = split(/\s+/, $to_rm);
next unless exists $data{$name};
my $local_hash = fast_hash(retrieve($name));
if ($clobber_divergent or $hash eq $local_hash or ! defined $hash) {rm($name)}
else {terminal::info("preserving local version of divergent attribute $name (use update -D to clobber it)");
$divergent{$name} = retrieve($name)}}
associate("parent::$target", $attributes) unless $no_parents;
dangerous('', sub {eval qx($target serialize -ipmu)});
dangerous('', sub {eval qx($target serialize -ipMu)});
map associate($_, $divergent{$_}), keys %divergent unless $clobber_divergent;
reload()})})}
cache('parent-identification', %parent_id_cache);
if (verify()) {hook('update-from-succeeded', $options, @targets);
terminal::info("Successfully updated. Run 'load-state before-update' to undo this change.") if $save_state}
elsif ($force) {hook('update-from-failed', $options, @targets);
terminal::warning('Failed to verify: at this point your object will not save properly, though backup copies will be created.',
'Run "load-state before-update" to undo the update and return to a working state.') if $save_state}
else {hook('update-from-failed', $options, @targets);
terminal::error('Verification failed after the upgrade was complete.');
terminal::info("$0 has been reverted to its pre-upgrade state.", "If you want to upgrade and keep the failure state, then run 'update-from $target --force'.") if $save_state;
return load_state('before-update') if $save_state}});
__
meta::function('usage', '"Usage: $0 action [arguments]\\nUnique actions (run \'$0 ls\' to see all actions):" . ls(\'-u\');');
meta::function('verify', <<'__');
file::write(my $other = $transient{temporary_filename} = temporary_name(), my $serialized_data = serialize());
chomp(my $observed = join '', qx|perl '$other' state|);
unlink $other if my $result = $observed eq (my $state = state());
terminal::error("Verification failed; expected $state but got $observed from $other") unless $result;
hook('after-verify', $result, observed => $observed, expected => $state);
$result;
__
meta::function('view', <<'__');
my $pdf_reader = pdf_reader();
chomp $pdf_reader;
system("$pdf_reader '$transient{pdf_output_file}'");
__
meta::function('vim', <<'__');
# Installs VIM highlighters.
file::write("$ENV{'HOME'}/.vim/syntax/$_.vim", retrieve("vim_highlighter::$_")) for grep s/^vim_highlighter:://o, keys %data;
__
meta::hook('before-shell::ad', <<'__');
ad('section::');
__
meta::indicator('cc', 'length ::retrieve(\'data::current-continuation\') ? "\\033[1;36mcc\\033[0;0m" : \'\';');
meta::indicator('locked', 'is_locked() ? "\\033[1;31mlocked\\033[0;0m" : \'\';');
meta::indicator('path', <<'__');
join "\033[1;30m/\033[0;0m", @{$transient{path}};
__
meta::internal_function('around_hook', <<'__');
# around_hook('hookname', @args, sub {
# stuff;
# });
# Invokes 'before-hookname' on @args before the sub runs, invokes the
# sub on @args, then invokes 'after-hookname' on @args afterwards.
# The after-hook is not invoked if the sub calls 'die' or otherwise
# unwinds the stack.
my $hook = shift @_;
my $f = pop @_;
hook("before-$hook", @_);
my $result = &$f(@_);
hook("after-$hook", @_);
$result;
__
meta::internal_function('associate', <<'__');
my ($name, $value, %options) = @_;
die "Namespace does not exist" unless exists $datatypes{namespace($name)};
$data{$name} = $value;
execute($name) if $options{'execute'};
$value;
__
meta::internal_function('attribute', <<'__');
my ($name) = @_;
$name =~ s/^[^:]*:://;
$name;
__
meta::internal_function('attribute_is', <<'__');
my ($a, %options) = @_;
my %inherited = parent_attributes(grep /^parent::/o, sort keys %data) if grep exists $options{$_}, qw/-u -U -d -D/;
my $criteria = $options{'--criteria'} || $options{'--namespace'} && "^$options{'--namespace'}::" || '.';
my %tests = ('-u' => sub {! $inherited{$a}},
'-d' => sub {$inherited{$a} && fast_hash(retrieve($a)) ne $inherited{$a}},
'-i' => sub {$transient{inherit}{namespace($a)}},
'-s' => sub {$a =~ /^state::/o},
'-m' => sub {$a =~ /^meta::/o});
return 0 unless scalar keys %tests == scalar grep ! exists $options{$_} || &{$tests{$_}}(), keys %tests;
return 0 unless scalar keys %tests == scalar grep ! exists $options{uc $_} || ! &{$tests{$_}}(), keys %tests;
$a =~ /$_/ || return 0 for @{$options{'--path'}};
$a =~ /$criteria/;
__
meta::internal_function('cache', <<'__');
my ($name, %pairs) = @_;
if (%pairs) {associate("cache::$name", join "\n", map {$pairs{$_} =~ s/\n//g; "$_ $pairs{$_}"} sort keys %pairs)}
else {map split(/\s/, $_, 2), split /\n/, retrieve("cache::$name")}
__
meta::internal_function('chmod_self', <<'__');
my ($mode_function) = @_;
my (undef, undef, $mode) = stat $0;
chmod &$mode_function($mode), $0;
__
meta::internal_function('dangerous', <<'__');
# Wraps a computation that may produce an error.
my ($message, $computation) = @_;
terminal::info($message) if $message;
my @result = eval {&$computation()};
terminal::warning(translate_backtrace($@)), return undef if $@;
wantarray ? @result : $result[0];
__
meta::internal_function('debug_trace', <<'__');
terminal::debug(join ', ', @_);
wantarray ? @_ : $_[0];
__
meta::internal_function('execute', <<'__');
my ($name, %options) = @_;
my $namespace = namespace($name);
eval {&{$datatypes{$namespace}}(attribute($name), retrieve($name))};
warn $@ if $@ && $options{'carp'};
__
meta::internal_function('exported', <<'__');
# Allocates a temporary file containing the concatenation of attributes you specify,
# and returns the filename. The filename will be safe for deletion anytime.
my $filename = temporary_name();
file::write($filename, cat(@_));
$filename;
__
meta::internal_function('extension_for', <<'__');
my $extension = $transient{extension}{namespace($_[0])};
$extension = &$extension($_[0]) if ref $extension eq 'CODE';
$extension || '';
__
meta::internal_function('fast_hash', <<'__');
my ($data) = @_;
my $piece_size = length($data) >> 3;
my @pieces = (substr($data, $piece_size * 8) . length($data), map(substr($data, $piece_size * $_, $piece_size), 0 .. 7));
my @hashes = (fnv_hash($pieces[0]));
push @hashes, fnv_hash($pieces[$_ + 1] . $hashes[$_]) for 0 .. 7;
$hashes[$_] ^= $hashes[$_ + 4] >> 16 | ($hashes[$_ + 4] & 0xffff) << 16 for 0 .. 3;
$hashes[0] ^= $hashes[8];
sprintf '%08x' x 4, @hashes[0 .. 3];
__
meta::internal_function('file::read', <<'__');
my $name = shift;
open my($handle), "<", $name;
my $result = join "", <$handle>;
close $handle;
$result;
__
meta::internal_function('file::write', <<'__');
use File::Path 'mkpath';
use File::Basename 'dirname';
my ($name, $contents, %options) = @_;
die "Choosing not to overwrite file $name" if $options{noclobber} and -f $name;
mkpath(dirname($name)) if $options{mkpath};
open my($handle), $options{append} ? '>>' : '>', $name or die "Can't open $name for writing";
print $handle $contents;
close $handle;
__
meta::internal_function('fnv_hash', <<'__');
# A rough approximation to the Fowler-No Voll hash. It's been 32-bit vectorized
# for efficiency, which may compromise its effectiveness for short strings.
my ($data) = @_;
my ($fnv_prime, $fnv_offset) = (16777619, 2166136261);
my $hash = $fnv_offset;
my $modulus = 2 ** 32;
$hash = ($hash ^ ($_ & 0xffff) ^ ($_ >> 16)) * $fnv_prime % $modulus for unpack 'L*', $data . substr($data, -4) x 8;
$hash;
__
meta::internal_function('hypothetically', <<'__');
# Applies a temporary state and returns a serialized representation.
# The original state is restored after this, regardless of whether the
# temporary state was successful.
my %data_backup = %data;
my ($side_effect) = @_;
my $return_value = eval {&$side_effect()};
%data = %data_backup;
die $@ if $@;
$return_value;
__
meta::internal_function('internal::main', <<'__');
disable();
$SIG{'INT'} = sub {snapshot(); exit 1};
$transient{initial} = state();
chomp(my $default_action = retrieve('data::default-action'));
my $function_name = shift(@ARGV) || $default_action || 'usage';
terminal::warning("unknown action: '$function_name'") and $function_name = 'usage' unless $externalized_functions{$function_name};
around_hook('main-function', $function_name, @ARGV, sub {
dangerous('', sub {
chomp(my $result = &$function_name(@ARGV));
print "$result\n" if $result})});
save() unless state() eq $transient{initial};
END {
enable();
}
__
meta::internal_function('invoke_editor_on', <<'__');
my ($data, %options) = @_;
my $editor = $options{editor} || $ENV{VISUAL} || $ENV{EDITOR} || die 'Either the $VISUAL or $EDITOR environment variable should be set to a valid editor';
my $options = $options{options} || $ENV{VISUAL_OPTS} || $ENV{EDITOR_OPTS} || '';
my $attribute = $options{attribute};
$attribute =~ s/\//-/g;
my $filename = temporary_name() . "-$attribute$options{extension}";
file::write($filename, $data);
system("$editor $options '$filename'");
my $result = file::read($filename);
unlink $filename;
$result;
__
meta::internal_function('is_locked', '!((stat($0))[2] & 0222);');
meta::internal_function('main', <<'__');
$SIG{'INT'} = sub {
snapshot();
exit 1;
};
my $initial_state = state();
chomp(my $default_action = retrieve('data::default-action'));
my $function_name = shift(@ARGV) || $default_action || 'usage';
terminal::message('warning', "Unknown action: '$function_name'") and $function_name = 'usage' unless $externalized_functions{$function_name};
chomp(my $result = &$function_name(@ARGV));
print "$result\n" if $result;
save() unless $initial_state eq state();
__
meta::internal_function('namespace', <<'__');
my ($name) = @_;
$name =~ s/::.*$//;
$name;
__
meta::internal_function('parent_attributes', <<'__');
my $attributes = sub {my ($name, $value) = split /\s+/o, $_; $name => ($value || 1)};
map &$attributes(), split /\n/o, join("\n", retrieve(@_));
__
meta::internal_function('parent_ordering', <<'__');
# Topsorts the parents by dependency chain. The simplest way to do this is to
# transitively compute the number of parents referred to by each parent.
my @parents = @_;
my %all_parents = map {$_ => 1} @parents;
my %parents_of = map {
my $t = $_;
my %attributes = parent_attributes($_);
$t => [grep /^parent::/, keys %attributes]} @parents;
my %parent_count;
my $parent_count;
$parent_count = sub {
my ($key) = @_;
return $parent_count{$key} if exists $parent_count{$key};
my $count = 0;
$count += $parent_count->($_) + exists $data{$_} for @{$parents_of{$key}};
$parent_count{$key} = $count};
my %inverses;
push @{$inverses{$parent_count->($_)} ||= []}, $_ for @parents;
grep exists $all_parents{$_}, map @{$inverses{$_}}, sort keys %inverses;
__
meta::internal_function('retrieve', <<'__');
my @results = map defined $data{$_} ? $data{$_} : retrieve_with_hooks($_), @_;
wantarray ? @results : $results[0];
__
meta::internal_function('retrieve_with_hooks', <<'__');
# Uses the hooks defined in $transient{retrievers}, and returns undef if none work.
my ($attribute) = @_;
my $result = undef;
defined($result = &$_($attribute)) and return $result for map $transient{retrievers}{$_}, sort keys %{$transient{retrievers}};
return undef;
__
meta::internal_function('select_keys', <<'__');
my %options = @_;
grep attribute_is($_, %options), sort keys %data;
__
meta::internal_function('separate_options', <<'__');
# Things with one dash are short-form options, two dashes are long-form.
# Characters after short-form are combined; so -auv4 becomes -a -u -v -4.
# Also finds equivalences; so --foo=bar separates into $$options{'--foo'} eq 'bar'.
# Stops processing at the -- option, and removes it. Everything after that
# is considered to be an 'other' argument.
# The only form not supported by this function is the short-form with argument.
# To pass keyed arguments, you need to use long-form options.
my @parseable;
push @parseable, shift @_ until ! @_ or $_[0] eq '--';
my @singles = grep /^-[^-]/, @parseable;
my @longs = grep /^--/, @parseable;
my @others = grep ! /^-/, @parseable;
my @singles = map /-(.{2,})/ ? map("-$_", split(//, $1)) : $_, @singles;
my %options;
/^([^=]+)=(.*)$/ and $options{$1} = $2 for @longs;
++$options{$_} for grep ! /=/, @singles, @longs;
({%options}, @others, @_);
__
meta::internal_function('strip', 'wantarray ? map {s/^\\s*|\\s*$//g; $_} @_ : $_[0] =~ /^\\s*(.*?)\\s*$/ && $1;');
meta::internal_function('table_display', <<'__');
# Displays an array of arrays as a table; that is, with alignment. Arrays are
# expected to be in column-major order.
sub maximum_length_in {
my $maximum = 0;
length > $maximum and $maximum = length for @_;
$maximum;
}
my @arrays = @_;
my @lengths = map maximum_length_in(@$_), @arrays;
my @row_major = map {my $i = $_; [map $$_[$i], @arrays]} 0 .. $#{$arrays[0]};
my $format = join ' ', map "%-${_}s", @lengths;
join "\n", map strip(sprintf($format, @$_)), @row_major;
__
meta::internal_function('temporary_name', <<'__');
use File::Temp 'tempfile';
my (undef, $temporary_filename) = tempfile("$0." . 'X' x 4, OPEN => 0);
$temporary_filename;
__
meta::internal_function('translate_backtrace', <<'__');
my ($trace) = @_;
$trace =~ s/\(eval (\d+)\)/$locations{$1 - 1}/g;
$trace;
__
meta::internal_function('with_exported', <<'__');
# Like exported(), but removes the file after running some function.
# Usage is with_exported(@files, sub {...});
my $f = pop @_;
my $name = exported(@_);
my $result = eval {&$f($name)};
terminal::warning("$@ when running with_exported()") if $@;
unlink $name;
$result;
__
meta::library('shell', <<'__');
# Functions for shell parsing and execution.
package shell;
use Term::ReadLine;
sub tokenize {grep length, split /\s+|("[^"\\]*(?:\\.)?")/o, join ' ', @_};
sub parse {
my ($fn, @args) = @_;
s/^"(.*)"$/\1/o, s/\\\\"/"/go for @args;
{function => $fn, args => [@args]}}
sub execute {
my %command = %{$_[0]};
die "undefined command: $command{function}" unless exists $externalized_functions{$command{function}};
&{"::$command{function}"}(@{$command{args}})}
sub run {execute(parse(tokenize(@_)))}
sub prompt {
my %options = @_;
my $name = $options{name} // ::name();
my $indicators = join '', map &{"::$_"}(), ::select_keys('--namespace' => 'indicator');
my $prefix = $transient{repl_prefix} // '';
"$prefix\033[1;32m$name\033[0;0m$indicators "}
sub repl {
my %options = @_;
my $term = new Term::ReadLine "$0 shell";
$term->ornaments(0);
my $attribs = $term->Attribs;
$attribs->{completion_entry_function} = $attribs->{list_completion_function};
my $autocomplete = $options{autocomplete} || sub {[sort(keys %data), grep !/-/, sort keys %externalized_functions]};
my $prompt = $options{prompt} || \&prompt;
my $parse = $options{parse} || sub {parse(tokenize(@_))};
my $command = $options{command} || sub {my ($command) = @_; ::around_hook('shell-command', $command, sub {print ::dangerous('', sub {execute($command)}), "\n"})};
length $_ && &$command(&$parse($_)) while ($attribs->{completion_word} = &$autocomplete(), defined($_ = $term->readline(&$prompt())))}
__
meta::library('terminal', <<'__');
# Functions for nice-looking terminal output.
package terminal;
my $process = ::name();
sub message {print STDERR "[$_[0]] $_[1]\n"}
sub color {
my ($name, $color) = @_;
*{"terminal::$name"} = sub {chomp($_), print STDERR "\033[1;30m$process(\033[1;${color}m$name\033[1;30m)\033[0;0m $_\n" for map join('', $_), @_}}
my %preloaded = (info => 32, progress => 32, state => 34, debug => 34, warning => 33, error => 31);
color $_, $preloaded{$_} for keys %preloaded;
__
meta::message_color('cc', '36');
meta::message_color('state', 'purple');
meta::message_color('states', 'yellow');
meta::parent('./sdoc', <<'__');
function::sdoc f3f3f3127961399a4c38152771c966ab
function::sdoc-html 7e7de47fe059a336309a4a0c06856401
function::sdocp c3d738d982ba87418a298ff58478a85b
meta::type::sdoc 22cd7315641d38c9d536344e83c36bed
meta::type::slibrary 95474943c4a5f8ff17d3cf66ddb7c386
parent::/home/spencertipping/bin/object 5788ae36d7790310e3b66d5ddd8b796e
retriever::html-sdoc 2a5d5aa45e2d7576f79e045177d8705c
retriever::sdoc 662061e9e41491e2a1debd6862ccf1e7
retriever::sdocp 330694ea14a23bb04b65c761075cd946
__
meta::parent('/home/spencertipping/bin/configuration', <<'__');
meta::type::configuration 7f5ba514d47ac29a3c226d0e331d9da4
parent::/home/spencertipping/bin/object 5788ae36d7790310e3b66d5ddd8b796e
__
meta::parent('/home/spencertipping/bin/literate-project', <<'__');
function::clean 8754de0fa0d5fc535c47bca1183f6480
function::compile 3f3a53221fad7dba741d689603ff562d
function::make 7234478722ac6a7cc9cf17c31a6e4283
function::render bcb18a54e479a3fad2f3c90d5c40e6e9
function::tex 43ac46c21e4f5fb6b29976a213c109c6
function::view b7f4f81d82cbd4051309e99659c0f661
internal_function::main fb128e779c38aeeb04c4a508db7176d4
meta::externalize_template ce51d3e099f0fad21cd01e5c53382e85
meta::functor::code-templates 2b8880f05f983ce5b8a19a01688b6001
meta::functor::tex-templates 6ac0ee994c9328fcfc41083083d97280
meta::template::beamer 3b73fe6a7ef271112a603b110630ad01
meta::template::code d29e41eab67368411a09753d6722f88d
meta::template::document 18688cac3eab39087165931beb4f82de
meta::template::enumeration 67d1cf0bb0e8946fc3b48cc30a4f5fa4
meta::template::math 057c8fe0d91c0b85138b01c1f6a1e3b3
meta::template::quotations f76a27311c8a9d09550dd669e98f9a7f
meta::template::sections 25cb0a42bf1c2f403f8f9c2e1c5e4105
meta::type::resource c521d2de1dcd7f9f7ec4df3557572009
meta::type::section fb3b53eaebc1a2433c1763eccdb781c4
parent::development 79168a5886862b99577653bce0c45c31
resource::header 165b42e25edb90043f5d3b544adee521
resource::header-languages 96d12558e40d0df64ff3176b767f4f3b
resource::header-listings 712557f5989b12ca325bdd2cd36020ef
resource::header-packages 323964d33482e6099b0248bb4eddf2ae
resource::header-refs e3999cbca586d5100eb782643071ee88
resource::header-resource a8df924fbb715de73b8e7518e0c9e0d6
template::item[] 2c2041b4c3b6584b6ae3610f5ab68939
vim_highlighter::cltex 40744d98b02b4b50fedb4d1e7e1fd151
__
meta::parent('/home/spencertipping/bin/object', <<'__');
bootstrap::html f44dd03cb0c904b3a5f69fbda5f018d0
bootstrap::initialization 1cf74e7209f32722a79b6e49e3907fd3
bootstrap::perldoc 5793df44bdd2526bb461272924abfd4b
function::ad 77a05d9a6fef7871b2c3e8e94b56870a
function::alias 8eeeeb4e064ef3aba7edf8f254427bc2
function::cat f684de6c8776617a437b76009114f52e
function::cc 12ea9176e388400704d823433c209b7a
function::ccc d151a9793edd83f80fb880b7f0ab9b34
function::child f5764adf0b4e892f147a9b6b68d4816f
function::clone bb42e04e10a8e54e88786b6fbc4fb213
function::cp 3fe69d1b58d90045ad520048977538c4
function::create 3010d55f4dfa59a998742e07823ed54d
function::current-state 6f03f86f1901e9ef07fdb5d4079a914c
function::disable 53b449708cc2ffdefa352e53bb7d847d
function::edit 9ce5ba1ae4607e8cf1975080bcde1cf4
function::enable 7de1cedc36841f5de8f9fdfbc3b65097
function::export 2374cd1dbf7616cb38cafba4e171075d
function::extern 1290a5223e2824763eecfb3a54961eff
function::grep 55c3cea8ff4ec2403be2a9d948e59f14
function::hash 6ee131d093e95b80039b4df9c7c84a02
function::hook 675cdb98b5dd8567bdd5a02ead6184b5
function::hooks 3d989899c616f7440429a2d9bf1cc44b
function::identity 6523885762fcc2f354fc25cf6ed126ce
function::import 5d0f0634cbd01274f2237717507198a2
function::initial-state 03d8ed608855a723124e79ca184d8e73
function::is 41564c8f21b12ab80824ac825266d805
function::load-state b6cf278a1f351f316fa6e070359b6081
function::lock 5d8db258704e6a8623fac796f62fac02
function::ls 01a23d51d5b529e03943bd57e33f92df
function::mv 4a0e338a6edb89ad1e2c779d51d4d47b
function::name 955ba2d1fe1d67cd78651a4042283b00
function::parents 3da9e63b5aae9e2f5dcc946a86d166aa
function::perl a0f341ea54391b63b6195e7992b6a686
function::rd 2adb16d7e819d2e87a27201744a581e7
function::reload 1589f4cf8374e0011991cb8907afca3e
function::rm 6f6fd7a6c25558eb469d78ea888f8551
function::rmparent fc2884910a6939a47898a778f277332c
function::save 778c0e1043b9c6c96fb8f266f8061624
function::save-state 5af59ebc4ad8965767e4dc106d3b557e
function::serialize a19ada2d2558ea9da3a7942fb913e15f
function::serialize-single aa77af032272f5a2664e21713739a223
function::sh 1b2f542ca9dd63ad437058b7f6f61aac
function::shb 7b2685a4041c25bc495816e472bdace5
function::shell a87f389b94713e5855e62241d649d01d
function::size 8d4bd7a84ece556717f8ba3bf258d33c
function::snapshot 56939a47f2758421669641e15ebd66eb
function::state 8c68044dccae28f33244d0c7e9e9acfb
function::touch 3991b1b7c7187566f50e5e58ce01fa06
function::unlock b4aac02f7f3fb700acf4acfd9b180ceb
function::update ac391dc90e507e7586c81850e7c2ecdd
function::update-from 631721c4dc30e11b2023a6703cbcef52
function::usage 5bdd370f5a56cfbf199e08d398091444
function::verify 0c0cc1dfeab7d705919df122f7850a4f
indicator::cc 3db7509c521ee6abfedd33d5f0148ed3
indicator::locked fc2b4f4ca0d6a334b9ac423d06c8f18c
indicator::path 9ec891df17cd45895f03a6124f9d065f
internal_function::around_hook 7cc876e7c5f78c34654337fc95255587
internal_function::associate 05a75afb70daee635eefec8ae037f593
internal_function::attribute dd6f010f9688977464783f60f5b6d3dd
internal_function::attribute_is 40bda8226322505e323ea6d405388f08
internal_function::cache eb9da45580a9ac0882baf98acd2ecd60
internal_function::chmod_self 2035e861eedab55ba0a9f6f5a068ca70
internal_function::dangerous 46c4baaa214ab3d05af43e28083d5141
internal_function::debug_trace 0faf9d9f4159d72dfe4481f6f3607ce1
internal_function::execute f0924e087d978ff2ab1e117124db3042
internal_function::exported ae35afef7d4762f2818aee5872c75be0
internal_function::extension_for 9de8261d69cc93e9b92072b89c89befd
internal_function::fast_hash ee5eba48f837fda0fe472645fdd8899a
internal_function::file::read e647752332c8e05e81646a3ff98f9a8e
internal_function::file::write 3e290fdcb353c6f842eb5a40f2e575f8
internal_function::fnv_hash c36d56f1e13a60ae427afc43ba025afc
internal_function::hypothetically b83e3f894a6df8623ccd370515dfd976
internal_function::internal::main f31f2945a19a668d92505f114ab29c78
internal_function::invoke_editor_on 5eb976796f0ec172d6ec036116a2f41e
internal_function::is_locked da12ced6aa38295251f7e748ffd22925
internal_function::namespace 784d2e96003550681a4ae02b8d6d0a27
internal_function::parent_attributes f6ccfaa982ab1a4d066043981aaca277
internal_function::parent_ordering 57b6da88f76b59f3fed9abfa61280e5e
internal_function::retrieve 8a34d1fe047fe1b40c3d2957c4a789eb
internal_function::retrieve_with_hooks 0f1b0220ccd973d57a2e96ff00458cf2
internal_function::select_keys a5e3532ec6d58151d0ee24416ea1e2b5
internal_function::separate_options 34ec41a6edaa15adde607a0db3ccfa36
internal_function::strip 14f490b10ebd519e829d8ae20ea4d536
internal_function::table_display d575f4dc873b2e0be5bd7352047fd904
internal_function::temporary_name 6f548d101fc68356515ffd0fc9ae0c93
internal_function::translate_backtrace d77a56d608473b3cd8a3c6cb84185e10
internal_function::with_exported df345d5095d5ed13328ddd07ea922b36
library::shell 6b9f3befb61a01e9132a440601f8ea0a
library::terminal 7e2d045782405934a9614fe04bcfe559
message_color::cc 2218ef0f7425de5c717762ffb100eb43
message_color::state 03621cd6ac0b1a40d703f41e26c5807f
message_color::states ac66eeeff487b5f43f88a78ea18b3d56
meta::configure 69c2e727c124521d074fde21f8bbc4db
meta::externalize aa44e27e0bbee6f0ca4de25d603a1fc7
meta::functor::editable 48246c608f363de66511400e00b26164
meta::type::alias 889d26d2df385e9ff8e2da7de4e48374
meta::type::bootstrap 51108ab2ddb8d966e927c8f62d9ef3e5
meta::type::cache 9267171f2eace476f64a1a670eaaf2c7
meta::type::data 120e1649a468d3b3fd3fb783b4168499
meta::type::function 8ea626198861dc59dd7f303eecb5ff88
meta::type::hook ff92aef328b6bdc6f87ddd0821f3e42f
meta::type::inc 78e0375b6725487cb1f0deca41e96bbe
meta::type::indicator feb54a2624e6983617685047c717427f
meta::type::internal_function eff3cf31e2635f51c83836f116c99d2f
meta::type::library 7622e8d65e03066668bade74715d65ad
meta::type::message_color 557a1b44979cbf77a7251fbdc4c5b82c
meta::type::meta c6250056816b58a9608dd1b2614246f8
meta::type::parent 09d1d03379e4e0b262e06939f4e00464
meta::type::retriever 71a29050bf9f20f6c71afddff83addc9
meta::type::state 84da7d5220471307f1f990c5057d3319
retriever::file 3bbc9d8a887a536044bafff1d54def7e
retriever::id 4da6080168d32445150cc4200af7af6e
retriever::object c7633990b4e01bdc783da7e545799f4f
retriever::perl f41938e6dbad317f62abffc1e4d28cca
__
meta::parent('configuration', <<'__');
meta::type::configuration d67e10a128e6b1d958c5b9d3bbe25aa4
parent::/home/spencertipping/bin/object 293415c4d45c10239298fa46b01cff17
__
meta::parent('development', <<'__');
parent::./sdoc ebc273b9984a53b0e3a491ac74272c9b
parent::configuration 835c872a2163166e001c4e07b5191353
parent::notes a32dbfde9ecfcf3433dfd6575ffdf418
parent::preprocessor 3b4cea3c3d232d9cca0496a72968df65
parent::repository 9cb8c47c563910cc13f9f45ba31d732e
parent::vim-highlighters 77c9231e0b6ba6e7aa6ed74193c02d2c
__
meta::parent('notes', <<'__');
function::note c6d52ffe73cd48cd95fdc561c59d9f63
function::notes 7229b326ac8686b2db6de98496bc7527
meta::type::note f81bea58841a438e4ee34608ab4f54c0
parent::object 5788ae36d7790310e3b66d5ddd8b796e
__
meta::parent('object', <<'__');
bootstrap::html f44dd03cb0c904b3a5f69fbda5f018d0
bootstrap::initialization 1cf74e7209f32722a79b6e49e3907fd3
bootstrap::perldoc 5793df44bdd2526bb461272924abfd4b
function::ad 77a05d9a6fef7871b2c3e8e94b56870a
function::alias 8eeeeb4e064ef3aba7edf8f254427bc2
function::cat f684de6c8776617a437b76009114f52e
function::cc 12ea9176e388400704d823433c209b7a
function::ccc d151a9793edd83f80fb880b7f0ab9b34
function::child f5764adf0b4e892f147a9b6b68d4816f
function::clone bb42e04e10a8e54e88786b6fbc4fb213
function::cp 3fe69d1b58d90045ad520048977538c4
function::create 3010d55f4dfa59a998742e07823ed54d
function::current-state 6f03f86f1901e9ef07fdb5d4079a914c
function::disable 53b449708cc2ffdefa352e53bb7d847d
function::edit 9ce5ba1ae4607e8cf1975080bcde1cf4
function::enable 7de1cedc36841f5de8f9fdfbc3b65097
function::export 2374cd1dbf7616cb38cafba4e171075d
function::extern 1290a5223e2824763eecfb3a54961eff
function::grep 55c3cea8ff4ec2403be2a9d948e59f14
function::hash 6ee131d093e95b80039b4df9c7c84a02
function::hook 675cdb98b5dd8567bdd5a02ead6184b5
function::hooks 3d989899c616f7440429a2d9bf1cc44b
function::identity 6523885762fcc2f354fc25cf6ed126ce
function::import 5d0f0634cbd01274f2237717507198a2
function::initial-state 03d8ed608855a723124e79ca184d8e73
function::is 41564c8f21b12ab80824ac825266d805
function::load-state b6cf278a1f351f316fa6e070359b6081
function::lock 5d8db258704e6a8623fac796f62fac02
function::ls 01a23d51d5b529e03943bd57e33f92df
function::mv 4a0e338a6edb89ad1e2c779d51d4d47b
function::name 955ba2d1fe1d67cd78651a4042283b00
function::parents 3da9e63b5aae9e2f5dcc946a86d166aa
function::perl a0f341ea54391b63b6195e7992b6a686
function::rd 2adb16d7e819d2e87a27201744a581e7
function::reload 1589f4cf8374e0011991cb8907afca3e
function::rm 6f6fd7a6c25558eb469d78ea888f8551
function::rmparent fc2884910a6939a47898a778f277332c
function::save 778c0e1043b9c6c96fb8f266f8061624
function::save-state 5af59ebc4ad8965767e4dc106d3b557e
function::serialize a19ada2d2558ea9da3a7942fb913e15f
function::serialize-single aa77af032272f5a2664e21713739a223
function::sh 1b2f542ca9dd63ad437058b7f6f61aac
function::shb 7b2685a4041c25bc495816e472bdace5
function::shell a87f389b94713e5855e62241d649d01d
function::size 8d4bd7a84ece556717f8ba3bf258d33c
function::snapshot 56939a47f2758421669641e15ebd66eb
function::state 8c68044dccae28f33244d0c7e9e9acfb
function::touch 3991b1b7c7187566f50e5e58ce01fa06
function::unlock b4aac02f7f3fb700acf4acfd9b180ceb
function::update ac391dc90e507e7586c81850e7c2ecdd
function::update-from 631721c4dc30e11b2023a6703cbcef52
function::usage 5bdd370f5a56cfbf199e08d398091444
function::verify 0c0cc1dfeab7d705919df122f7850a4f
indicator::cc 3db7509c521ee6abfedd33d5f0148ed3
indicator::locked fc2b4f4ca0d6a334b9ac423d06c8f18c
indicator::path 9ec891df17cd45895f03a6124f9d065f
internal_function::around_hook 7cc876e7c5f78c34654337fc95255587
internal_function::associate 05a75afb70daee635eefec8ae037f593
internal_function::attribute dd6f010f9688977464783f60f5b6d3dd
internal_function::attribute_is 40bda8226322505e323ea6d405388f08
internal_function::cache eb9da45580a9ac0882baf98acd2ecd60
internal_function::chmod_self 2035e861eedab55ba0a9f6f5a068ca70
internal_function::dangerous 46c4baaa214ab3d05af43e28083d5141
internal_function::debug_trace 0faf9d9f4159d72dfe4481f6f3607ce1
internal_function::execute f0924e087d978ff2ab1e117124db3042
internal_function::exported ae35afef7d4762f2818aee5872c75be0
internal_function::extension_for 9de8261d69cc93e9b92072b89c89befd
internal_function::fast_hash ee5eba48f837fda0fe472645fdd8899a
internal_function::file::read e647752332c8e05e81646a3ff98f9a8e
internal_function::file::write 3e290fdcb353c6f842eb5a40f2e575f8
internal_function::fnv_hash c36d56f1e13a60ae427afc43ba025afc
internal_function::hypothetically b83e3f894a6df8623ccd370515dfd976
internal_function::internal::main f31f2945a19a668d92505f114ab29c78
internal_function::invoke_editor_on 5eb976796f0ec172d6ec036116a2f41e
internal_function::is_locked da12ced6aa38295251f7e748ffd22925
internal_function::namespace 784d2e96003550681a4ae02b8d6d0a27
internal_function::parent_attributes f6ccfaa982ab1a4d066043981aaca277
internal_function::parent_ordering 57b6da88f76b59f3fed9abfa61280e5e
internal_function::retrieve 8a34d1fe047fe1b40c3d2957c4a789eb
internal_function::retrieve_with_hooks 0f1b0220ccd973d57a2e96ff00458cf2
internal_function::select_keys a5e3532ec6d58151d0ee24416ea1e2b5
internal_function::separate_options 34ec41a6edaa15adde607a0db3ccfa36
internal_function::strip 14f490b10ebd519e829d8ae20ea4d536
internal_function::table_display d575f4dc873b2e0be5bd7352047fd904
internal_function::temporary_name 6f548d101fc68356515ffd0fc9ae0c93
internal_function::translate_backtrace d77a56d608473b3cd8a3c6cb84185e10
internal_function::with_exported df345d5095d5ed13328ddd07ea922b36
library::shell 6b9f3befb61a01e9132a440601f8ea0a
library::terminal 7e2d045782405934a9614fe04bcfe559
message_color::cc 2218ef0f7425de5c717762ffb100eb43
message_color::state 03621cd6ac0b1a40d703f41e26c5807f
message_color::states ac66eeeff487b5f43f88a78ea18b3d56
meta::configure 69c2e727c124521d074fde21f8bbc4db
meta::externalize aa44e27e0bbee6f0ca4de25d603a1fc7
meta::functor::editable 48246c608f363de66511400e00b26164
meta::type::alias 889d26d2df385e9ff8e2da7de4e48374
meta::type::bootstrap 51108ab2ddb8d966e927c8f62d9ef3e5
meta::type::cache 9267171f2eace476f64a1a670eaaf2c7
meta::type::data 120e1649a468d3b3fd3fb783b4168499
meta::type::function 8ea626198861dc59dd7f303eecb5ff88
meta::type::hook ff92aef328b6bdc6f87ddd0821f3e42f
meta::type::inc 78e0375b6725487cb1f0deca41e96bbe
meta::type::indicator feb54a2624e6983617685047c717427f
meta::type::internal_function eff3cf31e2635f51c83836f116c99d2f
meta::type::library 7622e8d65e03066668bade74715d65ad
meta::type::message_color 557a1b44979cbf77a7251fbdc4c5b82c
meta::type::meta c6250056816b58a9608dd1b2614246f8
meta::type::parent 09d1d03379e4e0b262e06939f4e00464
meta::type::retriever 71a29050bf9f20f6c71afddff83addc9
meta::type::state 84da7d5220471307f1f990c5057d3319
retriever::file 3bbc9d8a887a536044bafff1d54def7e
retriever::id 4da6080168d32445150cc4200af7af6e
retriever::object c7633990b4e01bdc783da7e545799f4f
retriever::perl f41938e6dbad317f62abffc1e4d28cca
__
meta::parent('preprocessor', <<'__');
function::preprocess ab5526a02ff417d4c162357dc327e7c4
meta::functor::html-templates 2771200f87e9cbfeecfb5f8a0f796f18
meta::type::template bc4b0c80b5efc716b19e99b832c22bf3
parent::object 5788ae36d7790310e3b66d5ddd8b796e
retriever::pp 3b5f5c5d30c5a04f72056dedaacfe7b7
template::comment dfe273d2dad3d8159b847545e4e5c309
template::eval 1a0e2124a05056be4abc11803883c294
template::failing_conditional e3a4523110dd859e828f342185de7c62
template::include 47b5552d609d97fe7f2522d5c1027014
template::pinclude c07ff79bf8d642cceaa9ef844bfcb189
template::script-include 76be051ad116449ddebd10e7c3729afd
template::style-include 8e5a06b70e1b00379765f319bf6c8066
__
meta::parent('repository', <<'__');
function::dupdate 3203750417390913ae3892002b53bdc1
meta::type::cached_dependency b9dc0b20c2d3af0deb3b835b20cac4a7
parent::/home/spencertipping/bin/configuration 7df7136d772c7810d907d238e7cae3f2
retriever::http a23617a5787de41d1a89ad4496cacce3
__
meta::parent('vim-highlighters', <<'__');
function::vim cf9e37026f6cd1499a6dd258fbbcd060
meta::type::vim_highlighter 27990fddb6d7bd383b55dbdfee0d148d
parent::object 5788ae36d7790310e3b66d5ddd8b796e
__
meta::resource('header', <<'__');
- include resource::header-packages
- include resource::header-listings
- include resource::header-refs
- include resource::header-languages
- include resource::header-resource
__
meta::resource('header-languages', <<'__');
\lstnewenvironment{asmcode} {}{}
\lstnewenvironment{cppcode} {\lstset{language=c++}}{}
\lstnewenvironment{javacode} {\lstset{language=java}}{}
\lstnewenvironment{javascriptcode}{}{}
\lstnewenvironment{htmlcode} {\lstset{language=html}}{}
\lstnewenvironment{perlcode} {\lstset{language=perl}}{}
\lstnewenvironment{rubycode} {\lstset{language=ruby}}{}
__
meta::resource('header-listings', <<'__');
\definecolor{gray}{rgb}{0.6,0.6,0.6}
\usepackage{caption}
\DeclareCaptionFormat{listing}{\llap{\color{gray}#1\hspace{10pt}}\tt{}#3}
\captionsetup[lstlisting]{format=listing, singlelinecheck=false, margin=0pt, font={bf}}
\lstset{columns=fixed,basicstyle={\tt},numbers=left,firstnumber=auto,basewidth=0.5em,showstringspaces=false,numberstyle={\color{gray}\scriptsize}}
__
meta::resource('header-packages', <<'__');
\usepackage[utf8]{inputenc}
\usepackage{amsmath,amssymb,amsthm,pxfonts,listings,color}
\usepackage[colorlinks]{hyperref}
__
meta::resource('header-refs', '\\newcommand{\\Ref}[2]{\\hyperref[#2]{#1 \\ref*{#2}}}');
meta::resource('header-resource', '\\lstnewenvironment{resourcecode}{}{}');
meta::retriever('file', '-f $_[0] ? file::read($_[0]) : undef;');
meta::retriever('http', <<'__');
use LWP::Simple ();
return undef unless $_[0] =~ /^(?:http:)?\/\/(\w+.*)$/;
LWP::Simple::get("http://$1");
__
meta::retriever('id', '$_[0] =~ /^id::/ ? substr($_[0], 4) : undef;');
meta::retriever('object', <<'__');
# Fetch a property from another Perl object. This uses the 'cat' function.
return undef unless $_[0] =~ /^object::(.*?)::(.*)$/ && -x $1 && qx|$1 is '$2'|;
join '', qx|$1 cat '$2'|;
__
meta::retriever('perl', <<'__');
# Lets you use the result of evaluating some Perl expression
return undef unless $_[0] =~ /^perl::(.*)$/;
eval $1;
__
meta::retriever('pp', <<'__');
return undef unless namespace($_[0]) eq 'pp';
my $attr = retrieve(attribute($_[0]));
defined $attr ? preprocess($attr) : undef;
__
meta::section('a-big-quine', <<'__');
- sc A Big Quine | sec:a-big-quine
At the core of things, a self-modifying Perl script is just a big quine.\footnote{A ``quine'' being a program that prints its own source.} There are only two real differences:
- enumerate << end
- item Self-modifying Perl scripts print into their own files rather than to standard output.
- item They print modified versions of themselves, not the original source.
- end
\noindent If we're going to write such a script, it's good to start with a simple quine.
- s1 A basic quine | sec:a-basic-quine
Some languages make quine-writing easier than others. Perl actually makes it very simple. Here's one:
- perl examples/quine << end
my $code = <<'EOF';
print 'my $code = <<\'EOF\';', "\n", $code, "EOF\n"; print $code;
EOF
print 'my $code = <<\'EOF\';', "\n", $code, "EOF\n"; print $code;
- end
\noindent The logic is fairly straightforward, though it may not look like it. We're quoting a bunch of stuff using \verb|<<'EOF'|,\footnote{The single-quoted heredoc form doesn't do any
interpolation inside the document, which is ideal since we don't want to worry about escaping stuff.} and storing that into a string. We then put the quoted content outside of the heredoc
to let it execute. The duplication is necessary; we want to quote the content and then run it.\footnote{Later on I'll use {\tt eval} to reduce the amount of duplication.} The key is this
line:
- verbatim << end
print 'my $code = <<\'EOF\';', "\n", $code, "EOF\n"; print $code;
- end
\noindent This code prints the setup to define a new variable \verb|$code| and prints its existing content after that.
- s1 Reducing duplication | sec:reducing-duplication
We don't want to write everything in our quine twice. Rather, we want to store most stuff just once and have a quine that scales well. The easiest way to do this is to use a hash to store
the state, and serialize each key of the hash in the self-printing code. So instead of creating \verb|$code|, we'll create \verb|%data|:
- perl examples/quine-with-data << end
my %data;
$data{code} = <<'EOF';
print 'my %data;', "\n";
print '$data{', $_, '} = <<\'EOF\';', "\n$data{$_}EOF\n" for keys %data;
print $data{code};
EOF
print 'my %data;', "\n";
print '$data{', $_, '} = <<\'EOF\';', "\n$data{$_}EOF\n" for keys %data;
print $data{code};
- end
\noindent This is a good start. Here's how to add attributes without duplication:
- perl examples/quine-with-data-and-foo << end
my %data;
$data{foo} = <<'EOF';
a string
EOF
$data{code} = <<'EOF';
print 'my %data;', "\n";
print '$data{', $_, '} = <<\'EOF\';', "\n$data{$_}EOF\n" for keys %data;
print $data{code};
EOF
print 'my %data;', "\n";
print '$data{', $_, '} = <<\'EOF\';', "\n$data{$_}EOF\n" for keys %data;
print $data{code};
- end
- s1 Using {\tt eval} | sec:using-eval
The business about duplicating \verb|$data{code}| is easily remedied by just {\tt eval}ing \verb|$data{code}| at the end. This requires the {\tt eval} section to be duplicated, but it's
smaller than \verb|$data{code}|. Here's the quine with that transformation:\footnote{Note that these quines might not actually print themselves identically due to hash-key ordering. This
is fine; all of the keys are printed before we use them.}
- perl examples/quine-with-data-and-eval << end
my %data;
$data{foo} = <<'EOF';
a string
EOF
$data{code} = <<'EOF';
print 'my %data;', "\n";
print '$data{', $_, '} = <<\'EOF\';', "\n$data{$_}EOF\n" for keys %data;
print $data{bootstrap};
EOF
$data{bootstrap} = <<'EOF';
eval $data{code};
EOF
eval $data{code};
- end
\noindent The advantage of this approach is that all we'll ever have to duplicate is \verb|eval $data{code}| and \verb|my %data;|, which is fairly trivial. It's important that you
understand what's going on here, since this idea is integral to everything going forward.\footnote{Alternatively, it probably also works to accept the code so far as magic and take my word
for it that future code snippets do what they should. But it's probably less fun without the ``aha!'' moment.}
__
meta::section('adding-a-repl', <<'__');
- sc Adding a REPL | sec:adding-a-repl
There are some ergonomic problems with the script as it stands. First, it should have a shebang line so that we don't have to use {\tt perl} explicitly. But more importantly, it should
provide a REPL so that we don't have to keep calling it by name.
The first question is how this should be invoked. It would be cool if we could run the script without arguments and get the REPL, but that will require some changes to the current {\tt
code::main}. The ``right way'' to do it also requires a new data type.
- s1 The {\tt data} data type | sec:adding-a-repl-the-data-data-type
Sometimes we just want to store pieces of data without any particular meaning. We could use {\tt bootstrap::} for this, but it's cleaner to introduce a new data type altogether.
- perl snippets/define-form-data << end
meta::define_form 'data', sub {
# Define a basic editing interface:
my ($name, $value) = @_;
*{$name} = sub {
my ($command, $value) = @_;
return $data{"data::$name"} unless @_;
$data{"data::$name"} = $value if $command eq '=';
};
};
- end
This function we're defining lets us inspect and change a data attribute from the command line. Assuming {\tt data::foo}, for example:
- verbatim << end
$ perl script foo = bar
bar
$ perl script foo
bar
$ perl script foo = baz
baz
$
- end
- s1 Setting up the default action | sec:adding-a-repl-setting-up-the-default-action
The default action can be stored in a {\tt data::} attribute:
- verbatim << end
meta::data('default-action', <<'EOF');
shell
EOF
meta::code('main', <<'EOF');
...
my $command = shift @ARGV || $data{'data::default-action'};
print &$command(@ARGV);
...
EOF
- end
Since all values are chomped already, we don't have to worry about the newline caused by the heredoc.
- s1 Making the script executable | sec:adding-a-repl-making-the-script-executable
This isn't hard at all. It means one extra line in the bootstrap logic, and another extra line in {\tt save}:
- verbatim << end
meta::bootstrap('initialization', <<'EOF');
#!/usr/bin/perl
...
EOF
meta::function('save', <<'EOF');
...
close $file;
chmod 0744, $0; # Not perfect, but will fix later
...
EOF
- end
- s1 The {\tt shell} function | sec:adding-a-repl-the-shell-function
The idea here is to listen for commands from the user and simulate the \verb|@ARGV| interaction pattern. Readline is the simplest way to go about this:
- perl snippets/shell-function-1 << end
meta::function('shell', <<'EOF');
use Term::ReadLine;
my $term = new Term::ReadLine "$0 shell";
$term->ornaments(0);
my $output = $term->OUT || \*STDOUT;
while (defined($_ = $term->readline("$0\$ "))) {
my @args = grep length, split /\s+|("[^"\\]*(?:\\.)?")/o;
my $function_name = shift @args;
s/^"(.*)"$/\1/o, s/\\\\"/"/go for @args;
if ($function_name) {
chomp(my $result = eval {&$function_name(@args)});
warn $@ if $@;
print $output $result, "\n" unless $@;
}
}
EOF
- end
This shell function does some minimal quotation-mark parsing so that you can use multi-word arguments, but otherwise it's fairly basic. The script's name is used as the shell prompt.
It's OK to use {\tt use} inside of {\tt eval}ed functions. I think what happens is that it gets processed when the function is first created by {\tt meta::function}. But basically, Perl
does the right thing and it works just fine as long as the module exists.
- s1 Taking it to the max: tab-completion | sec:adding-a-repl-tab-completion
If you have the GNU Readline library installed (Perl defaults to something else otherwise), you can get tab-autocompletion just like you can in Bash. Here's a {\tt complete} function
written by my wife Joyce, modified slightly to make sense with this implementation:
- perl snippets/complete-function-1 << end
meta::function('complete', <<'EOF');
my @attributes = sort keys %data;
sub match {
my ($text, @options) = @_;
my @matches = sort grep /^$text/, @options;
if (@matches == 0) {return undef;}
elsif (@matches == 1) {return $matches [0];}
elsif (@matches > 1) {
return ((longest ($matches [0], $matches [@matches - 1])), @matches);
}
}
sub longest {
my ($s1, $s2) = @_;
return substr ($s1, 0, length $1) if ($s1 ^ $s2) =~ /^(\0*)/;
return '';
}
my ($text, $line) = @_;
match ($text, @attributes);
EOF
- end
Using this function is easy; we just add one line to {\tt shell}:
- verbatim << end
$term->Attribs->{attempted_completion_function} = \&complete;
while (defined($_ = $term->readline("$0\$ "))) {
...
- end
- s1 Final result | sec:adding-a-repl-final-result
Merging the shell and executable behavior in with the script from the last chapter, we now have:\footnote{You might notice that I'm still using {\tt EOF} as the marker in these scripts. As
soon as the script is rewritten it will replace the {\tt EOF}s with hashes; in general, you can use any valid delimiter the first time around and the script will take it from there.}
- perl examples/shell << end
#!/usr/bin/perl
my %data;
my %datatypes;
sub meta::define_form {
my ($namespace, $delegate) = @_;
$datatypes{$namespace} = $delegate;
*{"meta::${namespace}::implementation"} = $delegate;
*{"meta::$namespace"} = sub {
my ($name, $value) = @_;
chomp $value;
$data{"${namespace}::$name"} = $value;
$delegate->($name, $value);
};
}
meta::define_form 'bootstrap', sub {};
meta::define_form 'function', sub {
my ($name, $body) = @_;
*{$name} = eval "sub {\n$body\n}";
};
meta::define_form 'code', sub {
my ($name, $value) = @_;
eval $value;
};
meta::define_form 'data', sub {
# Define a basic editing interface:
my ($name, $value) = @_;
*{$name} = sub {
my ($command, $value) = @_;
return $data{"data::$name"} unless @_;
$data{"data::$name"} = $value if $command eq '=';
};
};
meta::bootstrap('initialization', <<'EOF');
#!/usr/bin/perl
my %data;
my %datatypes;
sub meta::define_form {
my ($namespace, $delegate) = @_;
$datatypes{$namespace} = $delegate;
*{"meta::${namespace}::implementation"} = $delegate;
*{"meta::$namespace"} = sub {
my ($name, $value) = @_;
chomp $value;
$data{"${namespace}::$name"} = $value;
$delegate->($name, $value);
};
}
meta::define_form 'bootstrap', sub {};
meta::define_form 'function', sub {
my ($name, $body) = @_;
*{$name} = eval "sub {\n$body\n}";
};
meta::define_form 'code', sub {
my ($name, $value) = @_;
eval $value;
};
meta::define_form 'data', sub {
# Define a basic editing interface:
my ($name, $value) = @_;
*{$name} = sub {
my ($command, $value) = @_;
return $data{"data::$name"} unless @_;
$data{"data::$name"} = $value if $command eq '=';
};
};
EOF
meta::data('default-action', <<'EOF');
shell
EOF
meta::function('serialize', <<'EOF');
my @keys = sort keys %data;
join "\n", $data{'bootstrap::initialization'},
map(serialize_single($_), grep !/^code::/, @keys),
map(serialize_single($_), grep /^code::/, @keys),
"\n__END__";
EOF
meta::function('serialize_single', <<'EOF');
my ($namespace, $name) = split /::/, $_[0], 2;
my $marker = '__' . fast_hash($data{$_[0]});
"meta::$namespace('$name', <<'$marker');\n$data{$_[0]}\n$marker";
EOF
meta::function('fnv_hash', <<'EOF');
my ($data) = @_;
my ($fnv_prime, $fnv_offset) = (16777619, 2166136261);
my $hash = $fnv_offset;
my $modulus = 2 ** 32;
$hash = ($hash ^ ($_ & 0xffff) ^ ($_ >> 16)) * $fnv_prime % $modulus
for unpack 'L*', $data . substr($data, -4) x 8;
$hash;
EOF
meta::function('fast_hash', <<'EOF');
my ($data) = @_;
my $piece_size = length($data) >> 3;
my @pieces = (substr($data, $piece_size * 8) . length($data),
map(substr($data, $piece_size * $_, $piece_size), 0 .. 7));
my @hashes = (fnv_hash($pieces[0]));
push @hashes, fnv_hash($pieces[$_ + 1] . $hashes[$_]) for 0 .. 7;
$hashes[$_] ^= $hashes[$_ + 4] >> 16 | ($hashes[$_ + 4] & 0xffff) << 16 for 0 .. 3;
$hashes[0] ^= $hashes[8];
sprintf '%08x' x 4, @hashes[0 .. 3];
EOF
meta::function('state', <<'EOF');
fast_hash(serialize());
EOF
meta::function('verify', <<'EOF');
my $serialized_data = serialize();
my $state = state();
my $temporary_filename = "$0.$state";
open my $file, '>', $temporary_filename;
print $file $serialized_data;
close $file;
chmod 0700, $temporary_filename;
chomp(my $observed_state = join '', qx|perl '$temporary_filename' state|);
my $result = $observed_state eq $state;
unlink $temporary_filename if $result;
$result;
EOF
meta::function('save', <<'EOF');
if (verify()) {
open my $file, '>', $0;
print $file serialize();
close $file;
chmod 0744, $0;
} else {
warn 'Verification failed';
}
EOF
meta::function('cat', <<'EOF');
join "\n", @data{@_};
EOF
meta::function('set', <<'EOF');
$data{$_[0]} = join '', ;
EOF
meta::function('complete', <<'EOF');
my @attributes = sort keys %data;
sub match {
my ($text, @options) = @_;
my @matches = sort grep /^$text/, @options;
if (@matches == 0) {return undef;}
elsif (@matches == 1) {return $matches [0];}
elsif (@matches > 1) {
return ((longest ($matches [0], $matches [@matches - 1])), @matches);
}
}
sub longest {
my ($s1, $s2) = @_;
return substr ($s1, 0, length $1) if ($s1 ^ $s2) =~ /^(\0*)/;
return '';
}
my ($text, $line) = @_;
match ($text, @attributes);
EOF
meta::function('shell', <<'EOF');
use Term::ReadLine;
my $term = new Term::ReadLine "$0 shell";
$term->ornaments(0);
my $output = $term->OUT || \*STDOUT;
$term->Attribs->{attempted_completion_function} = \&complete;
while (defined($_ = $term->readline("$0\$ "))) {
my @args = grep length, split /\s+|("[^"\\]*(?:\\.)?")/o;
my $function_name = shift @args;
s/^"(.*)"$/\1/o, s/\\\\"/"/go for @args;
if ($function_name) {
chomp(my $result = eval {&$function_name(@args)});
warn $@ if $@;
print $output $result, "\n" unless $@;
}
}
EOF
meta::function('edit', <<'EOF');
my $filename = '/tmp/' . rand();
open my $file, '>', $filename;
print $file $data{$_[0]};
close $file;
system($ENV{EDITOR} || $ENV{VISUAL} || '/usr/bin/nano', $filename);
open my $file, '<', $filename;
$data{$_[0]} = join '', <$file>;
close $file;
EOF
meta::code('main', <<'EOF');
my $initial_state = state();
my $command = shift @ARGV || $data{'data::default-action'};
print &$command(@ARGV);
save() if state() ne $initial_state;
EOF
__END__
- end
__
meta::section('archiving-state', <<'__');
- sc Archiving state | sec:archiving-state
Suppose you're about to do something risky with a script and you want to take a snapshot that you can restore to. You could copy into another file, but that's a brute-force approach and it
requires you to exit the script's shell. Better is to have some kind of internal state management, and that's where explicit states come into play.
Remember that \verb|%data| is just a variable; we can do all of the usual things with it. We can store a state by doing a partial serialization into an attribute, and we can restore from
that state by {\tt eval}ing that attribute. To do this we're going to need another namespace.
- perl snippets/state-type << end
meta::meta('type::state', <<'EOF');
# No action when a state is defined
meta::define_form 'state', \&meta::bootstrap::implementation;
EOF
- end
- s1 Saving state | sec:archiving-state-saving
It's tempting to think that this code would do what we want:
- verbatim << end
# Won't work:
associate("state::$_[0]", serialize());
- end
Unfortunately, {\tt serialize} generates three things that we don't want. These are the bootstrap section at the beginning, the call to {\tt internal::main()} at the end, and any attribute
in the {\tt state::} namespace.\footnote{If some states contained others, the script size would grow exponentially in the number of states.} We'll need to write a separate function to
serialize just what we want:
- perl snippets/current-state-function << end
meta::function('current-state', <<'EOF');
my @valid_keys = grep ! /^state::/, sort keys %data;
my @ordered_keys = (grep(/^meta::/, @valid_keys), grep(! /^meta::/, @valid_keys));
join "\n", map serialize_single($_), @ordered_keys;
EOF
- end
\noindent And here's a {\tt save-state} function to automate the state creation process:
- perl snippets/save-state-function << end
meta::function('save-state', <<'EOF');
my ($state_name) = @_;
associate("state::$state_name", &{'current-state'}());
EOF
- end
- s1 Loading state | sec:archiving-state-loading
This is not as straightforward as saving state. Because we're modifying \verb|%data| live, we have to be careful about what happens in the event that something goes wrong. We also don't
want to have stray \verb|%data| elements or externalized functions. The easiest way to defend against errors is to save the current state before applying a new one. Here's the
implementation of {\tt load-state}:
- perl snippets/load-state-function << end
meta::function('load-state', <<'EOF');
my ($state_name) = @_;
my $state = retrieve("state::$state_name");
&{'save-state'}('_'); # Make a backup
delete $data{$_} for grep ! /^state::/, keys %data;
%externalized_functions = ();
eval($state); # Apply the new state
warn $@ if $@;
verify(); # Make sure it worked
EOF
- end
If the load failed for some reason, you can restore using \verb|load-state _|. If it failed badly enough to bork your {\tt load-state} function, then you have a problem.
- s1 The {\tt hypothetically} function | sec:archiving-state-hypothetically
Related to state management is a function called {\tt hypothetically}, which lets you try something out and then revert. It's used internally to examine the state of a modified copy
without actually committing changes.\footnote{This is covered in \Ref{chapter}{sec:cloning-and-inheritance}.} Here's how it's defined:
- perl snippets/hypothetically-function << end
meta::internal_function('hypothetically', <<'EOF');
my %data_backup = %data;
my ($side_effect) = @_;
my $return_value = eval {&$side_effect()};
%data = %data_backup;
die $@ if $@;
$return_value;
EOF
- end
You can use it like this:
- verbatim << end
my $x = hypothetically(sub {
associate('data::foo', '10');
retrieve('data::foo');
});
my $y = retrieve('data::foo');
# now $x eq '10' and $y is undef
- end
__
meta::section('building-the-interface', <<'__');
- sc Building the interface | sec:building-the-interface
Now that we've got attribute storage working, let's build a command-line interface so that we don't have to edit these files by hand anymore. There are a couple of things that need to
happen. First, we need to get these scripts to overwrite themselves instead of printing to standard output. Second, we need a way to get and set entries in \verb|%data|. Starting with the
quine from the last section, here's one way to go about it:
- perl examples/cli-basic << end
my %data;
$data{cat} = <<'EOF';
sub cat {
print join "\n", @data{@_};
}
EOF
$data{set} = <<'EOF';
sub set {
$data{$_[0]} = join '', ;
}
EOF
$data{code} = <<'EOF';
# Eval functions into existence:
eval $data{cat};
eval $data{set};
# Run specified command:
my $command = shift @ARGV;
&$command(@ARGV);
# Save new state:
open my $fh, '>', $0;
print $fh 'my %data;', "\n";
print $fh '$data{', $_, '} = <<\'EOF\';', "\n$data{$_}EOF\n" for keys %data;
print $fh $data{bootstrap};
close $fh;
EOF
$data{bootstrap} = <<'EOF';
eval $data{code};
EOF
eval $data{code};
- end
\noindent Now we can modify its state:
- verbatim << end
$ perl examples/cli-basic cat cat
sub cat {
print join "\n", @data{@_};
}
$ perl examples/cli-basic set foo
bar
^D
$ perl examples/cli-basic cat foo
bar
$
- end
Not bad for a first implementation. This is a very minimal self-modifying Perl file, though it's useless at this point. It also has some fairly serious deficiencies (other than being
useless). I'll cover the serious problems later on, but first let's address the usability.
- s1 Using an editor | sec:using-an-editor
The first thing that would help this script be more useful is a function that let you edit things with a real text editor. Fortunately this isn't difficult:
- verbatim << end
$ cp examples/cli-basic temp
$ perl temp set edit
sub edit {
my $filename = '/tmp/' . rand();
open my $file, '>', $filename;
print $file $data{$_[0]};
close $file;
system($ENV{EDITOR} || $ENV{VISUAL} || '/usr/bin/nano', $filename);
open my $file, '<', $filename;
$data{$_[0]} = join '', <$file>;
close $file;
}
^D
$
- end
It won't work yet though. The reason is that we aren't {\tt eval}ing {\tt edit} yet; we need to manually edit the {\tt code} section and insert this line:
- verbatim << end
...
eval $data{cat};
eval $data{set};
eval $data{edit}; # <- insert this
...
- end
Now you can invoke a text editor on any defined attribute:\footnote{Don't modify {\tt bootstrap} or break the print code though! This will possibly nuke your object.}
- verbatim << end
$ perl examples/cli-editor edit cat
# hack away
$
- end
Here's the object at this point:
- perl examples/cli-editor << end
my %data;
$data{cat} = <<'EOF';
sub cat {
print join "\n", @data{@_};
}
EOF
$data{set} = <<'EOF';
sub set {
$data{$_[0]} = join '', ;
}
EOF
$data{edit} = <<'EOF';
sub edit {
my $filename = '/tmp/' . rand();
open my $file, '>', $filename;
print $file $data{$_[0]};
close $file;
system($ENV{EDITOR} || $ENV{VISUAL} || '/usr/bin/nano', $filename);
open my $file, '<', $filename;
$data{$_[0]} = join '', <$file>;
close $file;
}
EOF
$data{code} = <<'EOF';
# Eval functions into existence:
eval $data{cat};
eval $data{set};
eval $data{edit};
# Run specified command:
my $command = shift @ARGV;
&$command(@ARGV);
# Save new state:
open my $fh, '>', $0;
print $fh 'my %data;', "\n";
print $fh '$data{', $_, '} = <<\'EOF\';', "\n$data{$_}EOF\n" for keys %data;
print $fh $data{bootstrap};
close $fh;
EOF
$data{bootstrap} = <<'EOF';
eval $data{code};
EOF
eval $data{code};
- end
__
meta::section('cloning-and-inheritance', <<'__');
- sc Cloning and inheritance | sec:cloning-and-inheritance
This is probably the single coolest thing about self-modifying Perl programs. You've probably had this looming feeling that propagating updated versions of functions between scripts was
going to be a complete nightmare. For a long time this was indeed the case; I had shell scripts that copied attributes out of one script and into another. Luckily I got tired of doing things
that way and came up with the inheritance mechanism that's used now.
Inheritance isn't as simple as copying all of the attributes from one script into another. Certain namespaces like {\tt data::} are script-specific, for instance. We'll need to have some way
to keep track of which namespaces should be inherited.
Another issue is getting attributes from one script into another. My first implementation of inheritance retrieved each attribute individually. It used {\tt ls} and {\tt cat} for the
transfer, which involved $O(n)$ runs of whichever script was being inherited from. Obviously it was really slow. $O(n)$ runs of a function containing $n$ functions means $O(n^2)$ total time,
and Perl isn't blazingly fast at {\tt eval}ing functions. Later on I extended {\tt serialize} to return a bundle of attributes that the child then {\tt eval}ed.
- s1 Tracking inheritability | sec:cloning-and-inheritance-tracking-inheritability
We're going to need another toplevel field if we want to store data about data types. We can't use \verb|%data|, since we don't really want to save it (whatever we're storing would be
regenerated automatically anyway). What we really need is a way to store transient information:
- verbatim << end
my %data;
my %externalized_functions;
my %datatypes;
my %transient;
- end
\verb|%transient| does nothing except store stuff while the script is running, and all of its information is discarded when the script exits. It's basically just a temporary workspace
where we can stash stuff.
We can now use \verb|%transient| to store things about data types. For convenience let's define {\tt meta::configure} to do this for us:\footnote{For some reason I decided to store the
keys in the odd order of {\tt option}-{\tt namespace} instead of the other way around. I'm still not sure why I did it this way, but it doesn't seem to cause problems.}
- perl snippets/meta-configure << end
meta::meta('configure', <<'EOF');
sub meta::configure {
my ($datatype, %options) = @_;
$transient{$_}{$datatype} = $options{$_} for keys %options;
}
EOF
- end
Now we can add a configuration to each datatype we define:
- verbatim << end
meta::meta('type::function', <<'EOF');
meta::configure 'function', inherit => 1;
meta::define_form 'function', ...;
EOF
meta::meta('type::data', <<'EOF');
meta::configure 'data', inherit => 0;
meta::define_form 'data', ...;
EOF
meta::meta('type::internal_function', <<'EOF');
meta::configure 'internal_function', inherit => 1;
...
EOF
meta::meta('type::bootstrap', <<'EOF');
meta::configure 'bootstrap', inherit => 1;
...
EOF
meta::meta('type::state', <<'EOF');
meta::configure 'state', inherit => 0;
...
EOF
- end
- s1 Extensions to {\tt serialize} | sec:cloning-and-inheritance-extensions-to-serialize
{\tt serialize} needs to be able to give us a bundle of code to create just the attributes that should be inherited. While we're at it, it would also be nice if it handed us just the {\tt
meta::} attributes and then just the non-{\tt meta::} attributes. This way we can make sure that the {\tt meta::} attributes didn't break anything and bail out early if they did.
None of this is particularly challenging, but given that we're going to invoke {\tt serialize} externally we should probably fix the \verb|%options| stuff. (The last thing we want to write
is something like \verb|qx($script serialize partial 1 meta 1 inheritable 1)|). What we need is an adapter that turns command-line options into Perl hashes.\footnote{OK, I'm making a jump
here. Later it will become clearer why it's good to do it this way.} Here's a function that uses {\tt Getopt}-style parsing:
- perl snippets/separate-options-function << end
meta::internal_function('separate_options', <<'EOF');
# Things with one dash are short-form options, two dashes are long-form.
# Characters after short-form are combined; so -auv4 becomes -a -u -v -4.
# Also finds equivalences; so --foo=bar separates into $$options{'--foo'} eq 'bar'.
# Stops processing at the -- option, and removes it. Everything after that
# is considered to be an 'other' argument.
# The only form not supported by this function is the short-form with argument.
# To pass keyed arguments, you need to use long-form options.
my @parseable;
push @parseable, shift @_ until ! @_ or $_[0] eq '--';
my @singles = grep /^-[^-]/, @parseable;
my @longs = grep /^--/, @parseable;
my @others = grep ! /^-/, @parseable;
my @singles = map /-(.{2,})/ ? map("-$_", split(//, $1)) : $_, @singles;
my %options;
$options{$1} = $2 for grep /^([^=]+)=(.*)$/, @longs;
++$options{$_} for grep ! /=/, @singles, @longs;
({%options}, @others, @_);
EOF
- end
The output of this function is a reference to a hash of any keyword arguments (where short-form arguments are treated as increments) followed by any non-switch arguments (either because
they came after \verb|--| or because they didn't start with a dash at all. For example, processing the arguments \verb|-xy --z=foo bar| would yield
\verb|({-x => 1, -y => 1, --z => foo}, bar)|.\footnote{Given the similarity, I don't remember why I didn't just use {\tt Getopt::Long} for this stuff. I think I must have been having a NIH
day.}
Given the ability to pipe options into {\tt serialize} on the command-line, we just need to have it support a reasonably flexible selection interface. We'll later need to have {\tt ls}
support the same options, so let's factor the key selector into its own function:
- perl snippets/select-keys-function << end
meta::internal_function('select_keys', <<'EOF');
my %options = @_;
my $criteria = $options{'--criteria'} ||
$options{'--namespace'} && "^$options{'--namespace'}::" || '.';
grep /$criteria/ && (! $options{'-i'} || $transient{inherit}{namespace($_)}) &&
(! $options{'-I'} || ! $transient{inherit}{namespace($_)}) &&
(! $options{'-S'} || ! /^state::/o) &&
(! $options{'-m'} || /^meta::/o) &&
(! $options{'-M'} || ! /^meta::/o), sort keys %data;
EOF
- end
This function takes the \verb|%options| hash output by \verb|separate_options| as input and returns a list of keys into \verb|%data|. The somewhat odd logical structure of the {\tt grep}
predicate is just implication: ``if \verb|$options{'-i'}| is set, then the key's namespace must be inheritable.''
The new {\tt serialize} function is fairly simple; most of the heavy lifting is already done:
- perl snippets/serialize-final << end
meta::function('serialize', <<'EOF');
my ($options, @criteria) = separate_options(@_);
my $partial = $$options{'-p'};
my $criteria = join '|', @criteria;
my @attributes = map serialize_single($_),
select_keys(%$options, '-m' => 1, '--criteria' => $criteria),
select_keys(%$options, '-M' => 1, '--criteria' => $criteria);
my @final_array = @{$partial ? \@attributes :
[retrieve('bootstrap::initialization'),
@attributes,
'internal::main();', '',
'__END__']};
join "\n", @final_array;
EOF
- end
{\tt -m} and {\tt -M} select {\tt meta::} and non-{\tt meta::} attributes, respectively. We also provide criteria if the user has selected any. They're joined together with a pipe symbol
because that forms a disjunction inside a regular expression (and criteria are regexps for attribute names). We also, somewhat importantly, have a {\tt -p} switch to produce a partial
serialization. This leaves off the bootstrap code and the {\tt internal::main()} call. The only difference between this and {\tt current-state} is that {\tt current-state} also leaves out
{\tt state::} attributes.\footnote{This version of {\tt serialize} also will do that for you if you pass the {\tt -S} option.}
- s1 The {\tt update-from} function | sec:cloning-and-inheritance-the-update-from-function
Here's where things start to get interesting. {\tt update-from} handles the case where you have two scripts {\tt base} and {\tt child}, and you want {\tt child} to inherit stuff from {\tt
base}.\footnote{``Stuff'' is deliberately vague. Presumably we want to inherit every inheritable attribute though.} Here's a basic implementation:
- perl snippets/update-from-function << end
meta::function('update-from', <<'EOF');
my ($options, @targets) = separate_options(@_);
my %options = %$options;
@targets or die 'Must specify at least one target to update from';
my $save_state = ! ($options{'-n'} || $options{'--no-save'});
my $force = $options{'-f'} || $options{'--force'};
&{'save-state'}('before-update') if $save_state;
for my $target (@targets) {
eval qx($target serialize -ipm);
eval qx($target serialize -ipM);
reload(); # We're about to define this
unless (verify()) {
if ($force) {
warn 'Verification failed, but keeping new state';
} else {
warn "Verification failed after $target; reverting";
return &{'load-state'}('before-update') if $save_state;
}
}
}
EOF
- end
If {\tt child} has this function, you can update it this way (assuming you've set execute permissions):
- verbatim << end
$ ./child update-from ./base
$
- end
If all goes well it will execute without failing. {\tt base} will be run twice: once to grab inheritable {\tt meta::}-attributes, again to grab inheritable other attributes.
The {\tt reload} function just calls {\tt execute} on each member of \verb|%data|. It's there to make sure that the new attributes and the old attributes work well together. Here's the
definition:
- perl snippets/reload-function << end
meta::function('reload', <<'EOF');
execute($_) for grep ! /^bootstrap::/, keys %data;
EOF
- end
- s1 Managing parents | sec:cloning-and-inheritance-managing-parents
If you're using scripts as add-on modules, it gets tiring to issue $n$ {\tt update-from} commands when you're using $n$ modules. Further, you have to type out the whole path of the module,
which might be in a separate directory. Finally, attributes that later get deleted or renamed in the addon modules won't be cleaned up in the child script. The way we've got inheritance
set up is woefully incomplete.
The missing piece is parent tracking. We need to keep track of two things:
- enumerate << end
- item Which scripts have I inherited from?
- item Which properties did I inherit from each one?
- end
Fortunately this isn't too hard. We just need a new namespace and some new functions.
- s2 The {\tt parent::} namespace | sec:cloning-and-inheritance-the-parent-namespace
We can use the {\tt parent::} namespace to answer both of the above questions. Each attribute will take its name from the path to a script (e.g.~{\tt parent::/usr/bin/script1}), and its
contents will be a newline-separated string of the attributes inherited from that script. Here's the type definition:
- perl snippets/parent-type << end
meta::meta('type::parent', <<'EOF');
meta::configure 'parent', inherit => 1; # Transitive parents (explained below)
meta::define_form 'parent', \&meta::bootstrap::implementation;
EOF
- end
Now we need to update {\tt update-from} to populate this namespace. Before we do, though, we need to define uniqueness.
- s2 Uniqueness | sec:cloning-and-inheritance-uniqueness
Let's suppose you've got three objects {\tt a}, {\tt b}, and {\tt c}, and each inherits from the previous one.\footnote{I'm presupposing that inheritance works automatically even though
we haven't defined it quite yet.} Then {\tt b}'s parent is {\tt a}, and {\tt c}'s parent is {\tt b}. It's important for {\tt c} to know that {\tt a} is also its parent. The reason is
that otherwise you're forced to update {\tt b} before {\tt c}, since {\tt c} inherits only from {\tt b}.
It's ultimately for this reason that {\tt parent::} attributes get inherited. Inheritance is certainly transitive (if {\tt c} inherits from {\tt b}, which inherits from {\tt a}, then
{\tt c} inherits from {\tt a}). There are, however, some logistical matters to be dealt with. The most important one has to do with ordering.
It shouldn't matter in which order the parents are inherited from. This is an interesting requirement, because it means that the things an object inherits from each of its parents form
disjoint sets. The only way to pull this off is if each object keeps track of how it's different from its own parents. The attributes that the child has but the parent doesn't are
considered unique.
- s2 Updating {\tt ls} and {\tt serialize} | sec:cloning-and-inheritance-updating-ls-and-serialize
We need a way to ask {\tt ls} for the list of unique attributes for an object, and then ask {\tt serialize} to give us just those attributes. To do this, we'll add a {\tt -u} option (and
for symmetry a complement {\tt -U}) to \verb|select_keys|:
- perl snippets/select-keys-final << end
meta::internal_function('select_keys', <<'EOF');
my %options = @_;
my %inherited = map {$_ => 1} split /\n/o, join "\n",
retrieve(grep /^parent::/o, sort keys %data)
if $options{'-u'} or $options{'-U'};
my $criteria = $options{'--criteria'} ||
$options{'--namespace'} && "^$options{'--namespace'}::" || '.';
grep /$criteria/ && (! $options{'-u'} || ! $inherited{$_}) &&
(! $options{'-U'} || $inherited{$_}) &&
(! $options{'-i'} || $transient{inherit}{namespace($_)}) &&
(! $options{'-I'} || ! $transient{inherit}{namespace($_)}) &&
(! $options{'-S'} || ! /^state::/o) &&
(! $options{'-m'} || /^meta::/o) &&
(! $options{'-M'} || ! /^meta::/o), sort keys %data;
EOF
- end
The extra logic here searches through all of the {\tt parent::} attributes to find properties that the parents also contain. If any parent contains one, then the property isn't unique.
{\tt serialize} will get this new behavior automatically, since it just forwards its options to \verb|select_keys|. But we haven't modified {\tt ls} in a long time; it doesn't know
anything about options. There are actually quite a few enhancements that we could make to {\tt ls}, but for now let's keep it simple and change it only as necessary:
- perl snippets/ls-with-options << end
meta::function('ls', <<'EOF');
my ($options, @criteria) = separate_options(@_);
join "\n", select_keys('--criteria' => join '|', @criteria, %$options);
EOF
- end
Now you can say things like {\tt ./script ls -iu} to get a listing of attributes that are both inheritable and unique.
- s2 An updated {\tt update-from} function | sec:cloning-and-inheritance-an-updated-update-from-function
{\tt update-from} now has three new responsibilities. One is to record the fact that we updated from another script, which involves creating a new {\tt parent::} attribute for each
inheritance operation. Another is to ask each new parent which attributes it intends to define. The last thing it needs to do is clean up any attributes that some parent used to define
but no longer does.\footnote{Due to how {\tt update-from} is structured, this step actually happens first.} Here's the new implementation:
- perl snippets/update-from-final << end
my ($options, @targets) = separate_options(@_);
my %options = %$options;
@targets or die 'Must specify at least one target to update from';
my $save_state = ! ($options{'-n'} || $options{'--no-save'});
my $no_parents = $options{'-P'} || $options{'--no-parent'} || $options{'--no-parents'};
my $force = $options{'-f'} || $options{'--force'};
&{'save-state'}('before-update') if $save_state;
for my $target (@targets) {
# The -a flag will become relevant once we add formatting to 'ls'
my $attributes = join '', qx($target ls -aiu);
warn "Skipping unreachable object $target" unless $attributes;
if ($attributes) {
# Remove keys that the parent used to define but doesn't anymore:
rm(split /\n/, retrieve("parent::$target")) if $data{"parent::$target"};
associate("parent::$target", $attributes) unless $no_parents;
eval qx($target serialize -ipmu);
eval qx($target serialize -ipMu);
warn $@ if $@;
reload();
if (verify()) {
print "Successfully updated from $_[0]. ",
"Run 'load-state before-update' to undo this change.\n" if $save_state;
} elsif ($force) {
warn 'The object failed verification, but the failure state has been ' .
'kept because --force was specified.';
warn 'At this point your object will not save properly, though backup ' .
'copies will be created.';
print "Run 'load-state before-update' to undo the update and return to ",
"a working state.\n" if $save_state;
} else {
warn 'Verification failed after the upgrade was complete.';
print "$0 has been reverted to its pre-upgrade state.\n" if $save_state;
print "If you want to upgrade and keep the failure state, then run ",
"'update-from $target --force'." if $save_state;
return &{'load-state'}('before-update') if $save_state;
}
}
}
- end
- s2 The {\tt update} function | sec:cloning-and-inheritance-the-update-function
This new {\tt update-from} function contains all of the logic to perform individual updates, but it still requires you to list the parent objects. There isn't any need to do this
manually though, since if we look for {\tt parent::} attributes we can get the list. That's what {\tt update} does:
- perl snippets/update-function << end
meta::function('update', <<'EOF');
&{'update-from'}(@_, grep s/^parent:://o, sort keys %data);
EOF
- end
- s1 {\tt clone} and {\tt child} | sec:cloning-and-inheritance-clone-and-child
As things stand creating children from an object is a bit cumbersome. We have to manually copy the object and then run {\tt update-from} once to get the parent to work out, which seems
like too much work. Let's take care of copying first by creating a {\tt clone} function:
- perl snippets/clone-function << end
meta::function('clone', <<'EOF');
open my $file, '>', $_[0];
print $file serialize();
close $file;
chmod 0700, $_[0];
EOF
- end
Now you can clone an object as it exists at any given moment. More interesting, though, is the related {\tt child} function, which creates an object already setup for inheritance:
- perl snippets/child-function << end
meta::function('child', <<'EOF');
my ($child_name) = @_;
clone($child_name);
qx($child_name update-from $0 -n);
EOF
- end
{\tt object} implements this function; so, for example, you could inherit from it like this:
- verbatim << end
$ /path/to/object child ./foo
$ ./foo update -n
$ ./foo ls -a parent::
parent::/path/to/object
$
- end
__
meta::section('detecting-divergence', <<'__');
- sc Detecting divergence | sec:detecting-divergence
The inheritance system built in the last chapter has a couple of problems. One is a subtle race condition that can happen when a script has grandparents and out-of-date parents. The other is
the very real deficiency that objects can't have divergent attributes without those attributes then being overwritten on the next update.\footnote{I ran into this when writing Caterwaul; its
homepage is a self-modifying Perl file that needed a gray background instead of a white one when rendered as HTML. So I wrote divergence detection.}
Neither of these problems is actually very difficult to solve. All we need to do is store not only the name of the attribute that we inherited, but also the state it was in when we last read
it. We then make sure that the attribute we've got hasn't changed since we last read it before we update it. If it's in a different state, we assume that it was modified locally and preserve
our local copy.
- s1 Attribute hashing
Right now, {\tt parent::} attributes are stored as lists of attributes defined by that parent. What we really want is not only the names of those attributes, but their hashes as well. So,
for example, we inherited {\tt function::ls} from {\tt object} and its hash was {\tt 01a23d51d5b529e03943bd57e33f92df}. If our copy isn't the same, then we preserve our local one because
it's divergent.
{\tt function::update-from} uses {\tt ls} to get the attribute list that ends up making up the {\tt parent::} attribute. The last implementation used {\tt -aiu}; we'll add a new
command-line option called {\tt -h} that tells {\tt ls} to include hashes. So the output would look like this:
- verbatim << end
$ ./object ls -ahiu
...
meta::type::meta c6250056816b58a9608dd1b2614246f8
meta::type::parent 09d1d03379e4e0b262e06939f4e00464
meta::type::retriever 71a29050bf9f20f6c71afddff83addc9
meta::type::state 84da7d5220471307f1f990c5057d3319
...
$
- end
This is saying that {\tt object}'s copy of {\tt meta::type::meta} hashes to {\tt c6250056816b58a9608dd1b2614246f8}, so if ours doesn't, then we have diverged. This divergence detection is
taken care of by {\tt update-from}, which still evaluates everything the parent script sends but restores divergent attribute values after the fact. (I considered doing it the right way
and having the parent script not send divergent attributes, but this was more complicated than I wanted to implement.)
- s1 The code (lots of it)
In order to implement all of this stuff, we'll need to modify {\tt ls} and {\tt update-from}.
- s2 The new {\tt ls}
Here's the monster {\tt ls} that {\tt object} defines:\footnote{Don't worry if you don't get what's going on here. Most of it is just display logic.}
- resource snippets/updated-ls << end
meta::function('ls', <<'EOF');
my ($options, @criteria) = separate_options(@_);
my ($external, $shadows, $sizes, $flags, $long, $hashes, $parent_hashes) =
@$options{qw(-e -s -z -f -l -h -p)};
$sizes = $flags = $hashes = $parent_hashes = 1 if $long;
return table_display([grep ! exists $data{$externalized_functions{$_}},
sort keys %externalized_functions]) if $shadows;
my $criteria = join('|', @criteria);
my @definitions = select_keys('--criteria' => $criteria,
'--path' => $transient{path}, %$options);
my %inverses = map {$externalized_functions{$_} => $_} keys %externalized_functions;
my @externals = map $inverses{$_}, grep length, @definitions;
my @internals = grep length $inverses{$_}, @definitions;
my @sizes = map sprintf('%6d %6d', length(serialize_single($_)), length(retrieve($_))),
@{$external ? \@internals : \@definitions} if $sizes;
my @flags = map {my $k = $_; join '', map(is($k, "-$_") ? $_ : '-', qw(d i m u))}
@definitions if $flags;
my @hashes = map fast_hash(retrieve($_)), @definitions if $hashes;
my %inherited = parent_attributes(grep /^parent::/o, keys %data) if $parent_hashes;
my @parent_hashes = map $inherited{$_} || '-', @definitions if $parent_hashes;
join "\n", map strip($_), split /\n/,
table_display($external ? [grep length, @externals] : [@definitions],
$sizes ? ([@sizes]) : (), $flags ? ([@flags]) : (),
$hashes ? ([@hashes]) : (), $parent_hashes ? ([@parent_hashes]) : ());
EOF
- end
There isn't as much going on here as it looks like. Most of the logic is just figuring out what to display, as this {\tt ls} implementation supports many different display modes. A few
things I would like to point out, though, are:
- enumerate << end
- item
On line 8, I reference a function called \verb|table_display|. This just puts things into aligned columns; here's how {\tt object} defines it:
- resource snippets/table-display << end
meta::internal_function('table_display', <<'EOF');
# Displays an array of arrays as a table; that is, with alignment. Arrays are
# expected to be in column-major order.
sub maximum_length_in {
my $maximum = 0;
length > $maximum and $maximum = length for @_;
$maximum;
}
my @arrays = @_;
my @lengths = map maximum_length_in(@$_), @arrays;
my @row_major = map {my $i = $_; [map $$_[$i], @arrays]} 0 .. $#{$arrays[0]};
my $format = join ' ', map "%-${_}s", @lengths;
join "\n", map strip(sprintf($format, @$_)), @row_major;
EOF
- end
- item
On line 13, I reference \verb|$transient{path}|. This is a really useful shell feature that implicitly filters attributes when you type {\tt ls}, but otherwise has nothing to do with
what's going on here.
- item
On line 21, I reference a function called {\tt is()}. Remember the gnarly {\tt grep} conditional in \verb|select_keys|? That's now factored into {\tt function::is}. Here's {\tt
object}'s implementation:
- resource snippets/function-is << end
meta::function('is', <<'EOF');
my ($attribute, @criteria) = @_;
my ($options, @stuff) = separate_options(@criteria);
exists $data{$attribute} and attribute_is($attribute, %$options);
EOF
- end
- resource snippets/internal-function-attribute-is << end
meta::internal_function('attribute_is', <<'EOF');
my ($a, %options) = @_;
my %inherited = parent_attributes(grep /^parent::/o, sort keys %data) if
grep exists $options{$_}, qw/-u -U -d -D/;
my $criteria = $options{'--criteria'} ||
$options{'--namespace'} && "^$options{'--namespace'}::" || '.';
my %tests = ('-u' => sub {! $inherited{$a}},
'-d' => sub {$inherited{$a} && fast_hash(retrieve($a)) ne $inherited{$a}},
'-i' => sub {$transient{inherit}{namespace($a)}},
'-s' => sub {$a =~ /^state::/o},
'-m' => sub {$a =~ /^meta::/o});
# These checks make sure all tests are satisfied (a test is just a
# command-line option). I'm sure there's a clearer way to do it...
return 0 unless scalar keys %tests == scalar
grep ! exists $options{$_} || &{$tests{$_}}(), keys %tests;
return 0 unless scalar keys %tests == scalar
grep ! exists $options{uc $_} || ! &{$tests{$_}}(), keys %tests;
$a =~ /$_/ || return 0 for @{$options{'--path'}}; # I'll explain this later
$a =~ /$criteria/;
EOF
- end
- end
- s2 The new {\tt update-from}
__
meta::section('eval-backtraces', <<'__');
- sc {\tt eval} backtraces | sec:eval-backtraces
Our script is fairly awesome so far. It prevents us from creating attributes in namespaces that don't exist, since that would cause incorrect serialization, it verifies before it saves, etc.
But there's one problem. Take a look at the error messages we get:
- verbatim << end
$ perl examples/some-improvements
examples/some-improvements$ create foo::bar
Namespace foo does not exist at (eval 9) line 4.
examples/some-improvements$
- end
If there's a problem in some attribute, we have no information about the location of the error other than ``eval $n$'' and the line number relative to that. {\tt object} solves this problem:
- verbatim << end
$ object
object$ create foo::bar
[error] Namespace foo does not exist at internal_function::associate line 4.
object$
- end
The key is to wrap {\tt eval} in such a way that we can later resolve the meaningless numbers into useful locations. And to do this, we're going to need to modify the bootstrap code again.
- verbatim << end
my %data;
my %externalized_functions;
my %datatypes;
my %locations; # Maps eval-numbers to attribute names
- end
There's a beautiful hack to handle the {\tt eval} processing. Watch this (also in {\tt bootstrap::initialization}):\footnote{It actually doesn't have to be inside the bootstrap code, but it
doesn't change often and is useful to have around, so I decided to put it there to save time.}
- perl snippets/meta-eval-in << end
sub meta::eval_in {
my ($what, $where) = @_;
# Obtain next eval-number and alias it to the designated location
@locations{eval('__FILE__') =~ /\(eval (\d+)\)/} = ($where); # <- step 1
my $result = eval $what; # <- step 2
$@ =~ s/\(eval \d+\)/$where/ if $@;
warn $@ if $@;
$result;
}
- end
By {\tt eval}ing \verb|__FILE__|, we get the current eval number. So the next one will be whatever we {\tt eval} next. This means that in the shell sessions above, \verb|%locations| contains
a mapping from {\tt 9} to \verb|internal_function::associate|. Here's the function that converts an {\tt eval} index into an attribute name:
- perl snippets/translate-backtrace-function << end
meta::internal_function('translate_backtrace', <<'EOF');
my ($trace) = @_;
$trace =~ s/\(eval (\d+)\)/$locations{$1 - 1}/g;
$trace;
EOF
- end
Notice that we're subtracting one. The {\tt eval} number that triggered the error will be one greater than the one we stored.\footnote{Good API design would resolve this ahead-of-time rather
than at lookup time. I haven't gotten around to changing it yet though.}
Now that we have this mechanism, we can go back and convert {\tt eval} calls into \verb|meta::eval_in|:
- perl snippets/using-eval-in << end
meta::define_form 'function', sub {
my ($name, $value) = @_;
meta::externalize $name, "function::$name",
meta::eval_in("sub {\n$value\n}", "function::$name");
};
meta::define_form 'internal_function', sub {
my ($name, $value) = @_;
*{$name} =
meta::eval_in("sub {\n$value\n}", "internal_function::$name");
};
- end
__
meta::section('introduction', <<'__');
- sc Introduction | sec:introduction
I've gotten a lot of WTF's\footnote{\url{http://www.osnews.com/story/19266/WTFs_m}} about self-modifying Perl scripts. Rightfully so, too. There's no documentation (until now), the interface
is opaque and not particularly portable, and they aren't even very human-readable when edited:
- verbatim << end
...
meta::define_form 'meta', sub {
my ($name, $value) = @_;
meta::eval_in($value, "meta::$name");
};
meta::meta('configure', <<'__25976e07665878d3fae18f050160343f');
# A function to configure transients. Transients can be used to store any number of
# different things, but one of the more common usages is type descriptors.
sub meta::configure {
my ($datatype, %options) = @_;
$transient{$_}{$datatype} = $options{$_} for keys %options;
}
__25976e07665878d3fae18f050160343f
...
- end
Despite these shortcomings, though, I think they're fairly useful (this guide is a self-modifying Perl file, in fact). At the end, you'll have a script that is functionally equivalent to the
{\tt object} script, which I use as the prototype for all of the other ones.\footnote{See \url{http://github.com/spencertipping/perl-objects} for the full source.} The full source code for
this guide and accompanying examples is available at \url{http://github.com/spencertipping/writing-self-modifying-perl}.
Proceed only with fortitude, determination, and Perl v5.10.
__
meta::section('main', <<'__');
- documentclass report
- include resource::header
- title Writing Self-Modifying Perl
- author Spencer Tipping
- document << end
- maketitle
- tableofcontents
- include section::introduction
- sp The Basics
- include section::a-big-quine
- include section::building-the-interface
- include section::namespaces
- include section::serialization
- include section::adding-a-repl
- include section::some-improvements
- sp The Fun Stuff
- include section::rendering-as-html
- include section::eval-backtraces
- include section::archiving-state
- include section::cloning-and-inheritance
- include section::detecting-divergence
- include section::virtual-attributes
- end
__
meta::section('namespaces', <<'__');
- sc Namespaces | sec:namespaces
It's a bummer to have to add a new {\tt eval} line for every function we want to define. We could merge all of the functions into a single hash key, but that's too easy.\footnote{Aside from
being a lame cop-out, it also limits extensibility, as I'll explain later.} More appropriate is to assign a type to each hash key. This can be encoded in the name. For example, we might
convert the names like this:
- verbatim << end
set -> function::set
cat -> function::cat
edit -> function::edit
code -> code::main
- end
For reasons that I'll explain in a moment, we no longer need {\tt bootstrap}. The rules governing these types are:
- enumerate << end
- item When we see a new {\tt function::} key, evaluate its contents.
- item When we see a new {\tt code::} key, evaluate its contents. \label{item:run-code}
- end
\Ref{Rule}{item:run-code} is why we don't need {\tt bootstrap} anymore. Now you've probably noticed that these rules do exactly the same thing -- why are we differentiating between these
types then? Two reasons. First, we need to make sure that functions are evaluated before the code section is evaluated (otherwise the functions won't exist when we need them). Second, it's
because functions can be handled in a more useful way.
- s1 Handling functions more usefully | sec:namespaces-handling-functions-more-usefully
Remember how we had to write \verb|sub X {| and \verb|}| every time we wrote a function, despite the fact that the function name was identical to the name of the key in \verb|%data|?
That's fairly lame, and it could become misleading if the names ever weren't the same. We really should have the script handle this for us. So instead of writing the function signature, we
would just write its body:
- verbatim << end
# The body of 'cat':
print join "\n", @data{@_};
- end
\noindent and infer its name from the key. Perl is helpful here by giving us first-class access to the symbol table:
- perl snippets/create-function << end
sub create_function {
my ($name, $body) = @_;
*{$name} = eval "sub {\n$body\n}";
}
- end
If we're going to handle functions this way, we need to change the rule for {\tt function::} keys:
- quote << end
When we see a new {\tt function::} key, call \verb|create_function| on the key name (without the {\tt function::} part) and the value.
- end
- s1 Catching attribute creation | sec:namespaces-catching-attribute-creation
We can't observe when a new key is added to \verb|%data| as things are now. Fortunately this is easy to fix. Instead of writing lines that read \verb|$data{...} = ...|, we can write some
functions that perform this assignment for us, and in the process we can handle any side-effects like function creation. Here's a naive implementation:
- perl snippets/define-function-define-code << end
sub define_function {
my ($name, $value) = @_;
$data{$name} = $value;
create_function $name, $value;
}
sub define_code {
my ($name, $value) = @_;
$data{$name} = $value;
}
- end
Since we're always going to assign into \verb|%data|, we can abstract that step out:
- perl snippets/define-definer << end
sub define_definer {
my ($name, $handler) = @_;
*{$name} = sub {
my ($name, $value) = @_;
$data{$name} = $value;
&$handler($name, $value);
}
}
define_definer 'define_function', \&create_function;
define_definer 'define_code', sub {
my ($name, $value) = @_;
eval $value;
};
- end
To avoid the possibility of later collisions we should probably use a separate namespace for all of these functions, since really bad things happen if you inadvertently replace one. I use
the {\tt meta::} namespace for this purpose in my scripts.
At this point we've got the foundation for namespace creation. This is actually used with few modifications in the Perl objects I use on a regular basis. Here's \verb|meta::define_form|
lifted from {\tt object}:
- perl snippets/meta-define-form << end
sub meta::define_form {
my ($namespace, $delegate) = @_;
$datatypes{$namespace} = $delegate;
*{"meta::${namespace}::implementation"} = $delegate;
*{"meta::$namespace"} = sub {
my ($name, $value) = @_;
chomp $value;
$data{"${namespace}::$name"} = $value;
$delegate->($name, $value);
};
}
- end
The idea is the same as \verb|define_definer|, but with a few extra lines. We stash the delegate in a \verb|%datatypes| table for later reference. We also (redundantly, I notice) create a
function in the {\tt meta::} package so that we can refer to it when defining other forms. This lets us copy the behavior of namespaces but still have them be separate. The third line
that's different is \verb|chomp $value|, which is used because heredocs put an extra newline on the end of strings. \verb|meta::define_form| has the same interface as
\verb|define_definer|:
- perl snippets/meta-define-form-function-code << end
meta::define_form 'function', \&create_function;
meta::define_form 'code', sub {
my ($name, $value) = @_;
eval $value;
};
- end
Attribute definitions look a little different than they did before. The two \verb|define_form| calls above create the functions {\tt meta::function} and {\tt meta::code}, which will need
to be called this way:
- verbatim << end
meta::function('cat', <<'EOF');
print join "\n", @data{@_};
EOF
meta::code('main', <<'EOF');
# No more eval statements!
# Run command
...
# Save stuff
...
EOF
- end
Notice that we don't specify the full name of the attributes being created. \verb|meta::function('x', ...)| creates a key called {\tt function::x}; this was handled in the
\verb|define_form| logic.
- s1 Putting it all together | sec:namespaces-putting-it-all-together
At this point we're all set to write another script. The overall structure is still basically the same even though each piece has changed a little:
- perl examples/namespace-basic << end
my %data;
my %datatypes;
sub meta::define_form {
my ($namespace, $delegate) = @_;
$datatypes{$namespace} = $delegate;
*{"meta::${namespace}::implementation"} = $delegate;
*{"meta::$namespace"} = sub {
my ($name, $value) = @_;
chomp $value;
$data{"${namespace}::$name"} = $value;
$delegate->($name, $value);
};
}
meta::define_form 'function', sub {
my ($name, $body) = @_;
*{$name} = eval "sub {\n$body\n}";
};
meta::define_form 'code', sub {
my ($name, $value) = @_;
eval $value;
};
meta::function('cat', <<'EOF');
print join "\n", @data{@_};
EOF
meta::code('main', <<'EOF');
# Run specified command:
my $command = shift @ARGV;
&$command(@ARGV);
# Save new state:
open my $file, '>', $0;
# Copy above bootstrapping logic:
print $file <<'EOF2';
my %data;
my %datatypes;
sub meta::define_form {
my ($namespace, $delegate) = @_;
$datatypes{$namespace} = $delegate;
*{"meta::${namespace}::implementation"} = $delegate;
*{"meta::$namespace"} = sub {
my ($name, $value) = @_;
chomp $value;
$data{"${namespace}::$name"} = $value;
$delegate->($name, $value);
};
}
meta::define_form 'function', sub {
my ($name, $body) = @_;
*{$name} = eval "sub {\n$body\n}";
};
meta::define_form 'code', sub {
my ($name, $value) = @_;
eval $value;
};
EOF2
# Serialize attributes (everything else before code):
for (grep(!/^code::/, keys %data), grep(/^code::/, keys %data)) {
my ($namespace, $name) = split /::/, $_, 2;
print $file "meta::$namespace('$name', <<'EOF');\n$data{$_}\nEOF\n";
}
# Just for good measure:
print $file "\n__END__";
close $file;
EOF
__END__
- end
The most substantial changes were:
- enumerate << end
- item We're defining two hashes at the beginning, though we still just use \verb|%data|.
- item We're using delegate functions to define attributes rather than assigning directly into \verb|%data|.
- item Quoted values now get {\tt chomp}ed. I've added another \verb|\n| in the serialization logic to compensate for this.
- item The serialization logic is now order-specific; it puts {\tt code::} entries after other things.
- item The file now has an \verb|__END__| marker on it.
- end
- s1 Separating bootstrap code | sec:namespaces-separating-bootstrap-code
The bootstrap code is now large quoted string inside {\tt code::main}, which isn't optimal. Better is to break it out into its own attribute. To do this, we'll need a new namespace that
has no side-effect.\footnote{We can't use {\tt code::} because then the code would be evaluated twice; once because it's printed directly, and again because of the {\tt eval} in the {\tt
code::} delegate.} I'll call this namespace {\tt bootstrap::}.
- verbatim << end
meta::define_form 'bootstrap', sub {};
- end
There's a special member of the {\tt bootstrap::} namespace that contains the code in the beginning of the file:
- verbatim << end
meta::bootstrap('initialization', <<'EOF');
my %data;
my %datatypes;
...
EOF
- end
This condenses {\tt code::main} by a lot:
- perl snippets/bootstrapped-code-main << end
meta::code('main', <<'EOF');
# Run specified command:
my $command = shift @ARGV;
&$command(@ARGV);
# Save new state:
open my $file, '>', $0;
print $file $data{'bootstrap::initialization'};
# Serialize attributes (everything else before code):
for (grep(!/^code::/, keys %data), grep(/^code::/, keys %data)) {
my ($namespace, $name) = split /::/, $_, 2;
print $file "meta::$namespace('$name', <<'EOF');\n$data{$_}\nEOF\n";
}
# Just for good measure:
print $file "\n__END__";
close $file;
EOF
- end
Here's the final product, after adding the {\tt set} and {\tt edit} functions from before:
- perl examples/namespace-full << end
my %data;
my %datatypes;
sub meta::define_form {
my ($namespace, $delegate) = @_;
$datatypes{$namespace} = $delegate;
*{"meta::${namespace}::implementation"} = $delegate;
*{"meta::$namespace"} = sub {
my ($name, $value) = @_;
chomp $value;
$data{"${namespace}::$name"} = $value;
$delegate->($name, $value);
};
}
meta::define_form 'bootstrap', sub {};
meta::define_form 'function', sub {
my ($name, $body) = @_;
*{$name} = eval "sub {\n$body\n}";
};
meta::define_form 'code', sub {
my ($name, $value) = @_;
eval $value;
};
meta::bootstrap('initialization', <<'EOF');
my %data;
my %datatypes;
sub meta::define_form {
my ($namespace, $delegate) = @_;
$datatypes{$namespace} = $delegate;
*{"meta::${namespace}::implementation"} = $delegate;
*{"meta::$namespace"} = sub {
my ($name, $value) = @_;
chomp $value;
$data{"${namespace}::$name"} = $value;
$delegate->($name, $value);
};
}
meta::define_form 'bootstrap', sub {};
meta::define_form 'function', sub {
my ($name, $body) = @_;
*{$name} = eval "sub {\n$body\n}";
};
meta::define_form 'code', sub {
my ($name, $value) = @_;
eval $value;
};
EOF
meta::function('cat', <<'EOF');
print join "\n", @data{@_};
EOF
meta::function('set', <<'EOF');
$data{$_[0]} = join '', ;
EOF
meta::function('edit', <<'EOF');
my $filename = '/tmp/' . rand();
open my $file, '>', $filename;
print $file $data{$_[0]};
close $file;
system($ENV{EDITOR} || $ENV{VISUAL} || '/usr/bin/nano', $filename);
open my $file, '<', $filename;
$data{$_[0]} = join '', <$file>;
close $file;
EOF
meta::code('main', <<'EOF');
# Run specified command:
my $command = shift @ARGV;
&$command(@ARGV);
# Save new state:
open my $file, '>', $0;
print $file $data{'bootstrap::initialization'};
# Serialize attributes (everything else before code):
for (grep(!/^code::/, keys %data), grep(/^code::/, keys %data)) {
my ($namespace, $name) = split /::/, $_, 2;
print $file "meta::$namespace('$name', <<'EOF');\n$data{$_}\nEOF\n";
}
# Just for good measure:
print $file "\n__END__";
close $file;
EOF
__END__
- end
__
meta::section('rendering-as-html', <<'__');
- sc Rendering as HTML | sec:rendering-as-html
It turns out that browsers are very lenient about what they're willing to render as HTML. From what I've seen, anything that ends with {\tt .html} will be rendered this way, with extra stuff
inserted into the \verb|| as toplevel text. This is perfect for doing some fun stuff with self-modifying Perl files.
First, I should note that the actual UI implementation is in Caterwaul 0.x and is beyond the scope of this guide. But the basic ideas still apply. There are two things you'll want to do to
make your script HTML-renderable. The first is to create a {\tt bootstrap::} attribute with some markup in it. It's useful for it to be a {\tt bootstrap::} attribute because these are
unevaluated. Here's {\tt bootstrap::html} from {\tt object}:
- resource snippets/object/bootstrap::html << end
- end
If you copy this attribute into a Perl file, it should indeed render as a web page. There's really only one problem with it, which is that before {\tt bootstrap::html} is loaded you'll see a
bunch of garbage on the screen. Rather than solve this in any sane and correct way, I just implemented this hack in {\tt bootstrap::initialization}:\footnote{The backslash before the end of
the line isn't in the real {\tt object}, I just put it here to indicate that the line should be continued, not broken.}
- resource snippets/object/bootstrap::initialization-div << end
# For the benefit of HTML viewers (this is hack):
#
- end
This covers up the garbage until a function in {\tt interface.js} removes the covering \verb|
|.
By the way, I highly recommend using the HTML view of Perl objects while reading the rest of this guide. You can get to them here: \url{http://spencertipping.com/perl-objects} (each {\tt
.html} file is just a symlink to the Perl object of the same name).
__
meta::section('serialization', <<'__');
- sc Serialization | sec:serialization
Earlier I alluded to a glaring problem with these scripts as they stand. The issue is the {\tt EOF} marker we've been using. Here's what happens if we put a line containing {\tt EOF} into an
attribute:
- verbatim << end
$ cp examples/basic-meta-with-functions temp
$ perl temp set function::bif
print <<'EOF';
uh-oh...
EOF
^D
$ perl temp cat function::bif
Can't locate object method "EOF" via package "meta::function" at temp line 31.
$
- end
It's not hard to see what went wrong: {\tt temp} now has an attribute definition that looks like this:
- verbatim << end
meta::function('bif', <<'EOF');
print <<'EOF';
uh-oh...
EOF
EOF
- end
We need to come up with some end marker that isn't in the value being stored. For the moment let's use random numbers.\footnote{{\tt object} implements a simple FNV-hash and uses the hash of
the contents. I'll go over how to implement this a bit later.}
- s1 Fixing the {\tt EOF} markers | sec:serialization-fixing-the-eof-markers
There isn't a particularly compelling reason to inline the serialization logic in {\tt code::main}. Since we have a low-overhead way of defining functions, let's make a {\tt serialize}
function to return the state of a script as a string, along with a helper method \verb|serialize_single| to handle one attribute at a time:
- perl snippets/serialize-and-serialize-single << end
meta::function('serialize', <<'EOF');
my @keys = sort keys %data;
join "\n", $data{'bootstrap::initialization'},
map(serialize_single($_), grep !/^code::/, @keys),
map(serialize_single($_), grep /^code::/, @keys),
"\n__END__";
EOF
meta::function('serialize_single', <<'EOF');
my ($namespace, $name) = split /::/, $_[0], 2;
my $marker = '__' . int(rand(1 << 31));
"meta::$namespace('$name', <<'$marker');\n$data{$_[0]}\n$marker";
EOF
- end
Sorting the keys is important. We'll be verifying the output of the serialization function, so it needs to be stable.
Now {\tt code::main} is a bit simpler. With these new functions the file logic becomes:
- verbatim << end
open my $file, '>', $0;
print $file serialize();
close $file;
- end
- s1 Verifying serialization | sec:serialization-verifying-serialization
What we've been doing is very unsafe. There isn't a backup file, so if the serialization goes wrong then we'll blindly nuke our original script. This is a big problem, so let's fix it. The
new strategy will be to serialize to a temporary file, have that file generate a checksum, and make sure that the checksum is what we expect. Before we can implement such a mechanism,
though, we'll need a string hash function.
- s2 Implementing the Fowler-Noll Vo hash | sec:serialization-verifying-fnv-hash
At its core, the FNV-1a hash\footnote{\url{http://en.wikipedia.org/wiki/Fowler-Noll-Vo_hash_function}} is just a multiply-xor in a loop. Generally it's written like this:
- cpp snippets/fnv-hash.c << end
int hash (char *s) {
const int fnv_prime = 16777619; // Magic numbers
const int fnv_offset = 2166136261;
int result = fnv_offset;
char c;
while (c = *s++) {
result ^= c;
result *= fnv_prime;
}
return result;
}
- end
In Perl it's advantageous to vectorize this function for performance reasons. It isn't necessarily sound to do this, but empirically the results seem reasonably well-distributed. Here's
the function I ended up with:
- perl snippets/fnv-hash-function << end
meta::function('fnv_hash', <<'EOF');
my ($data) = @_;
my ($fnv_prime, $fnv_offset) = (16777619, 2166136261);
my $hash = $fnv_offset;
my $modulus = 2 ** 32;
$hash = ($hash ^ ($_ & 0xffff) ^ ($_ >> 16)) * $fnv_prime % $modulus
for unpack 'L*', $data . substr($data, -4) x 8;
$hash;
EOF
- end
This produces a 32-bit hash. Ideally we have something of at least 128 bits, just to reduce the likelihood of collision. When I was writing the 128-bit hash I went a bit overboard with
hash chaining (which doesn't matter because it isn't a cryptographic hash), but here's the full hash:
- perl snippets/fast-hash-function << end
meta::function('fast_hash', <<'EOF');
my ($data) = @_;
my $piece_size = length($data) >> 3;
my @pieces = (substr($data, $piece_size * 8) . length($data),
map(substr($data, $piece_size * $_, $piece_size), 0 .. 7));
my @hashes = (fnv_hash($pieces[0]));
push @hashes, fnv_hash($pieces[$_ + 1] . $hashes[$_]) for 0 .. 7;
$hashes[$_] ^= $hashes[$_ + 4] >> 16 | ($hashes[$_ + 4] & 0xffff) << 16 for 0 .. 3;
$hashes[0] ^= $hashes[8];
sprintf '%08x' x 4, @hashes[0 .. 3];
EOF
- end
The convolutedness of this logic is partially to accommodate for very short strings.
- s2 Fixing {\tt EOF} markers again | sec:serialization-verifying-fixing-eof-markers-again
It's probably fine to use random numbers for EOF markers, but I prefer using a hash of the content. While it's probably about the same either way, it intuitively feels less likely that a
string will contain its own hash.\footnote{And as we all know, intuition is key when making decisions in math and computer science...}
- perl snippets/serialize-single-hash << end
meta::function('serialize_single', <<'EOF');
my ($namespace, $name) = split /::/, $_[0], 2;
my $marker = '__' . fast_hash($data{$_[0]});
"meta::$namespace('$name', <<'$marker');\n$data{$_[0]}\n$marker";
EOF
- end
We can also use the script state to get a tempfile in the {\tt edit} function.\footnote{{\tt object} uses {\tt File::Temp} to get temporary filenames. This is a better solution than
anything involving pseudorandom names in {\tt /tmp}.}
- s2 Implementing the {\tt state} function | sec:serialization-verifying-state-function
The ``state'' of an object is just the hash of its serialization. (This is why it's useful to have the serialization logic factored out.)
- perl snippets/state-function-hash << end
meta::function('state', <<'EOF');
fast_hash(serialize());
EOF
- end
- s2 Implementing the {\tt verify} function | sec:serialization-verifying-verify-function
{\tt verify} writes a temporary copy, checks its checksum, and returns {\tt 0} or {\tt 1} depending on whether the checksum came out invalid or valid, respectively. If invalid, it leaves
the temporary file there for debugging purposes.
- perl snippets/verify-function << end
meta::function('verify', <<'EOF');
my $serialized_data = serialize();
my $state = state();
my $temporary_filename = "$0.$state";
open my $file, '>', $temporary_filename;
print $file $serialized_data;
close $file;
chmod 0700, $temporary_filename;
chomp(my $observed_state = join '', qx|perl '$temporary_filename' state|);
my $result = $observed_state eq $state;
unlink $temporary_filename if $result;
$result;
EOF
- end
- s1 Save logic | sec:serialization-save-logic
Now we can use {\tt verify} before overwriting \verb|$0|.
- perl snippets/save-function-and-broken-usage << end
meta::function('save', <<'EOF');
if (verify()) {
open my $file, '>', $0;
print $file serialize();
close $file;
} else {
warn 'Verification failed';
}
EOF
meta::code('main', <<'EOF');
...
save();
EOF
- end
- s1 {\tt code::main} fixes | sec:serialization-code-main-fixes
There's actually a fairly serious problem at this point. Every script saves itself unconditionally, which involves creating a temporary filename and verifying its contents. What happens
when we run one then? Something like this:
- verbatim << end
$ perl some-script cat function::cat
join "\n", @data{@_}; # Gets this much right
# Now calls save(), which calls verify() to create a new temp script:
> perl some-script.hash1 state
hash1 # Gets this much right
# Now calls save(), which calls verify() to create a new temp script:
> perl some-script.hash1.hash2 state
...
- end
That's not what we want at all. There's no reason to call {\tt save} unless a modification has occurred, so we can make this modification to {\tt code::main}:
- perl snippets/code-main-with-fixed-save << end
meta::code('main', <<'EOF');
my $initial_state = state();
my $command = shift @ARGV;
print &$command(@ARGV); # Also printing the result -- important for state
save() if state() ne $initial_state;
EOF
- end
- s1 Final result | sec:serialization-final-result
At this point we have an extensible and reasonably robust script. Here's what we've got so far:
- perl examples/verified << end
my %data;
my %datatypes;
sub meta::define_form {
my ($namespace, $delegate) = @_;
$datatypes{$namespace} = $delegate;
*{"meta::${namespace}::implementation"} = $delegate;
*{"meta::$namespace"} = sub {
my ($name, $value) = @_;
chomp $value;
$data{"${namespace}::$name"} = $value;
$delegate->($name, $value);
};
}
meta::define_form 'bootstrap', sub {};
meta::define_form 'function', sub {
my ($name, $body) = @_;
*{$name} = eval "sub {\n$body\n}";
};
meta::define_form 'code', sub {
my ($name, $value) = @_;
eval $value;
};
meta::bootstrap('initialization', <<'EOF');
my %data;
my %datatypes;
sub meta::define_form {
my ($namespace, $delegate) = @_;
$datatypes{$namespace} = $delegate;
*{"meta::${namespace}::implementation"} = $delegate;
*{"meta::$namespace"} = sub {
my ($name, $value) = @_;
chomp $value;
$data{"${namespace}::$name"} = $value;
$delegate->($name, $value);
};
}
meta::define_form 'bootstrap', sub {};
meta::define_form 'function', sub {
my ($name, $body) = @_;
*{$name} = eval "sub {\n$body\n}";
};
meta::define_form 'code', sub {
my ($name, $value) = @_;
eval $value;
};
EOF
meta::function('serialize', <<'EOF');
my @keys = sort keys %data;
join "\n", $data{'bootstrap::initialization'},
map(serialize_single($_), grep !/^code::/, @keys),
map(serialize_single($_), grep /^code::/, @keys),
"\n__END__";
EOF
meta::function('serialize_single', <<'EOF');
my ($namespace, $name) = split /::/, $_[0], 2;
my $marker = '__' . fast_hash($data{$_[0]});
"meta::$namespace('$name', <<'$marker');\n$data{$_[0]}\n$marker";
EOF
meta::function('fnv_hash', <<'EOF');
my ($data) = @_;
my ($fnv_prime, $fnv_offset) = (16777619, 2166136261);
my $hash = $fnv_offset;
my $modulus = 2 ** 32;
$hash = ($hash ^ ($_ & 0xffff) ^ ($_ >> 16)) * $fnv_prime % $modulus
for unpack 'L*', $data . substr($data, -4) x 8;
$hash;
EOF
meta::function('fast_hash', <<'EOF');
my ($data) = @_;
my $piece_size = length($data) >> 3;
my @pieces = (substr($data, $piece_size * 8) . length($data),
map(substr($data, $piece_size * $_, $piece_size), 0 .. 7));
my @hashes = (fnv_hash($pieces[0]));
push @hashes, fnv_hash($pieces[$_ + 1] . $hashes[$_]) for 0 .. 7;
$hashes[$_] ^= $hashes[$_ + 4] >> 16 | ($hashes[$_ + 4] & 0xffff) << 16 for 0 .. 3;
$hashes[0] ^= $hashes[8];
sprintf '%08x' x 4, @hashes[0 .. 3];
EOF
meta::function('state', <<'EOF');
fast_hash(serialize());
EOF
meta::function('verify', <<'EOF');
my $serialized_data = serialize();
my $state = state();
my $temporary_filename = "$0.$state";
open my $file, '>', $temporary_filename;
print $file $serialized_data;
close $file;
chmod 0700, $temporary_filename;
chomp(my $observed_state = join '', qx|perl '$temporary_filename' state|);
my $result = $observed_state eq $state;
unlink $temporary_filename if $result;
$result;
EOF
meta::function('save', <<'EOF');
if (verify()) {
open my $file, '>', $0;
print $file serialize();
close $file;
} else {
warn 'Verification failed';
}
EOF
meta::function('cat', <<'EOF');
join "\n", @data{@_};
EOF
meta::function('set', <<'EOF');
$data{$_[0]} = join '', ;
EOF
meta::function('edit', <<'EOF');
my $filename = '/tmp/' . rand();
open my $file, '>', $filename;
print $file $data{$_[0]};
close $file;
system($ENV{EDITOR} || $ENV{VISUAL} || '/usr/bin/nano', $filename);
open my $file, '<', $filename;
$data{$_[0]} = join '', <$file>;
close $file;
EOF
meta::code('main', <<'EOF');
my $initial_state = state();
my $command = shift @ARGV;
print &$command(@ARGV);
save() if state() ne $initial_state;
EOF
__END__
- end
__
meta::section('some-improvements', <<'__');
- sc Some improvements | sec:some-improvements
Let's step back for a minute and improve things a bit in preparation for some real awesomeness. There are few places that could use improvement. First, there isn't a way to get a list of
defined attributes on an object without opening it by hand. Second, the interface exposes too many functions to the user; in particular, things like {\tt complete} aren't useful from the
command line. Finally, every data type we define gets put into {\tt bootstrap::initialization}, which causes $O(n)$ redundancy in the size of the data type constructors.
- s1 Useful functions | sec:some-improvements-useful-functions
The most important thing to add is {\tt ls}, which gives you a listing of attributes:\footnote{{\tt object} contains a much more sophisticated version of {\tt ls}. It parses options and
applies filters to the listing, much like the UNIX {\tt ls} command. I'll go over how to implement this stuff in a later chapter.} Related are {\tt cp} and {\tt rm}, which do what you
would expect:
- perl snippets/ls-cp-and-rm-functions << end
meta::function('ls', <<'EOF');
join "\n", sort keys %data;
EOF
meta::function('cp', <<'EOF');
$data{$_[1]} = $data{$_[0]};
EOF
meta::function('rm', <<'EOF');
delete @data{@_};
EOF
- end
Another useful function is {\tt create}, which opens an editor for a new attribute:\footnote{We can already do this with {\tt edit}, but {\tt object} doesn't let you edit attributes that
don't exist. I'll include that behavior in these scripts before too long.}
- perl snippets/create-function << end
meta::function('create', <<'EOF');
return edit($_[0]) if exists $data{$_[0]};
$data{$_[0]} = $_[1] || "# Attribute $_[0]";
edit($_[0]);
EOF
- end
Now we can create stuff from inside the shell or command-line and have a civilized text-editor interface to do it.
- s1 Making some functions internal | sec:some-improvements-making-some-functions-internal
It would be nice to have a distinction between functions meant for public consumption and functions used just inside the script. For example, nobody's going to call \verb|fnv_hash| from
the command-line; they'd have to pass it a string in {\tt @ARGV}, which isn't practical. So it's time for a new toplevel mechanism, the \verb|%externalized_functions| table:
- verbatim << end
# In bootstrap::initialization:
my %data;
my %externalized_functions;
my %datatypes;
- end
\verb|%externalized_functions| maps every callable function to the attribute that defines it, and only the listed functions will be usable directly from the shell or the command-line. This
has an additional benefit of providing much better autocompletion, since the first word in the REPL always names a function.
- verbatim << end
meta::define_form 'data', sub {
my ($name, $value) = @_;
$externalized_functions{$name} = "data::$name";
*{$name} = ...;
};
meta::define_form 'function', sub {
my ($name, $value) = @_;
$externalized_functions{$name} = "function::$name";
*{$name} = ...;
};
- end
\noindent And here's the new data type:
- perl snippets/internal-function-type << end
meta::define_form 'internal_function', sub {
my ($name, $value) = @_;
*{$name} = eval "sub {\n$value\n}";
};
- end
We can now move \verb|fnv_hash|, \verb|fast_hash|, and {\tt complete} into this namespace.
We'll need to update {\tt shell} and {\tt complete} to leverage this new information:
- perl snippets/shell-2 << end
meta::function('shell', <<'EOF');
use Term::ReadLine;
my $term = new Term::ReadLine "$0 shell";
$term->ornaments(0);
my $output = $term->OUT || \*STDOUT;
$term->Attribs->{attempted_completion_function} = \&complete;
while (defined($_ = $term->readline("$0\$ "))) {
my @args = grep length, split /\s+|("[^"\\]*(?:\\.)?")/o;
my $function_name = shift @args;
s/^"(.*)"$/\1/o, s/\\\\"/"/go for @args;
if ($function_name) {
if ($externalized_functions{$function_name}) {
chomp(my $result = eval {&$function_name(@args)});
warn $@ if $@;
print $output $result, "\n" unless $@;
} else {
warn "Command not found: '$function_name' (use 'ls' to see available commands)";
}
}
}
EOF
- end
- perl snippets/complete-2 << end
meta::function('complete', <<'EOF');
my @functions = sort keys %externalized_functions;
my @attributes = sort keys %data;
sub match {
my ($text, @options) = @_;
my @matches = sort grep /^$text/, @options;
if (@matches == 0) {return undef;}
elsif (@matches == 1) {return $matches [0];}
elsif (@matches > 1) {
return ((longest ($matches [0], $matches [@matches - 1])), @matches);
}
}
sub longest {
my ($s1, $s2) = @_;
return substr ($s1, 0, length $1) if ($s1 ^ $s2) =~ /^(\0*)/;
return '';
}
my ($text, $line) = @_;
if ($line =~ / /) {
# Start matching attribute names.
match ($text, @attributes);
} else {
# Start of line, so it's a function.
match ($text, @functions);
}
EOF
- end
- s1 Separate attributes for data types | sec:some-improvements-separate-attributes-for-data-types
It's cumbersome to have all of the data types go in {\tt bootstrap::initialization}. Better is to break the code into separate attributes. To do this we'll need to restructure the scripts
a little bit.
Up until now the ``stuff first, code second'' approach has worked out all right. But now we want to evaluate stuff at the beginning and at the end, and if this keeps up it could get out of
hand. Better is to have {\tt serialize} generate a call into some function that will be defined, and do away with {\tt code::} altogether. We can use a new namespace {\tt meta::} for stuff
that needs to be evaluated at the beginning. So basically, instead of this:
- verbatim << end
bootstrap
types
functions
code
- end
\noindent we'd have this:
- verbatim << end
bootstrap
meta definitions
functions
call to internal::main()
- end
Here's what the new {\tt serialize} looks like:
- perl snippets/serialize-with-internal-main << end
my @keys = sort keys %data;
join "\n", $data{'bootstrap::initialization'},
map(serialize_single($_),
grep( /^meta::/, @keys),
grep(!/^meta::/, @keys)),
"internal::main();",
"__END__";
- end
And here's the definition for {\tt meta::} (it's identical to the one we used to have for {\tt code::}). This is the only \verb|define_form| invocation in {\tt bootstrap::initialization};
the others now reside in their own attributes.
- perl snippets/define-form-meta << end
meta::define_form 'meta', sub {
my ($name, $value) = @_;
eval $value;
};
- end
Here are the new type definitions:
- verbatim << end
meta::meta('type::data', <<'EOF');
meta::define_form 'data', sub {...};
EOF
meta::meta('type::function', <<'EOF');
meta::define_form 'function', sub {...};
EOF
meta::meta('type::bootstrap', <<'EOF');
meta::define_form 'bootstrap', sub {};
EOF
...
- end
- s2 Factoring externalization | sec:some-improvements-data-types-factoring-externalization
While we're cleaning up meta-stuff, it's worth thinking about factoring out externalization. There isn't a particularly good reason to keep manually assigning to
\verb|%externalized_functions|; better is to abstract this detail into a function. To do this, we'll want a meta-library:
- perl snippets/meta-externalize << end
meta::meta('externalize', <<'EOF');
sub meta::externalize {
my ($name, $attribute, $implementation) = @_;
$externalized_functions{$name} = $attribute;
*{$name} = $implementation;
}
EOF
- end
This meta-definition is available to the others because it sorts first.\footnote{Which is a horrible way to manage dependencies, but it's worked so far.} Now instead of manually
externalizing stuff, data types like {\tt function::} and {\tt data::} can just use {\tt meta::externalize}:
- perl snippets/function-type-with-externalize << end
meta::meta('type::function', <<'EOF');
meta::define_form 'function', sub {
my ($name, $value) = @_;
meta::externalize $name, "function::$name", eval "sub {\n$value\n}";
};
EOF
- end
- s1 Abstracting {\tt \%data} | sec:some-improvements-abstracting-data
Another issue worth fixing is that you can assign into \verb|%data| arbitrarily, particularly in ways that end up breaking deserialization. For instance, nothing is stopping you from
creating a key called {\tt foo::bar} even though there isn't a namespace called {\tt foo::}. This problem can be solved at the interface level (i.e.~inside {\tt edit}, {\tt set}, and
such), but it's probably more useful to go a step further and abstract all access to \verb|%data|.
Rather than writing to \verb|%data|, then, we'll use an internal function called {\tt associate}; and to read from it we'll use {\tt retrieve}. These two functions also benefit from a
couple more to separate out namespace components. The {\tt namespace} function gives you the base part, and the {\tt attribute} function gives you the rest.\footnote{All four of these
functions are taken directly from {\tt object}.}
- perl snippets/namespace-attribute-retrieve-associate-functions << end
meta::internal_function('namespace', <<'EOF');
my ($name) = @_;
$name =~ s/::.*$//;
$name;
EOF
meta::internal_function('attribute', <<'EOF');
my ($name) = @_;
$name =~ s/^[^:]*:://;
$name;
EOF
meta::internal_function('retrieve', <<'EOF');
my @results = map defined $data{$_} ? $data{$_} : file::read($_), @_;
wantarray ? @results : $results[0];
EOF
meta::internal_function('associate', <<'EOF');
my ($name, $value, %options) = @_;
my $namespace = namespace($name);
die "Namespace $namespace does not exist" unless $datatypes{$namespace};
$data{$name} = $value;
execute($name) if $options{'execute'};
EOF
- end
- s2 Dynamic execution | sec:some-improvements-abstracting-data-dynamic-execution
One problem with the way we've defined {\tt cp} is that you'll have to close and reopen the shell to get new functions to take effect. This is because while we're assigning into
\verb|%data|, we're not calling the handler associated with the namespace. The simplest way to fix this is to dynamically invoke that handler:
- perl snippets/execute-function << end
meta::internal_function('execute', <<'EOF');
my ($name, %options) = @_;
my $namespace = namespace($name);
eval {&{"meta::$namespace"}(attribute($name), retrieve($name))};
warn $@ if $@ && $options{'carp'};
EOF
- end
{\tt associate} is already hooked up to use this function; all you have to do is pass an extra option:
- verbatim << end
associate('function::foo', '...', execute => 1);
- end
- s1 Final result | sec:some-improvements-final-result
Integrating all of these improvements into the previous chapter's script yields this monumental piece of work:\footnote{This is the last full listing I'll provide here. The remaining
chapters cover the concepts required to get from here to {\tt object}. At this point the stuff going on in {\tt object} should more or less make sense, though you'll want to use {\tt ls-a}
rather than {\tt ls} to get a full listing of attributes.}
- perl examples/some-improvements << end
#!/usr/bin/perl
my %data;
my %externalized_functions;
my %datatypes;
sub meta::define_form {
my ($namespace, $delegate) = @_;
$datatypes{$namespace} = $delegate;
*{"meta::${namespace}::implementation"} = $delegate;
*{"meta::$namespace"} = sub {
my ($name, $value) = @_;
chomp $value;
$data{"${namespace}::$name"} = $value;
$delegate->($name, $value);
};
}
meta::define_form 'meta', sub {
my ($name, $value) = @_;
eval $value;
};
meta::meta('externalize', <<'EOF');
sub meta::externalize {
my ($name, $attribute, $implementation) = @_;
$externalized_functions{$name} = $attribute;
*{$name} = $implementation;
}
EOF
meta::meta('type::bootstrap', <<'EOF');
meta::define_form 'bootstrap', sub {};
EOF
meta::meta('type::function', <<'EOF');
meta::define_form 'function', sub {
my ($name, $body) = @_;
meta::externalize $name, "function::$name", eval "sub {\n$body\n}";
};
EOF
meta::meta('type::internal_function', <<'EOF');
meta::define_form 'internal_function', sub {
my ($name, $value) = @_;
*{$name} = eval "sub {\n$value\n}";
};
EOF
meta::meta('type::data', <<'EOF');
meta::define_form 'data', sub {
# Define a basic editing interface:
my ($name, $value) = @_;
meta::externalize $name, "data::$name", sub {
my ($command, $value) = @_;
return $data{"data::$name"} unless @_;
$data{"data::$name"} = $value if $command eq '=';
};
};
EOF
meta::bootstrap('initialization', <<'EOF');
#!/usr/bin/perl
my %data;
my %externalized_functions;
my %datatypes;
sub meta::define_form {
my ($namespace, $delegate) = @_;
$datatypes{$namespace} = $delegate;
*{"meta::${namespace}::implementation"} = $delegate;
*{"meta::$namespace"} = sub {
my ($name, $value) = @_;
chomp $value;
$data{"${namespace}::$name"} = $value;
$delegate->($name, $value);
};
}
meta::define_form 'meta', sub {
my ($name, $value) = @_;
eval $value;
};
EOF
meta::data('default-action', <<'EOF');
shell
EOF
meta::internal_function('namespace', <<'EOF');
my ($name) = @_;
$name =~ s/::.*$//;
$name;
EOF
meta::internal_function('attribute', <<'EOF');
my ($name) = @_;
$name =~ s/^[^:]*:://;
$name;
EOF
meta::internal_function('retrieve', <<'EOF');
my @results = map defined $data{$_} ? $data{$_} : file::read($_), @_;
wantarray ? @results : $results[0];
EOF
meta::internal_function('associate', <<'EOF');
my ($name, $value, %options) = @_;
my $namespace = namespace($name);
die "Namespace $namespace does not exist" unless $datatypes{$namespace};
$data{$name} = $value;
execute($name) if $options{'execute'};
EOF
meta::internal_function('execute', <<'EOF');
my ($name, %options) = @_;
my $namespace = namespace($name);
eval {&{"meta::$namespace"}(attribute($name), retrieve($name))};
warn $@ if $@ && $options{'carp'};
EOF
meta::function('serialize', <<'EOF');
my @keys = sort keys %data;
join "\n", $data{'bootstrap::initialization'},
map(serialize_single($_),
grep( /^meta::/, @keys),
grep(!/^meta::/, @keys)),
"internal::main();",
"__END__";
EOF
meta::function('serialize_single', <<'EOF');
my ($namespace, $name) = split /::/, $_[0], 2;
my $marker = '__' . fast_hash($data{$_[0]});
"meta::$namespace('$name', <<'$marker');\n$data{$_[0]}\n$marker";
EOF
meta::function('fnv_hash', <<'EOF');
my ($data) = @_;
my ($fnv_prime, $fnv_offset) = (16777619, 2166136261);
my $hash = $fnv_offset;
my $modulus = 2 ** 32;
$hash = ($hash ^ ($_ & 0xffff) ^ ($_ >> 16)) * $fnv_prime % $modulus
for unpack 'L*', $data . substr($data, -4) x 8;
$hash;
EOF
meta::function('fast_hash', <<'EOF');
my ($data) = @_;
my $piece_size = length($data) >> 3;
my @pieces = (substr($data, $piece_size * 8) . length($data),
map(substr($data, $piece_size * $_, $piece_size), 0 .. 7));
my @hashes = (fnv_hash($pieces[0]));
push @hashes, fnv_hash($pieces[$_ + 1] . $hashes[$_]) for 0 .. 7;
$hashes[$_] ^= $hashes[$_ + 4] >> 16 | ($hashes[$_ + 4] & 0xffff) << 16 for 0 .. 3;
$hashes[0] ^= $hashes[8];
sprintf '%08x' x 4, @hashes[0 .. 3];
EOF
meta::function('state', <<'EOF');
fast_hash(serialize());
EOF
meta::function('verify', <<'EOF');
my $serialized_data = serialize();
my $state = state();
my $temporary_filename = "$0.$state";
open my $file, '>', $temporary_filename;
print $file $serialized_data;
close $file;
chmod 0700, $temporary_filename;
chomp(my $observed_state = join '', qx|perl '$temporary_filename' state|);
my $result = $observed_state eq $state;
unlink $temporary_filename if $result;
$result;
EOF
meta::function('save', <<'EOF');
if (verify()) {
open my $file, '>', $0;
print $file serialize();
close $file;
chmod 0744, $0;
} else {
warn 'Verification failed';
}
EOF
meta::function('ls', <<'EOF');
join "\n", sort keys %data;
EOF
meta::function('cp', <<'EOF');
associate($_[1], retrieve($_[0]));
EOF
meta::function('rm', <<'EOF');
delete @data{@_};
EOF
meta::function('cat', <<'EOF');
join "\n", @data{@_};
EOF
meta::function('create', <<'EOF');
return edit($_[0]) if exists $data{$_[0]};
associate($_[0], $_[1] || "# Attribute $_[0]");
edit($_[0]);
EOF
meta::function('set', <<'EOF');
$data{$_[0]} = join '', ;
EOF
meta::function('complete', <<'EOF');
my @functions = sort keys %externalized_functions;
my @attributes = sort keys %data;
sub match {
my ($text, @options) = @_;
my @matches = sort grep /^$text/, @options;
if (@matches == 0) {return undef;}
elsif (@matches == 1) {return $matches [0];}
elsif (@matches > 1) {
return ((longest ($matches [0], $matches [@matches - 1])), @matches);
}
}
sub longest {
my ($s1, $s2) = @_;
return substr ($s1, 0, length $1) if ($s1 ^ $s2) =~ /^(\0*)/;
return '';
}
my ($text, $line) = @_;
if ($line =~ / /) {
# Start matching attribute names.
match ($text, @attributes);
} else {
# Start of line, so it's a function.
match ($text, @functions);
}
EOF
meta::internal_function('shell', <<'EOF');
use Term::ReadLine;
my $term = new Term::ReadLine "$0 shell";
$term->ornaments(0);
my $output = $term->OUT || \*STDOUT;
$term->Attribs->{attempted_completion_function} = \&complete;
while (defined($_ = $term->readline("$0\$ "))) {
my @args = grep length, split /\s+|("[^"\\]*(?:\\.)?")/o;
my $function_name = shift @args;
s/^"(.*)"$/\1/o, s/\\\\"/"/go for @args;
if ($function_name) {
if ($externalized_functions{$function_name}) {
chomp(my $result = eval {&$function_name(@args)});
warn $@ if $@;
print $output $result, "\n" unless $@;
} else {
warn "Command not found: '$function_name' (use 'ls' to see available commands)";
}
}
}
EOF
meta::function('edit', <<'EOF');
my $filename = '/tmp/' . rand();
open my $file, '>', $filename;
print $file retrieve($_[0]);
close $file;
system($ENV{EDITOR} || $ENV{VISUAL} || '/usr/bin/nano', $filename);
open my $file, '<', $filename;
associate($_[0]}, join '', <$file>);
close $file;
EOF
meta::internal_function('internal::main', <<'EOF');
my $initial_state = state();
my $command = shift @ARGV || retrieve('data::default-action');
print &$command(@ARGV);
save() if state() ne $initial_state;
EOF
internal::main();
__END__
- end
__
meta::section('virtual-attributes', <<'__');
- sc Virtual attributes
In the last chapter, {\tt retrieve} was defined to grab either an attribute or a file. But there are a lot more things you might want to do with it, including creating views of existing
data. This is exactly what virtual attributes are about.
- s1 Core implementation
Here's an updated {\tt retrieve}:
- resource snippets/updated-retrieve << end
meta::internal_function('retrieve', <<'EOF');
my @results = map defined $data{$_} ? $data{$_} : retrieve_with_hooks($_), @_;
wantarray ? @results : $results[0];
EOF
- end
This is exactly the same as the one before except that the hard-coded {\tt file::read} call has been replaced by a call to \verb|retrieve_with_hooks|. This provides the indirection
necessary to allow the user to introduce arbitrary retrievers. Before the user can do this, though, we'll need to make a new namespace:
- resource snippets/meta-retriever << end
meta::meta('type::retriever', <<'EOF');
meta::configure 'retriever', extension => '.pl', inherit => 1;
meta::define_form 'retriever', sub {
my ($name, $value) = @_;
$transient{retrievers}{$name} = meta::eval_in("sub {\n$value\n}", "retriever::$name");
};
EOF
- end
Nothing particularly interesting is going on here. We're just maintaining a ${\rm name} \rightarrow {\rm implementation}$ mapping in \verb|%transient|. This mapping is then used by
\verb|retrieve_with_hooks|, which asks {\tt retriever::} functions for their values until one of them returns something besides {\tt undef}:
- resource snippets/retrieve-with-hooks << end
meta::internal_function('retrieve_with_hooks', <<'EOF');
# Uses the hooks defined in $transient{retrievers}, and returns undef if none work.
my ($attribute) = @_;
my $result = undef;
defined($result = &$_($attribute)) and return $result for
map $transient{retrievers}{$_}, sort keys %{$transient{retrievers}};
return undef;
EOF
- end
- s1 Retrievers in {\tt object}
{\tt object} comes with four retrievers:
- enumerate << end
- item[] {\tt retriever::file}
Retrieves contents from a filename. This lets you say things like {\tt cat foo}, where {\tt foo} is a file. By extension, you can also copy files into named attributes using {\tt
function::cp}.
- resource snippets/retriever-file << end
meta::retriever('file', <<'EOF');
-f $_[0] ? file::read($_[0]) : undef;
EOF
- end
- item[] {\tt retriever::id}
Retrieves the string you are retrieving. This is primarily useful when you want a quick test value for something. For instance {\tt cat id::foobar} will print {\tt foobar}. Note that
{\tt id::} is not defined as a namespace, it's just a name pattern detected by {\tt retriever::id}.
- resource snippets/retriever-id << end
meta::retriever('id', <<'EOF');
$_[0] =~ /^id::/ ? substr($_[0], 4) : undef;
EOF
- end
- item[] {\tt retriever::object}
This is a fun one. It lets you ask other Perl objects for their attributes. For example, {\tt cat object::./foo::function::ls} fetches {\tt function::ls} from {\tt ./foo}. If {\tt
./foo} is unreachable or doesn't contain {\tt function::ls}, then {\tt undef} is returned (which allows other retrievers to try to resolve your virtual attribute).
- resource snippets/retriever-object << end
meta::retriever('object', <<'EOF');
# Fetch a property from another Perl object. This uses the 'cat' function.
return undef unless $_[0] =~ /^object::(.*?)::(.*)$/ && -x $1 && qx|$1 is '$2'|;
join '', qx|$1 cat '$2'|;
EOF
- end
- item[] {\tt retriever::perl}
This returns the result of evaluating a given Perl expression. For example, {\tt cat perl::3+4} prints {\tt 7}.
- resource snippets/retriever-perl << end
meta::retriever('perl', <<'EOF');
# Lets you use the result of evaluating some Perl expression
return undef unless $_[0] =~ /^perl::(.*)$/;
eval $1;
EOF
- end
- end
As you can see, there's not much involved in writing a retriever. The only particularly counterintuitive thing about it is returning {\tt undef} instead of {\tt 0} or the empty string.
- Fixing up {\tt edit}
Right now, {\tt function::edit} will happily let you edit a virtual attribute and then attempt to {\tt associate} it, causing a failure. This is obviously lame; here's a better {\tt edit}
function that takes care of this:
- resource snippets/updated-edit << end
meta::function('edit', <<'EOF');
my ($name, %options) = @_;
my $extension = extension_for($name);
die "$name is virtual or does not exist" unless exists $data{$name};
associate($name, invoke_editor_on($data{$name} // '', %options,
attribute => $name, extension => $extension), execute => 1)});
save() unless $data{'data::edit::no-save'};
'';
EOF
- end
- More interesting retrievers
I'm writing this document in a self-modifying Perl file that includes a simple preprocessor.\footnote{Unsurprisingly, it's called {\tt preprocessor} in the {\tt perl-objects} repository.}
In order to make this work, there are a bunch of attributes in the {\tt section::} namespace, and each one includes some preprocessor directives to either include other attributes or to
generate \TeX{} constructs (in some cases both).
The easiest way to render the document as \TeX{} is to just have a virtual attribute I can retrieve that will contain the fully preprocessed source. Fortunately, {\tt preprocessor}
provides {\tt retriever::pp} that does exactly this. Here's what that looks like:
- resource snippets/retriever-pp << end
meta::retriever('pp', <<'EOF');
return undef unless namespace($_[0]) eq 'pp';
my $attr = retrieve(attribute($_[0]));
defined $attr ? preprocess($attr) : undef;
EOF
- end
Notice that this retriever itself calls {\tt retrieve}! This is totally allowed as long as you don't create an infinite loop. Here, we're trimming the {\tt pp::} from the beginning of the
attribute and are just retrieving the rest of it. This is good practice, as it lets you combine virtual retrievers. For example, you could preprocess an external file by saying {\tt cat
pp::my-file}.
Another interesting retriever is the {\tt http} retriever defined in {\tt repository}. This lets you retrieve arbitrary web content through Perl's LWP library:
- resource snippets/retriever-http << end
meta::retriever('http', <<'EOF');
use LWP::Simple ();
return undef unless $_[0] =~ /^(?:http:)?\/\/(\w+.*)$/;
LWP::Simple::get("http://$1");
EOF
- end
If you have this retriever, you can say things like {\tt cat http://google.com} (or shorter, {\tt cat //google.com}) and get back the content sent by the web server. This is especially
useful when combined with the preprocessor, as it allows you to compile web-based resources into the final output.
__
meta::template('comment', '\'\'; # A mechanism for line or block comments.');
meta::template('eval', <<'__');
my $result = eval $_[0];
terminal::warning("Error during template evaluation: $@") if $@;
$result;
__
meta::template('failing_conditional', <<'__');
my ($commands) = @_;
my $should_return = $commands =~ / if (.*)$/ && ! eval $1;
terminal::warning("eval of template condition failed: $@") if $@;
$should_return;
__
meta::template('include', <<'__');
my ($commands) = @_;
return '' if template::failing_conditional($commands);
join "\n", map retrieve($_), split /\s+/, $commands;
__
meta::template('item[]', '"\\\\item[$_[0]]";');
meta::template('pinclude', <<'__');
# Just like the regular include, but makes sure to insert paragraph boundaries
# (this is required for SDoc to function properly).
my ($commands) = @_;
return '' if template::failing_conditional($commands);
my $text = join "\n\n", map retrieve($_), split /\s+/, $commands;
"\n\n$text\n\n";
__
meta::template('script-include', <<'__');
my ($name) = @_;
my $s = 'script';
my $script = retrieve($name);
"<$s>\n$script\n$s>";
__
meta::template('style-include', <<'__');
my ($name) = @_;
my $s = 'style';
my $style = retrieve($name);
"<$s>\n$style\n$s>";
__
meta::vim_highlighter('cltex', <<'__');
" Cleaner TeX
" Maintainer: Spencer Tipping
" Language: Cleaner TeX (a variant of LaTeX with support for a bunch of embedded languages)
if version < 600
syntax clear
elseif exists("b:current_syntax")
finish
endif
syn match cltEofMarker /<<\s*\w\+/ contained
syn region cltLineComment matchgroup=cltCode start=/^\s*- comment / end=/$/ contained
syn match cltLine /^\s*- .*$/ contains=cltEofMarker,cltLineComment
syn case match | syn include @cpp syntax/cpp.vim | unlet b:current_syntax
syn case match | syn include @java syntax/java.vim | unlet b:current_syntax
syn case match | syn include @asm syntax/asm.vim | unlet b:current_syntax
syn case match | syn include @javascript syntax/javascript.vim | unlet b:current_syntax
syn case match | syn include @html syntax/html.vim | unlet b:current_syntax
syn case match | syn include @perl syntax/perl.vim | unlet b:current_syntax
syn case match | syn include @ruby syntax/ruby.vim | unlet b:current_syntax
syn case match
syn region cltCpp matchgroup=cltCode start=/^\z(\s*\)- cpp .*<<\s*\z(\w\+\)$/ end=/^\z1- \z2$/ contains=@cpp
syn region cltJava matchgroup=cltCode start=/^\z(\s*\)- java .*<<\s*\z(\w\+\)$/ end=/^\z1- \z2$/ contains=@java
syn region cltAsm matchgroup=cltCode start=/^\z(\s*\)- asm .*<<\s*\z(\w\+\)$/ end=/^\z1- \z2$/ contains=@asm
syn region cltJavascript matchgroup=cltCode start=/^\z(\s*\)- javascript .*<<\s*\z(\w\+\)$/ end=/^\z1- \z2$/ contains=@javascript
syn region cltHtml matchgroup=cltCode start=/^\z(\s*\)- html .*<<\s*\z(\w\+\)$/ end=/^\z1- \z2$/ contains=@html
syn region cltPerl matchgroup=cltCode start=/^\z(\s*\)- perl .*<<\s*\z(\w\+\)$/ end=/^\z1- \z2$/ contains=@perl
syn region cltRuby matchgroup=cltCode start=/^\z(\s*\)- ruby .*<<\s*\z(\w\+\)$/ end=/^\z1- \z2$/ contains=@ruby
syn region cltResource matchgroup=cltCode start=/^\z(\s*\)- resource .*<<\s*\z(\w\+\)$/ end=/^\z1- \z2$/
syn region cltComment matchgroup=cltCode start=/^\z(\s*\)- comment .*<<\s*\z(\w\+\)$/ end=/^\z1- \z2$/
syn cluster cltStuff add=cltCpp,cltJava,cltAsm,cltJavascript,cltHtml,cltResource,cltComment,cltLine,cltPerl,cltRuby
syn region cltDocument start=/\%^/ end=/\%$/ contains=@cltStuff
hi link cltLine Special
hi link cltKeyword String
hi link cltResource String
hi link cltEofMarker String
hi link cltCode Special
hi link cltDocument Comment
hi link cltComment Type
hi link cltLineComment Type
let b:current_syntax = "cltex"
__
internal::main();
__END__