# Part of the A-A-P recipe executive: Aap commands in a recipe

# Copyright (C) 2002 Stichting NLnet Labs
# Permission to copy and use this file is specified in the file COPYING.
# If this file is missing you can find it here: http://www.a-a-p.org/COPYING

#
# These are functions available to the recipe.
#
# Some functions are used for translated items, such as dependencies and ":"
# commands.
#
# It's OK to do "from Commands import *", these things are supposed to be
# global.
#

import os
import os.path
import re
import string
import copy

from Depend import Depend
from Dictlist import string2dictlist, get_attrdict, listitem2str
from DoRead import read_recipe, recipe_dir
from Error import *
import Global
from Process import assert_var_name, recipe_error
from RecPos import rpdeepcopy
from Rule import Rule
from Util import *
from Work import getwork, getrpstack
import Cache
from Message import *


def aap_depend(line_nr, globals, targets, sources, cmd_line_nr, commands):
   """Add a dependency."""
   work = getwork(globals)
   rpstack = getrpstack(globals, line_nr)

   # Expand the targets into dictlists.
   targetlist = string2dictlist(rpstack,
             aap_eval(line_nr, globals, targets, Expand(1, Expand.quote_aap)))

   # Parse build attributes {attr = value} zero or more times.
   # Variables are not expanded now but when executing the build rules.
   build_attr, i = get_attrdict(rpstack, None, sources, 0, 0)

   # Expand the sources into dictlists.
   sourcelist = string2dictlist(rpstack,
         aap_eval(line_nr, globals, sources[i:], Expand(1, Expand.quote_aap)))

   # Add to the global lists of dependencies.
   # Make a copy of the RecPos stack, so that errors in "commands" can print
   # the recipe stack.  The last entry is going to be changed, thus it needs
   # to be a copy, the rest can be referenced.
   d = Depend(targetlist, build_attr, sourcelist, work,
                       rpdeepcopy(getrpstack(globals), cmd_line_nr), commands)
   work.add_dependency(rpstack, d, commands != '')


def aap_autodepend(line_nr, globals, arg, cmd_line_nr, commands):
   """Add a dependency check."""
   work = getwork(globals)
   rpstack = getrpstack(globals, line_nr)
   if not commands:
       recipe_error(rpstack, _(":autodepend requires build commands"))

   # Parse build attributes {attr = value} zero or more times.
   # Variables are not expanded now but when executing the build rules.
   build_attr, i = get_attrdict(rpstack, None, arg, 0, 0)

   # Expand the other arguments into a dictlist.
   arglist = string2dictlist(rpstack,
             aap_eval(line_nr, globals, arg[i:], Expand(1, Expand.quote_aap)))

   # Use a rule object to store the info, a rule is just like a autodepend,
   # except that the a autodepend uses filetype names instead of patterns.
   rule = Rule([], build_attr, arglist,
                       rpdeepcopy(getrpstack(globals), cmd_line_nr), commands)
   work.add_autodepend(rule)


def aap_rule(line_nr, globals, targets, sources, cmd_line_nr, commands):
   """Add a rule."""
   work = getwork(globals)
   rpstack = getrpstack(globals, line_nr)

   # Expand the targets into dictlists.
   targetlist = string2dictlist(rpstack,
             aap_eval(line_nr, globals, targets, Expand(1, Expand.quote_aap)))

   # Parse build attributes {attr = value} zero or more times.
   # Variables are not expanded now but when executing the build rules.
   build_attr, i = get_attrdict(rpstack, None, sources, 0, 0)

   # Expand the sources into dictlists.
   sourcelist = string2dictlist(rpstack,
         aap_eval(line_nr, globals, sources[i:], Expand(1, Expand.quote_aap)))

   rule = Rule(targetlist, build_attr, sourcelist,
                       rpdeepcopy(getrpstack(globals), cmd_line_nr), commands)
   work.add_rule(rule)


def aap_update(line_nr, globals, arg):
   """Handle ":update target ...": update target(s) now."""
   work = getwork(globals)
   rpstack = getrpstack(globals, line_nr)
   from DoBuild import target_update

   targets = string2dictlist(rpstack,
                 aap_eval(line_nr, globals, arg, Expand(1, Expand.quote_aap)))
   if len(targets) == 0:
       recipe_error(rpstack, _("Missing argument for :update"))

   for t in targets:
       target_update(work, work.get_node(t["name"]), 1, t)


def aap_error(line_nr, globals, arg):
   """Handle: ":error foo bar"."""
   rpstack = getrpstack(globals, line_nr)
   recipe_error(rpstack, aap_eval(line_nr, globals, arg,
                                                Expand(0, Expand.quote_aap)))

def aap_unknown(line_nr, globals, arg):
   """Handle: ":xxx arg".  Postponed until executing the line, so that an
      "@if aapversion > nr" can be used."""
   rpstack = getrpstack(globals, line_nr)
   recipe_error(rpstack, _('Unknown command: "%s"') % arg)

def aap_nothere(line_nr, globals, arg):
   """Handle using a toplevel command in build commands.  Postponed until
      executing the line, so that an "@if aapversion > nr" can be used."""
   rpstack = getrpstack(globals, line_nr)
   recipe_error(fp.rpstack, _('Command cannot be used here: "%s"') % arg)

#
############## start of commands used in a pipe
#

def _get_redir(line_nr, globals, raw_arg):
   """Get the redirection and pipe from the argument "raw_arg".
      Returns these items:
      1. the argument with $VAR expanded and redirection removed
      2. the file name for redirection or None
      3. the mode for redirection or None ("a" for append, "w" for write).
      4. a command following '|' or None
      When using ">file" also checks if the file doesn't exist yet."""
   rpstack = getrpstack(globals, line_nr)

   mode = None
   fname = None
   nextcmd = None

   # Loop over the argument, getting one token at a time.  Each token is
   # either non-white (possibly with quoting) or a span of white space.
   raw_arg_len = len(raw_arg)
   i = 0           # current index in raw_arg
   new_arg = ''    # argument without redirection so far
   while i < raw_arg_len:
       t, i = get_token(raw_arg, i)

       # Ignore trailing white space.
       if i == raw_arg_len and is_white(t[0]):
           break

       # After (a span of) white space, check for redirection or pipe.
       # Also at the start of the argument.
       if new_arg == '' or is_white(t[0]):
           if new_arg == '':
               # Use the token at the start of the argument.
               nt = t
               t = ''
           else:
               # Get the token after the white space
               nt, i = get_token(raw_arg, i)

           if nt[0] == '>':
               # Redirection: >, >> or >!
               if mode:
                   recipe_error(rpstack, _('redirection appears twice'))
               nt_len = len(nt)
               ni = 1      # index after the ">", ">>" or ">!"
               mode = 'w'
               overwrite = 0
               if nt_len > 1:
                   if nt[1] == '>':
                       mode = 'a'
                       ni = 2
                   elif nt[1] == '!':
                       overwrite = 1
                       ni = 2
               if ni >= nt_len:
                   # white space after ">", get next token for fname
                   redir = nt[:ni]
                   if i < raw_arg_len:
                       # Get the separating white space.
                       nt, i = get_token(raw_arg, i)
                   if i == raw_arg_len:
                       recipe_error(rpstack, _('Missing file name after %s')
                                                                      % redir)
                   # Get the file name
                   nt, i = get_token(raw_arg, i)
               else:
                   # fname follows immediately after ">"
                   nt = nt[ni:]

               # Expand $VAR in the file name.  No attributes are added.
               # Remove quotes from the result, it's used as a filename.
               fname = unquote(aap_eval(line_nr, globals, nt,
                                                 Expand(0, Expand.quote_aap)))
               if mode == "w" and not overwrite:
                   check_exists(rpstack, fname)

               # When redirection is at the start, ignore the white space
               # after it.
               if new_arg == '' and i < raw_arg_len:
                   t, i = get_token(raw_arg, i)

           elif nt[0] == '|':
               # Pipe: the rest of the argument is another command
               if mode:
                   recipe_error(rpstack, _("both redirection and '|'"))

               if len(nt) > 1:
                   nextcmd = nt[1:] + raw_arg[i:]
               else:
                   i = skip_white(raw_arg, i)
                   nextcmd = raw_arg[i:]
               if not nextcmd:
                   # Can't have an empty command.
                   recipe_error(rpstack, _("Nothing follows '|'"))
               if nextcmd[0] != ':':
                   # Must have an aap command here.
                   recipe_error(rpstack, _("Missing ':' after '|'"))
               break

           else:
               # No redirection or pipe: add to argument
               new_arg = new_arg + t + nt
       else:
           # Normal token: add to argument
           new_arg = new_arg + t

   if new_arg:
       arg = aap_eval(line_nr, globals, new_arg, Expand(0, Expand.quote_aap))
   else:
       arg = new_arg

   return arg, fname, mode, nextcmd


def _aap_pipe(line_nr, globals, cmd, pipein):
   """Handle the command that follows a '|'."""
   rpstack = getrpstack(globals, line_nr)
   items = string.split(cmd, None, 1)
   if len(items) == 1:     # command without argument, append empty argument.
       items.append('')

   if items[0] == ":assign":
       _pipe_assign(line_nr, globals, items[1], pipein)
   elif items[0] == ":cat":
       aap_cat(line_nr, globals, items[1], pipein)
   elif items[0] == ":filter":
       _pipe_filter(line_nr, globals, items[1], pipein)
   elif items[0] == ":print":
       aap_print(line_nr, globals, items[1], pipein)
   elif items[0] == ":tee":
       _pipe_tee(line_nr, globals, items[1], pipein)
   else:
       recipe_error(rpstack,
                          (_('Invalid command after \'|\': "%s"') % items[0]))


def _pipe_assign(line_nr, globals, raw_arg, pipein):
   """Handle: ":assign var".  Can only be used in a pipe."""
   rpstack = getrpstack(globals, line_nr)
   assert_var_name(raw_arg, rpstack)
   globals[raw_arg] = pipein


def aap_cat(line_nr, globals, raw_arg, pipein = None):
   """Handle: ":cat >foo $bar"."""
   rpstack = getrpstack(globals, line_nr)

   # get the special items out of the argument
   arg, fname, mode, nextcmd = _get_redir(line_nr, globals, raw_arg)

   # get the list of files from the remaining argument
   filelist = string2dictlist(rpstack, arg)
   if len(filelist) == 0:
       if pipein is None:
           recipe_error(rpstack,
                   _(':cat command requires at least one file name argument'))
       filelist = [ {"name" : "-"} ]

   result = ''
   if mode:
       # Open the output file for writing
       try:
           wf = open(fname, mode)
       except IOError, e:
           recipe_error(rpstack,
                         (_('Cannot open "%s" for writing') % fname) + str(e))

   # Loop over all arguments
   for item in filelist:
       fn = item["name"]
       if fn == '-':
           # "-" argument: use pipe input
           if pipein is None:
               recipe_error(rpstack, _('Using - not after a pipe'))
           if nextcmd:
               result = result + pipein
           else:
               lines = string.split(pipein, '\n')
       else:
           # file name argument: read the file
           try:
               rf = open(fn, "r")
           except IOError, e:
               recipe_error(rpstack,
                            (_('Cannot open "%s" for reading') % fn) + str(e))
           try:
               lines = rf.readlines()
               rf.close()
           except IOError, e:
               recipe_error(rpstack,
                                   (_('Cannot read from "%s"') % fn) + str(e))
           if nextcmd:
               # pipe output: append lines to the result
               for l in lines:
                   result = result + l

       if mode:
           # file output: write lines to the file
           try:
               wf.writelines(lines)
           except IOError, e:
               recipe_error(rpstack,
                                 (_('Cannot write to "%s"') % fname) + str(e))
       elif not nextcmd:
           # output to the terminal: print lines
           msg_print(lines)

   if mode:
       # close the output file
       try:
           wf.close()
       except IOError, e:
           recipe_error(rpstack, (_('Error closing "%s"') % fname) + str(e))


   if nextcmd:
       # pipe output: execute the following command
       _aap_pipe(line_nr, globals, nextcmd, result)
   elif mode:
       msg_info(_('Concatenated files into "%s"') % fname)


def _pipe_filter(line_nr, globals, raw_arg, pipein):
   """Handle: ":filter function ...".  Can only be used in a pipe."""
   rpstack = getrpstack(globals, line_nr)
   arg, fname, mode, nextcmd = _get_redir(line_nr, globals, raw_arg)

   # Replace "%s" with "_pipein".
   # TODO: make it possible to escape the %s somehow?
   s = string.find(arg, "%s")
   if s < 0:
       recipe_error(rpstack, _('%s missing in :filter argument'))
   cmd = arg[:s] + "_pipein" + arg[s + 2:]

   # Evaluate the expression.
   globals["_pipein"] = pipein
   try:
       result = str(eval(cmd, globals, globals))
   except StandardError, e:
       recipe_error(rpstack, _(':filter command failed') + str(e))
   del globals["_pipein"]

   if mode:
       # redirection: write output to a file
       _write2file(rpstack, fname, result, mode)
   elif nextcmd:
       # pipe output: execute next command
       _aap_pipe(line_nr, globals, nextcmd, result)
   else:
       # output to terminal: print the result
       msg_print(result)


def aap_print(line_nr, globals, raw_arg, pipein = None):
   """Handle: ":print foo $bar"."""
   rpstack = getrpstack(globals, line_nr)
   arg, fname, mode, nextcmd = _get_redir(line_nr, globals, raw_arg)

   if pipein:
       if arg:
           recipe_error(rpstack,
                      _(':print cannot have both pipe input and an argument'))
       arg = pipein

   if mode:
       if len(arg) == 0 or arg[-1] != '\n':
           arg = arg + '\n'
       _write2file(rpstack, fname, arg, mode)
   elif nextcmd:
       if len(arg) == 0 or arg[-1] != '\n':
           arg = arg + '\n'
       _aap_pipe(line_nr, globals, nextcmd, arg)
   else:
       msg_print(arg)


def _pipe_tee(line_nr, globals, raw_arg, pipein):
   """Handle: ":tee fname ...".  Can only be used in a pipe."""
   rpstack = getrpstack(globals, line_nr)
   arg, fname, mode, nextcmd = _get_redir(line_nr, globals, raw_arg)

   # get the list of files from the remaining argument
   filelist = string2dictlist(rpstack, arg)
   if len(filelist) == 0:
       recipe_error(rpstack,
                   _(':tee command requires at least one file name argument'))

   for f in filelist:
       fn = f["name"]
       check_exists(rpstack, fn)
       _write2file(rpstack, fn, pipein, "w")

   if mode:
       # redirection: write output to a file
       _write2file(rpstack, fname, pipein, mode)
   elif nextcmd:
       # pipe output: execute next command
       _aap_pipe(line_nr, globals, nextcmd, pipein)
   else:
       # output to terminal: print the result
       msg_print(pipein)


def _write2file(rpstack, fname, str, mode):
   """Write string "str" to file "fname" opened with mode "mode"."""
   try:
       f = open(fname, mode)
   except IOError, e:
       recipe_error(rpstack,
                         (_('Cannot open "%s" for writing') % fname) + str(e))
   try:
       f.write(str)
       f.close()
   except IOError, e:
       recipe_error(rpstack, (_('Cannot write to "%s"') % fname) + str(e))

#
############## end of commands used in a pipe
#

def aap_child(line_nr, globals, arg):
   """Handle ":child filename": execute a recipe."""
   rpstack = getrpstack(globals, line_nr)
   work = getwork(globals)

   # Get the argument and attributes.
   varlist = string2dictlist(rpstack,
                 aap_eval(line_nr, globals, arg, Expand(1, Expand.quote_aap)))
   if len(varlist) == 0:
       recipe_error(rpstack, _(":child requires an argument"))
   if len(varlist) > 1:
       recipe_error(rpstack, _(":child only accepts one argument"))

   name = varlist[0]["name"]

   force_refresh = Global.cmd_args.has_option("refresh-recipe")
   if ((force_refresh or not os.path.exists(name))
           and varlist[0].has_key("refresh")):
       # Need to create a node to refresh it.
       # Ignore errors, a check for existence is below.
       # Use a cached file when no forced refresh.
       from VersCont import refresh_node
       refresh_node(rpstack, globals,
                        work.get_node(name, 0, varlist[0]), not force_refresh)

   if not os.path.exists(name):
       if varlist[0].has_key("refresh"):
           recipe_error(rpstack, _('Cannot download recipe "%s"') % name)
       recipe_error(rpstack, _('Recipe "%s" does not exist') % name)

   try:
       cwd = os.getcwd()
   except OSError:
       recipe_error(rpstack, _("Cannot obtain current directory"))
   name = recipe_dir(os.path.abspath(name))

   # Execute the child recipe.  Make a copy of the globals to avoid
   # the child modifies them.
   new_globals = globals.copy()
   new_globals["exports"] = {}
   new_globals["dependencies"] = []
   read_recipe(rpstack, name, new_globals)

   # TODO: move dependencies from the child to the current recipe,
   # using the rules from the child

   # Move the exported variables to the globals of the current recipe
   exports = new_globals["exports"]
   for e in exports.keys():
       globals[e] = exports[e]

   # go back to the previous current directory
   try:
       if cwd != os.getcwd():
           # Note: This message is not translated, so that a parser
           # for the messages isn't confused by various languages.
           msg_changedir('Entering directory "' + cwd + '"')
           try:
               os.chdir(cwd)
           except OSError:
               recipe_error(rpstack,
                           _('Cannot change to directory "%s"') % cwd)
   except OSError:
       recipe_error(rpstack, _("Cannot obtain current directory"))


def aap_export(line_nr, globals, arg):
   """Export a variable to the parent recipe (if any)."""
   rpstack = getrpstack(globals, line_nr)
   varlist = string2dictlist(rpstack,
                 aap_eval(line_nr, globals, arg, Expand(1, Expand.quote_aap)))

   for i in varlist:
       n = i["name"]
       assert_var_name(n, rpstack)
       globals["exports"][n] = get_var_val(line_nr, globals, n)


def aap_attr(line_nr, globals, arg):
   """Add attributes to nodes."""
   aap_attribute(line_nr, globals, arg)


def aap_attribute(line_nr, globals, arg):
   """Add attributes to nodes."""
   work = getwork(globals)
   rpstack = getrpstack(globals, line_nr)

   # Get the optional leading attributes.
   if not arg:
       recipe_error(rpstack, _(":attr command requires an argument"))
   if arg[0] == '{':
       attrdict, i = get_attrdict(rpstack, globals, arg, 0, 1)
   else:
       attrdict = {}
       i = 0

   # Get the list of items.
   varlist = string2dictlist(rpstack, aap_eval(line_nr, globals,
                                        arg[i:], Expand(1, Expand.quote_aap)))
   if not varlist:
       recipe_error(rpstack, _(":attr command requires a file argument"))

   # Loop over all items, adding attributes to the node.
   for i in varlist:
       node = work.get_node(i["name"], 1, i)
       node.set_attributes(attrdict)


def aap_assign(line_nr, globals, varname, arg, dollar, extra):
   """Assignment command in a recipe.
      "varname" is the name of the variable.
      "arg" is the argument value (Python expression already expanded).
      When "dollar" is '$' don't expand $VAR items.
      When "extra" is '?' only assign when "varname" wasn't set yet.
      When "extra" is '+' append to "varname"."""
   # Skip the whole assignment for "var ?= val" if var was already set.
   if extra != '?' or not globals.has_key(varname):
       if dollar != '$':
           # Expand variables in "arg".
           val = aap_eval(line_nr, globals, arg, Expand(1, Expand.quote_aap))
       else:
           # Postpone expanding variables in "arg".  Set "$var$" to remember
           # it has to be done when using the variable.
           val = arg

       # append or set the value
       if extra == '+' and globals.has_key(varname):
           globals[varname] = (get_var_val(line_nr, globals, varname)
                                                                  + ' ' + val)
       else:
           globals[varname] = val

       exn = '$' + varname
       if dollar != '$':
           # Stop postponing variable expansion.
           if globals.has_key(exn):
               del globals[exn]
       else:
           # Postpone expanding variables in "arg".  Set "$var" to remember
           # it has to be done when using the variable.
           globals[exn] = 1


def aap_eval(line_nr, globals, arg, expand, startquote = '', skip_errors = 0):
   """Evaluate $VAR, $(VAR) and ${VAR} in "arg", which is a string.
      $VAR is expanded and the resulting string is combined with what comes
      before and after $VAR.  text$VAR  ->  "textval1"  "textval2".
      "expand" is an Expand object that specifies the way $VAR is expanded.
      When "startquote" isn't empty, work like "arg" was prededed by it.
      When "skip_errors" is non-zero, leave items with errors unexpanded,
      never fail.
      """
   rpstack = getrpstack(globals, line_nr)
   res = ''                    # resulting string so far
   inquote = startquote        # ' or " when inside quotes
   itemstart = 0               # index where a white separated item starts
   arg_len = len(arg)
   idx = 0
   while idx < arg_len:
       if arg[idx] == '$':
           idx = idx + 1
           if arg[idx] == '$':
               res = res + '$'     # reduce $$ to a single $
               idx = idx + 1
           elif arg[idx] == '#':
               res = res + '#'     # reduce $# to a single #
               idx = idx + 1
           else:
               # Remember what non-white text came before the $.
               before = res[itemstart:]

               exp = copy.copy(expand) # make a copy so that we can change it

               # Check for type of quoting.
               if arg[idx] == '-':
                   idx = idx + 1
                   exp.attr = 0
               elif arg[idx] == '+':
                   idx = idx + 1
                   exp.attr = 1

               # Check for '+' (include attributes) or '-' (exclude
               # attributes)
               if arg[idx] == '=':
                   idx = idx + 1
                   exp.quote = Expand.quote_none
               elif arg[idx] == "'":
                   idx = idx + 1
                   exp.quote = Expand.quote_aap
               elif arg[idx] == '"':
                   idx = idx + 1
                   exp.quote = Expand.quote_double
               elif arg[idx] == '\\':
                   idx = idx + 1
                   exp.quote = Expand.quote_bs
               elif arg[idx] == '!':
                   idx = idx + 1
                   exp.quote = Expand.quote_shell

               # Check for $(VAR) and ${VAR}.
               if arg[idx] == '(' or arg[idx] == '{':
                   s = skip_white(arg, idx + 1)
               else:
                   s = idx

               # get the variable name
               e = s
               while e < arg_len and varchar(arg[e]):
                   e = e + 1
               if e == s:
                   if skip_errors:
                       res = res + '$'
                       continue
                   recipe_error(rpstack, _("Invalid character after $"))
               varname = arg[s:e]
               if not globals.has_key(varname):
                   if skip_errors:
                       res = res + '$'
                       continue
                   recipe_error(rpstack, _('Unknown variable: "%s"') % varname)

               index = -1
               if s > idx:
                   # Inside () or {}
                   e = skip_white(arg, e)
                   if e < arg_len and arg[e] == '[':
                       # get index for $(var[n])
                       b = e
                       brak = 0
                       e = e + 1
                       # TODO: ignore [] inside quotes?
                       while e < arg_len and (arg[e] != ']' or brak > 0):
                           if arg[e] == '[':
                               brak = brak + 1
                           elif arg[e] == ']':
                               brak = brak - 1
                           e = e + 1
                       if e == arg_len or arg[e] != ']':
                           if skip_errors:
                               res = res + '$'
                               continue
                           recipe_error(rpstack, _("Missing ']'"))
                       v = aap_eval(line_nr, globals, arg[b+1:e],
                                Expand(0, Expand.quote_none), '', skip_errors)
                       try:
                           index = int(v)
                       except:
                           if skip_errors:
                               res = res + '$'
                               continue
                           recipe_error(rpstack,
                                 _('index does not evaluate to a number: "%s"')
                                                                          % v)
                       if index < 0:
                           if skip_errors:
                               res = res + '$'
                               continue
                           recipe_error(rpstack,
                               _('index evaluates to a negative number: "%d"')
                                                                      % index)
                       e = skip_white(arg, e + 1)

                   # Check for matching () and {}
                   if (e == arg_len
                           or (arg[idx] == '(' and arg[e] != ')')
                           or (arg[idx] == '{' and arg[e] != '}')):
                       if skip_errors:
                           res = res + '$'
                           continue
                       recipe_error(rpstack, _('No match for "%s"') % arg[idx])

                   # Continue after the () or {}
                   idx = e + 1
               else:
                   # Continue after the varname
                   idx = e

               # Find what comes after $VAR.
               # Need to remember whether it starts inside quotes.
               after_inquote = inquote
               s = idx
               while idx < arg_len:
                   if inquote:
                       if arg[idx] == inquote:
                           inquote = ''
                   elif arg[idx] == '"' or arg[idx] == "'":
                       inquote = arg[idx]
                   elif string.find(string.whitespace + "{", arg[idx]) != -1:
                       break
                   idx = idx + 1
               after = arg[s:idx]

               if exp.attr:
                   # Obtain any following attributes, advance to after them.
                   # Also expand $VAR inside the attributes.
                   attrdict, idx = get_attrdict(rpstack, globals, arg, idx, 1)
               else:
                   attrdict = {}

               if before == '' and after == '' and len(attrdict) == 0:
                   if index < 0:
                       # No rc-style expansion or index, use the value of
                       # $VAR as specified with quote-expansion
                       try:
                           res = res + get_var_val(line_nr, globals,
                                                                 varname, exp)
                       except TypeError:
                           if skip_errors:
                               res = res + '$'
                               continue
                           recipe_error(rpstack,
                                   _('Type of variable "%s" must be a string')
                                                                    % varname)
                   else:
                       # No rc-style expansion but does have an index.
                       # Get the Dictlist of the referred variable.
                       varlist = string2dictlist(rpstack,
                                       get_var_val(line_nr, globals, varname))
                       if len(varlist) < index + 1:
                           msg_warning(
                             _('Index "%d" is out of range for variable "%s"')
                                                           % (index, varname))
                       else:
                           res = res + expand_item(varlist[index], exp)
                   idx = s

               else:
                   # rc-style expansion of a variable

                   # Get the Dictlist of the referred variable.
                   # When an index is specified us that entry of the list.
                   # When index is out of range or the list is empty, use a
                   # list with one empty entry.
                   varlist1 = string2dictlist(rpstack,
                                       get_var_val(line_nr, globals, varname))
                   if (len(varlist1) == 0
                               or (index >= 0 and len(varlist1) < index + 1)):
                       if index >= 0:
                           msg_warning(
                             _('Index "%d" is out of range for variable "%s"')
                                                           % (index, varname))
                       varlist1 = [{"name": ""}]
                   elif index >= 0:
                       varlist1 = [ varlist1[index] ]

                   # Evaluate the "after" of $(VAR)after {attr = val}.
                   varlist2 = string2dictlist(rpstack,
                                      aap_eval(line_nr, globals, after,
                                                  Expand(1, Expand.quote_aap),
                                                  startquote = after_inquote),
                                                  startquote = after_inquote)
                   if len(varlist2) == 0:
                       varlist2 = [{"name": ""}]

                   # Remove quotes from "before", they are put back when
                   # needed.
                   lead = ''
                   q = ''
                   for c in before:
                       if q:
                           if c == q:
                               q = ''
                           else:
                               lead = lead + c
                       elif c == '"' or c == "'":
                           q = c
                       else:
                           lead = lead + c


                   # Combine "before", the list from $VAR, the list from
                   # "after" and the following attributes.
                   # Put "startquote" in front, because the terminalting quote
                   # will have been removed.
                   rcs = startquote
                   startquote = ''
                   for d1 in varlist1:
                       for d2 in varlist2:
                           if rcs:
                               rcs = rcs + ' '
                           s = lead + d1["name"] + d2["name"]
                           # If $VAR was in quotes put the result in quotes.
                           if after_inquote:
                               rcs = rcs + enquote(s, quote = after_inquote)
                           else:
                               rcs = rcs + expand_itemstr(s, exp)
                           if exp.attr:
                               for k in d1.keys():
                                   if k != "name":
                                       rcs = rcs + "{%s = %s}" % (k, d1[k])
                               for k in d2.keys():
                                   if k != "name":
                                       rcs = rcs + "{%s = %s}" % (k, d2[k])
                               for k in attrdict.keys():
                                   rcs = rcs + "{%s = %s}" % (k, attrdict[k])
                   res = res[0:itemstart] + rcs

       else:
           # No '$' at this position, include the character in the result.
           # Check if quoting starts or ends and whether white space separates
           # an item, this is used for expanding $VAR.
           if inquote:
               if arg[idx] == inquote:
                   inquote = ''
           elif arg[idx] == '"' or arg[idx] == "'":
               inquote = arg[idx]
           elif is_white(arg[idx]):
               itemstart = len(res) + 1
           res = res + arg[idx]
           idx = idx + 1
   return res


def expr2str(item):
   """Used to turn the result of a Python expression into a string.
      For a list the elements are separated with a space.
      Dollars are doubled to avoid them being recognized as variables."""
   import types
   if type(item) == types.ListType:
       s = ''
       for i in item:
           if s:
               s = s + ' '
           s = s + listitem2str(str(i))
   else:
       s = str(item)
   return string.replace(s, '$', '$$')


def aap_sufreplace(suffrom, sufto, string):
   """Replace suffixes in "string" from "suffrom" to "sufto"."""
   return re.sub(string.replace(suffrom, ".", "\\.") + "\\b", sufto, string)


def aap_shell(line_nr, globals, cmds):
   """Execute shell commands from the recipe."""
   s = aap_eval(line_nr, globals, cmds, Expand(0, Expand.quote_shell))

   if globals.has_key("target"):
       msg_extra(_('Shell commands for updating "%s":') % globals["target"])

   n = logged_system(s)
   if n != 0:
       recipe_error(getrpstack(globals, line_nr),
                          _("Shell returned %d when executing:\n%s") % (n, s))


def aap_system(line_nr, globals, cmds):
   """Implementation of ":system cmds".  Almost the same as aap_shell()."""
   aap_shell(line_nr, globals, cmds + '\n')


def aap_sys(line_nr, globals, cmds):
   """Implementation of ":sys cmds".  Almost the same as aap_shell()."""
   aap_shell(line_nr, globals, cmds + '\n')


def aap_copy(line_nr, globals, arg):
   """Implementation of ":copy -x from to"."""
   # It's in a separate module, it's quite a bit of stuff.
   from CopyMove import copy_move
   copy_move(line_nr, globals, arg, 1)


def aap_move(line_nr, globals, arg):
   """Implementation of ":move -x from to"."""
   # It's in a separate module, it's quite a bit of stuff.
   from CopyMove import copy_move
   copy_move(line_nr, globals, arg, 0)


def aap_delete(line_nr, globals, raw_arg):
   """Alias for aap_del()."""
   aap_del(line_nr, globals, raw_arg)


def aap_del(line_nr, globals, raw_arg):
   """Implementation of ":del -r file1 file2"."""
   # Evaluate $VAR things
   arg = aap_eval(line_nr, globals, raw_arg, Expand(0, Expand.quote_aap))
   rpstack = getrpstack(globals, line_nr)

   # flags:
   # -f      don't fail when not exists
   # -q      quiet
   # -r -R   recursive, delete directories
   try:
       flags, i = get_flags(arg, 0, "fqrR")
   except UserError, e:
       recipe_error(rpstack, e)
   if 'r' in flags or 'R' in flags:
       recursive = 1
   else:
       recursive = 0

   # Get the remaining arguments, should be at least one.
   arglist = string2dictlist(rpstack, arg[i:])
   if not arglist:
       recipe_error(rpstack, _(":del command requires an argument"))

   import glob
   from urlparse import urlparse

   def deltree(dir):
       """Recursively delete a directory or a file."""
       if os.path.isdir(dir):
           fl = glob.glob(os.path.join(dir, "*"))
           for f in fl:
               deltree(f)
           os.rmdir(dir)
       else:
           os.remove(dir)

   for a in arglist:
       fname = a["name"]
       scheme, mach, path, parm, query, frag = urlparse(fname, '', 0)
       if scheme != '':
           recipe_error(rpstack, _('Cannot delete remotely yet: "%s"') % fname)

       # Expand ~user and wildcards.
       fl = glob.glob(os.path.expanduser(fname))
       if len(fl) == 0 and not 'f' in flags:
           recipe_error(rpstack, _('No match for "%s"') % fname)

       for f in fl:
           try:
               if recursive:
                   deltree(f)
               else:
                   os.remove(f)
           except EnvironmentError, e:
               recipe_error(rpstack, (_('Could not delete "%s"') % f) + str(e))
           else:
               if os.path.exists(f):
                   recipe_error(rpstack, _('Could not delete "%s"') % f)
           if not 'q' in flags:
               msg_info(_('Deleted "%s"') % fname)


def aap_mkdir(line_nr, globals, raw_arg):
   """Implementation of ":mkdir dir1 dir2"."""
   # Evaluate $VAR things
   arg = aap_eval(line_nr, globals, raw_arg, Expand(0, Expand.quote_aap))
   rpstack = getrpstack(globals, line_nr)

   # flags:
   # -f   create file when it does not exist
   try:
       flags, i = get_flags(arg, 0, "f")
   except UserError, e:
       recipe_error(rpstack, e)

   # Get the arguments, should be at least one.
   arglist = string2dictlist(rpstack, arg[i:])
   if not arglist:
       recipe_error(rpstack, _(":mkdir command requires an argument"))

   from urlparse import urlparse

   for a in arglist:
       name = a["name"]
       scheme, mach, path, parm, query, frag = urlparse(name, '', 0)
       if scheme != '':
           recipe_error(rpstack, _('Cannot create remote directory yet: "%s"')
                                                                      % name)
       # Expand ~user, create directory
       dir = os.path.expanduser(name)

       # Skip creation when it already exists.
       if not ('f' in flags and os.path.isdir(dir)):
           try:
               os.mkdir(dir)
           except EnvironmentError, e:
               recipe_error(rpstack, (_('Could not create directory "%s"')
                                                              % dir) + str(e))


def aap_touch(line_nr, globals, raw_arg):
   """Implementation of ":touch file1 file2"."""
   # Evaluate $VAR things
   arg = aap_eval(line_nr, globals, raw_arg, Expand(0, Expand.quote_aap))
   rpstack = getrpstack(globals, line_nr)

   # flags:
   # -f   create file when it does not exist
   try:
       flags, i = get_flags(arg, 0, "f")
   except UserError, e:
       recipe_error(rpstack, e)

   # Get the arguments, should be at least one.
   arglist = string2dictlist(rpstack, arg[i:])
   if not arglist:
       recipe_error(rpstack, _(":touch command requires an argument"))

   from urlparse import urlparse
   import time

   for a in arglist:
       name = a["name"]
       scheme, mach, path, parm, query, frag = urlparse(name, '', 0)
       if scheme != '':
           recipe_error(rpstack, _('Cannot touch remote file yet: "%s"')
                                                                      % name)
       # Expand ~user, touch file
       name = os.path.expanduser(name)
       if os.path.exists(name):
           now = time.time()
           try:
               os.utime(name, (now, now))
           except EnvironmentError, e:
               recipe_error(rpstack, (_('Could not update time of "%s"')
                                                             % name) + str(e))
       else:
           if not 'f' in flags:
               recipe_error(rpstack,
                         _('"%s" does not exist (use :touch -f to create it)')
                                                                       % name)
           try:
               # create an empty file
               f = os.open(name, os.O_WRONLY + os.O_CREAT + os.O_EXCL)
               os.close(f)
           except EnvironmentError, e:
               recipe_error(rpstack, (_('Could not create "%s"')
                                                             % name) + str(e))


def flush_cache():
   """Called just before setting $CACHE."""
   # It's here so that only this module has to be imported in Process.py.
   Cache.dump_cache()


# dictionary of recipes that have been refreshed (using full path name).
recipe_refreshed = {}


def aap_include(line_nr, globals, arg):
   """Handle ":include filename": read the recipe into the current globals."""
   work = getwork(globals)
   rpstack = getrpstack(globals, line_nr)

   # Evaluate the arguments
   args = string2dictlist(rpstack,
                 aap_eval(line_nr, globals, arg, Expand(1, Expand.quote_aap)))
   if len(args) != 1:
       recipe_error(rpstack, _(":include requires one argument"))

   recname = args[0]["name"]

   # Refresh the recipe when invoked with the "-R" argument.
   if ((Global.cmd_args.has_option("refresh-recipe")
           or not os.path.exists(recname))
               and args[0].has_key("refresh")):
       fullname = full_fname(recname)
       if not recipe_refreshed.has_key(fullname):
           from VersCont import refresh_node

           # Create a node for the recipe and refresh it.
           node = work.get_node(recname, 0, args[0])
           if not refresh_node(rpstack, globals, node, 0):
               msg_warning(_('Could not update recipe "%s"') % recname)

           # Also mark it as updated when it failed, don't try again.
           recipe_refreshed[fullname] = 1

   read_recipe(rpstack, recname, globals)


def do_recipe_cmd(rpstack):
   """Return non-zero if a ":recipe" command in the current recipe may be
   executed."""

   # Return right away when not invoked with the "-R" argument.
   if not Global.cmd_args.has_option("refresh-recipe"):
       return 0

   # Skip when this recipe was already updated.
   recname = full_fname(rpstack[-1].name)
   if recipe_refreshed.has_key(recname):
       return 0

   return 1


def aap_recipe(line_nr, globals, arg):
   """Handle ":recipe {refresh = name_list}": may download this recipe."""

   work = getwork(globals)
   rpstack = getrpstack(globals, line_nr)

   # Return right away when not to be executed.
   if not do_recipe_cmd(rpstack):
       return

   # Register the recipe to have been updated.  Also when it failed, don't
   # want to try again.
   recname = full_fname(rpstack[-1].name)
   recipe_refreshed[recname] = 1

   short_name = shorten_name(recname)
   msg_info(_('Updating recipe "%s"') % short_name)

   orgdict, i = get_attrdict(rpstack, globals, arg, 0, 1)
   if not orgdict.has_key("refresh"):
       recipe_error(rpstack, _(":recipe requires a refresh attribute"))
   # TODO: warning for trailing characters?

   from VersCont import refresh_node

   # Create a node for the recipe and refresh it.
   node = work.get_node(short_name, 0, orgdict)
   if refresh_node(rpstack, globals, node, 0):
       # TODO: check if the recipe was completely read
       # TODO: no need for restart if the recipe didn't change

       # Restore the globals to the values from when starting to read the
       # recipe.
       start_globals = globals["_start_globals"]
       for k in globals.keys():
           if not start_globals.has_key(k):
               del globals[k]
       for k in start_globals.keys():
           globals[k] = start_globals[k]

       # read the new recipe file
       read_recipe(rpstack, recname, globals, reread = 1)

       # Throw an exception to cancel executing the rest of the script
       # generated from the old recipe.  This is catched in read_recipe()
       raise OriginUpdate

   msg_warning(_('Could not update recipe "%s"') % node.name)

#
# Generic function for getting the arguments of :refresh, :checkout, :commit,
# :checkin, :unlock and :publish
#
def get_verscont_args(line_nr, globals, arg, cmd):
   """"Handle ":cmd {attr = } file ..."."""
   rpstack = getrpstack(globals, line_nr)

   # Get the optional attributes that apply to all arguments.
   attrdict, i = get_attrdict(rpstack, globals, arg, 0, 1)

   # evaluate the arguments into a dictlist
   varlist = string2dictlist(rpstack,
             aap_eval(line_nr, globals, arg[i:], Expand(1, Expand.quote_aap)))
   if not varlist:
       recipe_error(rpstack, _(':%s requires an argument') % cmd)
   return attrdict, varlist


def do_verscont_cmd(rpstack, globals, action, attrdict, varlist):
   """Perform "action" on items in "varlist", using attributes in
      "attrdict"."""
   from VersCont import verscont_node, refresh_node
   work = getwork(globals)

   for item in varlist:
       node = work.get_node(item["name"], 1, item)
       node.set_attributes(attrdict)
       if action == "refresh":
           r = (node.may_refresh()
                             and refresh_node(rpstack, globals, node, 0) == 0)
       elif action == "publish":
           r = publish_node(rpstack, globals, node)
       else:
           r = verscont_node(rpstack, globals, node, action)
       if not r:
           msg_warning(_('%s failed for "%s"') % (action, item["name"]))


def verscont_cmd(line_nr, globals, arg, action):
   """Perform "action" for each item "varlist"."""
   rpstack = getrpstack(globals, line_nr)

   attrdict, varlist = get_verscont_args(line_nr, globals, arg, action)
   do_verscont_cmd(rpstack, globals, action, attrdict, varlist)


def aap_refresh(line_nr, globals, arg):
   """"Handle ":refresh {attr = val} file ..."."""
   verscont_cmd(line_nr, globals, arg, "refresh")

def aap_checkout(line_nr, globals, arg):
   """"Handle ":checkout {attr = val} file ..."."""
   verscont_cmd(line_nr, globals, arg, "checkout")

def aap_commit(line_nr, globals, arg):
   """"Handle ":commit {attr = val} file ..."."""
   verscont_cmd(line_nr, globals, arg, "commit")

def aap_checkin(line_nr, globals, arg):
   """"Handle ":checkin {attr = val} file ..."."""
   verscont_cmd(line_nr, globals, arg, "checkin")

def aap_unlock(line_nr, globals, arg):
   """"Handle ":unlock {attr = val} file ..."."""
   verscont_cmd(line_nr, globals, arg, "unlock")

def aap_publish(line_nr, globals, arg):
   """"Handle ":publish {attr = val} file ..."."""
   verscont_cmd(line_nr, globals, arg, "publish")

def aap_add(line_nr, globals, arg):
   """"Handle ":add {attr = val} file ..."."""
   verscont_cmd(line_nr, globals, arg, "add")

def aap_remove(line_nr, globals, arg):
   """"Handle ":remove {attr = val} file ..."."""
   verscont_cmd(line_nr, globals, arg, "remove")


def aap_verscont(line_nr, globals, arg):
   """"Handle ":verscont action {attr = val} [file ...]"."""
   rpstack = getrpstack(globals, line_nr)

   # evaluate the arguments into a dictlist
   varlist = string2dictlist(rpstack,
                 aap_eval(line_nr, globals, arg, Expand(1, Expand.quote_aap)))
   if not varlist:
       recipe_error(rpstack, _(':verscont requires an argument'))

   if len(varlist) > 1:
       arglist = varlist[1:]
   else:
       arglist = []
   do_verscont_cmd(rpstack, globals, varlist[0]["name"], varlist[0], arglist)


def aap_commitall(line_nr, globals, arg):
   """"Handle ":commitall {attr = val} file ..."."""
   attrdict, varlist = get_verscont_args(line_nr, globals, arg, "commitall")
   recipe_error(rpstack, _('Sorry, :commitall is not implemented yet'))

def aap_publishall(line_nr, globals, arg):
   """"Handle ":publishall {attr = val} file ..."."""
   attrdict, varlist = get_verscont_args(line_nr, globals, arg, "publishall")
   recipe_error(rpstack, _('Sorry, :publishall is not implemented yet'))


def aap_removeall(line_nr, globals, arg):
   """"Handle ":removeall {attr = val} [dir ...]"."""
   rpstack = getrpstack(globals, line_nr)

   # flags:
   # -l    local (non-recursive)
   # -r    recursive
   try:
       flags, i = get_flags(arg, 0, "lr")
   except UserError, e:
       recipe_error(rpstack, e)

   # Get the optional attributes that apply to all arguments.
   attrdict, i = get_attrdict(rpstack, globals, arg, i, 1)

   # evaluate the arguments into a dictlist
   varlist = string2dictlist(rpstack,
             aap_eval(line_nr, globals, arg[i:], Expand(1, Expand.quote_aap)))

   from VersCont import verscont_removeall

   if varlist:
       # Directory name arguments: Do each directory non-recursively
       for dir in varlist:
           for k in attrdict.keys():
               dir[k] = attrdict[k]
           verscont_removeall(rpstack, globals, dir, 'r' in flags)
   else:
       # No arguments: Do current directory recursively
       attrdict["name"] = "."
       verscont_removeall(rpstack, globals, attrdict, not 'l' in flags)


def aap_filetype(line_nr, globals, arg, cmd_line_nr, commands):
   """Add filetype detection from a file or in-line detection rules."""
   from Filetype import ft_add_rules, ft_read_file, DetectError
   rpstack = getrpstack(globals, line_nr)

   # look through the arguments
   args = string2dictlist(rpstack,
                 aap_eval(line_nr, globals, arg, Expand(0, Expand.quote_aap)))
   if len(args) > 1:
       recipe_error(rpstack, _('Too many arguments for :filetype'))
   if len(args) == 1 and commands:
       recipe_error(rpstack,
                        _('Cannot have file name and commands for :filetype'))
   if len(args) == 0 and not commands:
       recipe_error(rpstack,
                           _('Must have file name or commands for :filetype'))

   try:
       if commands:
           what = "lines"
           ft_add_rules(commands)
       else:
           fname = args[0]["name"]
           what = 'file "%s"' % fname
           ft_read_file(fname)
   except DetectError, e:
       recipe_error(rpstack, (_('Error in detection %s: ') % what) + str(e))


# vim: set sw=4 sts=4 tw=79 fo+=l: