text: #!/usr/bin/python2.5
  
  # Copyright (C) 2007 Google Inc.
  #
  # Licensed under the Apache License, Version 2.0 (the "License");
  # you may not use this file except in compliance with the License.
  # You may obtain a copy of the License at
  #
  #      http://www.apache.org/licenses/LICENSE-2.0
  #
  # Unless required by applicable law or agreed to in writing, software
  # distributed under the License is distributed on an "AS IS" BASIS,
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  # See the License for the specific language governing permissions and
  # limitations under the License.
  
  
  """Validates a GTFS file.
  
  For usage information run feedvalidator.py --help
  """
  
  import bisect
  import codecs
  import datetime
  from transitfeed.util import defaultdict
  import optparse
  import os
  import os.path
  import re
  import sys
  import time
  import transitfeed
  from transitfeed import TYPE_ERROR, TYPE_WARNING, TYPE_NOTICE
  from transitfeed import util
  import webbrowser
  
  def MaybePluralizeWord(count, word):
    if count == 1:
      return word
    else:
      return word + 's'
  
  
  def PrettyNumberWord(count, word):
    return '%d %s' % (count, MaybePluralizeWord(count, word))
  
  
  def UnCamelCase(camel):
    return re.sub(r'([a-z])([A-Z])', r'\1 \2', camel)
  
  
  def ProblemCountText(error_count, warning_count):
    results = []
    if error_count:
      results.append(PrettyNumberWord(error_count, 'error'))
    if warning_count:
      results.append(PrettyNumberWord(warning_count, 'warning'))
  
    return ' and '.join(results)
  
  
  def CalendarSummary(schedule):
    today = datetime.date.today()
    summary_end_date = today + datetime.timedelta(days=60)
    start_date, end_date = schedule.GetDateRange()
  
    if not start_date or not end_date:
      return {}
  
    start_date_object = transitfeed.DateStringToDateObject(start_date)
    end_date_object = transitfeed.DateStringToDateObject(end_date)
    if not start_date_object or not end_date_object:
      return {}
  
    # Get the list of trips only during the period the feed is active.
    # As such we have to check if it starts in the future and/or if
    # if it ends in less than 60 days.
    date_trips_departures = schedule.GenerateDateTripsDeparturesList(
                                max(today, start_date_object),
                                min(summary_end_date, end_date_object))
  
    if not date_trips_departures:
      return {}
  
    # Check that the dates which will be shown in summary agree with these
    # calculations. Failure implies a bug which should be fixed. It isn't good
    # for users to discover assertion failures but means it will likely be fixed.
    assert start_date <= date_trips_departures[0][0].strftime("%Y%m%d")
    assert end_date >= date_trips_departures[-1][0].strftime("%Y%m%d")
  
    # Generate a map from int number of trips in a day to a list of date objects
    # with that many trips. The list of dates is sorted.
    trips_dates = defaultdict(lambda: [])
    trips = 0
    for date, day_trips, day_departures in date_trips_departures:
      trips += day_trips
      trips_dates[day_trips].append(date)
    mean_trips = trips / len(date_trips_departures)
    max_trips = max(trips_dates.keys())
    min_trips = min(trips_dates.keys())
  
    calendar_summary = {}
    calendar_summary['mean_trips'] = mean_trips
    calendar_summary['max_trips'] = max_trips
    calendar_summary['max_trips_dates'] = FormatDateList(trips_dates[max_trips])
    calendar_summary['min_trips'] = min_trips
    calendar_summary['min_trips_dates'] = FormatDateList(trips_dates[min_trips])
    calendar_summary['date_trips_departures'] = date_trips_departures
    calendar_summary['date_summary_range'] = "%s to %s" % (
        date_trips_departures[0][0].strftime("%a %b %d"),
        date_trips_departures[-1][0].strftime("%a %b %d"))
  
    return calendar_summary
  
  
  def FormatDateList(dates):
    if not dates:
      return "0 service dates"
  
    formatted = [d.strftime("%a %b %d") for d in dates[0:3]]
    if len(dates) > 3:
      formatted.append("...")
    return "%s (%s)" % (PrettyNumberWord(len(dates), "service date"),
                        ", ".join(formatted))
  
  
  class CountingConsoleProblemAccumulator(transitfeed.SimpleProblemAccumulator):
    """Accumulate problems and count errors and warnings.
  
    Args:
      ignore_types: list of error type names that will be ignored. E.g.
                    ['ExpirationDate', 'UnusedStop']
    """
  
    def __init__(self, ignore_types=None):
      self._error_count = 0
      self._warning_count = 0
      self._notice_count = 0
      self._ignore_types = ignore_types or set()
  
    def _Report(self, e):
      if e.__class__.__name__ in self._ignore_types:
        return
      transitfeed.SimpleProblemAccumulator._Report(self, e)
      if e.IsError():
        self._error_count += 1
      elif e.IsWarning():
        self._warning_count += 1
      elif e.IsNotice():
        self._notice_count += 1
  
    def ErrorCount(self):
      return self._error_count
  
    def WarningCount(self):
      return self._warning_count
  
    def NoticeCount(self):
      return self._notice_count
  
    def FormatCount(self):
      return ProblemCountText(self.ErrorCount(), self.WarningCount())
  
    def HasIssues(self):
      return self.ErrorCount() or self.WarningCount()
  
    def HasNotices(self):
      return self.NoticeCount()
  
  class BoundedProblemList(object):
    """A list of one type of ExceptionWithContext objects with bounded size."""
    def __init__(self, size_bound):
      self._count = 0
      self._exceptions = []
      self._size_bound = size_bound
  
    def Add(self, e):
      self._count += 1
      try:
        bisect.insort(self._exceptions, e)
      except TypeError:
        # The base class ExceptionWithContext raises this exception in __cmp__
        # to signal that an object is not comparable. Instead of keeping the most
        # significant issue keep the first reported.
        if self._count <= self._size_bound:
          self._exceptions.append(e)
      else:
        # self._exceptions is in order. Drop the least significant if the list is
        # now too long.
        if self._count > self._size_bound:
          del self._exceptions[-1]
  
    def _GetDroppedCount(self):
      return self._count - len(self._exceptions)
  
    def __repr__(self):
      return "<BoundedProblemList %s>" % repr(self._exceptions)
  
    count = property(lambda s: s._count)
    dropped_count = property(_GetDroppedCount)
    problems = property(lambda s: s._exceptions)
  
  
  class LimitPerTypeProblemAccumulator(transitfeed.ProblemAccumulatorInterface):
    """Accumulate problems up to a maximum number per type.
  
    Args:
      limit_per_type: maximum number of errors and warnings to keep of each type
      ignore_types: list of error type names that will be ignored. E.g.
                    ['ExpirationDate', 'UnusedStop']
    """
  
    def __init__(self, limit_per_type, ignore_types=None):
      # {TYPE_WARNING: {"ClassName": BoundedProblemList()}}
      self._type_to_name_to_problist = {
        TYPE_WARNING: defaultdict(lambda: BoundedProblemList(limit_per_type)),
        TYPE_ERROR: defaultdict(lambda: BoundedProblemList(limit_per_type)),
        TYPE_NOTICE: defaultdict(lambda: BoundedProblemList(limit_per_type))
      }
      self._ignore_types = ignore_types or set()
  
    def HasIssues(self):
      return (self._type_to_name_to_problist[TYPE_ERROR] or
              self._type_to_name_to_problist[TYPE_WARNING])
  
    def HasNotices(self):
      return self._type_to_name_to_problist[TYPE_NOTICE]
  
    def _Report(self, e):
      if e.__class__.__name__ in self._ignore_types:
        return
      self._type_to_name_to_problist[e.GetType()][e.__class__.__name__].Add(e)
  
    def ErrorCount(self):
      error_sets = self._type_to_name_to_problist[TYPE_ERROR].values()
      return sum(map(lambda v: v.count, error_sets))
  
    def WarningCount(self):
      warning_sets = self._type_to_name_to_problist[TYPE_WARNING].values()
      return sum(map(lambda v: v.count, warning_sets))
  
    def ProblemList(self, problem_type, class_name):
      """Return the BoundedProblemList object for given type and class."""
      return self._type_to_name_to_problist[problem_type][class_name]
  
    def ProblemListMap(self, problem_type):
      """Return the map from class name to BoundedProblemList object."""
      return self._type_to_name_to_problist[problem_type]
  
  
  class HTMLCountingProblemAccumulator(LimitPerTypeProblemAccumulator):
    def FormatType(self, level_name, class_problist):
      """Write the HTML dumping all problems of one type.
  
      Args:
        level_name: string such as "Error" or "Warning"
        class_problist: sequence of tuples (class name,
            BoundedProblemList object)
  
      Returns:
        HTML in a string
      """
      class_problist.sort()
      output = []
      for classname, problist in class_problist:
        output.append('<h4 class="issueHeader"><a name="%s%s">%s</a></h4><ul>\n' %
                      (level_name, classname, UnCamelCase(classname)))
        for e in problist.problems:
          self.FormatException(e, output)
        if problist.dropped_count:
          output.append('<li>and %d more of this type.' %
                        (problist.dropped_count))
        output.append('</ul>\n')
      return ''.join(output)
  
    def FormatTypeSummaryTable(self, level_name, name_to_problist):
      """Return an HTML table listing the number of problems by class name.
  
      Args:
        level_name: string such as "Error" or "Warning"
        name_to_problist: dict mapping class name to an BoundedProblemList object
  
      Returns:
        HTML in a string
      """
      output = []
      output.append('<table>')
      for classname in sorted(name_to_problist.keys()):
        problist = name_to_problist[classname]
        human_name = MaybePluralizeWord(problist.count, UnCamelCase(classname))
        output.append('<tr><td>%d</td><td><a href="#%s%s">%s</a></td></tr>\n' %
                      (problist.count, level_name, classname, human_name))
      output.append('</table>\n')
      return ''.join(output)
  
    def FormatException(self, e, output):
      """Append HTML version of e to list output."""
      d = e.GetDictToFormat()
      for k in ('file_name', 'feedname', 'column_name'):
        if k in d.keys():
          d[k] = '<code>%s</code>' % d[k]
      if 'url' in d.keys():
        d['url'] = '<a href="%(url)s">%(url)s</a>' % d
  
      problem_text = e.FormatProblem(d).replace('\n', '<br>')
      problem_class = 'problem'
      if e.IsNotice():
        problem_class += ' notice'
      output.append('<li>')
      output.append('<div class="%s">%s</div>' %
                    (problem_class, transitfeed.EncodeUnicode(problem_text)))
      try:
        if hasattr(e, 'row_num'):
          line_str = 'line %d of ' % e.row_num
        else:
          line_str = ''
        output.append('in %s<code>%s</code><br>\n' %
                      (line_str, transitfeed.EncodeUnicode(e.file_name)))
        row = e.row
        headers = e.headers
        column_name = e.column_name
        table_header = ''  # HTML
        table_data = ''  # HTML
        for header, value in zip(headers, row):
          attributes = ''
          if header == column_name:
            attributes = ' class="problem"'
          table_header += '<th%s>%s</th>' % (attributes, header)
          table_data += '<td%s>%s</td>' % (attributes, value)
        # Make sure output is encoded into UTF-8
        output.append('<table class="dump"><tr>%s</tr>\n' %
                      transitfeed.EncodeUnicode(table_header))
        output.append('<tr>%s</tr></table>\n' %
                      transitfeed.EncodeUnicode(table_data))
      except AttributeError as e:
        pass  # Hope this was getting an attribute from e ;-)
      output.append('<br></li>\n')
  
    def FormatCount(self):
      return ProblemCountText(self.ErrorCount(), self.WarningCount())
  
    def CountTable(self):
      output = []
      output.append('<table class="count_outside">\n')
      output.append('<tr>')
      if self.ProblemListMap(TYPE_ERROR):
        output.append('<td><span class="fail">%s</span></td>' %
                      PrettyNumberWord(self.ErrorCount(), "error"))
      if self.ProblemListMap(TYPE_WARNING):
        output.append('<td><span class="fail">%s</span></td>' %
                      PrettyNumberWord(self.WarningCount(), "warning"))
      output.append('</tr>\n<tr>')
      if self.ProblemListMap(TYPE_ERROR):
        output.append('<td>\n')
        output.append(self.FormatTypeSummaryTable("Error",
                      self.ProblemListMap(TYPE_ERROR)))
        output.append('</td>\n')
      if self.ProblemListMap(TYPE_WARNING):
        output.append('<td>\n')
        output.append(self.FormatTypeSummaryTable("Warning",
                      self.ProblemListMap(TYPE_WARNING)))
        output.append('</td>\n')
      output.append('</table>')
      return ''.join(output)
  
    def WriteOutput(self, feed_location, f, schedule, extension):
      """Write the html output to f."""
      if self.HasIssues():
        if self.ErrorCount() + self.WarningCount() == 1:
          summary = ('<span class="fail">Found this problem:</span>\n%s' %
                     self.CountTable())
        else:
          summary = ('<span class="fail">Found these problems:</span>\n%s' %
                     self.CountTable())
      else:
        summary = '<span class="pass">feed validated successfully</span>'
  
      if self.HasNotices():
        summary = ('<h3 class="issueHeader">Notices:</h3>' + 
            self.FormatType("Notice", self.ProblemListMap(TYPE_NOTICE).items()) +
            summary)
  
      basename = os.path.basename(feed_location)
      feed_path = (feed_location[:feed_location.rfind(basename)], basename)
  
      agencies = ', '.join(['<a href="%s">%s</a>' % (a.agency_url, a.agency_name)
                            for a in schedule.GetAgencyList()])
      if not agencies:
        agencies = '?'
  
      dates = "No valid service dates found"
      (start, end) = schedule.GetDateRange()
      if start and end:
        def FormatDate(yyyymmdd):
          src_format = "%Y%m%d"
          dst_format = "%B %d, %Y"
          try:
            return time.strftime(dst_format,
                                 time.strptime(yyyymmdd, src_format))
          except ValueError:
            return yyyymmdd
  
        formatted_start = FormatDate(start)
        formatted_end = FormatDate(end)
        dates = "%s to %s" % (formatted_start, formatted_end)
  
      calendar_summary = CalendarSummary(schedule)
      if calendar_summary:
        calendar_summary_html = """<br>
  During the upcoming service dates %(date_summary_range)s:
  <table>
  <tr><th class="header">Average trips per date:</th><td class="header">%(mean_trips)s</td></tr>
  <tr><th class="header">Most trips on a date:</th><td class="header">%(max_trips)s, on %(max_trips_dates)s</td></tr>
  <tr><th class="header">Least trips on a date:</th><td class="header">%(min_trips)s, on %(min_trips_dates)s</td></tr>
  </table>""" % calendar_summary
      else:
        calendar_summary_html = ""
  
      output_prefix = """
  <html>
  <head>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  <title>FeedValidator: %(feed_file)s</title>
  <style>
  body {font-family: Georgia, serif; background-color: white}
  .path {color: gray}
  div.problem {max-width: 500px}
  table.dump td,th {background-color: khaki; padding: 2px; font-family:monospace}
  table.dump td.problem,th.problem {background-color: dc143c; color: white; padding: 2px; font-family:monospace}
  table.count_outside td {vertical-align: top}
  table.count_outside {border-spacing: 0px; }
  table {border-spacing: 5px 0px; margin-top: 3px}
  h3.issueHeader {padding-left: 0.5em}
  h4.issueHeader {padding-left: 1em}
  .pass {background-color: lightgreen}
  .fail {background-color: yellow}
  .notice {background-color: yellow}
  .pass, .fail {font-size: 16pt}
  .header {background-color: white; font-family: Georgia, serif; padding: 0px}
  th.header {text-align: right; font-weight: normal; color: gray}
  .footer {font-size: 10pt}
  </style>
  </head>
  <body>
  GTFS validation results for feed:<br>
  <code><span class="path">%(feed_dir)s</span><b>%(feed_file)s</b></code><br>
  FeedValidator extension used: %(extension)s
  <br><br>
  <table>
  <tr><th class="header">Agencies:</th><td class="header">%(agencies)s</td></tr>
  <tr><th class="header">Routes:</th><td class="header">%(routes)s</td></tr>
  <tr><th class="header">Stops:</th><td class="header">%(stops)s</td></tr>
  <tr><th class="header">Trips:</th><td class="header">%(trips)s</td></tr>
  <tr><th class="header">Shapes:</th><td class="header">%(shapes)s</td></tr>
  <tr><th class="header">Effective:</th><td class="header">%(dates)s</td></tr>
  </table>
  %(calendar_summary)s
  <br>
  %(problem_summary)s
  <br><br>
  """ % { "feed_file": feed_path[1],
          "feed_dir": feed_path[0],
          "agencies": agencies,
          "routes": len(schedule.GetRouteList()),
          "stops": len(schedule.GetStopList()),
          "trips": len(schedule.GetTripList()),
          "shapes": len(schedule.GetShapeList()),
          "dates": dates,
          "problem_summary": summary,
          "calendar_summary": calendar_summary_html,
          "extension": extension}
  
  # In output_suffix string
  # time.strftime() returns a regular local time string (not a Unicode one) with
  # default system encoding. And decode() will then convert this time string back
  # into a Unicode string. We use decode() here because we don't want the operating
  # system to do any system encoding (which may cause some problem if the string
  # contains some non-English characters) for the string. Therefore we decode it
  # back to its original Unicode code print.
  
      time_unicode = (time.strftime('%B %d, %Y at %I:%M %p %Z').
                      decode(sys.getfilesystemencoding()))
      output_suffix = """
  <div class="footer">
  Generated by <a href="https://github.com/google/transitfeed/wiki/FeedValidator">
  FeedValidator</a> version %s on %s.
  </div>
  </body>
  </html>""" % (transitfeed.__version__, time_unicode)
  
      f.write(transitfeed.EncodeUnicode(output_prefix))
      if self.ProblemListMap(TYPE_ERROR):
        f.write('<h3 class="issueHeader">Errors:</h3>')
        f.write(self.FormatType("Error", self.ProblemListMap(TYPE_ERROR).items()))
      if self.ProblemListMap(TYPE_WARNING):
        f.write('<h3 class="issueHeader">Warnings:</h3>')
        f.write(self.FormatType("Warning", self.ProblemListMap(TYPE_WARNING).items()))
      f.write(transitfeed.EncodeUnicode(output_suffix))
  
  
  def RunValidationOutputFromOptions(feed, options):
    """Validate feed, output results per options and return an exit code."""
    if options.output.upper() == "CONSOLE":
      return RunValidationOutputToConsole(feed, options)
    else:
      return RunValidationOutputToFilename(feed, options, options.output)
  
  
  def RunValidationOutputToFilename(feed, options, output_filename):
    """Validate feed, save HTML at output_filename and return an exit code."""
    try:
      output_file = open(output_filename, 'w')
      exit_code = RunValidationOutputToFile(feed, options, output_file)
      output_file.close()
    except IOError as e:
      print('Error while writing %s: %s' % (output_filename, e))
      output_filename = None
      exit_code = 2
  
    if options.manual_entry and output_filename:
      webbrowser.open('file://%s' % os.path.abspath(output_filename))
  
    return exit_code
  
  
  def RunValidationOutputToFile(feed, options, output_file):
    """Validate feed, write HTML to output_file and return an exit code."""
    accumulator = HTMLCountingProblemAccumulator(options.limit_per_type,
                                                 options.error_types_ignore_list)
    problems = transitfeed.ProblemReporter(accumulator)
    schedule, exit_code = RunValidation(feed, options, problems)
    if isinstance(feed, basestring):
      feed_location = feed
    else:
      feed_location = getattr(feed, 'name', repr(feed))
    accumulator.WriteOutput(feed_location, output_file, schedule, options.extension)
    return exit_code
  
  
  def RunValidationOutputToConsole(feed, options):
    """Validate feed, print reports and return an exit code."""
    accumulator = CountingConsoleProblemAccumulator(
        options.error_types_ignore_list)
    problems = transitfeed.ProblemReporter(accumulator)
    _, exit_code = RunValidation(feed, options, problems)
    return exit_code
  
  
  def RunValidation(feed, options, problems):
    """Validate feed, returning the loaded Schedule and exit code.
  
    Args:
      feed: GTFS file, either path of the file as a string or a file object
      options: options object returned by optparse
      problems: transitfeed.ProblemReporter instance
  
    Returns:
      a transitfeed.Schedule object, exit code and plain text string of other
      problems
      Exit code is 2 if an extension is provided but can't be loaded, 1 if
      problems are found and 0 if the Schedule is problem free.
      plain text string is '' if no other problems are found.
    """
    util.CheckVersion(problems, options.latest_version)
  
    # TODO: Add tests for this flag in testfeedvalidator.py
    if options.extension:
      try:
        __import__(options.extension)
        extension_module = sys.modules[options.extension]
      except ImportError:
        # TODO: Document extensions in a wiki page, place link here
        print(("Could not import extension %s! Please ensure it is a proper "
              "Python module." % options.extension))
        exit(2)
    else:
      extension_module = transitfeed
  
    gtfs_factory = extension_module.GetGtfsFactory()
  
    print('validating %s' % feed)
    print('FeedValidator extension used: %s' % options.extension)
    loader = gtfs_factory.Loader(feed, problems=problems, extra_validation=False,
                                 memory_db=options.memory_db,
                                 check_duplicate_trips=\
                                 options.check_duplicate_trips,
                                 gtfs_factory=gtfs_factory)
    schedule = loader.Load()
    # Start validation: children are already validated by the loader.
    schedule.Validate(service_gap_interval=options.service_gap_interval,
                      validate_children=False)
  
    if feed == 'IWantMyvalidation-crash.txt':
      # See tests/testfeedvalidator.py
      raise Exception('For testing the feed validator crash handler.')
  
    accumulator = problems.GetAccumulator()
    if accumulator.HasIssues():
      print('ERROR: %s found' % accumulator.FormatCount())
      return schedule, 1
    else:
      print('feed validated successfully')
      return schedule, 0
  
  
  def main():
    (feed, options) = ParseCommandLineArguments()
    return RunValidationFromOptions(feed, options)
  
  
  def ParseCommandLineArguments():
    usage = \
  '''%prog [options] [<input GTFS.zip>]
  
  Validates GTFS file (or directory) <input GTFS.zip> and writes a HTML
  report of the results to validation-results.html.
  
  If <input GTFS.zip> is omitted the filename is read from the console. Dragging
  a file into the console may enter the filename.
  
  For more information see
  https://github.com/google/transitfeed/wiki/FeedValidator
  '''
  
    parser = util.OptionParserLongError(
        usage=usage, version='%prog '+transitfeed.__version__)
    parser.add_option('-n', '--noprompt', action='store_false',
                      dest='manual_entry',
                      help='do not prompt for feed location or load output in '
                      'browser')
    parser.add_option('-o', '--output', dest='output', metavar='FILE',
                      help='write html output to FILE or --output=CONSOLE to '
                      'print all errors and warnings to the command console')
    parser.add_option('-p', '--performance', action='store_true',
                      dest='performance',
                      help='output memory and time performance (Availability: '
                      'Unix')
    parser.add_option('-m', '--memory_db', dest='memory_db',  action='store_true',
                      help='Use in-memory sqlite db instead of a temporary file. '
                           'It is faster but uses more RAM.')
    parser.add_option('-d', '--duplicate_trip_check',
                      dest='check_duplicate_trips', action='store_true',
                      help='Check for duplicate trips which go through the same '
                      'stops with same service and start times')
    parser.add_option('-l', '--limit_per_type',
                      dest='limit_per_type', action='store', type='int',
                      help='Maximum number of errors and warnings to keep of '
                      'each type')
    parser.add_option('--latest_version', dest='latest_version',
                      action='store',
                      help='a version number such as 1.2.1 or None to get the '
                      'latest version from the project site. Output a warning if '
                      'transitfeed.py is older than this version.')
    parser.add_option('--service_gap_interval',
                      dest='service_gap_interval',
                      action='store',
                      type='int',
                      help='the number of consecutive days to search for with no '
                      'scheduled service. For each interval with no service '
                      'having this number of days or more a warning will be '
                      'issued')
    parser.add_option('--extension',
                      dest='extension',
                      help='the name of the Python module that containts a GTFS '
                      'extension that is to be loaded and used while validating '
                      'the specified feed.')
    parser.add_option('--error_types_ignore_list',
                      dest='error_types_ignore_list',
                      help='a comma-separated list of error and warning type '
                      'names to be ignored during validation (e.g. '
                      '"ExpirationDate,UnusedStop"). Bad error type names will '
                      'be silently ignored!')
  
    parser.set_defaults(manual_entry=True, output='validation-results.html',
                        memory_db=False, check_duplicate_trips=False,
                        limit_per_type=5, latest_version='',
                        service_gap_interval=13)
    (options, args) = parser.parse_args()
  
    if not len(args) == 1:
      if options.manual_entry:
        feed = raw_input('Enter Feed Location: ')
      else:
        parser.error('You must provide the path of a single feed')
    else:
      feed = args[0]
    feed = feed.strip('"')
  
    # transform options.error_types_ignore_list into a valid list
    if options.error_types_ignore_list:
      options.error_types_ignore_list = options.error_types_ignore_list.split(',')
    else:
      options.error_types_ignore_list = None
  
    return (feed, options)
  
  
  def RunValidationFromOptions(feed, options):
    """Validate feed, run in profiler if in options, and return an exit code."""
    if options.performance:
      return ProfileRunValidationOutputFromOptions(feed, options)
    else:
      return RunValidationOutputFromOptions(feed, options)
  
  
  def ProfileRunValidationOutputFromOptions(feed, options):
    """Run RunValidationOutputFromOptions, print profile and return exit code."""
    import cProfile
    import pstats
    # runctx will modify a dict, but not locals(). We need a way to get rv back.
    locals_for_exec = locals()
    cProfile.runctx('rv = RunValidationOutputFromOptions(feed, options)',
                    globals(), locals_for_exec, 'validate-stats')
  
    # Only available on Unix, http://docs.python.org/lib/module-resource.html
    import resource
    print("Time: %d seconds" % (
        resource.getrusage(resource.RUSAGE_SELF).ru_utime +
        resource.getrusage(resource.RUSAGE_SELF).ru_stime))
  
    # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/286222
    # http://aspn.activestate.com/ASPN/Cookbook/ "The recipes are freely
    # available for review and use."
    def _VmB(VmKey):
      """Return size from proc status in bytes."""
      _proc_status = '/proc/%d/status' % os.getpid()
      _scale = {'kB': 1024.0, 'mB': 1024.0*1024.0,
                'KB': 1024.0, 'MB': 1024.0*1024.0}
  
       # get pseudo file  /proc/<pid>/status
      try:
          t = open(_proc_status)
          v = t.read()
          t.close()
      except:
          raise Exception("no proc file %s" % _proc_status)
          return 0  # non-Linux?
       # get VmKey line e.g. 'VmRSS:  9999  kB\n ...'
      try:
          i = v.index(VmKey)
          v = v[i:].split(None, 3)  # whitespace
      except:
          return 0 # v is empty
  
      if len(v) < 3:
          raise Exception("%s" % v)
          return 0  # invalid format?
       # convert Vm value to bytes
      return int(float(v[1]) * _scale[v[2]])
  
    # I ran this on over a hundred GTFS files, comparing VmSize to VmRSS
    # (resident set size). The difference was always under 2% or 3MB.
    print("Virtual Memory Size: %d bytes" % _VmB('VmSize:'))
  
    # Output report of where CPU time was spent.
    p = pstats.Stats('validate-stats')
    p.strip_dirs()
    p.sort_stats('cumulative').print_stats(30)
    p.sort_stats('cumulative').print_callers(30)
    return locals_for_exec['rv']
  
  
  if __name__ == '__main__':
    util.RunWithCrashHandler(main)
  