# 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
import stat
import types
import tempfile
import sys
# See the remark in Commands.py about using "pre".
if sys.version[0] == '2':
    exec "import pre as re"
else:
    import re
import glob

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.

    # If already done return quickly.
    import __builtin__
    if __builtin__.__dict__.has_key("_"):
	return

    # 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.
    # An IOError is generated when the module "a-a-p" cannot be found.
    # 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, IOError):
	def nogettext(s):
	    return s
        __builtin__.__dict__['_'] = nogettext
        __builtin__.__dict__['N_'] = nogettext


# String of characters that are considered white space
white_chars = ' \t'

def is_white(c):
    """Return 1 if "c" is a space or a Tab."""
    return c and c in white_chars


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 line[i] in white_chars:
	    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 line[i] in white_chars:
	    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 c in string.digits or c in string.letters or c == "_"

def skip_varchars(arg, idx):
    """Skip over varable name chars in arg[idx:], return the index."""
    e = idx
    try:
	while 1:
	    c = arg[e]
	    if not (c in string.digits or c in string.letters or c == "_"):
		break
	    e = e + 1
    except:
	pass
    return e


def unquote(recdict, 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(recdict, _('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 1:
	    if line[i] == ' ':
		col = col + 1
	    elif line[i] == '\t':
		col = col + 8 - (col % 8)
	    else:
		break
	    i = i + 1
    except IndexError:
	pass
    return col


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.rcstyle = 0	# rc-style expansion
	self.skip_errors = skip_errors  # ignore errors
	self.optional = 0	# empty when not defined


def get_var_val(line_nr, recdict, name, argexpand = None):
    """Get the value of variable "name", expanding it when postponed evaluation
       was specified for the assignment.
       When the variable "name" does not exist returen None."""
    from Commands import expand

    val = recdict.get(name)
    if val is None:
	return None

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

    # Return if no expansion to be done.
    if not argexpand:
	return val

    # Return when expanding doesn't change the value:
    # - Include attributes and aap quoting is the default.
    # - There are no attributes and aap quoting.
    if argexpand.quote == Expand.quote_aap and (argexpand.attr or not '{' in val):
	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.
    # Be permissive about errors and keep whitespace when there are no
    # attributes are to be removed.
    from Dictlist import dictlist2str, str2dictlist

    try:
	dl = str2dictlist([], val)
	if argexpand.quote != Expand.quote_aap:
	    use_val = 0
	else:
	    use_val = 1
	    for d in dl:
		if len(d.keys()) > 1:
		    use_val = 0
		    break
	if use_val:
	    res = val
	else:
	    res = dictlist2str(dl, argexpand)
    except UserError, e:
	res = val	    # ignore the error, return unexpanded
	if not argexpand.skip_errors:
	    from Process import get_error_msg
	    from Work import getrpstack
	    msg_warning(recdict, get_error_msg(getrpstack(recdict, line_nr),
				 (_('Error expanding "%s": ') % val) + str(e)))

    return res


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


def expand_itemstr(str, argexpand):
    """Expand the string value of an item accoding to "argexpand"."""
    if argexpand.quote == Expand.quote_shell:
	if os.name == "posix":
	    # On Unix a mix of double and single quotes works well
	    # Also escape characters that have a special meaning.
	    from Dictlist import listitem2str
	    res = listitem2str(str, ' \t&;|$<>', '&;|')
	else:
	    # On MS-Windows double quotes works well
	    res = double_quote(str)
    else:
	if argexpand.quote == Expand.quote_none:
	    res = str
	elif argexpand.quote == Expand.quote_aap:
	    from Dictlist import listitem2str
	    res = listitem2str(str)
	elif argexpand.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
    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, recdict, fname):
    """Make sure the directory for file "fname" exists."""
    dirname = os.path.dirname(fname)
    if dirname:
	assert_dir(rpstack, recdict, dirname)

def assert_dir(rpstack, recdict, dirname):
    """Make sure the directory "dirname" exists."""
    if not os.path.exists(dirname):
	msg_info(recdict, _('Creating directory "%s"') % dirname)
	try:
	    os.makedirs(dirname)
	except StandardError, e:
	    from Process import recipe_error
	    recipe_error(rpstack, (_('Cannot create directory "%s": ')
							   % dirname) + str(e))
    elif not os.path.isdir(dirname):
	from Process import recipe_error
	recipe_error(rpstack, _('"%s" exists but is not a directory') % dirname)


def dir_contents(dir, recurse = 0, join = 1):
    """Return a list with the contents of directory "dir".  Takes care not to
       expand wildcards in "dir".
       When "recurse" is non-zero, recursively get contents of directories.
       When "join" is zero, don't join "dir" to the resulting list.
       Raises an Exception when something is wrong."""
    cwd = os.getcwd()
    os.chdir(dir)
    try:
	# Get contents of "dir".
	list = glob.glob("*")
	if os.name == "posix":
	    list = list + glob.glob(".*")

	# When recursive, get contents of each directory in "dir".
	if recurse:
	    newlist = []
	    for item in list:
		if os.path.isdir(item):
		    newlist.extend(dir_contents(item, 1))
		else:
		    newlist.append(item)
	    list = newlist
    finally:
	os.chdir(cwd)

    if join:
	newlist = []
	for l in list:
	    newlist.append(os.path.join(dir, l))
	return newlist

    return list


def deltree(dir):
    """Recursively delete a directory or a file."""
    if os.path.isdir(dir):
	for f in dir_contents(dir):
	    deltree(f)
	os.rmdir(dir)
    else:
	os.remove(dir)


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 var2list(l):
    """Turn a variable with a string value into a list."""
    from Dictlist import str2list
    return str2list([], l)


def var2dictlist(l):
    """Turn a variable with a string value into a dictlist."""
    from Dictlist import str2dictlist
    return str2dictlist([], l)


def get_attr(fname):
    """Obtain the attribute dictionary for a file name.
       Only works in a recipe, because it uses Global.globals."""
    from Work import getwork
    node = getwork(Global.globals).find_node(fname)
    if node:
	return node.attributes
    return {}


def src2obj(source):
    """Turn a string (list of file names) to their respective object files."""
    from Dictlist import listitem2str
    dictlist = var2list(source)
    res = ''
    for name in dictlist:
	n = srcitem2obj(Global.globals, name)
	if res:
	    res = res + ' '
	res = res + listitem2str(n)
    return res


def srcitem2obj(recdict, name, attrdict = {}):
    """Turn one file name to its object file name: Prepend $BDIR and add
       $OBJSUF.  If "attrdict" is specified, use "var_" attributes for the
       variables."""
    from Work import getwork
    node = getwork(recdict).find_node(name)
    if attrdict.has_key("var_BDIR"):
	bdir = attrdict.get("var_BDIR")
    elif node and node.attributes.has_key("var_BDIR"):
	bdir = node.attributes.get("var_BDIR")
    else:
	bdir = get_var_val(0, recdict, "BDIR")
    if attrdict.has_key("var_OBJSUF"):
	objsuf = attrdict.get("var_OBJSUF")
    elif node and node.attributes.has_key("var_OBJSUF"):
	bdir = node.attributes.get("var_OBJSUF")
    else:
	objsuf = get_var_val(0, recdict, "OBJSUF")
    name = os.path.join(bdir, name)

    i = string.rfind(name, '.')
    if i > 0 and i > string.rfind(name, '/'):
	n = name[:i] + objsuf
    else:
	n = name + objsuf
    return n


def get_sys_option(cmd):
    """Check for system command options.  Ignore anything that is not a valid
       option.
       Return the remaining command and a dictionary of the options."""
    from Dictlist import parse_attr

    cmd_len = len(cmd)
    idx = 0
    dict = {}
    while 1:
	idx = skip_white(cmd, idx)
	if idx >= cmd_len or cmd[idx] != '{':
	    break
	try:
	    name, val, idx = parse_attr([], cmd, idx)
	except UserError:
	    # An error probably means the shell command starts with "{".
	    name = None

	if name == 'q' or name == 'quiet':
	    dict['quiet'] = val
	elif name == 'i' or name == 'interactive':
	    dict['interactive'] = val
	elif name == 'l' or name == 'log':
	    dict['log'] = val
	else:
	    break

    return cmd[idx:], dict


def logged_system(recdict, cmd):
    """Execute a system command.
       Display the command and log the output.
       Returns the return value of os.system()."""

    # 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.
    tmpfilex = ''
    if msg_logname():
	tmpfile = tempfname()
	if os.name == "posix":
	    tmpfilex = tempfname()
    else:
	tmpfile = None

    newcmd = ''
    append = ""
    for line in string.split(cmd, '\n'):
	if not line:
	    continue	# skip empty lines
	rest, dict = get_sys_option(line)
	if dict.get("quiet"):
	    msg_log(line, msgt_system)
	else:
	    msg_system(recdict, line)
	if dict.get("interactive"):
	    newcmd = newcmd + rest + '\n'
	elif dict.get("log"):
	    rd = ">"
	    if not tmpfile:	    # not logging, throw it in the bit bucket
		of = "/dev/null"
	    else:
		of = tmpfile	    # write or append to the logfile
		if append:
		    rd = ">>"
	    if os.name == "posix":
		newcmd = newcmd + ("{ %s; echo $? > %s; } 2>&1 %s%s\n"
						    % (rest, tmpfilex, rd, of))
	    else:
		if of == "/dev/null":
		    newcmd = newcmd + ("%s\n" % (rest))
		else:
		    newcmd = newcmd + ("%s %s%s\n" % (rest, rd, of))
	    append = "-a "	    # append next command output
	elif tmpfile and string.find(rest, '>') < 0:
	    if os.name == "posix":
		newcmd = newcmd + ("{ %s; echo $? > %s; } 2>&1 | tee %s%s\n"
					   % (rest, tmpfilex, append, tmpfile))
	    else:
		rd = ">"
		if append:
		    rd = ">>"
		newcmd = newcmd + ("%s %s%s\n" % (rest, rd, tmpfile))
	    append = "-a "	    # append next command output
	else:
	    newcmd = newcmd + rest + '\n'

    if os.name == "posix":
	res = os.system(newcmd)
    else:
	# system() on MS-Windows can handle only one command at a time and must
	# not end in a NL.  Do the same on other non-Unix systems for now.
	# TODO: system() isn't available on the Mac
	# TODO: system() always returns zero for Windows 9x
	for line in string.split(newcmd, '\n'):
	    res = os.system(line)
	    if res:
		break

    if tmpfile:
	# Read the file that contains the exit status of the executed command.
	# "res" could be the exit status of "tee".
	if os.name == "posix":
	    rr = '0'
	    try:
		f = open(tmpfilex)
		rr = f.read(1)[0]
		f.close()
	    except:
		pass
	    if rr != '0':
		res = int(rr)
		if res == 0:
		    res = 1

	# Append the output to the logfile.
	try:
	    f = open(tmpfile)
	    text = f.read()
	    f.close()
	except:
	    text = ''
	if text:
	    msg_log(text, msgt_result)
	    if os.name != "posix":
		# No "tee" command, echo the system command output.
		print text

	# Always delete the temp files
	try:
	    os.remove(tmpfile)
	except:
	    pass
	if tmpfilex:
	    try:
		os.remove(tmpfilex)
	    except:
		pass

    return res


def redir_system(cmd, use_tee = 1):
    """Execute "cmd" with the shell and catch the output.
       Calls redir_system_int() to do the real work, passing the global
       variable dictionary."""
    return redir_system_int(Global.globals, cmd, use_tee)


def redir_system_int(recdict, cmd, use_tee = 1):
    """Execute "cmd" with the shell and catch the output.
       Returns two things: a number (non-zero for success) and the output of
       the command.  The caller may want to use msg_log() for the text.
       """
    msg_system(recdict, cmd)

    tmpfile = tempfname()
    if os.name == "posix":
	tmpexit = tempfname()
	if use_tee:
	    cmd = "{ %s; echo $? > %s; } 2>&1 | tee %s" % (cmd, tmpexit, tmpfile)
	else:
	    cmd = "{ %s; echo $? > %s; } 2>&1 > %s" % (cmd, tmpexit, tmpfile)
    else:
	cmd = "%s > %s" % (cmd, tmpfile)
	tmpexit = ''

    try:
	try:
	    ok = (os.system(cmd) == 0)
	except StandardError, e:
	    msg_warning(recdict, (_('Executing "%s" failed: ') % cmd) + str(e))
	    ok = 0

	if tmpexit:
	    rr = '0'
	    try:
		f = open(tmpexit)
		rr = f.read(1)[0]
		f.close()
	    except:
		pass
	    if rr != '0':
		ok = 0

	# Read the output of the command, also when it failed (might explain
	# why it failed).
	text = ''
	try:
	    f = open(tmpfile)
	    text = f.read()
	    f.close()
	except StandardError, e:
	    msg_warning(recdict,
			 (_('Reading output of "%s" failed: ') % cmd) + str(e))
	    ok = 0

    # always remove the tempfiles, even when system() failed.
    finally:
	if tmpexit:
	    try:
		os.remove(tmpexit)
	    except:
		pass
	try:
	    os.remove(tmpfile)
	except:
	    pass

    return ok, text


def async_system(rpstack, recdict, cmd):
    """Run command "cmd" with the shell, without waiting for it to finish."""
    cmd, dict = get_sys_option(cmd)
    if not dict.get("quiet"):
	msg_system(recdict, cmd)
    try:
	if os.name == "posix":
	    # Unix: use fork/exec
	    if not os.fork():
		n = os.system(cmd)
		os._exit(n)
	else:
	    # MS-Windows: use spawn
	    if os.environ.has_key("SHELL"):
		shell = os.environ["SHELL"]
	    elif os.environ.has_key("COMSPEC"):
		shell = os.environ["COMSPEC"]
	    elif program_path("cmd.exe"):
		shell = "cmd.exe"
	    else:
		shell = "command.com"
	    os.spawnv(os.P_DETACH, shell, cmd)
    except StandardError, e:
	from Process import recipe_error
	recipe_error(rpstack, (_('Could not execute "%s": ') % cmd) + str(e))


def assert_aap_dir(recdict):
    """Create the "AAPDIR" directory if it doesn't exist yet.
       Also check if the old name exists and rename it (for backward
       compatibility).  TODO: remove this later.
       Return non-zero if it exists or could be created."""
    if not os.path.exists(Global.aap_dirname):
	# Rename the old AAP dir if present.  TODO: remove this later.
	if (os.path.exists(Global.aap_dirname_old)
		and os.path.isdir(Global.aap_dirname_old)):
	    try:
		os.rename(Global.aap_dirname_old, Global.aap_dirname)
		msg_warning(recdict, _('Renamed "%s" to "%s"')
				% (Global.aap_dirname_old, Global.aap_dirname))
	    except StandardError, e:
		msg_error(recdict,
			(_('Could not rename "%s" directory to %s: ')
		       % (Global.aap_dirname_old, Global.aap_dirname)) + str(e))
		return 0
	    return 1

	try:
	    os.mkdir(Global.aap_dirname)
	except StandardError, e:
	    msg_error(recdict, (_('Warning: Could not create "%s" directory: ')
						% Global.aap_dirname) + str(e))
	    return 0
    return 1


def in_aap_dir(fname):
    """Return the path of file "fname" in the aap directory."""
    return os.path.join(Global.aap_dirname, fname)


def skip_commands():
    """Return non-zero when build commands are to be skipped: -n and/or -t
       command line argument.  But don't skip when force_build was set."""
    return (not Global.force_build
	    and (Global.cmd_args.options.get("touch")
		or Global.cmd_args.options.get("nobuild")))


def program_path(name, path = None, pathext = None, skip = None):
    """Return the full name of a program "name" if it can be found in the
       search path.  None otherwise.
       If "path" is given, use it as the search path (string of items separated
       with os.pathsep).
       If "pathext" is given, try these suffixes (string of items separated
       with os.pathsep).
       If "skip" is given, skip this directory."""
    # Decide on the list of directories to examine.
    if not path is None:
	envpath = path
    elif os.environ.has_key('PATH'):
        envpath = os.environ['PATH']
    else:
        envpath = os.defpath
    if not type(envpath) is types.ListType:
	dirlist = string.split(envpath, os.pathsep)
    # When using a default path also include our own directory.
    if path is None:
	dirlist = [ Global.aap_rootdir ] + dirlist

    # Decide on the list of suffixes to use.
    if not pathext is None:
	if pathext:
	    if type(pathext) is types.ListType:
		extlist = pathext
	    else:
		extlist = string.split(pathext, os.pathsep)
	else:
	    extlist = [ '' ]	# empty pathext means not using a suffix
    elif os.environ.get('PATHEXT'):
        extlist = string.split(os.environ['PATHEXT'], os.pathsep)
    elif os.name in [ 'dos', 'os2', 'nt' ]:
	extlist = [ '', '.exe', '.com', '.bat', '.cmd' ]
    else:
	extlist = [ '' ] 
    # If the file name already includes an extention, don't try adding another
    # one.
    if string.find(os.path.basename(name), '.') > 0:
	extlist = [ '' ]

    for dir in dirlist:
	if skip and os.path.abspath(dir) == os.path.abspath(skip):
	    continue
	for ext in extlist:
	    fullname = os.path.join(dir, name) + ext
	    if os.path.isfile(fullname):
		if os.name == "posix":
		    try:
			st = os.stat(fullname)
		    except:
			continue
		    if (stat.S_IMODE(st[stat.ST_MODE]) & 0111) == 0:
			continue
		return fullname

    return None


def default_dirs():
    """Return a list of directories to look for default.aap, default.act,
       etc."""
    if os.name == "posix":
	return [ "/usr/local/share/aap",
	     os.path.expanduser(os.path.join("~", ".aap")) ]

    if os.environ.get("HOME"):
	res = [ os.path.join(os.environ.get("HOME"), "aap") ]
    elif os.environ.get("HOMEDRIVE"):
	h = os.environ.get("HOMEDRIVE")
	if os.environ.get("HOMEPATH"):
	    h = os.path.join(h, os.environ.get("HOMEPATH"))
	else:
	    h = h + '/'
	res = [ os.path.join(h, "aap") ]
    elif os.path.isdir("c:/"):
	res = [ "c:/aap" ]
    else:
	res = []
    
    return res


# Compiling a regexp is slow.  This cache keeps the result.
compiled_re = {}

def cre_match(regexp, text):
    """Call re.match while caching the compiled regexp."""
    if compiled_re.has_key(regexp):
	cre = compiled_re[regexp]
    else:
	cre = re.compile(regexp)
	compiled_re[regexp] = cre
    return cre.match(text)

def cre_search(regexp, text):
    """Call re.search while caching the compiled regexp."""
    if compiled_re.has_key(regexp):
	cre = compiled_re[regexp]
    else:
	cre = re.compile(regexp)
	compiled_re[regexp] = cre
    return cre.search(text)


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