# Copyright (C) 2021 Daiki Ueno
# This file is part of GnuTLS.
# GnuTLS is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# GnuTLS is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see
# .
from typing import Mapping, NamedTuple, Optional, Sequence
import datetime
import io
import textwrap
class Section(NamedTuple):
meta: Mapping[str, str]
options: Sequence[Mapping[str, str]]
@classmethod
def from_json(cls, json):
return cls(meta=json['meta'], options=json['options'])
@classmethod
def default(cls):
return DEFAULT_SECTION
# Default options
DEFAULT_SECTION = Section(
meta={
'desc': 'Version, usage and configuration options',
},
options=[{
'long-option': 'version',
'short-option': 'v',
'arg-type': 'keyword',
'arg-optional': '',
'desc': 'output version information and exit',
'detail': textwrap.fill(textwrap.dedent("""\
Output version of program and exit.
The default mode is `v', a simple version.
The `c' mode will print copyright information and
`n' will print the full copyright notice.\
"""), width=72, fix_sentence_endings=True)
}, {
'long-option': 'help',
'short-option': 'h',
'desc': 'display extended usage information and exit',
'detail': 'Display usage information and exit.'
}, {
'long-option': 'more-help',
'short-option': '!',
'desc': 'extended usage information passed thru pager',
'detail': 'Pass the extended usage information through a pager.'
}]
)
ARG_TYPE_TO_VALUE = {
'string': 'str',
'number': 'num',
'file': 'file',
'keyword': 'arg',
}
def default_arg_name(s: str) -> str:
return ARG_TYPE_TO_VALUE[s]
def usage(meta: Mapping[str, str], sections: Sequence[Section]) -> str:
prog_name = sections[0].meta['prog-name']
prog_title = sections[0].meta["prog-title"]
out = io.StringIO()
out.write(f'{prog_name} - {prog_title}\n')
argument = sections[0].meta.get('argument', '')
out.write(
f'Usage: {prog_name} '
f'[ - [] | --[{{=| }}] ]... {argument}\n'
)
for section in sections:
desc = section.meta["desc"]
out.write('\n')
if desc != '':
out.write(f'{desc}:\n\n')
for option in section.options:
if 'deprecated' in option:
continue
long_opt = option['long-option']
short_opt = option.get('short-option')
arg_type = option.get('arg-type')
if short_opt:
header = f' -{short_opt}, --{long_opt}'
else:
header = f' --{long_opt}'
if arg_type:
arg = ARG_TYPE_TO_VALUE.get(arg_type, 'arg')
if 'arg-optional' in option:
header += f'[={arg}]'
else:
header += f'={arg}'
if len(header) < 30:
header = header.ljust(30)
elif arg_type:
header += ' '
else:
header += ' '
alias = option.get('aliases')
if alias:
option_desc = f"an alias for the '{alias}' option"
else:
option_desc = option['desc']
out.write(f'{header}{option_desc}\n')
conflict_opts = option.get('conflicts', '').split()
if len(conflict_opts) == 1:
out.write(
f"\t\t\t\t- prohibits the option '{conflict_opts[0]}'\n"
)
elif len(conflict_opts) > 1:
conflict_opts_concatenated = '\n'.join([
f'\t\t\t\t{conflict_opt}' for conflict_opt in conflict_opts
])
out.write(
'\t\t\t\t- prohibits these options:\n' +
conflict_opts_concatenated + '\n'
)
require_opts = option.get('requires', '').split()
if len(require_opts) == 1:
out.write(
f"\t\t\t\t- requires the option '{require_opts[0]}'\n"
)
elif len(require_opts) > 1:
require_opts_concatenated = '\n'.join([
f'\t\t\t\t{require_opt}' for require_opt in require_opts
])
out.write(
'\t\t\t\t- requires these options:\n' +
require_opts_concatenated + '\n'
)
file_exists = option.get('file-exists', 'no')
if file_exists == 'yes':
out.write('\t\t\t\t- file must pre-exist\n')
disable_prefix = option.get('disable-prefix')
if disable_prefix:
out.write(
f"\t\t\t\t- disabled as '--{disable_prefix}{long_opt}'\n"
)
if 'enabled' in option:
out.write('\t\t\t\t- enabled by default\n')
if 'max' in option:
max_count = option.get('max')
assert max_count == 'NOLIMIT', \
f'max keyword with value {max_count} is not supported'
out.write('\t\t\t\t- may appear multiple times\n')
arg_min = option.get('arg-min')
arg_max = option.get('arg-max')
if arg_min and arg_max:
out.write(
'\t\t\t\t- it must be in the range:\n'
f'\t\t\t\t {int(arg_min)} to {int(arg_max)}\n'
)
out.write(textwrap.dedent('''
Options are specified by doubled hyphens and their name or by a single
hyphen and the flag character.
'''))
if 'argument' in sections[0].meta:
out.write(('Operands and options may be intermixed. '
'They will be reordered.\n'))
out.write('\n' + sections[0].meta['detail'] + '\n')
bug_email = meta.get('bug-email')
if bug_email:
out.write('\n' + f'Please send bug reports to: <{bug_email}>' + '\n')
return out.getvalue()
LICENSES = {
'gpl3+': textwrap.dedent('''\
This is free software. It is licensed for use, modification and
redistribution under the terms of the GNU General Public License,
version 3 or later
''')
}
FULL_LICENSES = {
'gpl3+': textwrap.dedent('''\
This is free software. It is licensed for use, modification and
redistribution under the terms of the GNU General Public License,
version 3 or later
@prog_name@ is free software: you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation,
either version 3 of the License, or (at your option) any later version.
@prog_name@ is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty
of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
''')
}
def version(meta: Mapping[str, str], what='c') -> str:
prog_name = meta['prog-name']
version = meta.get('version', '0.0.0')
license = meta.get('license', 'unknown')
if license:
license_text: Optional[str] = LICENSES[license]
full_license_text: Optional[str] = FULL_LICENSES[license]
else:
license_text = None
full_license_text = None
copyright_year = meta.get('copyright-year',
str(datetime.date.today().year))
copyright_holder = meta.get('copyright-holder', 'COPYRIGHT HOLDER')
bug_email = meta.get('bug-email')
out = io.StringIO()
if what == 'v':
out.write(f'{prog_name} {version}')
elif what == 'c':
out.write(textwrap.dedent(f'''\
{prog_name} {version}
Copyright (C) {copyright_year} {copyright_holder}
'''))
if license_text:
out.write(license_text)
if bug_email:
out.write(textwrap.dedent(f'''\
Please send bug reports to: <{bug_email}>\
'''))
elif what == 'n':
out.write(textwrap.dedent(f'''\
{prog_name} {version}
Copyright (C) {copyright_year} {copyright_holder}
'''))
if full_license_text:
out.write(full_license_text.replace('@prog_name@', prog_name))
if bug_email:
out.write(textwrap.dedent(f'''\
Please send bug reports to: <{bug_email}>\
'''))
return out.getvalue()