blob: 35dde4bf10620ebc6f894d17047d29eea8b584b0 [file] [log] [blame]
Hung-Te Lin76c55b22015-03-31 14:47:14 +08001#!/usr/bin/python2
Hung-Te Lin34f3d382015-04-10 18:18:23 +08002# -*- coding: utf-8 -*-
Hung-Te Lin76c55b22015-03-31 14:47:14 +08003# Copyright 2015 The Chromium OS Authors. All rights reserved.
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
7"""Authoritative source for Chromium OS region/locale configuration.
8
9Run this module to display all known regions (use --help to see options).
10"""
11
12from __future__ import print_function
13
14import argparse
15import collections
16import json
17import re
18import sys
19
20import yaml
21
22
23# The regular expression to check values in Region.keyboards and Region.locales.
24# Keyboards should come with xkb: protocol, or the input methods (ime:, m17n:).
25# Examples: xkb:us:intl:eng, ime:ime:zh-t:cangjie
26KEYBOARD_PATTERN = re.compile(r'^xkb:\w+:\w*:\w+$|'
27 r'^(ime|m17n|t13n):[\w:-]+$')
28# Locale should be a combination of language and location.
29# Examples: en-US, ja.
30LOCALE_PATTERN = re.compile(r'^(\w+)(-[A-Z0-9]+)?$')
31
32
33class Enum(frozenset):
34 """An enumeration type.
35
36 Usage:
37 To create a enum object:
38 dummy_enum = Enum(['A', 'B', 'C'])
39
40 To access a enum object, use:
41 dummy_enum.A
42 dummy_enum.B
43 """
44
45 def __getattr__(self, name):
46 if name in self:
47 return name
48 raise AttributeError
49
50
51class RegionException(Exception):
52 """Exception in Region handling."""
53 pass
54
55
56def MakeList(value):
57 """Converts the given value to a list.
58
59 Returns:
60 A list of elements from "value" if it is iterable (except string);
61 otherwise, a list contains only one element.
62 """
63 if (isinstance(value, collections.Iterable) and
64 not isinstance(value, basestring)):
65 return list(value)
66 return [value]
67
68
69class Region(object):
70 """Comprehensive, standard locale configuration per country/region.
71
72 See :ref:`regions-values` for detailed information on how to set these values.
73 """
74 # pylint gets confused by some of the docstrings.
75 # pylint: disable=C0322
76
77 # ANSI = US-like
78 # ISO = UK-like
79 # JIS = Japanese
80 # KS = Korean
81 # ABNT2 = Brazilian (like ISO but with an extra key to the left of the
82 # right shift key)
83 KeyboardMechanicalLayout = Enum(['ANSI', 'ISO', 'JIS', 'KS', 'ABNT2'])
84
85 region_code = None
86 """A unique identifier for the region. This may be a lower-case
87 `ISO 3166-1 alpha-2 code
88 <http://en.wikipedia.org/wiki/ISO_3166-1_alpha-2>`_ (e.g., ``us``),
89 a variant within an alpha-2 entity (e.g., ``ca.fr``), or an
90 identifier for a collection of countries or entities (e.g.,
91 ``latam-es-419`` or ``nordic``). See :ref:`region-codes`.
92
93 Note that ``uk`` is not a valid identifier; ``gb`` is used as it is
94 the real alpha-2 code for the UK."""
95
96 keyboards = None
97 """A list of keyboard layout identifiers (e.g., ``xkb:us:intl:eng``
98 or ``m17n:ar``). This field was designed to be the physical keyboard layout
99 in the beginning, and then becomes a list of OOBE keyboard selection, which
100 then includes non-physical layout elements like input methods (``ime:``).
101 To avoid confusion, physical layout is now defined by
102 :py:attr:`keyboard_mechanical_layout`, and this is reserved for logical
103 layouts.
104
105 This is identical to the legacy VPD ``keyboard_layout`` value."""
106
107 time_zones = None
108 """A list of default `tz database time zone
109 <http://en.wikipedia.org/wiki/List_of_tz_database_time_zones>`_
110 identifiers (e.g., ``America/Los_Angeles``). See
111 `timezone_settings.cc <http://goo.gl/WSVUeE>`_ for supported time
112 zones.
113
114 This is identical to the legacy VPD ``initial_timezone`` value."""
115
116 locales = None
117 """A list of default locale codes (e.g., ``en-US``); see
118 `l10n_util.cc <http://goo.gl/kVkht>`_ for supported locales.
119
120 This is identital to the legacy VPD ``initial_locale`` field."""
121
122 keyboard_mechanical_layout = None
123 """The keyboard's mechanical layout (``ANSI`` [US-like], ``ISO``
124 [UK-like], ``JIS`` [Japanese], ``ABNT2`` [Brazilian] or ``KS`` [Korean])."""
125
126 description = None
127 """A human-readable description of the region.
128 This defaults to :py:attr:`region_code` if not set."""
129
130 notes = None
131 """Implementation notes about the region. This may be None."""
132
133 numeric_id = None
134 """An integer for mapping into Chrome OS HWID.
135 Please never change this once it is assigned."""
136
Hung-Te Lin436b6cc2015-04-03 12:15:47 +0800137 regulatory_domain = None
138 """An ISO 3166-1 alpha 2 upper-cased two-letter region code for setting
139 Wireless regulatory. See crosbug.com/p/38745 for more details.
140
141 When omitted, this will derive from region_code."""
142
Hung-Te Lin76c55b22015-03-31 14:47:14 +0800143 FIELDS = ['region_code', 'keyboards', 'time_zones', 'locales', 'description',
Hung-Te Lin436b6cc2015-04-03 12:15:47 +0800144 'keyboard_mechanical_layout', 'numeric_id', 'regulatory_domain']
Hung-Te Lin76c55b22015-03-31 14:47:14 +0800145 """Names of fields that define the region."""
146
147
148 def __init__(self, region_code, keyboards, time_zones, locales,
149 keyboard_mechanical_layout, description=None, notes=None,
Hung-Te Lin436b6cc2015-04-03 12:15:47 +0800150 numeric_id=None, regdomain=None):
Hung-Te Lin76c55b22015-03-31 14:47:14 +0800151 """Constructor.
152
153 Args:
154 region_code: See :py:attr:`region_code`.
155 keyboards: See :py:attr:`keyboards`. A single string is accepted for
156 backward compatibility.
157 time_zones: See :py:attr:`time_zones`.
158 locales: See :py:attr:`locales`. A single string is accepted
159 for backward compatibility.
160 keyboard_mechanical_layout: See :py:attr:`keyboard_mechanical_layout`.
161 description: See :py:attr:`description`.
162 notes: See :py:attr:`notes`.
163 numeric_id: See :py:attr:`numeric_id`. This must be None or a
164 non-negative integer.
Hung-Te Lin436b6cc2015-04-03 12:15:47 +0800165 regdomain: See :py:attr:`regulatory_domain`.
Hung-Te Lin76c55b22015-03-31 14:47:14 +0800166 """
Hung-Te Lin436b6cc2015-04-03 12:15:47 +0800167
168 def regdomain_from_region(region):
169 if region.find('.') >= 0:
170 region = region[:region.index('.')]
171 if len(region) == 2:
172 return region.upper()
173 return None
174
Hung-Te Lin76c55b22015-03-31 14:47:14 +0800175 # Quick check: should be 'gb', not 'uk'
176 if region_code == 'uk':
177 raise RegionException("'uk' is not a valid region code (use 'gb')")
178
179 self.region_code = region_code
180 self.keyboards = MakeList(keyboards)
181 self.time_zones = MakeList(time_zones)
182 self.locales = MakeList(locales)
183 self.keyboard_mechanical_layout = keyboard_mechanical_layout
184 self.description = description or region_code
185 self.notes = notes
186 self.numeric_id = numeric_id
Hung-Te Lin436b6cc2015-04-03 12:15:47 +0800187 self.regulatory_domain = (regdomain or regdomain_from_region(region_code))
Hung-Te Lin76c55b22015-03-31 14:47:14 +0800188
189 if self.numeric_id is not None:
190 if not isinstance(self.numeric_id, int):
191 raise TypeError('Numeric ID is %r but should be an integer' %
192 (self.numeric_id,))
193 if self.numeric_id < 0:
194 raise ValueError('Numeric ID is %r but should be non-negative' %
195 self.numeric_id)
196
197 for f in (self.keyboards, self.locales):
198 assert all(isinstance(x, str) for x in f), (
199 'Expected a list of strings, not %r' % f)
200 for f in self.keyboards:
201 assert KEYBOARD_PATTERN.match(f), (
202 'Keyboard pattern %r does not match %r' % (
203 f, KEYBOARD_PATTERN.pattern))
204 for f in self.locales:
205 assert LOCALE_PATTERN.match(f), (
206 'Locale %r does not match %r' % (
207 f, LOCALE_PATTERN.pattern))
Hung-Te Lin436b6cc2015-04-03 12:15:47 +0800208 assert (self.regulatory_domain and
209 len(self.regulatory_domain) == 2 and
210 self.regulatory_domain.upper() == self.regulatory_domain), (
211 "Regulatory domain settings error for region %s" % region_code)
Hung-Te Lin76c55b22015-03-31 14:47:14 +0800212
213 def __repr__(self):
214 return 'Region(%s)' % (', '.join([getattr(self, x) for x in self.FIELDS]))
215
216 def GetFieldsDict(self):
217 """Returns a dict of all substantive fields.
218
219 notes and description are excluded.
220 """
221 return dict((k, getattr(self, k)) for k in self.FIELDS)
222
223_KML = Region.KeyboardMechanicalLayout
224REGIONS_LIST = [
225 Region('au', 'xkb:us::eng', 'Australia/Sydney', 'en-AU', _KML.ANSI,
226 'Australia', None, 1),
227 Region('be', 'xkb:be::nld', 'Europe/Brussels', 'en-GB', _KML.ISO, 'Belgium',
228 'Flemish (Belgian Dutch) keyboard; British English language for '
229 'neutrality', 2),
230 Region('br', 'xkb:br::por', 'America/Sao_Paulo', 'pt-BR', _KML.ABNT2,
231 'Brazil (ABNT2)',
232 ('ABNT2 = ABNT NBR 10346 variant 2. This is the preferred layout '
233 'for Brazil. ABNT2 is mostly an ISO layout, but it 12 keys between '
234 'the shift keys; see http://goo.gl/twA5tq'), 3),
235 Region('br.abnt', 'xkb:br::por', 'America/Sao_Paulo', 'pt-BR', _KML.ISO,
236 'Brazil (ABNT)',
237 ('Like ABNT2, but lacking the extra key to the left of the right '
238 'shift key found in that layout. ABNT2 (the "br" region) is '
239 'preferred to this layout'), 4),
240 Region('br.usintl', 'xkb:us:intl:eng', 'America/Sao_Paulo', 'pt-BR',
241 _KML.ANSI, 'Brazil (US Intl)',
242 'Brazil with US International keyboard layout. ABNT2 ("br") and '
243 'ABNT1 ("br.abnt1 ") are both preferred to this.', 5),
244 Region('ca.ansi', 'xkb:us::eng', 'America/Toronto', 'en-CA', _KML.ANSI,
245 'Canada (US keyboard)',
246 'Canada with US (ANSI) keyboard. Not for en/fr hybrid ANSI '
247 'keyboards; for that you would want ca.hybridansi. See '
248 'http://goto/cros-canada', 6),
249 Region('ca.fr', 'xkb:ca::fra', 'America/Toronto', 'fr-CA', _KML.ISO,
250 'Canada (French keyboard)',
251 ('Canadian French (ISO) keyboard. The most common configuration for '
252 'Canadian French SKUs. See http://goto/cros-canada'), 7),
253 Region('ca.hybrid', 'xkb:ca:eng:eng', 'America/Toronto', 'en-CA', _KML.ISO,
254 'Canada (hybrid ISO)',
255 ('Canada with hybrid (ISO) xkb:ca:eng:eng + xkb:ca::fra keyboard, '
256 'defaulting to English language and keyboard. Used only if there '
257 'needs to be a single SKU for all of Canada. See '
258 'http://goto/cros-canada'), 8),
259 Region('ca.hybridansi', 'xkb:ca:eng:eng', 'America/Toronto', 'en-CA',
260 _KML.ANSI, 'Canada (hybrid ANSI)',
261 ('Canada with hybrid (ANSI) xkb:ca:eng:eng + xkb:ca::fra keyboard, '
262 'defaulting to English language and keyboard. Used only if there '
263 'needs to be a single SKU for all of Canada. See '
264 'http://goto/cros-canada'), 9),
265 Region('ca.multix', 'xkb:ca:multix:fra', 'America/Toronto', 'fr-CA',
266 _KML.ISO, 'Canada (multilingual)',
267 ("Canadian Multilingual keyboard; you probably don't want this. See "
268 'http://goto/cros-canada'), 10),
269 Region('ch', 'xkb:ch::ger', 'Europe/Zurich', 'en-US', _KML.ISO,
270 'Switzerland',
271 'German keyboard, but US English to be language-neutral; used in '
272 'the common case that there is only a single Swiss SKU.', 11),
273 Region('de', 'xkb:de::ger', 'Europe/Berlin', 'de', _KML.ISO, 'Germany',
274 None, 12),
275 Region('es', 'xkb:es::spa', 'Europe/Madrid', 'es', _KML.ISO, 'Spain',
276 None, 13),
277 Region('fi', 'xkb:fi::fin', 'Europe/Helsinki', 'fi', _KML.ISO, 'Finland',
278 None, 14),
279 Region('fr', 'xkb:fr::fra', 'Europe/Paris', 'fr', _KML.ISO, 'France',
280 None, 15),
281 Region('gb', 'xkb:gb:extd:eng', 'Europe/London', 'en-GB', _KML.ISO, 'UK',
282 None, 16),
283 Region('ie', 'xkb:gb:extd:eng', 'Europe/Dublin', 'en-GB', _KML.ISO,
284 'Ireland', None, 17),
285 Region('in', 'xkb:us::eng', 'Asia/Calcutta', 'en-US', _KML.ANSI, 'India',
286 None, 18),
287 Region('it', 'xkb:it::ita', 'Europe/Rome', 'it', _KML.ISO, 'Italy',
288 None, 19),
289 Region('latam-es-419', 'xkb:es::spa', 'America/Mexico_City', 'es-419',
290 _KML.ISO, 'Hispanophone Latin America',
291 ('Spanish-speaking countries in Latin America, using the Iberian '
292 '(Spain) Spanish keyboard, which is increasingly dominant in '
293 'Latin America. Known to be correct for '
294 'Chile, Colombia, Mexico, Peru; '
295 'still unconfirmed for other es-419 countries. The old Latin '
296 'American layout (xkb:latam::spa) has not been approved; before '
297 'using that you must seek review through http://goto/vpdsettings. '
298 'See also http://goo.gl/Iffuqh. Note that 419 is the UN M.49 '
Hung-Te Lin436b6cc2015-04-03 12:15:47 +0800299 'region code for Latin America'), 20, 'MX'),
Hung-Te Lin76c55b22015-03-31 14:47:14 +0800300 Region('my', 'xkb:us::eng', 'Asia/Kuala_Lumpur', 'ms', _KML.ANSI,
301 'Malaysia', None, 21),
302 Region('nl', 'xkb:us:intl:eng', 'Europe/Amsterdam', 'nl', _KML.ANSI,
303 'Netherlands', None, 22),
304 Region('nordic', 'xkb:se::swe', 'Europe/Stockholm', 'en-US', _KML.ISO,
305 'Nordics',
306 ('Unified SKU for Sweden, Norway, and Denmark. This defaults '
307 'to Swedish keyboard layout, but starts with US English language '
308 'for neutrality. Use if there is a single combined SKU for Nordic '
Hung-Te Lin436b6cc2015-04-03 12:15:47 +0800309 'countries.'), 23, 'SE'),
Hung-Te Lin76c55b22015-03-31 14:47:14 +0800310 Region('nz', 'xkb:us::eng', 'Pacific/Auckland', 'en-NZ', _KML.ANSI,
311 'New Zealand', None, 24),
312 Region('ph', 'xkb:us::eng', 'Asia/Manila', 'en-US', _KML.ANSI,
313 'Philippines', None, 25),
314 Region('ru', 'xkb:ru::rus', 'Europe/Moscow', 'ru', _KML.ANSI, 'Russia',
315 'For R31+ only; R30 and earlier must use US keyboard for login', 26),
316 Region('se', 'xkb:se::swe', 'Europe/Stockholm', 'sv', _KML.ISO, 'Sweden',
317 ('Use this if there separate SKUs for Nordic countries (Sweden, '
318 'Norway, and Denmark), or the device is only shipping to Sweden. '
319 "If there is a single unified SKU, use 'nordic' instead."), 27),
320 Region('sg', 'xkb:us::eng', 'Asia/Singapore', 'en-GB', _KML.ANSI,
321 'Singapore', None, 28),
322 Region('us', 'xkb:us::eng', 'America/Los_Angeles', 'en-US', _KML.ANSI,
323 'United States', None, 29),
324 Region('jp', 'xkb:jp::jpn', 'Asia/Tokyo', 'ja', _KML.JIS,
325 'Japan', None, 30),
Hung-Te Linb6203bf2015-04-10 10:43:30 +0800326 Region('za', 'xkb:us:intl:eng', 'Africa/Johannesburg', 'en-ZA', _KML.ANSI,
327 'South Africa', None, 31),
Hung-Te Lin76c55b22015-03-31 14:47:14 +0800328 Region('hk',
329 ['xkb:us::eng', 'ime:zh-t:cangjie', 'ime:zh-t:quick',
330 'ime:zh-t:array', 'ime:zh-t:dayi', 'ime:zh-t:zhuyin',
331 'ime:zh-t:pinyin'],
332 'Asia/Hong_Kong', ['zh-TW', 'en-GB', 'zh-CN'], _KML.ANSI,
333 'Hong Kong', None, 33),
334 Region('cz', ['xkb:cz::cze', 'xkb:cz:qwerty:cze'], 'Europe/Prague',
335 ['cs', 'en-GB'], _KML.ISO, 'Czech Republic', None, 35),
Hung-Te Linb6203bf2015-04-10 10:43:30 +0800336 Region('th', ['xkb:us::eng', 'm17n:th', 'm17n:th_pattajoti', 'm17n:th_tis'],
337 'Asia/Bangkok', ['th', 'en-GB'], _KML.ANSI, 'Thailand', None, 36),
Hung-Te Lin76c55b22015-03-31 14:47:14 +0800338 Region('tw',
339 ['xkb:us::eng', 'ime:zh-t:zhuyin', 'ime:zh-t:array',
340 'ime:zh-t:dayi', 'ime:zh-t:cangjie', 'ime:zh-t:quick',
341 'ime:zh-t:pinyin'],
342 'Asia/Taipei', ['zh-TW', 'en-US'], _KML.ANSI, 'Taiwan', None, 38),
343 Region('pl', 'xkb:pl::pol', 'Europe/Warsaw', ['pl', 'en-GB'], _KML.ANSI,
344 'Poland', None, 39),
345]
346"""A list of :py:class:`regions.Region` objects for
347all **confirmed** regions. A confirmed region is a region whose
348properties are known to be correct and may be used to launch a device."""
349
350
351UNCONFIRMED_REGIONS_LIST = []
352"""A list of :py:class:`regions.Region` objects for
353**unconfirmed** regions. These are believed to be correct but
354unconfirmed, and all fields should be verified (and the row moved into
355:py:data:`regions.Region.REGIONS_LIST`) before
356launch. See <http://goto/vpdsettings>.
357
358Currently, non-Latin keyboards must use an underlying Latin keyboard
359for VPD. (This assumption should be revisited when moving items to
360:py:data:`regions.Region.REGIONS_LIST`.) This is
361currently being discussed on <http://crbug.com/325389>.
362
363Some timezones may be missing from ``timezone_settings.cc`` (see
364http://crosbug.com/p/23902). This must be rectified before moving
365items to :py:data:`regions.Region.REGIONS_LIST`.
366"""
367
368INCOMPLETE_REGIONS_LIST = []
369"""A list of :py:class:`regions.Region` objects for
370**incomplete** regions. These may contain incorrect information, and all
371fields must be reviewed before launch. See http://goto/vpdsettings.
372"""
373
374
375def ConsolidateRegions(regions):
376 """Consolidates a list of regions into a dict.
377
378 Args:
379 regions: A list of Region objects. All objects for any given
380 region code must be identical or we will throw an exception.
381 (We allow duplicates in case identical region objects are
382 defined in both regions.py and the overlay, e.g., when moving
383 items to the public overlay.)
384
385 Returns:
386 A dict from region code to Region.
387
388 Raises:
389 RegionException: If there are multiple regions defined for a given
390 region, and the values for those regions differ.
391 """
392 # Build a dict from region_code to the first Region with that code.
393 region_dict = {}
394 for r in regions:
395 existing_region = region_dict.get(r.region_code)
396 if existing_region:
397 if existing_region.GetFieldsDict() != r.GetFieldsDict():
398 raise RegionException(
399 'Conflicting definitions for region %r: %r, %r' %
400 (r.region_code, existing_region.GetFieldsDict(),
401 r.GetFieldsDict()))
402 else:
403 region_dict[r.region_code] = r
404
405 return region_dict
406
407
408def BuildRegionsDict(include_all=False):
409 """Builds a dictionary mapping from code to :py:class:`regions.Region` object.
410
411 The regions include:
412
413 * :py:data:`regions.REGIONS_LIST`
414 * :py:data:`regions_overlay.REGIONS_LIST`
415 * Only if ``include_all`` is true:
416
417 * :py:data:`regions.UNCONFIRMED_REGIONS_LIST`
418 * :py:data:`regions.INCOMPLETE_REGIONS_LIST`
419
420 A region may only appear in one of the above lists, or this function
421 will (deliberately) fail.
422 """
423 regions = list(REGIONS_LIST)
424 if include_all:
425 regions += UNCONFIRMED_REGIONS_LIST + INCOMPLETE_REGIONS_LIST
426
427 # Build dictionary of region code to list of regions with that
428 # region code. Check manually for duplicates, since the region may
429 # be present both in the overlay and the public repo.
430 return ConsolidateRegions(regions)
431
432
433REGIONS = BuildRegionsDict()
434
435
436def main(args=sys.argv[1:], out=None):
437 parser = argparse.ArgumentParser(description=(
438 'Display all known regions and their parameters. '))
439 parser.add_argument('--format',
440 choices=('human-readable', 'csv', 'json', 'yaml'),
441 default='human-readable',
442 help='Output format (default=%(default)s)')
443 parser.add_argument('--all', action='store_true',
444 help='Include unconfirmed and incomplete regions')
445 parser.add_argument('--output', default=None,
446 help='Specify output file')
Hung-Te Lin34f3d382015-04-10 18:18:23 +0800447 parser.add_argument('--overlay', default=None,
448 help='Specify a Python file to overlay extra data')
Hung-Te Lin76c55b22015-03-31 14:47:14 +0800449 args = parser.parse_args(args)
450
Hung-Te Lin34f3d382015-04-10 18:18:23 +0800451 if args.overlay is not None:
452 execfile(args.overlay)
453
Hung-Te Lin76c55b22015-03-31 14:47:14 +0800454 regions_dict = BuildRegionsDict(args.all)
Hung-Te Lin34f3d382015-04-10 18:18:23 +0800455
Hung-Te Lin76c55b22015-03-31 14:47:14 +0800456 if out is None:
457 if args.output is None:
458 out = sys.stdout
459 else:
460 out = open(args.output, 'w')
461
462 # Handle YAML and JSON output.
463 if args.format == 'yaml' or args.format == 'json':
464 data = {}
465 for region in regions_dict.values():
466 item = {}
467 for field in Region.FIELDS:
468 item[field] = getattr(region, field)
469 data[region.region_code] = item
470 if args.format == 'yaml':
471 yaml.dump(data, out)
472 else:
473 json.dump(data, out)
474 return
475
476 # Handle CSV or plain-text output: build a list of lines to print.
477 lines = [Region.FIELDS]
478
479 def CoerceToString(value):
480 """Returns the arguments in simple string type.
481
482 If value is a list, concatenate its values with commas. Otherwise, just
483 return value.
484 """
485 if isinstance(value, list):
486 return ','.join(value)
487 else:
488 return str(value)
489 for region in sorted(regions_dict.values(), key=lambda v: v.region_code):
490 lines.append([CoerceToString(getattr(region, field))
491 for field in Region.FIELDS])
492
493 if args.format == 'csv':
494 # Just print the lines in CSV format.
495 for l in lines:
496 print(','.join(l))
497 elif args.format == 'human-readable':
498 num_columns = len(lines[0])
499
500 # Calculate maximum length of each column.
501 max_lengths = []
502 for column_no in xrange(num_columns):
503 max_lengths.append(max(len(line[column_no]) for line in lines))
504
505 # Print each line, padding as necessary to the max column length.
506 for line in lines:
507 for column_no in xrange(num_columns):
508 out.write(line[column_no].ljust(max_lengths[column_no] + 2))
509 out.write('\n')
510 else:
511 exit('Sorry, unknown format specified: %s' % args.format)
512
513
514if __name__ == '__main__':
515 main()