| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128 |
- """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())
|