Взаимодействие с пользователем

2025-05-01 3614 17

При создании программ довольно часто возникает необходимость взаимодействовать с пользователем. Самый простой и универсальный способ — это использование стандартного ввода и вывода (stdin и stdout). Мы уже касались этой темы ранее, когда рассматривали работу со строками. Однако в этой главе мы углубимся в эту тему: подробно разберём, как считывать данные от пользователя, выводить информацию в консоль, а также научимся обрабатывать аргументы командной строки.

Умение эффективно работать со стандартным вводом/выводом и параметрами запуска — важная часть разработки CLI-приложений (command-line interface), позволяющая сделать программу более гибкой и удобной для пользователя. Мы рассмотрим базовые техники и подходы, которые пригодятся как в небольших скриптах, так и в более серьёзных утилитах.

Стандартные потоки ввода/вывода

Самый простой способ взаимодействия с пользователем в любой программе, это использование стандартных потоков ввода и вывода. Любая операционная система предоставляет программе стандартные потоки ввода и вывода - stdin, stdout и stderr.

В языке Zig для взаимодействия со стандартными потоками существует два подхода: явный и неявный. Неявный способ заключается в использовании функций, которые внутри себя автоматически обращаются к стандартным потокам. Например, мы уже неоднократно использовали функцию std.debug.print для вывода информации на экран. Хотя на первый взгляд она просто печатает текст, на самом деле она отправляет данные в стандартный поток ошибок (stderr), не требуя от нас явного указания потока.

Чтобы явно взаимодействовать со стандартными потоками ввода и вывода в языке Zig, следует использовать функции из модуля std.io. Эти функции предоставляют прямой доступ к соответствующим потокам, позволяя более гибко управлять вводом и выводом данных в приложении. Вот основные из них:

Если обратить внимание на сигнатуры этих функций, можно заметить, что они возвращают объекты типа std.fs.File, который представляет собой файловый дескриптор. Это связано с тем, что в Unix-подобных системах многие интерфейсы взаимодействия с операционной системой представлены в виде файлов.

Стандартные потоки ввода и вывода — не исключение: они также считаются файлами. Объект std.fs.File в Zig представляет собой универсальный дескриптор, который можно использовать как для чтения, так и для записи данных, независимо от того, работаете ли вы с обычными файлами или с потоками вроде stdin, stdout и stderr.

Как работать с файлами в Zig мы рассмотрим в следующей главе, а пока давайте продолжим изучение работы с потоками ввода и вывода. Для того чтобы взаимодействовать с потоком вам необходимы методы для чтения или записи данных в них. И файловый дескриптор предоставляет вам методы writer и reader, которые возвращают объекты типа std.io.GenericWriter и std.io.GenericReader соответственно. Как мы могли уже догадаться это не что иное как универсальные интерфейсы для чтения и записи данных, не зависимо от того, что за источник вы используете - стандартный поток, файл или сетевой сокет.

Давайте рассмотрим наиболее часто используемые методы этих интерфейсов и начнем мы с интерфейса std.io.GenericReader:

Для интерфейса std.io.GenericWriter часто используются следующие методы:

Давайте теперь рассмотрим как мы можем использовать эти методы со стандартными потоками ввода и вывода и напишем игру угадай число. Суть нашей игры будет проста - мы задумываем некоторое число, а пользователь пытается угадать его. У пользователя есть пять попыток, при каждом ответе пользователя мы подсказываем ему больше или меньше наше число.

const std = @import("std");

pub fn main() !void {
    // Инициализация генератора случайных чисел
    var prng = std.Random.DefaultPrng.init(blk: {
        var seed: u64 = undefined;
        try std.posix.getrandom(std.mem.asBytes(&seed));
        break :blk seed;
    });
    const rand = prng.random();

    // Загадываем число от 0 до 100
    const secret_number = rand.intRangeAtMost(u8, 0, 100);
    const max_attempts = 5;
    var attempts: u8 = 0;

    const stdin = std.io.getStdIn().reader();
    const stdout = std.io.getStdOut().writer();

    try stdout.print("Я загадал число от 0 до 100. У тебя {d} попытки, чтобы угадать!\n", .{max_attempts});

    while (attempts < max_attempts) {
        attempts += 1;
        try stdout.print("Попытка {d}/{d}. Введи число: ", .{ attempts, max_attempts });

        // Читаем ввод пользователя
        var input_buf: [10]u8 = undefined;
        const input_len = try stdin.readUntilDelimiterOrEof(&input_buf, '\n') orelse {
            try stdout.writeAll("Ошибка ввода.\n");
            continue;
        };
        const input_str = std.mem.trim(u8, input_len, &std.ascii.whitespace);

        // Парсим число
        const guess = std.fmt.parseInt(u8, input_str, 10) catch |err| {
            try stdout.print("Это не число! Ошибка: {s}\n", .{@errorName(err)});
            continue;
        };

        // Проверяем число
        if (guess == secret_number) {
            try stdout.print("Поздравляю! Ты угадал число {d} с {d} попытки!\n", .{ secret_number, attempts });
            return;
        } else if (guess < secret_number) {
            try stdout.print("Моё число БОЛЬШЕ чем {d}.\n", .{guess});
        } else {
            try stdout.print("Моё число МЕНЬШЕ чем {d}.\n", .{guess});
        }
    }

    try stdout.print("Ты проиграл! Я загадал число {d}.\n", .{secret_number});
}
$ zig build run
Я загадал число от 0 до 100. У тебя 5 попытки, чтобы угадать!
Попытка 1/5. Введи число: 10
Моё число БОЛЬШЕ чем 10.
Попытка 2/5. Введи число: 50
Моё число БОЛЬШЕ чем 50.
Попытка 3/5. Введи число: 80
Моё число БОЛЬШЕ чем 80.
Попытка 4/5. Введи число: 90
Моё число БОЛЬШЕ чем 90.
Попытка 5/5. Введи число: 95
Моё число БОЛЬШЕ чем 95.
Ты проиграл! Я загадал число 98.

Давайте разберем нашу программу. Первым шагом инициализируется генератор случайных чисел. Для этого используется стандартный генератор Xoshiro256 из библиотеки std.Random, который обеспечивает хорошее соотношение производительности и качества генерации, и является подходящим для большинства задач, не требующих криптографической стойкости. В стандартной библиотеке также доступны другие генераторы, ознакомиться с которыми можно в документации модуля std.Random.

Генератор требует инициализации начальными данными (seed). В качестве источника случайных байт используется системная POSIX-функция getrandom, которая обеспечивает надёжную инициализацию генератора.

После генерации случайного числа в диапазоне [0, 100] получаем доступ к стандартным потокам ввода и вывода с помощью std.io.getStdIn() и std.io.getStdOut() соответственно.

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

Ввод осуществляется с использованием метода readUntilDelimiterOrEof, который читает данные до символа-разделителя (например, новой строки) или до конца входного потока. Для хранения ввода используется буфер размером 10 байт — этого достаточно, учитывая, что вводимое значение ограничено тремя символами (включая возможные пробелы и символ новой строки).

При возникновении ошибки ввода, программа использует метод writeAll для вывода сообщения об ошибке и продолжает цикл запроса ввода.

После получения строки выполняется её предварительная обработка — с помощью std.mem.trim удаляются хвостовые пробелы, чтобы избежать ошибок при последующем парсинге. Затем, с использованием std.fmt.parseInt, строка преобразуется в целое число. В случае ошибки парсинга пользователю выводится соответствующее сообщение и на этот раз мы используем метод print для вывода ошибки.

Если преобразование прошло успешно, введённое число сравнивается с загаданным. В зависимости от результата пользователь получает сообщение о том, больше его число, меньше или совпадает с загаданным. При совпадении игра завершается досрочно.

Как мы видим, использование стандартных потоков ввода и вывода — задача достаточно простая. Однако у неё есть одна важная особенность, которую не всегда учитывают. Давайте разберёмся, что именно мы могли упустить при работе с потоками ввода и вывода.

Буфферезированный ввод/вывод

Запись и чтение из потоков ввода-вывода представляют собой обращение к операционной системе. Как известно, любое взаимодействие с ОС происходит через системные вызовы (syscalls), которые, в большинстве случаев, являются достаточно ресурсоёмкими операциями.

Стандартные библиотеки многих языков программирования стремятся минимизировать количество подобных вызовов, иногда реализуя функциональность средствами, не требующими обращения к операционной системе. Однако ввод и вывод через стандартные потоки не могут быть реализованы без участия ОС, поскольку требуют взаимодействия с реальными устройствами ввода-вывода (например, консолью или терминалом).

Каждый системный вызов сопровождается переключением из пользовательского режима в режим ядра (kernel mode), что само по себе является дорогой операцией с точки зрения производительности. Частые переключения контекста могут существенно замедлить выполнение программы, особенно в случаях, когда выполняется множество мелких операций ввода-вывода.

Чтобы избежать такого узкого места, многие функции ввода-вывода реализованы с использованием буферизации. Суть буферизации заключается в том, что данные временно накапливаются во внутреннем буфере, а затем передаются в конечное устройство или поток (будь то файл, консоль или сетевое соединение) одним крупным блоком. Это позволяет значительно сократить количество системных вызовов и повысить общую производительность.

Как обычно работает буферизация? Допустим, вы читаете данные из потока, используя буфер размером 10 байт. В случае использования буферизованного читателя (чтения с буфером), библиотека чтения не ограничивается только этим небольшим объёмом. Вместо этого она заранее считывает из источника (например, файла или сокета) значительно больший объём данных — чаще всего это 4096 байт, что соответствует стандартному размеру блока файловой системы. Полученные данные помещаются во внутренний буфер, и далее при чтении пользователю возвращаются данные из этого буфера, пока он не опустеет. Лишь после этого происходит следующий системный вызов для подгрузки новой порции данных.

Буферизация записи работает зеркально. При использовании буферизованного писателя данные сначала накапливаются во внутреннем буфере. Когда буфер заполняется (например, теми же 4096 байтами), происходит однократная запись всего содержимого буфера в выходной поток. После этого буфер очищается и становится готовым к приёму новых данных.

Такой подход позволяет значительно снизить количество обращений к операционной системе и повысить общую производительность ввода-вывода, особенно в случаях, когда выполняется множество мелких операций.

Давайте теперь рассмотрим, как буфферезировать работу с потоками ввода/вывода на zig:

const std = @import("std");

pub fn main() !void {
    // Инициализация генератора случайных чисел
    var prng = std.Random.DefaultPrng.init(blk: {
        var seed: u64 = undefined;
        try std.posix.getrandom(std.mem.asBytes(&seed));
        break :blk seed;
    });
    const rand = prng.random();

    // Загадываем число от 0 до 100
    const secret_number = rand.intRangeAtMost(u8, 0, 100);
    const max_attempts = 5;
    var attempts: u8 = 0;

    const stdin_stream = std.io.getStdIn().reader();
    var buffered_stdin = std.io.bufferedReader(stdin_stream);
    const br = buffered_stdin.reader();

    const stdout_stream = std.io.getStdOut().writer();
    var buffered_stdout = std.io.bufferedWriter(stdout_stream);
    const stdout = buffered_stdout.writer();

    try stdout.print("Я загадал число от 0 до 100. У тебя {d} попытки, чтобы угадать!\n", .{max_attempts});
    try buffered_stdout.flush();

    while (attempts < max_attempts) {
        try buffered_stdout.flush();

        attempts += 1;
        try stdout.print("Попытка {d}/{d}. Введи число: ", .{ attempts, max_attempts });
        try buffered_stdout.flush();

        // Читаем ввод пользователя
        var input_buf: [10]u8 = undefined;
        const input_len = try br.readUntilDelimiterOrEof(&input_buf, '\n') orelse {
            try stdout.writeAll("Ошибка ввода.\n");
            continue;
        };
        const input_str = std.mem.trim(u8, input_len, &std.ascii.whitespace);

        // Парсим число
        const guess = std.fmt.parseInt(u8, input_str, 10) catch |err| {
            try stdout.print("Это не число! Ошибка: {s}\n", .{@errorName(err)});
            continue;
        };

        // Проверяем число
        if (guess == secret_number) {
            try stdout.print("Поздравляю! Ты угадал число {d} с {d} попытки!\n", .{ secret_number, attempts });
            try buffered_stdout.flush();
            return;
        } else if (guess < secret_number) {
            try stdout.print("Моё число БОЛЬШЕ чем {d}.\n", .{guess});
        } else {
            try stdout.print("Моё число МЕНЬШЕ чем {d}.\n", .{guess});
        }
    }

    try stdout.print("Ты проиграл! Я загадал число {d}.\n", .{secret_number});
    try buffered_stdout.flush();
}

Чтобы превратить стандартные потоки stdin и stdout в буферизованные, достаточно обернуть их с помощью функций std.io.bufferedReader и std.io.bufferedWriter. После этого мы можем получить соответствующие интерфейсы reader и writer — так же, как мы делали это при работе с обычными потоками.

Второе отличие нового кода заключается в том, что, поскольку объём данных, выводимых в поток stdout, сравнительно невелик, пользователю может не сразу отобразиться результат вывода. Это связано с тем, что буфер вывода может оставаться незаполненным, а, следовательно, данные не будут автоматически сброшены в конечный поток. Чтобы гарантировать вывод сообщения на экран, необходимо явно вызвать метод flush у буферизованного писателя. Метод flush принудительно сбрасывает содержимое буфера в целевой поток, даже если буфер не заполнен.

В рамках нашего примера использование буферизованных потоков не даёт значительных преимуществ, поскольку объём операций ввода-вывода небольшой. Тем не менее, мы модифицировали код с буферизацией в образовательных целях — чтобы продемонстрировать, как работают буферизованные потоки и как правильно с ними взаимодействовать.

Парсинг аргументов командной строки

Один из типов приложений, которые вы можете разрабатывать — это утилиты командной строки. В Unix-мире широко распространён подход, при котором для решения небольшой задачи создаётся отдельная утилита. Такая утилита принимает аргументы командной строки и выполняет определённые действия в зависимости от переданных параметров. Позднее подобные утилиты можно объединять в цепочки с помощью оператора конвейера (|), тем самым решая более сложные задачи за счёт композиции простых инструментов.

Мы уже рассмотрели, как обрабатывать входящий поток данных в таких утилитах. Теперь давайте разберёмся, как получать и обрабатывать аргументы командной строки в CLI-приложениях.

В языке Zig для этого доступно два основных подхода. Один из них работает только на Unix-подобных системах, второй является кроссплатформенным. Начнём с более простого варианта, предназначенного исключительно для Unix-систем:

const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    var args = std.process.args();

    // Первый аргумент это имя нашей программы
    const prog_name = args.next().?;

    var verbose = false;
    var output_file: ?[]const u8 = null;
    var input_files = std.ArrayList([]const u8).init(allocator);
    defer input_files.deinit();

    while (args.next()) |arg| {
        if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) {
            printHelp(prog_name);
            return;
        } else if (std.mem.eql(u8, arg, "--verbose") or std.mem.eql(u8, arg, "-v")) {
            verbose = true;
        } else if (std.mem.eql(u8, arg, "--output") or std.mem.eql(u8, arg, "-o")) {
            if (args.next()) |next_arg| {
                output_file = next_arg;
            } else {
                std.debug.print("Ошибка: отсутствует аргумент для --output\n", .{});
                return error.MissingArgument;
            }
        } else {
            try input_files.append(arg);
        }

        std.debug.print("{s}\n", .{arg});
    }

    if (verbose) {
        std.debug.print("Режим verbose включен\n", .{});
    }

    if (output_file) |out| {
        std.debug.print("Выходной файл: {s}\n", .{out});
    }

    std.debug.print("Входные файлы:\n", .{});
    for (input_files.items) |file| {
        std.debug.print(" - {s}\n", .{file});
    }
}

fn printHelp(program_name: []const u8) void {
    std.debug.print(
        \\Использование: {s} [ОПЦИИ] [ФАЙЛЫ...]
        \\Опции:
        \\  -h, --help       Показать эту справку
        \\  -v, --verbose    Включить подробный вывод
        \\  -o, --output     Указать выходной файл
        \\
    , .{program_name});
}

Давайте запустим нашу программу с разными флагами:

$ zig build run -- --help
Использование: /Users/roman/Projects/zig/simple/zig-out/bin/simple [ОПЦИИ] [ФАЙЛЫ...]
Опции:
  -h, --help       Показать эту справку
  -v, --verbose    Включить подробный вывод
  -o, --output     Указать выходной файл


$ zig build run -- --verbose test
--verbose
test
Режим verbose включен
Входные файлы:
 - test

$ zig build run -- --verbose --output test xxx zzz
--verbose
--output
xxx
zzz
Режим verbose включен
Выходной файл: test
Входные файлы:
 - xxx
 - zzz

В нашем примере для получения параметров командной строки мы используем функцию std.process.args, которая возвращает итератор по аргументам, переданным программе. Первый элемент, полученный от этого итератора, всегда представляет собой путь к исполняемому файлу. Его можно либо просто пропустить, вызвав метод next, либо сохранить в переменную, как это сделано в нашем примере — например, для использования в справке или сообщениях об ошибке.

Стоит отметить, что std.process.args предоставляет лишь простой итератор по аргументам, разделённым по пробелам. Он не выполняет никакой дополнительной логики по разбору параметров: флаги, позиционные аргументы, ключи с значениями — всё это придётся обрабатывать вручную. Разделение опциональных и позиционных аргументов, проверка корректности значений и логика обработки — вся эта работа остаётся на разработчике, как продемонстрировано в нашем примере.

Важно учитывать, что std.process.args работает только на Unix-подобных системах. На Windows для аналогичной задачи необходимо использовать std.process.argsWithAllocator, поскольку в этой операционной системе аргументы командной строки обрабатываются иначе и требуют выделения памяти через аллокатор.

Давайте теперь рассмотрим, как наш пример изменится при использовании std.process.argsWithAllocator, что позволит сделать приложение кроссплатформенным.

const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    var args = try std.process.argsWithAllocator(allocator);
    defer args.deinit();

    // Первый аргумент это имя нашей программы
    const prog_name = args.next().?;

    var verbose = false;
    var output_file: ?[]const u8 = null;
    var input_files = std.ArrayList([]const u8).init(allocator);
    defer input_files.deinit();

    while (args.next()) |arg| {
        if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) {
            printHelp(prog_name);
            return;
        } else if (std.mem.eql(u8, arg, "--verbose") or std.mem.eql(u8, arg, "-v")) {
            verbose = true;
        } else if (std.mem.eql(u8, arg, "--output") or std.mem.eql(u8, arg, "-o")) {
            if (args.next()) |next_arg| {
                output_file = next_arg;
            } else {
                std.debug.print("Ошибка: отсутствует аргумент для --output\n", .{});
                return error.MissingArgument;
            }
        } else {
            try input_files.append(arg);
        }

        std.debug.print("{s}\n", .{arg});
    }

    if (verbose) {
        std.debug.print("Режим verbose включен\n", .{});
    }

    if (output_file) |out| {
        std.debug.print("Выходной файл: {s}\n", .{out});
    }

    std.debug.print("Входные файлы:\n", .{});
    for (input_files.items) |file| {
        std.debug.print(" - {s}\n", .{file});
    }
}

fn printHelp(program_name: []const u8) void {
    std.debug.print(
        \\Использование: {s} [ОПЦИИ] [ФАЙЛЫ...]
        \\Опции:
        \\  -h, --help       Показать эту справку
        \\  -v, --verbose    Включить подробный вывод
        \\  -o, --output     Указать выходной файл
        \\
    , .{program_name});
}

Как видим, для того чтобы сделать наш код универсальным, нам понадобилось изменить всего две строки: вместо std.process.args мы используем std.process.argsWithAllocator. Возникает логичный вопрос — почему мы сразу не выбрали кроссплатформенный вариант?

Дело в том, что argsWithAllocator требует явного выделения памяти, поэтому мы обязаны передавать в него аллокатор. Это немного усложняет код и приводит к дополнительным накладным расходам, связанным с аллокациями. Если вы точно знаете, что ваше приложение будет запускаться только в Unix-средах, вы можете упростить реализацию и избежать лишних аллокаций, используя std.process.args.

Использование zig-clap

Конечно парсить параметры командной строки в каждом приложении вручную не совсем та задача, которой бы мы хотели заниматься. Поэтому во многих языках программирования есть готовые библиотеки, которые берут на себя всю работу по разбору параметров командной строки за вас. В zig для таких нужд есть прекрасная библиотека zig-clap. Давайте рассмотрим как может выглядеть наш пример в сипользованием zig-clap:

const std = @import("std");
const clap = @import("clap");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    // Определяем параметры парсера
    const params = comptime clap.parseParamsComptime(
        \\-h, --help             Display this help and exit.
        \\-v, --verbose          Enable verbose output.
        \\-o, --output <str>     Set output file.
        \\<str>...             Input files.
    );

    // Создаем кастомный парсер, который включает парсер для строк
    const parsers = .{
        .str = clap.parsers.string,
    };

    // Парсим аргументы
    var diag = clap.Diagnostic{};
    var res = clap.parse(
        clap.Help,
        &params,
        parsers,
        .{
            .diagnostic = &diag,
            .allocator = allocator,
        },
    ) catch |err| {
        diag.report(std.io.getStdErr().writer(), err) catch {};
        return err;
    };
    defer res.deinit();

    // Обработка флагов и аргументов
    if (res.args.help != 0) {
        try clap.help(
            std.io.getStdErr().writer(),
            clap.Help,
            &params,
            .{},
        );
        return;
    }

    const verbose = res.args.verbose != 0;
    const output_file = if (res.args.output) |o| o else null;

    if (verbose) {
        std.debug.print("Режим verbose включен\n", .{});
    }

    if (output_file) |out| {
        std.debug.print("Выходной файл: {s}\n", .{out});
    }

    // Обработка позиционных аргументов
    std.debug.print("Входные файлы:\n", .{});
    for (res.positionals[0]) |file| {
        std.debug.print(" - {s}\n", .{file});
    }
}

Если мы запустим теперь нашу программу то увидим следующее:

$ zig build run -- --help
    -h, --help
            Display this help and exit.

    -v, --verbose
            Enable verbose output.

    -o, --output <str>
            Set output file.

    <str>...
            Input files.

$ zig build run -- -v -o dest file1 file2
Режим verbose включен
Выходной файл: dest
Входные файлы:
 - file1
 - file2

Как видно, использование библиотеки zig-clap избавляет нас от необходимости самостоятельно парсить аргументы командной строки. Вместо этого мы можем воспользоваться удобным методом parseParamsComptime, в который передаётся список ожидаемых параметров. Далее библиотека распарсит переданный ей шаблон аргументов и сформирует правильный код обработки аргументов.

Библиотека позволяет заранее — на этапе компиляции — определить, какие аргументы мы хотим принимать, какие из них обязательные, какие опциональные, и какие имеют имена (флаги, такие как –help или -v). Всё это описывается через специальную структуру параметров.

Таким образом, zig-clap берёт на себя всю рутину: проверку корректности параметров, генерацию справки (–help) и распределение значений по именованным переменным. Это делает код более компактным, понятным и надёжным.

Переменные окружения

Ещё один удобный способ взаимодействия с пользователем — использование переменных окружения. Это особенно полезно, когда необходимо передавать в программу параметры при каждом запуске, но не хочется указывать их вручную в командной строке. В таком случае вы можете заранее задать переменные окружения в операционной системе и затем использовать их значения в программе.

В Zig для чтения переменных окружения есть два подхода:

Если вам нужно прочитать всего одну переменную, проще воспользоваться getEnvVarOwned. А если требуется работать с несколькими переменными — будет удобнее использовать getEnvMap.

Давайте рассмотрим, как прочитать значение переменной окружения в программе:

const std = @import("std");

pub fn main() !void {
    const allocator = std.heap.page_allocator;

    // Получение переменной окружения
    if (std.process.getEnvVarOwned(allocator, "USE_LOGGER")) |logger| {
        defer allocator.free(logger);
        std.debug.print("USE_LOGGER: {s}\n", .{logger});
    } else |err| {
        std.debug.print("Ошибка получения переменной: {}\n", .{err});
    }
}

Давайте теперь запустим нашу программу с задание переменной окружения и без:

$ zig build run
Ошибка получения переменной: error.EnvironmentVariableNotFound

$ USE_LOGGER=true zig build run
USE_LOGGER: true

Как мы видим, если переменная окружения не задана, то нам возвращается ошибка EnvironmentVariableNotFound, которую мы можем обработать и выставить значение по умолчанию например. Если же переменная окружения задана, то мы успешно получаем ее значение и можем использовать его в нашей программе.

Заключение

В этой главе мы рассмотрели основные способы взаимодействия с пользователем через консольные приложения в языке Zig. Мы изучили работу со стандартными потоками ввода/вывода (stdin, stdout, stderr), научились создавать интерактивные приложения, способные считывать информацию от пользователя и выводить данные на экран.

Также мы изучили различные подходы к обработке аргументов командной строки — от ручного парсинга с помощью стандартных функций std.process.args() и std.process.argsWithAllocator() до использования специализированной библиотеки zig-clap, которая значительно упрощает эту задачу.

Умение эффективно работать со стандартным вводом/выводом и аргументами командной строки является фундаментальным навыком при разработке CLI-приложений. Эти знания позволяют создавать гибкие и удобные для пользователя инструменты, соответствующие принципам UNIX-философии — “делать одну вещь, но делать её хорошо”.

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

#command line args #stdin #stdout #zig #zigbook

0%