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