text: # -*- coding: utf-8 -*-
  '''
  Approximate the Unix find(1) command and return a list of paths that
  meet the specified criteria.
  
  The options include match criteria:
      name    = file-glob                 # case sensitive
      iname   = file-glob                 # case insensitive
      regex   = file-regex                # case sensitive
      iregex  = file-regex                # case insensitive
      type    = file-types                # match any listed type
      user    = users                     # match any listed user
      group   = groups                    # match any listed group
      size    = [+-]number[size-unit]     # default unit = byte
      mtime   = interval                  # modified since date
      grep    = regex                     # search file contents
  and/or actions:
      delete [= file-types]               # default type = 'f'
      exec    = command [arg ...]         # where {} is replaced by pathname
      print  [= print-opts]
  and/or depth criteria:
     maxdepth = maximum depth to transverse in path
     mindepth = minimum depth to transverse before checking files or directories
  
  The default action is 'print=path'.
  
  file-glob:
      *                = match zero or more chars
      ?                = match any char
      [abc]            = match a, b, or c
      [!abc] or [^abc] = match anything except a, b, and c
      [x-y]            = match chars x through y
      [!x-y] or [^x-y] = match anything except chars x through y
      {a,b,c}          = match a or b or c
  
  file-regex:
      a Python re (regular expression) pattern
  
  file-types: a string of one or more of the following:
      a: all file types
      b: block device
      c: character device
      d: directory
      p: FIFO (named pipe)
      f: plain file
      l: symlink
      s: socket
  
  users:
      a space and/or comma separated list of user names and/or uids
  
  groups:
      a space and/or comma separated list of group names and/or gids
  
  size-unit:
      b: bytes
      k: kilobytes
      m: megabytes
      g: gigabytes
      t: terabytes
  
  interval:
      [<num>w] [<num>[d]] [<num>h] [<num>m] [<num>s]
  
      where:
          w: week
          d: day
          h: hour
          m: minute
          s: second
  
  print-opts: a comma and/or space separated list of one or more of
  the following:
  
      group: group name
      md5:   MD5 digest of file contents
      mode:  file permissions (as as integer)
      mtime: last modification time (as time_t)
      name:  file basename
      path:  file absolute path
      size:  file size in bytes
      type:  file type
      user:  user name
  '''
  
  # Import python libs
  from __future__ import absolute_import, print_function
  import logging
  import os
  import re
  import stat
  import shutil
  import sys
  import time
  from subprocess import Popen, PIPE
  try:
      import grp
      import pwd
      # TODO: grp and pwd are both used in the code, we better make sure that
      # that code never gets run if importing them does not succeed
  except ImportError:
      pass
  
  # Import 3rd-party libs
  import salt.ext.six as six
  
  # Import salt libs
  import salt.utils
  import salt.defaults.exitcodes
  from salt.utils.filebuffer import BufferedReader
  
  # Set up logger
  log = logging.getLogger(__name__)
  
  _REQUIRES_PATH = 1
  _REQUIRES_STAT = 2
  _REQUIRES_CONTENTS = 4
  
  _FILE_TYPES = {'b': stat.S_IFBLK,
                 'c': stat.S_IFCHR,
                 'd': stat.S_IFDIR,
                 'f': stat.S_IFREG,
                 'l': stat.S_IFLNK,
                 'p': stat.S_IFIFO,
                 's': stat.S_IFSOCK,
                 stat.S_IFBLK: 'b',
                 stat.S_IFCHR: 'c',
                 stat.S_IFDIR: 'd',
                 stat.S_IFREG: 'f',
                 stat.S_IFLNK: 'l',
                 stat.S_IFIFO: 'p',
                 stat.S_IFSOCK: 's'}
  
  _INTERVAL_REGEX = re.compile(r'''
                               ^\s*
                               (?P<modifier>[+-]?)
                               (?: (?P<week>   \d+ (?:\.\d*)? ) \s* [wW]  )? \s*
                               (?: (?P<day>    \d+ (?:\.\d*)? ) \s* [dD]  )? \s*
                               (?: (?P<hour>   \d+ (?:\.\d*)? ) \s* [hH]  )? \s*
                               (?: (?P<minute> \d+ (?:\.\d*)? ) \s* [mM]  )? \s*
                               (?: (?P<second> \d+ (?:\.\d*)? ) \s* [sS]  )? \s*
                               $
                               ''',
                               flags=re.VERBOSE)
  
  
  def _parse_interval(value):
      '''
      Convert an interval string like 1w3d6h into the number of seconds, time
      resolution (1 unit of the smallest specified time unit) and the modifier(
      '+', '-', or '').
          w = week
          d = day
          h = hour
          m = minute
          s = second
      '''
      match = _INTERVAL_REGEX.match(str(value))
      if match is None:
          raise ValueError('invalid time interval: \'{0}\''.format(value))
  
      result = 0
      resolution = None
      for name, multiplier in [('second', 1),
                               ('minute', 60),
                               ('hour', 60 * 60),
                               ('day', 60 * 60 * 24),
                               ('week', 60 * 60 * 24 * 7)]:
          if match.group(name) is not None:
              result += float(match.group(name)) * multiplier
              if resolution is None:
                  resolution = multiplier
  
      return result, resolution, match.group('modifier')
  
  
  def _parse_size(value):
      scalar = value.strip()
  
      if scalar.startswith(('-', '+')):
          style = scalar[0]
          scalar = scalar[1:]
      else:
          style = '='
  
      if len(scalar) > 0:
          multiplier = {'b': 2 ** 0,
                        'k': 2 ** 10,
                        'm': 2 ** 20,
                        'g': 2 ** 30,
                        't': 2 ** 40}.get(scalar[-1].lower())
          if multiplier:
              scalar = scalar[:-1].strip()
          else:
              multiplier = 1
      else:
          multiplier = 1
  
      try:
          num = int(scalar) * multiplier
      except ValueError:
          try:
              num = int(float(scalar) * multiplier)
          except ValueError:
              raise ValueError('invalid size: "{0}"'.format(value))
  
      if style == '-':
          min_size = 0
          max_size = num
      elif style == '+':
          min_size = num
          max_size = six.MAXSIZE
      else:
          min_size = num
          max_size = num + multiplier - 1
  
      return min_size, max_size
  
  
  class Option(object):
      '''
      Abstract base class for all find options.
      '''
      def requires(self):
          return _REQUIRES_PATH
  
  
  class NameOption(Option):
      '''
      Match files with a case-sensitive glob filename pattern.
      Note: this is the 'basename' portion of a pathname.
      The option name is 'name', e.g. {'name' : '*.txt'}.
      '''
      def __init__(self, key, value):
          self.regex = re.compile(value.replace('.', '\\.')
                                       .replace('?', '.?')
                                       .replace('*', '.*') + '$')
  
      def match(self, dirname, filename, fstat):
          return self.regex.match(filename)
  
  
  class InameOption(Option):
      '''
      Match files with a case-insensitive glob filename pattern.
      Note: this is the 'basename' portion of a pathname.
      The option name is 'iname', e.g. {'iname' : '*.TXT'}.
      '''
      def __init__(self, key, value):
          self.regex = re.compile(value.replace('.', '\\.')
                                       .replace('?', '.?')
                                       .replace('*', '.*') + '$',
                                  re.IGNORECASE)
  
      def match(self, dirname, filename, fstat):
          return self.regex.match(filename)
  
  
  class RegexOption(Option):
      '''
      Match files with a case-sensitive regular expression.
      Note: this is the 'basename' portion of a pathname.
      The option name is 'regex', e.g. {'regex' : '.*\\.txt'}.
      '''
      def __init__(self, key, value):
          try:
              self.regex = re.compile(value)
          except re.error:
              raise ValueError('invalid regular expression: "{0}"'.format(value))
  
      def match(self, dirname, filename, fstat):
          return self.regex.match(filename)
  
  
  class IregexOption(Option):
      '''
      Match files with a case-insensitive regular expression.
      Note: this is the 'basename' portion of a pathname.
      The option name is 'iregex', e.g. {'iregex' : '.*\\.txt'}.
      '''
      def __init__(self, key, value):
          try:
              self.regex = re.compile(value, re.IGNORECASE)
          except re.error:
              raise ValueError('invalid regular expression: "{0}"'.format(value))
  
      def match(self, dirname, filename, fstat):
          return self.regex.match(filename)
  
  
  class TypeOption(Option):
      '''
      Match files by their file type(s).
      The file type(s) are specified as an optionally comma and/or space
      separated list of letters.
          b = block device
          c = character device
          d = directory
          f = regular (plain) file
          l = symbolic link
          p = FIFO (named pipe)
          s = socket
      The option name is 'type', e.g. {'type' : 'd'} or {'type' : 'bc'}.
      '''
      def __init__(self, key, value):
          # remove whitespace and commas
          value = "".join(value.strip().replace(',', '').split())
          self.ftypes = set()
          for ftype in value:
              try:
                  self.ftypes.add(_FILE_TYPES[ftype])
              except KeyError:
                  raise ValueError('invalid file type "{0}"'.format(ftype))
  
      def requires(self):
          return _REQUIRES_STAT
  
      def match(self, dirname, filename, fstat):
          return stat.S_IFMT(fstat[stat.ST_MODE]) in self.ftypes
  
  
  class OwnerOption(Option):
      '''
      Match files by their owner name(s) and/or uid(s), e.g. 'root'.
      The names are a space and/or comma separated list of names and/or integers.
      A match occurs when the file's uid matches any user specified.
      The option name is 'owner', e.g. {'owner' : 'root'}.
      '''
      def __init__(self, key, value):
          self.uids = set()
          for name in value.replace(',', ' ').split():
              if name.isdigit():
                  self.uids.add(int(name))
              else:
                  try:
                      self.uids.add(pwd.getpwnam(value).pw_uid)
                  except KeyError:
                      raise ValueError('no such user "{0}"'.format(name))
  
      def requires(self):
          return _REQUIRES_STAT
  
      def match(self, dirname, filename, fstat):
          return fstat[stat.ST_UID] in self.uids
  
  
  class GroupOption(Option):
      '''
      Match files by their group name(s) and/or uid(s), e.g. 'admin'.
      The names are a space and/or comma separated list of names and/or integers.
      A match occurs when the file's gid matches any group specified.
      The option name is 'group', e.g. {'group' : 'admin'}.
      '''
      def __init__(self, key, value):
          self.gids = set()
          for name in value.replace(',', ' ').split():
              if name.isdigit():
                  self.gids.add(int(name))
              else:
                  try:
                      self.gids.add(grp.getgrnam(value).gr_gid)
                  except KeyError:
                      raise ValueError('no such group "{0}"'.format(name))
  
      def requires(self):
          return _REQUIRES_STAT
  
      def match(self, dirname, filename, fstat):
          return fstat[stat.ST_GID] in self.gids
  
  
  class SizeOption(Option):
      '''
      Match files by their size.
      Prefix the size with '-' to find files the specified size and smaller.
      Prefix the size with '+' to find files the specified size and larger.
      Without the +/- prefix, match the exact file size.
      The size can be suffixed with (case-insensitive) suffixes:
          b = bytes
          k = kilobytes
          m = megabytes
          g = gigabytes
          t = terabytes
      The option name is 'size', e.g. {'size' : '+1G'}.
      '''
      def __init__(self, key, value):
          self.min_size, self.max_size = _parse_size(value)
  
      def requires(self):
          return _REQUIRES_STAT
  
      def match(self, dirname, filename, fstat):
          return self.min_size <= fstat[stat.ST_SIZE] <= self.max_size
  
  
  class MtimeOption(Option):
      '''
      Match files modified since the specified time.
      The option name is 'mtime', e.g. {'mtime' : '3d'}.
      The value format is [<num>w] [<num>[d]] [<num>h] [<num>m] [<num>s]
      where num is an integer or float and the case-insensitive suffixes are:
          w = week
          d = day
          h = hour
          m = minute
          s = second
      Whitespace is ignored in the value.
      '''
      def __init__(self, key, value):
          secs, resolution, modifier = _parse_interval(value)
          self.mtime = time.time() - int(secs / resolution) * resolution
          self.modifier = modifier
  
      def requires(self):
          return _REQUIRES_STAT
  
      def match(self, dirname, filename, fstat):
          if self.modifier == '-':
              return fstat[stat.ST_MTIME] >= self.mtime
          else:
              return fstat[stat.ST_MTIME] <= self.mtime
  
  
  class GrepOption(Option):
      '''Match files when a pattern occurs within the file.
      The option name is 'grep', e.g. {'grep' : '(foo)|(bar}'}.
      '''
      def __init__(self, key, value):
          try:
              self.regex = re.compile(value)
          except re.error:
              raise ValueError('invalid regular expression: "{0}"'.format(value))
  
      def requires(self):
          return _REQUIRES_CONTENTS | _REQUIRES_STAT
  
      def match(self, dirname, filename, fstat):
          if not stat.S_ISREG(fstat[stat.ST_MODE]):
              return None
          dfilename = os.path.join(dirname, filename)
          with BufferedReader(dfilename, mode='rb') as bread:
              for chunk in bread:
                  if self.regex.search(chunk):
                      return dfilename
          return None
  
  
  class PrintOption(Option):
      '''
      Return information about a matched file.
      Print options are specified as a comma and/or space separated list of
      one or more of the following:
          group  = group name
          md5    = MD5 digest of file contents
          mode   = file mode (as integer)
          mtime  = last modification time (as time_t)
          name   = file basename
          path   = file absolute path
          size   = file size in bytes
          type   = file type
          user   = user name
      '''
      def __init__(self, key, value):
          self.need_stat = False
          self.print_title = False
          self.fmt = []
          for arg in value.replace(',', ' ').split():
              self.fmt.append(arg)
              if arg not in ['name', 'path']:
                  self.need_stat = True
          if len(self.fmt) == 0:
              self.fmt.append('path')
  
      def requires(self):
          return _REQUIRES_STAT if self.need_stat else _REQUIRES_PATH
  
      def execute(self, fullpath, fstat, test=False):
          result = []
          for arg in self.fmt:
              if arg == 'path':
                  result.append(fullpath)
              elif arg == 'name':
                  result.append(os.path.basename(fullpath))
              elif arg == 'size':
                  result.append(fstat[stat.ST_SIZE])
              elif arg == 'type':
                  result.append(
                      _FILE_TYPES.get(stat.S_IFMT(fstat[stat.ST_MODE]), '?')
                  )
              elif arg == 'mode':
                  result.append(int(oct(fstat[stat.ST_MODE])[-3:]))
              elif arg == 'mtime':
                  result.append(fstat[stat.ST_MTIME])
              elif arg == 'user':
                  uid = fstat[stat.ST_UID]
                  try:
                      result.append(pwd.getpwuid(uid).pw_name)
                  except KeyError:
                      result.append(uid)
              elif arg == 'group':
                  gid = fstat[stat.ST_GID]
                  try:
                      result.append(grp.getgrgid(gid).gr_name)
                  except KeyError:
                      result.append(gid)
              elif arg == 'md5':
                  if stat.S_ISREG(fstat[stat.ST_MODE]):
                      md5digest = salt.utils.get_hash(fullpath, 'md5')
                      result.append(md5digest)
                  else:
                      result.append('')
  
          if len(result) == 1:
              return result[0]
          else:
              return result
  
  
  class DeleteOption(TypeOption):
      '''
      Deletes matched file.
      Delete options are one or more of the following:
          a: all file types
          b: block device
          c: character device
          d: directory
          p: FIFO (named pipe)
          f: plain file
          l: symlink
          s: socket
      '''
      def __init__(self, key, value):
          if 'a' in value:
              value = 'bcdpfls'
          super(self.__class__, self).__init__(key, value)
  
      def execute(self, fullpath, fstat, test=False):
          if test:
              return fullpath
          try:
              if os.path.isfile(fullpath) or os.path.islink(fullpath):
                  os.remove(fullpath)
              elif os.path.isdir(fullpath):
                  shutil.rmtree(fullpath)
          except (OSError, IOError) as exc:
              return None
          return fullpath
  
  
  class ExecOption(Option):
      '''
      Execute the given command, {} replaced by filename.
      Quote the {} if commands might include whitespace.
      '''
      def __init__(self, key, value):
          self.command = value
  
      def execute(self, fullpath, fstat, test=False):
          try:
              command = self.command.replace('{}', fullpath)
              print(salt.utils.shlex_split(command))
              p = Popen(salt.utils.shlex_split(command),
                        stdout=PIPE,
                        stderr=PIPE)
              (out, err) = p.communicate()
              if err:
                  log.error(
                      'Error running command: {0}\n\n{1}'.format(
                      command,
                      salt.utils.to_str(err)))
              return "{0}:\n{1}\n".format(command, salt.utils.to_str(out))
  
          except Exception as e:
              log.error(
                  'Exception while executing command "{0}":\n\n{1}'.format(
                      command,
                      e))
              return '{0}: Failed'.format(fullpath)
  
  
  class Finder(object):
      def __init__(self, options):
          self.actions = []
          self.maxdepth = None
          self.mindepth = 0
          self.test = False
          criteria = {_REQUIRES_PATH: list(),
                      _REQUIRES_STAT: list(),
                      _REQUIRES_CONTENTS: list()}
          if 'mindepth' in options:
              self.mindepth = options['mindepth']
              del options['mindepth']
          if 'maxdepth' in options:
              self.maxdepth = options['maxdepth']
              del options['maxdepth']
          if 'test' in options:
              self.test = options['test']
              del options['test']
          for key, value in six.iteritems(options):
              if key.startswith('_'):
                  # this is a passthrough object, continue
                  continue
              if value is None or len(str(value)) == 0:
                  raise ValueError('missing value for "{0}" option'.format(key))
              try:
                  obj = globals()[key.title() + "Option"](key, value)
              except KeyError:
                  raise ValueError('invalid option "{0}"'.format(key))
              if hasattr(obj, 'match'):
                  requires = obj.requires()
                  if requires & _REQUIRES_CONTENTS:
                      criteria[_REQUIRES_CONTENTS].append(obj)
                  elif requires & _REQUIRES_STAT:
                      criteria[_REQUIRES_STAT].append(obj)
                  else:
                      criteria[_REQUIRES_PATH].append(obj)
              if hasattr(obj, 'execute'):
                  self.actions.append(obj)
          if len(self.actions) == 0:
              self.actions.append(PrintOption('print', ''))
          # order criteria so that least expensive checks are done first
          self.criteria = criteria[_REQUIRES_PATH] + \
                          criteria[_REQUIRES_STAT] + \
                          criteria[_REQUIRES_CONTENTS]
  
      def find(self, path):
          '''
          Generate filenames in path that satisfy criteria specified in
          the constructor.
          This method is a generator and should be repeatedly called
          until there are no more results.
          '''
          for dirpath, dirs, files in os.walk(path):
              depth = dirpath[len(path) + len(os.path.sep):].count(os.path.sep)
              if depth >= self.mindepth:
                  for name in dirs + files:
                      fstat = None
                      matches = True
                      fullpath = None
                      for criterion in self.criteria:
                          if fstat is None and criterion.requires() & _REQUIRES_STAT:
                              fullpath = os.path.join(dirpath, name)
                              try:
                                  fstat = os.stat(fullpath)
                              except OSError:
                                  fstat = os.lstat(fullpath)
                          if not criterion.match(dirpath, name, fstat):
                              matches = False
                              break
  
                      if matches:
                          if fullpath is None:
                              fullpath = os.path.join(dirpath, name)
                          for action in self.actions:
                              if (fstat is None and
                                      action.requires() & _REQUIRES_STAT):
                                  try:
                                      fstat = os.stat(fullpath)
                                  except OSError:
                                      fstat = os.lstat(fullpath)
                              result = action.execute(fullpath, fstat, test=self.test)
                              if result is not None:
                                  yield result
  
              if depth == self.maxdepth:
                  dirs[:] = []
  
  
  def find(path, options):
      '''
      WRITEME
      '''
      finder = Finder(options)
      for path in finder.find(path):
          yield path
  
  
  def _main():
      if len(sys.argv) < 2:
          sys.stderr.write('usage: {0} path [options]\n'.format(sys.argv[0]))
          sys.exit(salt.defaults.exitcodes.EX_USAGE)
  
      path = sys.argv[1]
      criteria = {}
  
      for arg in sys.argv[2:]:
          key, value = arg.split('=')
          criteria[key] = value
      try:
          finder = Finder(criteria)
      except ValueError as ex:
          sys.stderr.write('error: {0}\n'.format(ex))
          sys.exit(salt.defaults.exitcodes.EX_GENERIC)
  
      for result in finder.find(path):
          print(result)
  
  if __name__ == '__main__':
      _main()
  