#!/usr/bin/env python3
# SPDX-License-Identifier: WTFPL

# sort stdin like sort(1) but consider digit characters as numbers to sort them.
# e.g. sort(1) could output this:
# - foo1.bar
# - foo10.bar
# - foo2.bar
# and sort-with-numbers would output:
# - foo1.bar
# - foo2.bar
# - foo10.bar
# sort-with-numbers can perform "version sorting" naturally

import argparse
import locale
import re
import sys


def fileinput_sep(files=None, *, linesep="\n", openhook=None):
    if files is None:
        files = sys.argv[1:]
    if not files:
        files = ("-",)

    for file in files:
        if file == "-":
            fp = sys.stdin
        else:
            if openhook:
                fp = openhook(file)
            else:
                fp = open(file, "r")

        with fp:
            yield from iter_lines(fp, linesep=linesep)


def iter_lines(fp, *, linesep="\n", bufsize=4096):
    buf = ""
    pos = -1
    while True:
        if pos < 0:
            read = fp.read(bufsize)
            if not read:
                break

            buf += read
        else:
            yield buf[:pos + 1]
            buf = buf[pos + 1:]

        pos = buf.find(linesep)

    yield buf


def extract_ints(line, trim_whitespace=False):
    # split the string into parts:
    # - string with digits are coerced to int and thus compared as ints
    # - non-digits are compared as string

    # since we then rely in python's standard comparison operators
    # a str cannot be compared to an int...
    # so we prepend a type id that makes only compatible types are compared
    # `(type_id, real_value)`
    # - 0 if int
    # - 1 if str
    # when 2 type_id are different, python will sort them apart
    # without considering the real_value to compare

    ret = []
    for part_str in re.findall(r"\D+|\d+", line):
        if trim_whitespace:
            part_str = part_str.strip()

        try:
            part_as_num = int(part_str)
        except ValueError:
            # not a number, keep as is
            ret.append((1, part_str))
        else:
            ret.append((0, part_as_num))

    return tuple(ret)


def collate_strs(tup):
    return tuple(
        (type, part)
        if type == 0
        else (type, locale.strxfrm(part.rstrip("\x00")))
        for type, part in tup
    )


# user locale for collation
locale.setlocale(locale.LC_ALL, "")

parser = argparse.ArgumentParser()
parser.add_argument(
    "-z", help="NUL separated lines", dest="sep", action="store_const",
    const="\x00", default="\n",
)
parser.add_argument(
    "--trim-whitespace", help="trim whitespace", action="store_true",
)
parser.add_argument("files", nargs="*")
args = parser.parse_args()

all_lines = list(fileinput_sep(args.files, linesep=args.sep))
all_lines.sort(
    key=lambda line:
        collate_strs(extract_ints(line, args.trim_whitespace)))
print("".join(all_lines), end="")
