# 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:
