Spaces:
Paused
Paused
| """Command-line interface for the :mod:`idna` package. | |
| Invoked via ``python -m idna``. See :func:`main` for the entry point. | |
| """ | |
| import argparse | |
| import sys | |
| from collections.abc import Iterable | |
| from itertools import chain | |
| from typing import IO, Optional | |
| from . import IDNAError, decode, encode | |
| from .core import _alabel_prefix, _unicode_dots_re | |
| from .package_data import __version__ | |
| def _looks_like_alabel(s: str) -> bool: | |
| """Return True if any label in ``s`` carries the ``xn--`` ACE prefix.""" | |
| prefix = _alabel_prefix.decode("ascii") | |
| return any(label.lower().startswith(prefix) for label in _unicode_dots_re.split(s)) | |
| def _build_parser() -> argparse.ArgumentParser: | |
| parser = argparse.ArgumentParser( | |
| prog="python -m idna", | |
| description=( | |
| "Convert a domain name between its Unicode (U-label) and " | |
| "ASCII-compatible (A-label) forms. With no mode flag, the " | |
| "direction is chosen from the first input — if it contains " | |
| "an xn-- label the stream is decoded, otherwise it is " | |
| "encoded — and the same mode is applied to every remaining " | |
| "input. UTS #46 mapping is applied by default; pass " | |
| "--strict to disable it. When no domains are given on the " | |
| "command line and stdin is piped, one domain per line is " | |
| "read from stdin." | |
| ), | |
| ) | |
| mode = parser.add_mutually_exclusive_group() | |
| mode.add_argument( | |
| "-e", | |
| "--encode", | |
| dest="mode", | |
| action="store_const", | |
| const="encode", | |
| help="Encode the input to its ASCII A-label form.", | |
| ) | |
| mode.add_argument( | |
| "-d", | |
| "--decode", | |
| dest="mode", | |
| action="store_const", | |
| const="decode", | |
| help="Decode the input from its ASCII A-label form.", | |
| ) | |
| parser.add_argument( | |
| "--strict", | |
| action="store_true", | |
| help="Disable the default UTS #46 mapping and apply IDNA 2008 rules verbatim.", | |
| ) | |
| parser.add_argument( | |
| "--version", | |
| action="version", | |
| version=f"idna {__version__}", | |
| ) | |
| parser.add_argument( | |
| "domain", | |
| nargs="*", | |
| help="One or more domain names to convert. Omit to read from stdin.", | |
| ) | |
| return parser | |
| def _iter_stdin(stream: IO[str]) -> Iterable[str]: | |
| """Yield non-empty stripped lines from ``stream``, ignoring blanks.""" | |
| for line in stream: | |
| stripped = line.strip() | |
| if stripped: | |
| yield stripped | |
| def _convert_one(domain: str, mode: str, uts46: bool) -> bool: | |
| """Convert ``domain`` and write the result; return ``False`` on failure.""" | |
| try: | |
| if mode == "decode": | |
| print(decode(domain, uts46=uts46)) | |
| else: | |
| print(encode(domain, uts46=uts46).decode("ascii")) | |
| except IDNAError as err: | |
| print(f"idna: {mode} failed for {domain!r}: {err}", file=sys.stderr) | |
| return False | |
| return True | |
| def main(argv: Optional[list[str]] = None) -> int: | |
| """Entry point for ``python -m idna``. | |
| When more than one domain is supplied (via positional arguments or | |
| piped stdin) and no mode flag is given, the first input determines | |
| the direction and that mode is applied uniformly to the rest. | |
| :param argv: Argument list excluding the program name. Defaults to | |
| :data:`sys.argv` when ``None``. | |
| :returns: ``0`` on success, ``1`` if any conversion fails. | |
| """ | |
| parser = _build_parser() | |
| args = parser.parse_args(argv) | |
| uts46 = not args.strict | |
| if args.domain: | |
| domains: Iterable[str] = args.domain | |
| elif not sys.stdin.isatty(): | |
| domains = _iter_stdin(sys.stdin) | |
| else: | |
| parser.error("a domain argument is required when stdin is a terminal") | |
| iterator = iter(domains) | |
| first = next(iterator, None) | |
| if first is None: | |
| return 0 | |
| mode = args.mode or ("decode" if _looks_like_alabel(first) else "encode") | |
| results = [_convert_one(domain, mode, uts46) for domain in chain([first], iterator)] | |
| return 0 if all(results) else 1 | |
| if __name__ == "__main__": | |
| sys.exit(main()) | |