Перейти к содержанию

liza-text: приложение командной строки

Реализовано в 0.0.24. liza-text проверяет текстовые файлы и считает Unicode scalar values, читая файл до EOF чанками по 2 КиБ. Аргументы, пути и содержимое файла имеют разные контракты: путь не обязан быть корректным UTF-8, даже когда проверяемый файл должен им быть.

Сборка и запуск

Из корня репозитория:

python3 -m liza build examples/apps/liza_text.liza -o build/liza-text
./build/liza-text check examples/data/utf8.txt
./build/liza-text count examples/data/utf8.txt
./build/liza-text info examples/data/utf8.txt

На Windows результат — build/liza-text.exe; запуск в PowerShell:

.\build\liza-text.exe info "C:\Users\Alex\Documents\Лиза text.txt"

Можно сразу собрать и запустить через компилятор. Всё после -- передаётся исполняемой программе, без повторной интерпретации оболочкой:

python3 -m liza run examples/apps/liza_text.liza -- info examples/data/utf8.txt

Программа принимает ровно команду и путь. - считается обычным именем файла; stdin, рекурсивный обход, wildcard expansion и дополнительные флаги в этот инструмент не входят. Оболочка может сама раскрыть шаблон до запуска.

Точный вывод

Для файла Лиза 😀 с завершающим LF:

Команда stdout stderr Код
check OK\n Пусто 0
count 7\n Пусто 0
info Bytes: 14\nCode points: 7\n Пусто 0

Здесь \n обозначает один байт LF, включая перенаправленный вывод Windows. info считает байты содержимого файла, без платформенного перевода строк. NUL и BOM допустимы и учитываются; счётчик измеряет кодовые точки, не графемы. Пустой файл даёт 0 байт и 0 кодовых точек.

При некорректном UTF-8 все три команды оставляют stdout пустым и пишут в stderr:

Invalid UTF-8 at byte 12345

Смещение начинается с нуля и указывает на начало некорректной последовательности относительно файла, даже если она пересекает границу чтения.

Код Значение Диагностика в stderr
0 Успех Нет
1 Некорректный UTF-8 Invalid UTF-8 at byte N\n
2 Неверная команда или число аргументов Usage: liza-text <check\|count\|info> <file>\n
3 Ошибка ввода-вывода Cannot open file\n, Cannot read file\n или Cannot close file\n; при ошибке самого вывода сообщение не гарантируется
4 Превышение поддерживаемого размера u64 Input exceeds supported size\n

Успешный результат выводится только после EOF, закрытия файла и успешного finish. Если само чтение или закрытие не удалось, результат проверки текста не выводится. Ошибка чтения/подсчёта размера имеет приоритет над последующей ошибкой закрытия. Ошибка записи диагностики меняет код завершения на 3. При отказе записи stdout или stderr уже принятый ОС префикс остаётся в потоке: транзакционность вывода не обещается.

Аргументы и lifetime

cli.count()                 // u32, эффект io
cli.arg(index)              // Option<Arg>, эффект io
cli.path_arg(index)         // Option<Path>, эффект io
cli.equals(arg, "check")   // bool, pure

index имеет тип u32. Индекс 0 — имя программы, переданное средой запуска; это не гарантия абсолютного пути к исполняемому файлу. Отсутствующий индекс, включая 4294967295, возвращает None. Пустой аргумент отличается от отсутствующего. Получение аргументов имеет эффект io; сравнение уже полученного неизменяемого аргумента — pure.

Arg и Path — два различных непрозрачных типа. Они заимствуют неизменяемые данные запуска, доступные до возвращения из main. Их можно копировать, перемещать, передавать и возвращать между функциями, в том числе generic. copy копирует только заимствованное представление, не создаёт собственного хранилища строки. Пользователь ничего не освобождает.

В 0.0.24 они не могут быть полями пользовательских структур/вариантов, элементами массивов, views, региональных буферов или Vec. Option и Result могут переносить их между функциями, но обёртка не снимает запрет размещения в таких агрегатах. Возвращаемый тип main по-прежнему u32: заимствование не выходит за время жизни аргументов процесса.

Нет конструкторов Arg(...)/Path(...), доступа к полям, преобразований в число, Text, View<u8> или произвольный адрес. Не вводятся String, конкатенация, нормализация и изменение аргументов.

cli.equals сравнивает целиком, с учётом регистра, без нормализации и locale: на POSIX — с UTF-8 байтами литерала, на Windows — с его UTF-16 представлением. Некорректный для Unicode аргумент не мешает использовать его как путь. Литерал с встроенным NUL не совпадает ни с одним аргументом.

Путь остаётся платформенным

match cli.path_arg(2) {
    None => { return 2 }
    Some(path) => {
        match file.open_path(path) {
            Err(error) => { return 3 }
            Ok(input) => {
                match file.close(move input) {
                    Ok(closed) => { return 0 }
                    Err(error) => { return 3 }
                }
            }
        }
    }
}

Этот фрагмент — тело функции с эффектом io. file.open_path(Path) -> Result<File,FileError> открывает файл только для чтения. Полученный File — самостоятельный ресурс с прежними обязанностями закрытия. Существующий file.open(Text) для литералов не изменён.

Платформа Представление и открытие
Linux/macOS Исходные NUL-terminated байты argv передаются в open, без проверки UTF-8
Windows UTF-16 code units аргументов wmain передаются в CreateFileW, без ANSI code page и UTF-8 round trip

Файловая система может отклонить имя по собственным правилам. Например, не-UTF-8 имя проверяется реальным файлом на Linux; файловая система macOS может отвергать такие имена. На Windows сохраняются и непарные UTF-16 суррогаты, если файловая система допускает имя. Пустой путь возвращает FileError.InvalidPath; ограничения длины и доступа определяет ОС.

На Windows используется широкая точка входа wmain. Подготовку аргументов выполняет CRT перед входом в программу; эта работа может включать выделение памяти. Она не объявляется бесплатной и не входит в эффект отдельного вызова cli.arg. После инициализации cli.count, cli.arg и cli.path_arg не создают строк и не выделяют память. Открытие пути не копирует содержимое файла и не перекодирует Path.

stdout и stderr

Работающие функции stdout сохранены. Для stderr добавлены:

stdout stderr
console.write(Text) console.err(Text)
console.println(Text) console.errln(Text)
console.print_u64(u64) console.err_u64(u64)
console.print_i64(i64) console.err_i64(i64)
console.write_bytes(View<u8>) console.err_bytes(View<u8>)

Все возвращают Result<u64,IoFailure> и требуют io. Успех означает, что переданные данные приняты потоком ОС, не физическую запись на диск и не гарантию отображения пользователем. При ошибке written сообщает успешно записанный префикс в байтах. errln добавляет LF отдельной записью.

На Windows текст терминала записывается через WriteConsoleW, а файл или pipe — через WriteFile с UTF-8 байтами. Выбор делается по handle каждого потока отдельно, согласно контракту Windows Console. Кодовая страница процесса не меняется. Raw bytes не перекодируются.

Исходник приложения

examples/apps/liza_text.liza
// Build: python3 -m liza build examples/apps/liza_text.liza -o build/liza-text
fn diagnostic(message:Text, code:u32)->u32:io {
    match console.errln(message) {Ok(n)=>{return code} Err(e)=>{return 3}}
}

fn add_bytes(total:u64, length:u32)->Option<u64> {
    if u64(length)>18446744073709551615-total {return None}
    return Some(total+u64(length))
}

fn report(mode:u32, total:u64, result:Result<u64,Utf8StreamError>)->u32:io {
    match result {
        Err(error)=>{
            match error.kind {
                Utf8StreamErrorKind.SizeOverflow=>{return diagnostic("Input exceeds supported size",4)}
                Utf8StreamErrorKind.InvalidEncoding=>{
                    match console.err("Invalid UTF-8 at byte ") {Ok(n)=>{} Err(e)=>{return 3}}
                    match console.err_u64(error.offset) {Ok(n)=>{} Err(e)=>{return 3}}
                    match console.errln("") {Ok(n)=>{} Err(e)=>{return 3}}
                    return 1
                }
            }
        }
        Ok(count)=>{
            if mode==0 {
                match console.println("OK") {Ok(n)=>{return 0} Err(e)=>{return 3}}
            }
            if mode==2 {
                match console.write("Bytes: ") {Ok(n)=>{} Err(e)=>{return 3}}
                match console.print_u64(total) {Ok(n)=>{} Err(e)=>{return 3}}
                match console.println("") {Ok(n)=>{} Err(e)=>{return 3}}
                match console.write("Code points: ") {Ok(n)=>{} Err(e)=>{return 3}}
            }
            match console.print_u64(count) {Ok(n)=>{} Err(e)=>{return 3}}
            match console.println("") {Ok(n)=>{} Err(e)=>{return 3}}
            return 0
        }
    }
}

fn run(mode:u32, path:Path)->u32:io {
    match file.open_path(path) {
        Err(error)=>{return diagnostic("Cannot open file",3)}
        Ok(input)=>{
            var state=utf8_stream.create()
            var total:u64=0
            var status:u32=0
            while true {
                match file.read_chunk(input) {
                    Err(error)=>{status=3;break}
                    Ok(chunk)=>{
                        if chunk.eof {break}
                        match add_bytes(total,chunk.len) {
                            None=>{status=4;break}
                            Some(next)=>{total=next}
                        }
                        bytes=chunk.data
                        data=view bytes[0..chunk.len]
                        state=utf8_stream.feed(state,data)
                    }
                }
            }
            match file.close(move input) {
                Err(error)=>{if status==0 {return diagnostic("Cannot close file",3)}}
                Ok(closed)=>{}
            }
            if status==3 {return diagnostic("Cannot read file",3)}
            if status==4 {return diagnostic("Input exceeds supported size",4)}
            return report(mode,total,utf8_stream.finish(state))
        }
    }
}

fn usage()->u32:io {return diagnostic("Usage: liza-text <check|count|info> <file>",2)}

fn main()->u32:io {
    if cli.count()!=3 {return usage()}
    match cli.arg(1) {
        None=>{return usage()}
        Some(command)=>{
            var mode:u32=0
            if cli.equals(command,"check") {mode=0}
            else {
                if cli.equals(command,"count") {mode=1}
                else {if cli.equals(command,"info") {mode=2} else {return usage()}}
            }
            match cli.path_arg(2) {None=>{return usage()} Some(path)=>{return run(mode,path)}}
        }
    }
}

Разбор аргументов, цикл чтения, счётчики, обработка Result и формат вывода написаны на Liza. Платформенная часть ограничена получением аргументов, открытием пути и записью в поток. Динамического String для приложения не потребовалось.

Проверки O0/O2 покрывают точные stdout/stderr и коды завершения, Unicode и пробелы в путях, нестандартные платформенные имена, ошибки чтения/закрытия, broken pipe и файлы больше 2 КиБ. Переполнение проверяется без огромного файла через граничные счётчики. Отдельно проверяется отсутствие аллокаций, копирования строк и перекодирования в тракте получения аргумента и открытия Path; внутренние затраты CRT и ОС не входят в эту проверку.