Module: NOMS::BashOn

Included in:
Array, FalseClass, Hash, NilClass, Object, String, TrueClass
Defined in:
lib/noms/bashon.rb

Constant Summary collapse

@@manpage =
<<EOF
=head1 NAME

bashon - Serialization library for bash object notation

=head1 SYNOPSIS

   require 'bashon'

   puts var.to_bashon('var')

=head1 DESCRIPTION

This module enables you to serialize some ruby objects as "bashon" objects
("bashon meaning bash object notation"). Mostly, this means that bash code is
emitted that creates functions returning (echoing) the proper value.

The name that is passed to the B<to_bashon> method is the name of the function
that will output the serialized value. It also forms the root of a hierarchy of
function names that are used for serializing complex (array and hash) types.

The output is expected to be evaluated using the L<eval> command in bash.

=head2 Types

=head3 Nils

An unset command is emitted.

=head3 Booleans

A function is created with the specified name that can be used as a truth
test. That is, the function exits with a true value when serializing the
C<true> literal and with a false value when serializing the C<false>
literal. This enables you to use the resulting functions directly in
conditionals in bash.

=head3 Other simple types

A function is created with the specified name that outputs the value of the
type using B<to_s>.

=head3 Complex types

The important thing to note about complex types (hashes and arrays) is that
the values in the collection are always the I<names of functions> that, when
invoked, will yield a bashon-serialized value. This is especially important
to remember when the value of a hash entry or array element is itself a
complex type.

=head4 Arrays

A function is created with the specified name that outputs a space-separated,
ordered list of function names. Each function name is also created; when the
function is invoked, it will yield the value of that element.

head4 Hashes

A function is created with the specified name that outputs a space-separated
list of hash keys. When the same function is invoked with a hash key as an
argument, it outputs the name of a function. That function, when invoked,
will yield the value of that element.

=head1 EXAMPLES

In the following example, each example is shown with three blocks: the ruby
code snippet emitting the serialization, its standard output, and a shell
session in which the resulting code has been eval'd

This shows how a boolean value is serialized:

   puts true.to_bashon('var')

   function var { return 0; }

   $ if var; then echo got it; fi
   got it

This shows how a nil value is serialized:

   puts nil.to_bashon('var')

   unset var

   $ if [ -z "$(var)" ]; then echo No var; fi
   No var

This shows how a string is serialized and used:

   puts "catalog data".to_bashon('var')

   function var { echo catalog data; }

   $ string=$(var)
   $ echo $string
   catalago data

This shows how an array of strings is serialized:

   puts ["file1", "file2", "file3"].to_bashon('var')

   function var { echo var_0 var_1 var_2; } ; function var_0 { echo file1; };function var_1 { echo file2; };function var_2 { echo file3; }

   $ for filefun in $(var); do file=$($filefun); touch $file; done; ls file*
   file1   file2   file3

And a hash of strings:

   var = { "log" => "logfile.txt",
           "err" => "errfile.txt",
           "input" => "data.txt" }
   puts var.to_bashon('var')

   function var { case "$1" in log) echo var_log;; err) echo var_err;; input) echo var_input;; '') echo log err input;; esac; }; function var_log { echo logfile.txt; };function var_err { echo errfile.txt; };function var_input { echo data.txt; }

   $ for key in $(var); do valfun=$(var $key); val=$($valfun); echo $key=$val; done
   log=logfile.txt
   err=errfile.txt
   input=data.txt
   $ echo "Error message" >>$($(var err))

This shows how to deal with a heterogeneous array:

   var = ['one', { 'type' => 'number', 'value' => 2 }, false]
   puts var.to_bashon('var')

function var { echo var_0 var_1 var_2; } ; function var_0 { echo one; };function var_1 { case "$1" in value) echo var_1_value;; type) echo var_1_type;; '') echo value type;; esac; }; function var_1_value { echo 2; };function var_1_type { echo number; };function var_2 { return 1; }

   $ for elfun $(var)
   > do
   >    $elfun

A deeply nested hash:

   cfg = { "prod" => { "db" => { "host" => "dbhost001",
                                 "port" => 3389 },
                       "url" => "https://api/",
                       "log" => { "file" => "/var/log/app.log",
                                  "debug" => false },
                       "notify" => [ "www", "ops" ] },
           "qa" => { "db" => { "host" => "qa02",
                               "port" => 18009 },
                     "url" => "https://qa02:18008/v2/",
                     "log" => { "file" => "~qa/build9/log/app.log",
                                "debug" => true },
                     "notify" => [ "build", "test" ] }
          }
   puts cfg.to_bashon('cfg')

function cfg { case "$1" in prod) echo cfg_prod;; qa) echo cfg_qa;; '') echo prod qa;; esac; }; function cfg_prod { case "$1" in notify) echo cfg_prod_notify;; log) echo cfg_prod_log;; url) echo cfg_prod_url;; db) echo cfg_prod_db;; '') echo notify log url db;; esac; }; function cfg_prod_notify { echo cfg_prod_notify_0 cfg_prod_notify_1; } ; function cfg_prod_notify_0 { echo www; };function cfg_prod_notify_1 { echo ops; };function cfg_prod_log { case "$1" in debug) echo cfg_prod_log_debug;; file) echo cfg_prod_log_file;; '') echo debug file;; esac; }; function cfg_prod_log_debug { return 1; };function cfg_prod_log_file { echo /var/log/app.log; };function cfg_prod_url { echo https://api/; };function cfg_prod_db { case "$1" in port) echo cfg_prod_db_port;; host) echo cfg_prod_db_host;; '') echo port host;; esac; }; function cfg_prod_db_port { echo 3389; };function cfg_prod_db_host { echo dbhost001; };function cfg_qa { case "$1" in notify) echo cfg_qa_notify;; log) echo cfg_qa_log;; url) echo cfg_qa_url;; db) echo cfg_qa_db;; '') echo notify log url db;; esac; }; function cfg_qa_notify { echo cfg_qa_notify_0 cfg_qa_notify_1; } ; function cfg_qa_notify_0 { echo build; };function cfg_qa_notify_1 { echo test; };function cfg_qa_log { case "$1" in debug) echo cfg_qa_log_debug;; file) echo cfg_qa_log_file;; '') echo debug file;; esac; }; function cfg_qa_log_debug { return 0; };function cfg_qa_log_file { echo ~qa/build9/log/app.log; };function cfg_qa_url { echo https://qa02:18008/v2/; };function cfg_qa_db { case "$1" in port) echo cfg_qa_db_port;; host) echo cfg_qa_db_host;; '') echo port host;; esac; }; function cfg_qa_db_port { echo 18009; };function cfg_qa_db_host { echo qa02; }

   $ env=prod
   $ curl $($(cfg $env) url)/report.sql | \
   > mysql -h $($($(cfg $env) db) host) -p $($($(cfg $env) db) port) | \
   > tee -a $($($(cfg $env) log) file) | \
   > mailx -s "Report" `for m in $($($(cfg $env) notify)); do echo $($m); done`
   $ $($($(cfg $env) log) debug) && echo `date` `whoami`>>$($($(cfg $env) log) file)

=head1 BUGS

Right now string serialization makes no attempt to quote strings. If the
string contains a shell metacharacter, results can be unexpected. In fact,
results can be a bit unexpected anyway. I've tried different quoting schemes
and they all get real ugly real fast. In fact, things can be unexpected with
just a run of spaces or a newline.

You can't have a space in hash keys, but there's no helpful error message
until you eval the result. The problem is that it's really hard to return a
list of things that is useful for a 'for' loop where the list of things might
contain a space.

There's no good way of determining the "type" of something you encounter. You
can test the output of a function, and if each of the words in it is itself a
function, it's an array. However, if they're not, there's no way to
distinguish between a string that has multiple words and a list of hash keys.

Possibly arrays should work exactly like hashes, returning a list of indexes
instead of keys. Right now you can't access an array by index at all.

=head1 NOTES

Why not use variables instead of functions? I went down that road, but the
problem is that even bash 4, with its associative arrays, doesn't provide
enough flexibility to do nested structures, and you run into all kinds of
quoting difficulties and whatnot when you try to serialize to variables.

For example, you can serialize an array of strings in bash like this:

   declare -a var
   var=([0]=one [1]=two [2]=three)

But what if the values in the array are themselves arrays? Or associative
arrays? Bash can't handle that. Also, all your quoting has to be right in
order for that assignment to work, which is really difficult to get right
in a general solution with the possibility of arbitrarily nested structures.

Also, I'll note that arrays and associative arrays aren't really that
convenient in bash. I'm not sure that $(var key1) is really any harder to use
or read than ${var[key1]}.

So, I went with functions, and for consistency's sake serialized everything as
functions. That way you always know that when you get something back from
fetching a hash entry, you have to call it, there's no ambiguity about whether
it's a raw value or a hash.

There's another approach to this, which is to serialize everything into some
kind of text structure and then provide bash functions/commands to operate on
this blob of text. For example:

   puts var.to_json

   var='{"key1":"val1","key2":["val2one","val2two"],"key3":{"subkey1":1,"subkey2":2}}'

   $ json_get "$var" key1
   val1
   $ json_get "$var" key2
   ["val2one","val2two"]
   $ json_get "$var" key2 0
   val2one
   $ json_get "$var" key3
   {"subkey1":1,"subkey2":2}
   $ json_keys "$var"
   key1
   key2
   key3
   $ json_keys "$var" key3
   subkey1
   subkey2
   $ json_keys "$var" key3 subkey2
   2

This is viable but I think it's less convenient to use from bash.

=head1 AUTHOR

Jeremy Brinkley, E<lt>[email protected]<gt>

=cut
EOF

Instance Method Summary collapse

Instance Method Details

#name_key(name) ⇒ Object



23
24
25
# File 'lib/noms/bashon.rb', line 23

def name_key(name)
   name.join('_').gsub(/[^a-zA-Z0-9\_]/, '_')
end