#!/usr/bin/mawk -We
# *********************************************************************
# nblparser: experimental NoSQL Brokering Language (NBL) interpreter.
#
# Copyright (c) 2003,2006 Carlo Strozzi
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; version 2 dated June, 1991.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
#
# *********************************************************************
# $Id: nblparser,v 1.7 2006/03/10 11:26:13 carlo Exp $

BEGIN {
  NULL = ""; FS = OFS = "\t"; tmptable_cmd = "tmptable"; pfx = "nosql_"

  # Get local settings.
  nosql_install = ENVIRON["NOSQL_INSTALL"]
  stdout = ENVIRON["NOSQL_STDOUT"]
  stderr = ENVIRON["NOSQL_STDERR"]

  # Set default values if necessary.
  if (nosql_install == NULL) nosql_install = "/usr/local/nosql"
  if (stdout == NULL) stdout = "/dev/stdout"
  if (stderr == NULL) stderr = "/dev/stderr"

  # Process command-line options and arguments.

  while (ARGV[++i] != NULL) {
    # Turn long options into their short form.
    if (ARGV[i] == "-i" || ARGV[i] == "--input") i_file = ARGV[++i]
    else if (ARGV[i] == "-o" || ARGV[i] == "--output") o_file = ARGV[++i]
    else if (ARGV[i] == "-d" || ARGV[i] == "--delete") {
       tmptable_cmd = "tmptable --delete " ARGV[++i]
    }
    else if (ARGV[i] == "-U" || ARGV[i] == "--unsafe") unsafe = 1
    else if (ARGV[i] == "-D" || ARGV[i] == "--directory") dir = ARGV[++i]
    else if (ARGV[i] == "-P" || ARGV[i] == "--prefix") pfx = ARGV[++i]
    else if (ARGV[i] == "-n" || ARGV[i] == "--no-hide") nohide = 1
    else if (ARGV[i] == "-h") {
       system("grep -v '^#' " nosql_install "/help/nblparser.txt")
       exit(rc=1)
    }
    else if (ARGV[i] == "--show-copying") {
       system("cat " nosql_install "/doc/COPYING")
       exit(rc=1)
    }
    else if (ARGV[i] == "--show-warranty") {
       system("cat " nosql_install "/doc/WARRANTY")
       exit(rc=1)
    }
    else if (s_files == "") s_files = ARGV[i]
    else s_names = ARGV[i]
  }

  ARGC = 1					# Fix argv[]

  if (o_file == NULL) o_file = stdout
  if (i_file != NULL) { ARGV[1] = i_file; ARGC = 2 }

  if (s_files == "") {
     print "usage: nblparser [options] file-schema [name-schema]" > stderr
     exit(rc=1)
  } else if (s_files !~ /^\// && dir != "") s_files = dir "/" s_files

  # Load file-schema into memory
  i=0
  if ((getline < s_files) <= 0) {
     print "nblparser: could not read schema file '"s_files"'" > stderr
     exit(rc=1)
  }

  # Remove SOH markers.
  gsub(/\001/, "")

  # Load the column position array.
  i=0; while (++i <= NF) p[$i] = i

  # check for required fields.
  if (!p[pfx "object"] || !p[pfx "path"]) {
     print "nblparser: missing field(s) in schema file '"s_files"'" > stderr
     exit(rc=1)
  }

  # load s_files body.
  i=0; while (getline schema[++i] < s_files > 0);
  schema[0] = i				# save array length

  close(s_files);

  # Load name-schema into memory.
  if (s_names != "") {
    if (s_names !~ /^\// && dir != "") s_names = dir "/" s_names
    i=0
    if ((getline < s_names) <= 0) {
       print "nblparser: could not read schema file '"s_names"'" > stderr
       exit(rc=1)
    }

    # Remove SOH markers.
    gsub(/\001/, "")

    # Load the column position array.
    i=0; while (++i <= NF) fp[$i] = i

    # check for required fields.
    if (!fp[pfx "name"] || !fp[pfx "flags"]) {
       print "nblparser: missing field(s) in schema file '"s_names"'" > stderr
       exit(rc=1)
    }

    # load s_names body.
    i=0
    while ((getline fschema[++i] < s_names) > 0) {
      split(fschema[i],a,"\t")
      value = tolower(a[fp[pfx "flags"]])
      if (value ~ /h/) hide = hide " " a[fp[pfx "name"]]

      # work-out key column names.

      if (value ~ /[Kk]/) {
	 if (keylist[a[fp[pfx "object"]]] != "")
	     keylist[a[fp[pfx "object"]]] = keylist[a[fp[pfx "object"]]] ","
	 keylist[a[fp[pfx "object"]]] = \
			keylist[a[fp[pfx "object"]]] a[fp[pfx "name"]]
      }
    }
    close(s_names);
    fschema[0] = i			# save array length
  }

  FS = " "				# set default FS

  print "#!/bin/sh\nset -e" > o_file
  if (dir != "") print "cd " dir > o_file
}

# Read input NBL statements.

# skip comments and empty lines.
/^[ \t]*(#|$)/ { next }

{ sub(/^[ \t]+/,"") }			# Trim leading blanks and tabs.

# add new NBL verbs as needed.

$1 == "use"		{ handle_use();			next }
$1 == "setvar"		{ handle_setvar();		next }
$1 == "read"		{ handle_read();		next }
$1 == "expr"		{ handle_expr("expression");	next }
$1 == "expression"	{ handle_expr($1);		next }
$1 == "select"		{ handle_expr($1);		next } # deprecated
$1 == "compute"		{ handle_expr($1);		next } # deprecated
$1 == "columns"		{ handle_column();		next }
$1 == "@columns"	{ handle_column("revert");	next }
$1 == "join"		{ handle_join();		next }
$1 == "@join"		{ handle_join("outer");		next }
$1 == "order-by"	{ handle_orderby();		next }
$1 == "@order-by"	{ handle_orderby("revert");	next }
$1 == "unique-by"	{ handle_uniqueby();		next }
$1 == "@unique-by"	{ handle_uniqueby("revert");	next }
$1 == "remember-as"	{ handle_remember();		next }
$1 == "totals"		{ handle_totals();		next }
$1 == "@totals"		{ handle_totals("currency");	next }
$1 == "format"		{ handle_format();		next }
$1 == "@format"		{ handle_format("list");	next }
$1 == "system"		{ handle_system();		next }
$1 == "labels"		{ handle_labels();		next }
$1 == "end"		{ exit(0) }

# ending stuff.
END {
  if (rc) exit(rc)

  # debug
  #if (schema_row[1] != "") {
  #   printf("#") > o_file
  #   i=0
  #   while (schema_row[++i] != "") printf(" %s", schema_row[i]) > o_file
  #   printf("\n") > o_file
  #}

  # Skip hiding command in a number of cases, included when the
  # latest shell statement ends with ')', as a result of the
  # 'remember-as' statement.

  if (!nohide && hide != "" && sh_cmd[sh_cmd[0]] !~ /\)$/)
     sh_cmd[sh_cmd[0]] = sh_cmd[sh_cmd[0]] " |\nnotcolumn " hide

  if (labels != "")
     sh_cmd[sh_cmd[0]] = sh_cmd[sh_cmd[0]] " |\nsetnames " labels

  i=0
  while (sh_cmd[++i] != "") print sh_cmd[i] > o_file
}

function check_cmd(handler) {

  if (schema_row[p[pfx "object"]] == "") {
     print "nblparser: NBL '" handler "': no active table, use 'read' first" > stderr
     exit(rc=1)
  }
}

# NBL handlers

function handle_setvar(			i, target) {

  if (NF != 2 && NF != 3) {
     print "nblparser: NBL usage: set name [value]" > stderr
     exit(rc=1)
  }

  if ($2 !~ /^[A-Za-z_][A-Za-z0-9_]*$/) {
     print "nblparser: NBL 'set': bad name in assignment" > stderr
     exit(rc=1)
  }

  # The value part must be acceptable as a directory or file name,
  # with no other path components.
  if ($3 != "" && ($3 ~ /^\.\.$/ || $3 !~ /^[-_.,=:+A-Za-z0-9]*$/)) {
     print "nblparser: NBL 'set': bad value in assignment" > stderr
     exit(rc=1)
  }

  # Replace value in schema "path" field. The following statement
  # causes 'mawk -f' to print warning messages to stderr, but it
  # is correct the way it is.

  target = "\$\[" $2 "\]"
  while (++i <= schema[0]) gsub(target,$3,schema[i])
}


function handle_read(		i,a,k,file,partial,cmd) {

  if (schema_row[p[pfx "object"]] != "") {
     print "nblparser: NBL 'read': cannot open multiple tables" > stderr
     exit(rc=1)
  }

  if (NF < 2 || NF > 3) {
     print "nblparser: NBL usage: read table [key]" > stderr
     exit(rc=1)
  }

  delete schema_row

  # a partial key string must begin with a "@" sign.
  partial = sub(/^@/,"",$3)

  if ($2 !~ /^\$?[A-Za-z_][A-Za-z0-9_.]*$/) {
     print "nblparser: NBL 'read': bad table or variable name" > stderr
     exit(rc=1)
  }

  # check whether variable name.

  if ($2 ~ /^\$/) {

     # variable names are more restrictive than table names.

     if ($2 !~ /^\$[A-Za-z_][A-Za-z0-9_]*$/) {
	print "nblparser: NBL 'read': bad variable name" > stderr
	exit(rc=1)
     }

     schema_row[p[pfx "object"]] = $2	# needed by other handlers

     # we assume that "set -e" is used in the output sh(1) script,
     # so that if test(1) fails the script terminates.

     file = "\"" $2 "\""
     cmd = "test -f " file "; "
  }

  if ($3 != "") {
     if ($3 !~ /^[A-Za-z0-9]+$/) {
     	print "nblparser: NBL 'read': bad key specified" > stderr
     	exit(rc=1)
     }
  }

  # lookup target table in schema, unless "$table"

  if (file == "") {

    while (++i <= schema[0]) {
       split(schema[i],a,"\t")
       if (a[p[pfx "object"]] == $2) {
	  split(schema[i],schema_row,"\t")
	  if (dir != "" && schema_row[p[pfx "path"]] !~ /^\//)
		schema_row[p[pfx "path"]] = \
				dir "/" schema_row[p[pfx "path"]]
	  break				# bail-out if table found
       }
    }

    if (schema_row[p[pfx "object"]] == "") {
       print "nblparser: NBL 'read': unknown table '" $2 "'" > stderr
       exit(rc=1)
    }

    if (schema_row[p[pfx "path"]] ~ /\$\[/) {
       print "nblparser: NBL 'read': variable tokens in path name, use 'set' first" > stderr
       exit(rc=1)
    }

    file = schema_row[p[pfx "path"]]
  }
	
  if ($3 != "") {
     post_filter = $3
     if (partial) {
	sh_cmd[++sh_cmd[0]] = cmd "keysearch -p '" $3 "' " file
	gsub(/[]\\\$()\[\|\^\*\?\.]/,"\\\\&",post_filter)
	post_filter = "awk '$1 ~ /^(\\001|" post_filter ")/'"
     }
     else {
	sh_cmd[++sh_cmd[0]] = cmd "keysearch '" $3 "' " file
	gsub(/\\/,"\\\\&",post_filter)
	post_filter = "awk '$1 ~ /^\\001/ || $1 == \"" post_filter "\"'"
     }
  }
  else sh_cmd[++sh_cmd[0]] = cmd "cat " file

  edit_file = schema_row[p[pfx "edit"]]

  if (edit_file != "") {
     sh_cmd[sh_cmd[0]] = sh_cmd[sh_cmd[0]] " |\nupdtable"

     key_field = keylist[schema_row[p[pfx "object"]]]

     if (key_field != "")
	sh_cmd[sh_cmd[0]] = sh_cmd[sh_cmd[0]] " --key-columns " key_field

     sh_cmd[sh_cmd[0]] = sh_cmd[sh_cmd[0]] " " edit_file

     if (post_filter != "")
	sh_cmd[sh_cmd[0]] = sh_cmd[sh_cmd[0]] " |\n" post_filter
  }
}


function handle_expr(what,		a,i,j,regexp) {

  check_cmd(what)

  if (NF < 2) {
     print "nblparser: NBL usage: " what " statements" > stderr
     exit(rc=1)
  }

  regexp = "^" what "[ \t]+"
  sub(regexp,"")

  if (what == "select") what = "getrow"

  # Forbid AWK's dangerous instructions, if necessary. The following
  # is a rough check, which may be stricter than necessary in some cases,
  # but better to be safe than sorry.

  if (!unsafe) {

     i = split($0,a,/[^a-z]+/)

     while (++j <= i) {
       if (a[j] ~ /^(printf?|getline|system)$/) {
	  print "nblparser: unsafe AWK instruction specified" > stderr
	  exit(rc=1)
       }
     }
  }

  # escape sh(1) special characters in statements.

  gsub(/\\/, "\\\\\\")
  gsub(/\$/, "\\$")
  gsub(/`/, "\\`")
  gsub(/"/,"\\\"")

  if (what == "expression") {
     what = "awktable -H"
     $0 = $0 "{print}"
  }
  sh_cmd[sh_cmd[0]] = sh_cmd[sh_cmd[0]] " |\n" what " \"" $0 "\""
}


function handle_column(revert,			i,a) {

  if (revert != "") {
     check_cmd("@columns")
     sh_cmd[sh_cmd[0]] = sh_cmd[sh_cmd[0]] " |\ngetcolumn -r"
  }
  else {
     check_cmd("columns")
     sh_cmd[sh_cmd[0]] = sh_cmd[sh_cmd[0]] " |\ngetcolumn"
  }


  if (NF < 2) {
     print "nblparser: NBL usage: columns col [col ...]" > stderr
     exit(rc=1)
  }

  sub(/^@?columns[ \t]+/,"")
  split($0,a)
  while (a[++i] != "") {
     if (a[i] !~ /^[A-Za-z_][A-Za-z0-9_]*$/) {
	print "nblparser: NBL 'columns': bad column names(s) specified" > stderr
	exit(rc=1)
     }
  }

  sh_cmd[sh_cmd[0]] = sh_cmd[sh_cmd[0]] " " $0
}


function handle_join(outer,		i,a,b,k,\
					join_row,tbl,cmd,file) {

  if (outer != "") check_cmd("@join")
  else check_cmd("join")

  if (NF < 2 || NF > 4) {
     print "nblparser: NBL usage: [@]join table1 [table2 [columns]]" > stderr
     exit(rc=1)
  }

  # set default arguments

  if ($3 == "") $3 = "-"
  if ($4 == "") $4 = "-"

  # either one or the other table, but not both, must be on stdin.

  if (($2 == "-" && $3 == "-") || ($2 != "-" && $3 != "-")) {
     print "nblparser: NBL 'join': one (and only one) of the input tables must be on stdin" > stderr
     exit(rc=1)
  }

  if (($2 != "-" && $2 !~ /^@?\$?[A-Za-z_][A-Za-z0-9_]*$/) || \
      ($3 != "-" && $3 !~ /^@?\$?[A-Za-z_][A-Za-z0-9_]*$/)) {
     print "nblparser: NBL 'join': bad table or variable name" > stderr
     exit(rc=1)
  }

  cmd = "jointable"
  if (outer != "") cmd = cmd " --all"

  if ($2 != "-") tbl = $2
  else tbl = $3

  # check whether variable name.

  if (tbl ~ /^\$/) {

     # we assume that "set -e" is used in the output sh(1) script,
     # so that if test(1) fails the script terminates.

     file = "\"" tbl "\""
  }

  # lookup target table in schema, unless "$table"

  if (file == "") {

    while (++i <= schema[0]) {
       split(schema[i],a,"\t")
       if (a[p[pfx "object"]] == tbl) {
	  split(schema[i],join_row,"\t")
	  break					# bail-out if table found
       }
    }

    if (join_row[p[pfx "object"]] == "") {
       print "nblparser: NBL 'join': unknown table '" tbl "'" > stderr
       exit(rc=1)
    }

    if (join_row[p[pfx "path"]] ~ /\$\[/) {
       print "nblparser: NBL 'join': variable tokens in file name, use 'set' first" > stderr
       exit(rc=1)
    }

    file = join_row[p[pfx "path"]]

    # Try to infer the key column(s) from the file name, if possible.

    jlist = file

    # Handle both table and index file names (the index first!). Note
    # that _k and _x were choosen because no real column name can begin
    # with an uderscore, so there's no risk of ambiguities. Note also
    # that we need to strip everything up to _x first, as in index
    # files the actual key columns are those that come after _x, and
    # they may not necessarily be the same as the key columns of the
    # main table. That is, given the main table 'table._k.col1.col2',
    # it is quite possible to have an index file name like this:
    # 'table._k.col1.col2._x.col3.col4.col5

    if (sub(/.*\._x\./,"",jlist) || sub(/.*\._k\./,"",jlist)) {
       gsub(/\./,",",jlist)
       sub(/-.*$/,"",jlist)		# remove possible "-suffix".
    }
    else jlist = ""
  }

  # join column(s) from both tables, if specified.

  if ($4 != "" && $4 != "-") {
     if ($4 !~ /^@?([A-Za-z][A-Za-z0-9_]*)(,[A-Za-z][A-Za-z0-9_]*)*$/) {
     	print "nblparser: NBL 'join': bad column name(s) specified" > stderr
     	exit(rc=1)
     }

     jlist = $4
  }

  if (jlist != "") cmd = cmd " --column " jlist

  if ($2 != "-") cmd = cmd " " file " -"
  else cmd = cmd " - " file

  sh_cmd[sh_cmd[0]] = sh_cmd[sh_cmd[0]] " |\n" cmd
}


function handle_orderby(revert,		i,cmd) {

  if (revert != "") check_cmd("@order-by")
  else check_cmd("order-by")

  cmd = "sorttable"
  if (revert != "") cmd = cmd " -r"

  if ($2 == "-") NF=1			# allow "-" to mean "all columns"

  for (i=2; i<=NF; i++) {
      if ($i !~ /^[A-Za-z][A-Za-z0-9_]+(:[Mbdfinr])?$/) {
	 print "nblparser: NBL 'order-by': bad column name specified" > stderr
	 exit(rc=1)
      }
      cmd = cmd " " $i
  }
  sh_cmd[sh_cmd[0]] = sh_cmd[sh_cmd[0]] " |\n" cmd
}

function handle_uniqueby(revert,		i,cmd) {

  if (revert != "") check_cmd("@unique-by")
  else check_cmd("unique-by")

  cmd = "sorttable -u"
  if (revert != "") cmd = cmd " -r"

  if ($2 == "-") NF=1			# allow "-" to mean "all columns"

  for (i=2; i<=NF; i++) {
      if ($i !~ /^[A-Za-z][A-Za-z0-9_]+(:[Mbdfinr])?$/) {
	 print "nblparser: NBL 'unique-by': bad column name specified" > stderr
	 exit(rc=1)
      }
      cmd = cmd " " $i
  }
  sh_cmd[sh_cmd[0]] = sh_cmd[sh_cmd[0]] " |\n" cmd
}


function handle_use(			i,j,a,t,tbl,file,row) {

  if (schema_row[p[pfx "object"]] != "") {
     print "nblparser: the 'use' statement, if used, must come first" > stderr
     exit(rc=1)
  }

  if (locklist != "") {
     print "nblparser: multiple 'use' statements are not allowed" > stderr
     exit(rc=1)
  }

  if (NF < 2) {
     print "nblparser: NBL usage: use table [table ...]" > stderr
     exit(rc=1)
  }

  sub(/^use[ \t]+/,"")

  j = split($0,tbl)
  
  # for each listed table.
  for (t=1; t<=j; t++) {

     if (tbl[t] !~ /^\$?[A-Za-z_][A-Za-z0-9_]*$/) {
        print "nblparser: NBL 'use': bad table or variable name" > stderr
        exit(rc=1)
     }

     file = ""

     # check whether it is a variable name.

     if (tbl[t] ~ /^\$/) {

        #schema_row[p[pfx "object"]] = tbl[t]	# needed by other handlers

        # we assume that "set -e" is used in the output sh(1) script,
        # so that if test(1) fails the script terminates.

        file = "\"" tbl[t] "\""
	print "test -f " file > o_file
     }

     # lookup target table in schema, unless "$table"

     if (file == "") {

       while (++i <= schema[0]) {
          split(schema[i],a,"\t")
          if (a[p[pfx "object"]] == tbl[t]) {
	     split(schema[i],row,"\t")

	     # If an edit buffer is used, then lock that one instead
	     # of main table.

	     if (row[p[pfx "edit"]] != "") file = row[p[pfx "edit"]]
	     else file = row[p[pfx "path"]]
	     if (dir != "" && file !~ /^\//) file = dir "/" file
	     break				# bail-out if table found
          }
       }

       if (file == "") {
          print "nblparser: NBL 'use': unknown table '" tbl[t] "'" > stderr
          exit(rc=1)
       }

       if (file ~ /\$\[/) {
          print "nblparser: NBL 'use': variable tokens in path name, use 'set' first" > stderr
          exit(rc=1)
       }

       locklist = locklist " " file ".lock"
     }
  }

  print "lockfile -r6 -l40 -s8 " locklist > o_file
  print "trap 'rm -f " locklist "' 0" > o_file
  print "trap 'exit 2' 1 2 3 15" > o_file
}


function handle_remember() {

  check_cmd("remember-as")

  if (NF != 2) {
     print "nblparser: NBL usage: remember-as name" > stderr
     exit(rc=1)
  }

  # accept only valid variable names. Lower-case is mandatory, not to
  # interfere with the usual shell environment variables.

  if ($2 !~ /^[a-z][a-z0-9_]*$/) {
     print "nblparser: NBL 'remember-as': bad name specified" > stderr
     exit(rc=1)
  }

  sh_cmd[sh_cmd[0]] = $2 "=$(\n" sh_cmd[sh_cmd[0]] " |\n" tmptable_cmd "\n)"

  delete schema_row		# a new 'read' is necessary from now on.
}


function handle_format(list) {

  if (!nohide && hide != "") {
     sh_cmd[sh_cmd[0]] = sh_cmd[sh_cmd[0]] " |\nnotcolumn " hide
     nohide = 1			# tell END{} to hide no more.
  }

  if (list != "") {
     check_cmd("@format")
     sh_cmd[sh_cmd[0]] = sh_cmd[sh_cmd[0]] " |\ntabletolist --justify"
  }
  else {
     check_cmd("format")
     sh_cmd[sh_cmd[0]] = sh_cmd[sh_cmd[0]] " |\nprtable"
  }

  delete schema_row		# a new 'read' is necessary from now on.
}


function handle_labels(			a,i) {

  if (NF < 2) {
     print "nblparser: NBL usage: labels label [label ...]" > stderr
     exit(rc=1)
  }

  sub(/^labels[ \t]+/,"")
  split($0,a)
  while (a[++i] != "") {
     if (a[i] !~ /^[A-Za-z_][A-Za-z0-9_]*$/) {
	print "nblparser: NBL 'labels': bad label names(s) specified" > stderr
	exit(rc=1)
     }
  }

  labels = $0
}


function handle_totals(currency,	i,a) {

  if (currency != "") {
     check_cmd("@totals")
     sh_cmd[sh_cmd[0]] = sh_cmd[sh_cmd[0]] " |\ntotaltable -c"
  }
  else {
     check_cmd("totals")
     sh_cmd[sh_cmd[0]] = sh_cmd[sh_cmd[0]] " |\ntotaltable"
  }

  sub(/^@?totals[ \t]*/,"")
  split($0,a)
  while (a[++i] != "") {
     if (a[i] !~ /^[A-Za-z_][A-Za-z0-9_]*$/) {
	print "nblparser: NBL 'totals': bad column names(s) specified" > stderr
	exit(rc=1)
     }
  }

  sh_cmd[sh_cmd[0]] = sh_cmd[sh_cmd[0]] " " $0
}


function handle_system() {

  if (!unsafe) {
     print "nblparser: NBL 'system': command not allowed" > stderr
     exit(rc=1)
  }

  if (NF < 2) {
     print "nblparser: NBL usage: system commands" > stderr
     exit(rc=1)
  }

  sub(/^system[ \t]*/,"")

  if (schema_row[p[pfx "object"]] != "")
	sh_cmd[sh_cmd[0]] = sh_cmd[sh_cmd[0]] " |"
  sh_cmd[sh_cmd[0]] = sh_cmd[sh_cmd[0]] "\n" $0
}

# End of program
