# Part of the A-A-P recipe executive: Utility functions

# 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

#
# Util: utility functions
#
# It's OK to do "from Util import *", these things are supposed to be global.
#

import string
import os.path

from Util import *
from Error import *
from Message import *


def i18n_init():
    """Set up Internationalisation: setlocale() and gettext()."""
    # Chicken-egg problem: Should give informational messages here, but since
    # the options haven't been parsed yet we don't know if the user wants us to
    # be verbose.  Let's keep quiet.

    # Set the locale to the users default.
    try:
	import locale
	locale.setlocale(locale.LC_ALL, '')
    except ImportError:
	pass

    # Set up for translating messages, if possible.
    # When the gettext module is missing this results in an ImportError.
    # An older version of gettext doesn't support install(), it generates an
    # AttributeError.
    # If not possible, define the _() and N_() functions to do nothing.
    # Make them builtin so that they are available everywhere.
    try:
	import gettext
	gettext.install("a-a-p")
    except (ImportError, AttributeError):
	def nogettext(s):
	    return s
	import __builtin__
        __builtin__.__dict__['_'] = nogettext
        __builtin__.__dict__['N_'] = nogettext


def is_white(c):
    """Return 1 if "c" is a space or a Tab."""
    return c == ' ' or c == '\t'


def skip_white(line, i):
    """Skip whitespace, starting at line[i].  Return the index of the next
    non-white character, or past the end of "line"."""
    try:
	while is_white(line[i]):
	    i = i + 1
    except IndexError:
	pass
    return i


def skip_to_white(line, i):
    """Skip non-whitespace, starting at line[i].  Return the index of the next
    white character, or past the end of "line"."""
    try:
	while not is_white(line[i]):
	    i = i + 1
    except IndexError:
	pass
    return i


def get_token(arg, i):
    """Get one white-space separated token from arg[i:].
       Handles single and double quotes and keeps them (see get_item() to
       remove quotes).
       A sequence of white space is also a token.
       Returns the token and the index after it."""
    # If "arg" starts with white space, return the span of white space.
    if is_white(arg[i]):
	e = skip_white(arg, i)
	return arg[i:e], e

    # Isolate the token until white space or end of the argument.
    inquote = ''
    arg_len = len(arg)
    token = ''
    while i < arg_len:
	if inquote:
	    if arg[i] == inquote:
		inquote = ''
	elif arg[i] == "'" or arg[i] == '"':
	    inquote = arg[i]
	elif is_white(arg[i]):
	    break
	token = token + arg[i]
	i = i + 1

    return token, i


def check_exists(rpstack, fname):
    """Give an error message if file "fname" already exists."""
    if os.path.exists(fname):
	from Process import recipe_error
	recipe_error(rpstack, _('File already exists: "%s"') % fname)


def varchar(c):
    """Return 1 when "c" is a variable name character, 0 otherwise."""
    return string.find(string.digits + string.letters + "_", c) != -1


def unquote(str):
    """Remove quotes from "str".  Assumes aap style quoting."""
    res = ''
    inquote = ''
    for c in str:
	if c == inquote:
	    inquote = ''	# End of quoted text.
	elif not inquote and (c == '"' or c == "'"):
	    inquote = c		# Start of quoted text.
	else:
	    res = res + c

    if inquote:
	msg_info(_('Missing quote in "%s"') % str)
    return res


def enquote(s, quote = '"'):
    """Put quotes around "s" so that it is handled as one item.  Uses aap style
    quoting (a mix of single and double quotes)."""
    result = quote
    slen = len(s)
    i = 0
    while i < slen:
	if s[i] == quote:
	    if quote == '"':
		result = result + "'\"'\"'"
	    else:
		result = result + '"\'"\'"'
	else:
	    result = result + s[i]
	i = i + 1
    return result + quote


def double_quote(str):
    """Put double quotes around "str" when it contains white space or a quote.
       Contained double quotes are doubled."""
    quote = ''
    for c in str:
	if c == "'" or c == '"' or is_white(c):
	    quote = '"'
	    break

    if not quote:
	return str	# no quoting required

    res = quote
    for c in str:
	if c == '"':
	    res = res + '"'
	res = res + c
    return res + quote


def bs_quote(str):
    """Escape special characters in "str" with a backslash.  Special characters
       are white space, quotes and backslashes."""
    res = ''
    for c in str:
	if c == '\\' or c == "'" or c == '"' or is_white(c):
	    res = res + '\\'
	res = res + c
    return res


def get_indent(line):
    """Count the number of columns of indent at the start of "line"."""
    i = 0
    col = 0
    try:
	while is_white(line[i]):
	    if line[i] == ' ':
		col = col + 1
	    else:
		col = col + 8 - (col % 8)
	    i = i + 1
    except IndexError:
	pass
    return col


def get_flags(arg, idx, flags):
    """Check the start of "arg[idx:]" for flags in the form -f.
       Caller must have skipped white space.
       Returns the detected flags and the index for what follows.
       When there is an error throws a UserError."""
    res = ''
    i = idx
    arg_len = len(arg)
    while i + 1 < arg_len:
	if arg[i] != '-':		# end of flags, stop
	    break
	if is_white(arg[i + 1]):	# "-" by itself is not a flag, stop
	    break
	i = i + 1
	if arg[i] == '-':		# "--" ends flags, skip it and stop
	    i = i + 1
	    break
	while i < arg_len:
	    if is_white(arg[i]):
		i = skip_white(arg, i)
		break
	    if not arg[i] in flags:
		raise UserError, _('Flag "%s" not supported') % arg[i]
	    res = res + arg[i]
	    i = i + 1

    return res, skip_white(arg, i)


class Expand:
    """Kind of expansion used for $VAR."""
    quote_none = 0	    # no quoting
    quote_aap = 1	    # quoting with " and '
    quote_double = 2	    # quoting with ", backslash for escaping
    quote_bs = 3	    # escaping with backslash
    quote_shell = 4	    # quoting with backslash or " for the shell

    def __init__(self, attr = 1, quote = quote_aap, skip_errors = 0):
	self.attr = attr	# include attributes
	self.quote = quote	# quoting with " and '
	self.skip_errors = skip_errors  # ignore errors


def get_var_val(line_nr, globals, name, expand = None):
    """Get the value of variable "name", expanding it when postponed evaluation
       was specified for the assignment."""
    from Commands import aap_eval
    import types

    val = globals[name]
    # Automatically convert a number to a string.
    if isinstance(val, types.IntType) or isinstance(val, types.LongType):
	val = str(val)
    if globals.has_key('$' + name):
	val = aap_eval(line_nr, globals, val, Expand(1, Expand.quote_aap))

    if not expand:
	return val
    if expand.attr and expand.quote == Expand.quote_aap:
	# Attributes and aap quoting is the default, nothing to do
	return val

    # Remove attributes and/or change the quoting.  This is done by turning the
    # string into a dictlist and then back into a string.
    from Dictlist import dictlist2str, string2dictlist

    try:
	res = dictlist2str(string2dictlist([], val), expand)
    except UserError, e:
	if expand.skip_errors:
	    res = val	    # ignore the error, return unexpanded
	else:
	    from Process import recipe_error
	    from Work import getrpstack
	    recipe_error(getrpstack(globals, line_nr),
				    (_('Error expanding "%s"') % val) + str(e))

    return res


def expand_item(item, expand, key = "name"):
    """Expand one "item" (one entry of a variable converted to a dictlist),
    according to "expand"."""
    res = expand_itemstr(item[key], expand)
 
    if expand.attr:
	from Dictlist import dictlistattr2str
	res = res + dictlistattr2str(item)
    return res


def expand_itemstr(str, expand):
    """Expand the string value of an item accoding to "expand"."""
    if expand.quote == Expand.quote_shell:
	if os.name == "posix":
	    # On Unix a mix of double and single quotes works well
	    quote = Expand.quote_aap
	else:
	    # On MS-Windows double quotes works well
	    quote = quote_double
    else:
	quote = expand.quote

    if quote == Expand.quote_none:
	res = str
    elif quote == Expand.quote_aap:
	from Dictlist import listitem2str
	res = listitem2str(str)
    elif quote == Expand.quote_double:
	res = double_quote(str)
    else:
	res = bs_quote(str)
    return res


def oct2int(s):
    """convert string "s", which is an octal number, to an int.  Isn't there a
    standard Python function for this?"""
    v = 0
    for c in s:
	if not c in string.octdigits:
	    raise UserError, _('non-octal chacacter encountered in "%s"') % s
	v = v * 8 + int(c)
    return v


def tempfname():
    """Return the name of a temporary file which is for private use."""
    # TODO: create a directory with 0700 permissions, so that it's private
    import tempfile
    return tempfile.mktemp()


def full_fname(name):
    """Make a full, uniform file name out of "name".  Used to be able to
       compare filenames with "./" and "../" things in them, also after
       changing directories."""
    return os.path.abspath(os.path.normpath(name))


def shorten_name(name, dir = None):
    """Shorten a file name when it's relative to directory "dir".
       If "dir" is not given, use the current directory.
       Prefers using "../" when part of "dir" matches."""
    if dir is None:
	dir = os.getcwd()
    dir_len = len(dir)
    if dir[dir_len - 1] != '/':
	dir = dir + '/'		# make sure "dir" ends in a slash
	dir_len = dir_len + 1

    # Skip over the path components that are equal
    name_len = len(name)
    i = 0
    slash = -1
    while i < dir_len and i < name_len:
	if dir[i] != name[i]:
	    break
	if dir[i] == '/':
	    slash = i
	i = i + 1

    # If nothing is equal, return the full name
    if slash <= 0:
	return name

    # For a full match with "dir" return the name without it.
    if i == dir_len:
	return name[dir_len:]

    # Insert "../" for the components in "dir" that are not equal.
    # Example: dir    = "/foo/test"
    #	       name   = "/foo/bdir/foo.o"
    #	       result = "../bdir/foo.o"
    back = ''
    while i < dir_len:
	if dir[i] == '/':
	    back = back + "../"
	i = i + 1

    return back + name[slash + 1:]


def shorten_dictlist(dictlist):
    """Shorten a dictlist to the current directory.  Returns a copy of the
       dictlist with identical attributes and shortened names."""
    dir = os.getcwd()
    newlist = []
    for item in dictlist:
	new_item = {}
	for k in item.keys():
	    if k == "name":
		if item.has_key("_node") and k == "name":
		    new_item[k] = shorten_name(item["_node"].get_name(), dir)
		else:
		    new_item[k] = shorten_name(item[k], dir)
	    else:
		new_item[k] = item[k]
	newlist.append(new_item)
    return newlist


def aap_checkdir(rpstack, fname):
    """Make sure the directory for "fname" exists."""
    bd = os.path.dirname(fname)
    if bd and not os.path.exists(bd):
	msg_info(_('Creating directory "%s"') % bd)
	try:
	    os.makedirs(bd)
	except EnvironmentError, e:
	    from Process import recipe_error
	    recipe_error(rpstack, (_('Could not create directory "%s"')
								% bd) + str(e))

def date2secs(str):
    """Convert a string like "12 days" to a number of seconds."""
    str_len = len(str)
    i = 0
    while i < str_len:
	if not str[i] in string.digits:
	    break
	i = i + 1
    if i == 0:
	raise UserError, _('Must start with a number: "%s"') % str
    nr = int(str[:i])
    i = skip_white(str, i)
    if str[i:] == "day":
	return nr * 24 * 60 * 60
    if str[i:] == "hour":
	return nr * 60 * 60
    if str[i:] == "min":
	return nr * 60
    if str[i:] == "sec":
	return nr
    raise UserError, _('Must have day, hour, min or sec: "%s"') % str


def sort_list(l):
    """Sort a list and return the result.  The sorting is done in-place, thus
       the original list is changed.  It's just a workaround for the Python
       sort method on lists returning None instead of the list."""
    l.sort()
    return l


def logged_system(cmd):
    """Execute a system command.  Display the command and log the output."""
    msg_system(cmd)

    # Redirect the output of each line to a file.
    # Don't do this for lines that contain redirection themselves.
    # TODO: make this work on non-Posix systems.
    if msg_logname():
	newcmd = ''
	tmpfile = tempfname()
	for line in string.split(cmd, '\n'):
	    if string.find(line, '>') < 0:
		newcmd = newcmd + ("(%s) 2>&1 | tee %s\n" % (line, tmpfile))
	    else:
		newcmd = newcmd + line + '\n'
    else:
	tmpfile = None
	newcmd = cmd

    # TODO: system() isn't available on the Mac
    # TODO: system() always returns zero for Windows
    res =  os.system(newcmd)

    if tmpfile:
	# Append the output to the logfile.
	try:
	    f = open(tmpfile)
	    text = f.read()
	    f.close()
	except:
	    text = ''
	# Always delete the temp file
	try:
	    os.remove(tmpfile)
	except:
	    pass
	if text:
	    msg_log(text, msgt_result)

    return res


def assert_aap_dir():
    """Create the "aap" directory if it doesn't exist yet.
       Return non-zero if it exists or could be created."""
    if not os.path.exists("aap"):
	try:
	    os.mkdir("aap")
	except StandardError, e:
	    print _('Warning: Could not create "aap" directory: '), e
	    return 0
    return 1

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