1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3# vim:fenc=utf-8 ff=unix ft=python ts=4 sw=4 sts=4 si et
4"""
5pip-licenses
6
7MIT License
8
9Copyright (c) 2018 raimon
10
11Permission is hereby granted, free of charge, to any person obtaining a copy
12of this software and associated documentation files (the "Software"), to deal
13in the Software without restriction, including without limitation the rights
14to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15copies of the Software, and to permit persons to whom the Software is
16furnished to do so, subject to the following conditions:
17
18The above copyright notice and this permission notice shall be included in all
19copies or substantial portions of the Software.
20
21THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27SOFTWARE.
28"""
29import argparse
30import codecs
31import glob
32import os
33import sys
34from collections import Counter
35from email import message_from_string
36from email.parser import FeedParser
37from enum import Enum, auto
38from functools import partial
39from typing import List, Optional, Sequence, Text
40
41try:
42    from pip._internal.utils.misc import get_installed_distributions
43except ImportError:  # pragma: no cover
44    try:
45        from pip import get_installed_distributions
46    except ImportError:
47        def get_installed_distributions():
48            from pip._internal.metadata import (
49                get_default_environment, get_environment,
50            )
51            from pip._internal.metadata.pkg_resources import (
52                Distribution as _Dist,
53            )
54            from pip._internal.utils.compat import stdlib_pkgs
55
56            env = get_default_environment()
57            dists = env.iter_installed_distributions(
58                local_only=True,
59                skip=stdlib_pkgs,
60                include_editables=True,
61                editables_only=False,
62                user_only=False,
63            )
64            return [dist._dist for dist in dists]
65
66from prettytable import PrettyTable
67
68try:
69    from prettytable.prettytable import ALL as RULE_ALL
70    from prettytable.prettytable import FRAME as RULE_FRAME
71    from prettytable.prettytable import HEADER as RULE_HEADER
72    from prettytable.prettytable import NONE as RULE_NONE
73    PTABLE = True
74except ImportError:  # pragma: no cover
75    from prettytable import ALL as RULE_ALL
76    from prettytable import FRAME as RULE_FRAME
77    from prettytable import HEADER as RULE_HEADER
78    from prettytable import NONE as RULE_NONE
79    PTABLE = False
80
81open = open  # allow monkey patching
82
83__pkgname__ = 'pip-licenses'
84__version__ = '3.5.3'
85__author__ = 'raimon'
86__license__ = 'MIT'
87__summary__ = ('Dump the software license list of '
88               'Python packages installed with pip.')
89__url__ = 'https://github.com/raimon49/pip-licenses'
90
91
92FIELD_NAMES = (
93    'Name',
94    'Version',
95    'License',
96    'LicenseFile',
97    'LicenseText',
98    'NoticeFile',
99    'NoticeText',
100    'Author',
101    'Description',
102    'URL',
103)
104
105
106SUMMARY_FIELD_NAMES = (
107    'Count',
108    'License',
109)
110
111
112DEFAULT_OUTPUT_FIELDS = (
113    'Name',
114    'Version',
115)
116
117
118SUMMARY_OUTPUT_FIELDS = (
119    'Count',
120    'License',
121)
122
123
124METADATA_KEYS = (
125    'home-page',
126    'author',
127    'license',
128    'summary',
129    'license_classifier',
130)
131
132# Mapping of FIELD_NAMES to METADATA_KEYS where they differ by more than case
133FIELDS_TO_METADATA_KEYS = {
134    'URL': 'home-page',
135    'Description': 'summary',
136    'License-Metadata': 'license',
137    'License-Classifier': 'license_classifier',
138}
139
140
141SYSTEM_PACKAGES = (
142    __pkgname__,
143    'pip',
144    'PTable' if PTABLE else 'prettytable',
145    'setuptools',
146    'wheel',
147)
148
149LICENSE_UNKNOWN = 'UNKNOWN'
150
151
152def get_packages(args: "CustomNamespace"):
153
154    def get_pkg_included_file(pkg, file_names):
155        """
156        Attempt to find the package's included file on disk and return the
157        tuple (included_file_path, included_file_contents).
158        """
159        included_file = LICENSE_UNKNOWN
160        included_text = LICENSE_UNKNOWN
161        pkg_dirname = "{}-{}.dist-info".format(
162            pkg.project_name.replace("-", "_"), pkg.version)
163        patterns = []
164        [patterns.extend(sorted(glob.glob(os.path.join(pkg.location,
165                                                       pkg_dirname,
166                                                       f))))
167         for f in file_names]
168        for test_file in patterns:
169            if os.path.exists(test_file):
170                included_file = test_file
171                with open(test_file, encoding='utf-8',
172                          errors='backslashreplace') as included_file_handle:
173                    included_text = included_file_handle.read()
174                break
175        return (included_file, included_text)
176
177    def get_pkg_info(pkg):
178        (license_file, license_text) = get_pkg_included_file(
179            pkg,
180            ('LICENSE*', 'LICENCE*', 'COPYING*')
181        )
182        (notice_file, notice_text) = get_pkg_included_file(
183            pkg,
184            ('NOTICE*',)
185        )
186        pkg_info = {
187            'name': pkg.project_name,
188            'version': pkg.version,
189            'namever': str(pkg),
190            'licensefile': license_file,
191            'licensetext': license_text,
192            'noticefile': notice_file,
193            'noticetext': notice_text,
194        }
195        metadata = None
196        if pkg.has_metadata('METADATA'):
197            metadata = pkg.get_metadata('METADATA')
198
199        if pkg.has_metadata('PKG-INFO') and metadata is None:
200            metadata = pkg.get_metadata('PKG-INFO')
201
202        if metadata is None:
203            for key in METADATA_KEYS:
204                pkg_info[key] = LICENSE_UNKNOWN
205
206            return pkg_info
207
208        feed_parser = FeedParser()
209        feed_parser.feed(metadata)
210        parsed_metadata = feed_parser.close()
211
212        for key in METADATA_KEYS:
213            pkg_info[key] = parsed_metadata.get(key, LICENSE_UNKNOWN)
214
215        if metadata is not None:
216            message = message_from_string(metadata)
217            pkg_info['license_classifier'] = \
218                find_license_from_classifier(message)
219
220        if args.filter_strings:
221            for k in pkg_info:
222                if isinstance(pkg_info[k], list):
223                    for i, item in enumerate(pkg_info[k]):
224                        pkg_info[k][i] = item. \
225                            encode(args.filter_code_page, errors="ignore"). \
226                            decode(args.filter_code_page)
227                else:
228                    pkg_info[k] = pkg_info[k]. \
229                        encode(args.filter_code_page, errors="ignore"). \
230                        decode(args.filter_code_page)
231
232        return pkg_info
233
234    pkgs = get_installed_distributions()
235    ignore_pkgs_as_lower = [pkg.lower() for pkg in args.ignore_packages]
236    pkgs_as_lower = [pkg.lower() for pkg in args.packages]
237
238    fail_on_licenses = set()
239    if args.fail_on:
240        fail_on_licenses = set(map(str.strip, args.fail_on.split(";")))
241
242    allow_only_licenses = set()
243    if args.allow_only:
244        allow_only_licenses = set(map(str.strip, args.allow_only.split(";")))
245
246    for pkg in pkgs:
247        pkg_name = pkg.project_name
248
249        if pkg_name.lower() in ignore_pkgs_as_lower:
250            continue
251
252        if pkgs_as_lower and pkg_name.lower() not in pkgs_as_lower:
253            continue
254
255        if not args.with_system and pkg_name in SYSTEM_PACKAGES:
256            continue
257
258        pkg_info = get_pkg_info(pkg)
259
260        license_names = select_license_by_source(
261            args.from_,
262            pkg_info['license_classifier'],
263            pkg_info['license'])
264
265        if fail_on_licenses:
266            failed_licenses = license_names.intersection(fail_on_licenses)
267            if failed_licenses:
268                sys.stderr.write(
269                    "fail-on license {} was found for package "
270                    "{}:{}".format(
271                        '; '.join(sorted(failed_licenses)),
272                        pkg_info['name'],
273                        pkg_info['version'])
274                )
275                sys.exit(1)
276
277        if allow_only_licenses:
278            uncommon_licenses = license_names.difference(allow_only_licenses)
279            if len(uncommon_licenses) == len(license_names):
280                sys.stderr.write(
281                    "license {} not in allow-only licenses was found"
282                    " for package {}:{}".format(
283                        '; '.join(sorted(uncommon_licenses)),
284                        pkg_info['name'],
285                        pkg_info['version'])
286                )
287                sys.exit(1)
288
289        yield pkg_info
290
291
292def create_licenses_table(
293        args: "CustomNamespace", output_fields=DEFAULT_OUTPUT_FIELDS):
294    table = factory_styled_table_with_args(args, output_fields)
295
296    for pkg in get_packages(args):
297        row = []
298        for field in output_fields:
299            if field == 'License':
300                license_set = select_license_by_source(
301                    args.from_, pkg['license_classifier'], pkg['license'])
302                license_str = '; '.join(sorted(license_set))
303                row.append(license_str)
304            elif field == 'License-Classifier':
305                row.append('; '.join(sorted(pkg['license_classifier']))
306                           or LICENSE_UNKNOWN)
307            elif field.lower() in pkg:
308                row.append(pkg[field.lower()])
309            else:
310                row.append(pkg[FIELDS_TO_METADATA_KEYS[field]])
311        table.add_row(row)
312
313    return table
314
315
316def create_summary_table(args: "CustomNamespace"):
317    counts = Counter(
318        '; '.join(sorted(select_license_by_source(
319            args.from_, pkg['license_classifier'], pkg['license'])))
320        for pkg in get_packages(args))
321
322    table = factory_styled_table_with_args(args, SUMMARY_FIELD_NAMES)
323    for license, count in counts.items():
324        table.add_row([count, license])
325    return table
326
327
328class JsonPrettyTable(PrettyTable):
329    """PrettyTable-like class exporting to JSON"""
330
331    def _format_row(self, row, options):
332        resrow = {}
333        for (field, value) in zip(self._field_names, row):
334            if field not in options["fields"]:
335                continue
336
337            resrow[field] = value
338
339        return resrow
340
341    def get_string(self, **kwargs):
342        # import included here in order to limit dependencies
343        # if not interested in JSON output,
344        # then the dependency is not required
345        import json
346
347        options = self._get_options(kwargs)
348        rows = self._get_rows(options)
349        formatted_rows = self._format_rows(rows, options)
350
351        lines = []
352        for row in formatted_rows:
353            lines.append(row)
354
355        return json.dumps(lines, indent=2, sort_keys=True)
356
357
358class JsonLicenseFinderTable(JsonPrettyTable):
359    def _format_row(self, row, options):
360        resrow = {}
361        for (field, value) in zip(self._field_names, row):
362            if field == 'Name':
363                resrow['name'] = value
364
365            if field == 'Version':
366                resrow['version'] = value
367
368            if field == 'License':
369                resrow['licenses'] = [value]
370
371        return resrow
372
373    def get_string(self, **kwargs):
374        # import included here in order to limit dependencies
375        # if not interested in JSON output,
376        # then the dependency is not required
377        import json
378
379        options = self._get_options(kwargs)
380        rows = self._get_rows(options)
381        formatted_rows = self._format_rows(rows, options)
382
383        lines = []
384        for row in formatted_rows:
385            lines.append(row)
386
387        return json.dumps(lines, sort_keys=True)
388
389
390class CSVPrettyTable(PrettyTable):
391    """PrettyTable-like class exporting to CSV"""
392
393    def get_string(self, **kwargs):
394
395        def esc_quotes(val):
396            """
397            Meta-escaping double quotes
398            https://tools.ietf.org/html/rfc4180
399            """
400            try:
401                return val.replace('"', '""')
402            except UnicodeDecodeError:  # pragma: no cover
403                return val.decode('utf-8').replace('"', '""')
404            except UnicodeEncodeError:  # pragma: no cover
405                return val.encode('unicode_escape').replace('"', '""')
406
407        options = self._get_options(kwargs)
408        rows = self._get_rows(options)
409        formatted_rows = self._format_rows(rows, options)
410
411        lines = []
412        formatted_header = ','.join(['"%s"' % (esc_quotes(val), )
413                                     for val in self._field_names])
414        lines.append(formatted_header)
415        for row in formatted_rows:
416            formatted_row = ','.join(['"%s"' % (esc_quotes(val), )
417                                      for val in row])
418            lines.append(formatted_row)
419
420        return '\n'.join(lines)
421
422
423class PlainVerticalTable(PrettyTable):
424    """PrettyTable for outputting to a simple non-column based style.
425
426    When used with --with-license-file, this style is similar to the default
427    style generated from Angular CLI's --extractLicenses flag.
428    """
429
430    def get_string(self, **kwargs):
431        options = self._get_options(kwargs)
432        rows = self._get_rows(options)
433
434        output = ''
435        for row in rows:
436            for v in row:
437                output += '{}\n'.format(v)
438            output += '\n'
439
440        return output
441
442
443def factory_styled_table_with_args(
444        args: "CustomNamespace", output_fields=DEFAULT_OUTPUT_FIELDS):
445    table = PrettyTable()
446    table.field_names = output_fields
447    table.align = 'l'
448    table.border = args.format_ in (FormatArg.MARKDOWN, FormatArg.RST,
449                                    FormatArg.CONFLUENCE, FormatArg.JSON)
450    table.header = True
451
452    if args.format_ == FormatArg.MARKDOWN:
453        table.junction_char = '|'
454        table.hrules = RULE_HEADER
455    elif args.format_ == FormatArg.RST:
456        table.junction_char = '+'
457        table.hrules = RULE_ALL
458    elif args.format_ == FormatArg.CONFLUENCE:
459        table.junction_char = '|'
460        table.hrules = RULE_NONE
461    elif args.format_ == FormatArg.JSON:
462        table = JsonPrettyTable(table.field_names)
463    elif args.format_ == FormatArg.JSON_LICENSE_FINDER:
464        table = JsonLicenseFinderTable(table.field_names)
465    elif args.format_ == FormatArg.CSV:
466        table = CSVPrettyTable(table.field_names)
467    elif args.format_ == FormatArg.PLAIN_VERTICAL:
468        table = PlainVerticalTable(table.field_names)
469
470    return table
471
472
473def find_license_from_classifier(message):
474    licenses = []
475    for k, v in message.items():
476        if k == 'Classifier' and v.startswith('License'):
477            license = v.split(' :: ')[-1]
478
479            # Through the declaration of 'Classifier: License :: OSI Approved'
480            if license != 'OSI Approved':
481                licenses.append(license)
482
483    return licenses
484
485
486def select_license_by_source(from_source, license_classifier, license_meta):
487    license_classifier_set = set(license_classifier) or {LICENSE_UNKNOWN}
488    if (from_source == FromArg.CLASSIFIER or
489            from_source == FromArg.MIXED and len(license_classifier) > 0):
490        return license_classifier_set
491    else:
492        return {license_meta}
493
494
495def get_output_fields(args: "CustomNamespace"):
496    if args.summary:
497        return list(SUMMARY_OUTPUT_FIELDS)
498
499    output_fields = list(DEFAULT_OUTPUT_FIELDS)
500
501    if args.from_ == FromArg.ALL:
502        output_fields.append('License-Metadata')
503        output_fields.append('License-Classifier')
504    else:
505        output_fields.append('License')
506
507    if args.with_authors:
508        output_fields.append('Author')
509
510    if args.with_urls:
511        output_fields.append('URL')
512
513    if args.with_description:
514        output_fields.append('Description')
515
516    if args.with_license_file:
517        if not args.no_license_path:
518            output_fields.append('LicenseFile')
519
520        output_fields.append('LicenseText')
521
522        if args.with_notice_file:
523            output_fields.append('NoticeText')
524            if not args.no_license_path:
525                output_fields.append('NoticeFile')
526
527    return output_fields
528
529
530def get_sortby(args: "CustomNamespace"):
531    if args.summary and args.order == OrderArg.COUNT:
532        return 'Count'
533    elif args.summary or args.order == OrderArg.LICENSE:
534        return 'License'
535    elif args.order == OrderArg.NAME:
536        return 'Name'
537    elif args.order == OrderArg.AUTHOR and args.with_authors:
538        return 'Author'
539    elif args.order == OrderArg.URL and args.with_urls:
540        return 'URL'
541
542    return 'Name'
543
544
545def create_output_string(args: "CustomNamespace"):
546    output_fields = get_output_fields(args)
547
548    if args.summary:
549        table = create_summary_table(args)
550    else:
551        table = create_licenses_table(args, output_fields)
552
553    sortby = get_sortby(args)
554
555    if args.format_ == FormatArg.HTML:
556        return table.get_html_string(fields=output_fields, sortby=sortby)
557    else:
558        return table.get_string(fields=output_fields, sortby=sortby)
559
560
561def create_warn_string(args: "CustomNamespace"):
562    warn_messages = []
563    warn = partial(output_colored, '33')
564
565    if args.with_license_file and not args.format_ == FormatArg.JSON:
566        message = warn(('Due to the length of these fields, this option is '
567                        'best paired with --format=json.'))
568        warn_messages.append(message)
569
570    if args.summary and (args.with_authors or args.with_urls):
571        message = warn(('When using this option, only --order=count or '
572                        '--order=license has an effect for the --order '
573                        'option. And using --with-authors and --with-urls '
574                        'will be ignored.'))
575        warn_messages.append(message)
576
577    return '\n'.join(warn_messages)
578
579
580class CustomHelpFormatter(argparse.HelpFormatter):  # pragma: no cover
581    def __init__(
582        self, prog: Text, indent_increment: int = 2,
583        max_help_position: int = 24, width: Optional[int] = None
584    ) -> None:
585        max_help_position = 30
586        super().__init__(
587            prog, indent_increment=indent_increment,
588            max_help_position=max_help_position, width=width)
589
590    def _format_action(self, action: argparse.Action) -> str:
591        flag_indent_argument: bool = False
592        text = self._expand_help(action)
593        separator_pos = text[:3].find('|')
594        if separator_pos != -1 and 'I' in text[:separator_pos]:
595            self._indent()
596            flag_indent_argument = True
597        help_str = super()._format_action(action)
598        if flag_indent_argument:
599            self._dedent()
600        return help_str
601
602    def _expand_help(self, action: argparse.Action) -> str:
603        if isinstance(action.default, Enum):
604            default_value = enum_key_to_value(action.default)
605            return self._get_help_string(action) % {'default': default_value}
606        return super()._expand_help(action)
607
608    def _split_lines(self, text: Text, width: int) -> List[str]:
609        separator_pos = text[:3].find('|')
610        if separator_pos != -1:
611            flag_splitlines: bool = 'R' in text[:separator_pos]
612            text = text[separator_pos + 1:]
613            if flag_splitlines:
614                return text.splitlines()
615        return super()._split_lines(text, width)
616
617
618class CustomNamespace(argparse.Namespace):
619    from_: "FromArg"
620    order: "OrderArg"
621    format_: "FormatArg"
622    summary: bool
623    output_file: str
624    ignore_packages: List[str]
625    packages: List[str]
626    with_system: bool
627    with_authors: bool
628    with_urls: bool
629    with_description: bool
630    with_license_file: bool
631    no_license_path: bool
632    with_notice_file: bool
633    filter_strings: bool
634    filter_code_page: str
635    fail_on: Optional[str]
636    allow_only: Optional[str]
637
638
639class CompatibleArgumentParser(argparse.ArgumentParser):
640    def parse_args(self, args: Optional[Sequence[Text]] = None,
641                   namespace: CustomNamespace = None) -> CustomNamespace:
642        args = super().parse_args(args, namespace)
643        self._verify_args(args)
644        return args
645
646    def _verify_args(self, args: CustomNamespace):
647        if args.with_license_file is False and (
648                args.no_license_path is True or
649                args.with_notice_file is True):
650            self.error(
651                "'--no-license-path' and '--with-notice-file' require "
652                "the '--with-license-file' option to be set")
653        if args.filter_strings is False and \
654                args.filter_code_page != 'latin1':
655            self.error(
656                "'--filter-code-page' requires the '--filter-strings' "
657                "option to be set")
658        try:
659            codecs.lookup(args.filter_code_page)
660        except LookupError:
661            self.error(
662                "invalid code page '%s' given for '--filter-code-page, "
663                "check https://docs.python.org/3/library/codecs.html"
664                "#standard-encodings for valid code pages"
665                % args.filter_code_page)
666
667
668class NoValueEnum(Enum):
669    def __repr__(self):  # pragma: no cover
670        return '<%s.%s>' % (self.__class__.__name__, self.name)
671
672
673class FromArg(NoValueEnum):
674    META = M = auto()
675    CLASSIFIER = C = auto()
676    MIXED = MIX = auto()
677    ALL = auto()
678
679
680class OrderArg(NoValueEnum):
681    COUNT = C = auto()
682    LICENSE = L = auto()
683    NAME = N = auto()
684    AUTHOR = A = auto()
685    URL = U = auto()
686
687
688class FormatArg(NoValueEnum):
689    PLAIN = P = auto()
690    PLAIN_VERTICAL = auto()
691    MARKDOWN = MD = M = auto()
692    RST = REST = R = auto()
693    CONFLUENCE = C = auto()
694    HTML = H = auto()
695    JSON = J = auto()
696    JSON_LICENSE_FINDER = JLF = auto()
697    CSV = auto()
698
699
700def value_to_enum_key(value: str) -> str:
701    return value.replace('-', '_').upper()
702
703
704def enum_key_to_value(enum_key: Enum) -> str:
705    return enum_key.name.replace('_', '-').lower()
706
707
708def choices_from_enum(enum_cls: NoValueEnum) -> List[str]:
709    return [key.replace('_', '-').lower()
710            for key in enum_cls.__members__.keys()]
711
712
713MAP_DEST_TO_ENUM = {
714    'from_': FromArg,
715    'order': OrderArg,
716    'format_': FormatArg,
717}
718
719
720class SelectAction(argparse.Action):
721    def __call__(
722        self, parser: argparse.ArgumentParser,
723        namespace: argparse.Namespace,
724        values: Text,
725        option_string: Optional[Text] = None,
726    ) -> None:
727        enum_cls = MAP_DEST_TO_ENUM[self.dest]
728        values = value_to_enum_key(values)
729        setattr(namespace, self.dest, getattr(enum_cls, values))
730
731
732def create_parser():
733    parser = CompatibleArgumentParser(
734        description=__summary__,
735        formatter_class=CustomHelpFormatter)
736
737    common_options = parser.add_argument_group('Common options')
738    format_options = parser.add_argument_group('Format options')
739    verify_options = parser.add_argument_group('Verify options')
740
741    parser.add_argument(
742        '-v', '--version',
743        action='version',
744        version='%(prog)s ' + __version__)
745
746    common_options.add_argument(
747        '--from',
748        dest='from_',
749        action=SelectAction, type=str,
750        default=FromArg.MIXED, metavar='SOURCE',
751        choices=choices_from_enum(FromArg),
752        help='R|where to find license information\n'
753             '"meta", "classifier, "mixed", "all"\n'
754             '(default: %(default)s)')
755    common_options.add_argument(
756        '-o', '--order',
757        action=SelectAction, type=str,
758        default=OrderArg.NAME, metavar='COL',
759        choices=choices_from_enum(OrderArg),
760        help='R|order by column\n'
761             '"name", "license", "author", "url"\n'
762             '(default: %(default)s)')
763    common_options.add_argument(
764        '-f', '--format',
765        dest='format_',
766        action=SelectAction, type=str,
767        default=FormatArg.PLAIN, metavar='STYLE',
768        choices=choices_from_enum(FormatArg),
769        help='R|dump as set format style\n'
770             '"plain", "plain-vertical" "markdown", "rst", \n'
771             '"confluence", "html", "json", \n'
772             '"json-license-finder",  "csv"\n'
773             '(default: %(default)s)')
774    common_options.add_argument(
775        '--summary',
776        action='store_true',
777        default=False,
778        help='dump summary of each license')
779    common_options.add_argument(
780        '--output-file',
781        action='store', type=str,
782        help='save license list to file')
783    common_options.add_argument(
784        '-i', '--ignore-packages',
785        action='store', type=str,
786        nargs='+', metavar='PKG',
787        default=[],
788        help='ignore package name in dumped list')
789    common_options.add_argument(
790        '-p', '--packages',
791        action='store', type=str,
792        nargs='+', metavar='PKG',
793        default=[],
794        help='only include selected packages in output')
795    format_options.add_argument(
796        '-s', '--with-system',
797        action='store_true',
798        default=False,
799        help='dump with system packages')
800    format_options.add_argument(
801        '-a', '--with-authors',
802        action='store_true',
803        default=False,
804        help='dump with package authors')
805    format_options.add_argument(
806        '-u', '--with-urls',
807        action='store_true',
808        default=False,
809        help='dump with package urls')
810    format_options.add_argument(
811        '-d', '--with-description',
812        action='store_true',
813        default=False,
814        help='dump with short package description')
815    format_options.add_argument(
816        '-l', '--with-license-file',
817        action='store_true',
818        default=False,
819        help='dump with location of license file and '
820             'contents, most useful with JSON output')
821    format_options.add_argument(
822        '--no-license-path',
823        action='store_true',
824        default=False,
825        help='I|when specified together with option -l, '
826             'suppress location of license file output')
827    format_options.add_argument(
828        '--with-notice-file',
829        action='store_true',
830        default=False,
831        help='I|when specified together with option -l, '
832             'dump with location of license file and contents')
833    format_options.add_argument(
834        '--filter-strings',
835        action="store_true",
836        default=False,
837        help='filter input according to code page')
838    format_options.add_argument(
839        '--filter-code-page',
840        action="store", type=str,
841        default="latin1",
842        metavar="CODE",
843        help='I|specify code page for filtering '
844             '(default: %(default)s)')
845
846    verify_options.add_argument(
847        '--fail-on',
848        action='store', type=str,
849        default=None,
850        help='fail (exit with code 1) on the first occurrence '
851             'of the licenses of the semicolon-separated list')
852    verify_options.add_argument(
853        '--allow-only',
854        action='store', type=str,
855        default=None,
856        help='fail (exit with code 1) on the first occurrence '
857             'of the licenses not in the semicolon-separated list')
858
859    return parser
860
861
862def output_colored(code, text, is_bold=False):
863    """
864    Create function to output with color sequence
865    """
866    if is_bold:
867        code = '1;%s' % code
868
869    return '\033[%sm%s\033[0m' % (code, text)
870
871
872def save_if_needs(output_file, output_string):
873    """
874    Save to path given by args
875    """
876    if output_file is None:
877        return
878
879    try:
880        with open(output_file, 'w', encoding='utf-8') as f:
881            f.write(output_string)
882        sys.stdout.write('created path: ' + output_file + '\n')
883        sys.exit(0)
884    except IOError:
885        sys.stderr.write('check path: --output-file\n')
886        sys.exit(1)
887
888
889def main():  # pragma: no cover
890    parser = create_parser()
891    args = parser.parse_args()
892
893    output_string = create_output_string(args)
894
895    output_file = args.output_file
896    save_if_needs(output_file, output_string)
897
898    print(output_string)
899    warn_string = create_warn_string(args)
900    if warn_string:
901        print(warn_string, file=sys.stderr)
902
903
904if __name__ == '__main__':  # pragma: no cover
905    main()
906