JavaRush /Курсы /Swift SELF /Рабочая директория CLI

Рабочая директория CLI

Swift SELF
55 уровень , 2 лекция
Открыта

1. Рабочая директория и CWD

Когда вы пишете CLI, вы невольно становитесь немного “туристическим гидом” по файловой системе: пользователь даёт путь, а вы должны понять, куда именно он имел в виду попасть. И тут всплывает ключевое понятие: у процесса есть текущая рабочая директория (Current Working Directory, CWD). От неё зависят все относительные пути.

Представьте, что CLI — это человек с фонариком в руках. Фонарик освещает “здесь и сейчас”. Относительный путь — это “пройди 10 шагов вперёд”, а абсолютный — “телепортируйся по GPS‑координатам”. Если вы не знаете, где человек стоит, “10 шагов вперёд” превращается в лотерею.

В Swift получить CWD очень просто — через FileManager.

Пример: читаем CWD как строку


import Foundation

let cwd = FileManager.default.currentDirectoryPath
print("CWD = \(cwd)") // например: CWD = /Users/alex/Projects/LibraryCLI

Важно: CWD — это не папка с исходниками и не “где лежит Package.swift”. Это именно “где сейчас находится терминал/процесс”. Запуск через swift run ... — просто один из распространённых способов стартовать CLI, но CWD всё равно берётся из окружения процесса.

2. Относительные и абсолютные пути

Когда начинающий программист видит пути, он часто думает так: “Абсолютный путь — это который начинается с /”. На Unix‑системах (macOS/Linux) это чаще всего правда, но полезнее мыслить не про символы, а про смысл: абсолютный путь не зависит от CWD, относительный — зависит.

Давайте зафиксируем это в маленькой табличке. Здесь нет магии, только здравый смысл.

Ввод пользователя Тип по смыслу От чего зависит результат
/tmp/LibraryCLI/data
абсолютный ни от чего (в пределах одной ФС)
LibraryCLI/data
относительный от CWD процесса
../LibraryCLI/data
относительный от CWD процесса
data/library.json
относительный от CWD процесса

Почему это важно именно для CLI? Потому что CLI часто запускают “не из той папки”. Сегодня вы стоите в /Users/me/Projects, завтра — в /tmp, послезавтра CI запускает вас вообще в какой‑нибудь служебной директории. Если вы не контролируете интерпретацию относительных путей, приложение будет “иногда работать”, а это, как известно, худший вид неработы.

3. URL для файловых путей

На этом дне курса мы рассматриваем URL не как “ссылку на сайт”, а как тип для адреса в файловой системе (file URL). И это не снобизм, а практическая защита от багов: URL умеет корректно склеивать пути, добавлять компоненты и расширения, а также хранить информацию “это директория или файл”.

FileManager.default.currentDirectoryPath отдаёт строку, но дальнейшая работа с файловыми путями удобнее в URL, потому что:

  1. URL явно отделяет “путь” от “текстового представления”
  2. URL даёт безопасные операции построения пути без ручной конкатенации
  3. URL естественно комбинируется с FileManager, который часто принимает именно URL или path

Пример: CWD как file URL

import Foundation

let cwdPath = FileManager.default.currentDirectoryPath
let cwdURL = URL(fileURLWithPath: cwdPath, isDirectory: true)

print(cwdURL.path)           // например: /Users/alex/Projects/LibraryCLI
print(cwdURL.absoluteString) // например: file:///Users/alex/Projects/LibraryCLI/

Обратите внимание на разницу:

  • path — это “нормальный путь” для файловой системы (то, что обычно пойдёт в FileManager).
  • absoluteString — строка URL, часто с file://. Удобно посмотреть глазами, но это не то, что вы обычно хотите передавать в API, ожидающие путь.

Если у вас когда‑нибудь было ощущение “я передал путь, а оно почему-то говорит file:///...” — поздравляю, вы познакомились с absoluteString. Неприятно, но поучительно.

4. Разрешение пути: строка → абсолютный URL

Сейчас будет центральный момент. Наша цель: взять строку, которую дал пользователь (например data/library.json или /var/lib/app/data), и получить абсолютный file‑URL, с которым дальше можно работать в FileManager.

И вот здесь есть искушение, которое ломало судьбы тысячам программ (и, возможно, паре ваших будущих проектов): хочется сделать split(separator: "/"), потом склеить “правильными слэшами”, потом обработать .., потом внезапно вспомнить про Windows… и в итоге написать мини‑версию файловой системы. Спойлер: вы проиграете. Всегда.

Вместо этого используем URL(fileURLWithPath:relativeTo:).

Пример: функция “разрешить путь” (resolve)

import Foundation

func resolvePath(_ input: String, relativeTo baseDir: URL) -> URL {
    // Для абсолютного пути baseDir будет проигнорирован.
    // Для относительного — input интерпретируется относительно baseDir.
    URL(fileURLWithPath: input, relativeTo: baseDir)
}

let base = URL(fileURLWithPath: "/tmp", isDirectory: true)

let a = resolvePath("LibraryCLI/data", relativeTo: base)
let b = resolvePath("/var/log", relativeTo: base)

print(a.path) // /tmp/LibraryCLI/data
print(b.path) // /var/log

Здесь важно понять логику: нам не нужно заранее определять “абсолютный это или относительный”. Мы не пишем if input.hasPrefix("/"). Мы даём эту работу системному API, который именно для этого и существует.

Почти вся “устойчивость” такого кода — в том, что он короткий. Чем меньше кода, тем меньше мест, где можно ошибиться.

Где это использовать в CLI

В живом CLI пути появляются обычно в двух местах: либо пользователь явно передал путь (например, “сохрани базу вот сюда”), либо вы выбрали путь по умолчанию (“храним данные приложения в нашей папке”). Политику “где хранить по умолчанию” мы разберём в следующей лекции дня, а сейчас сфокусируемся на механике: если пользователь дал строку пути, мы должны корректно превратить её в URL.

С точки зрения кода это выглядит так: у нас есть String? из аргументов (потому что пользователь мог его не передать), и есть baseDir, которым чаще всего будет CWD.

Пример: “пользовательский путь или CWD+дефолт”

import Foundation

let cwdURL = URL(fileURLWithPath: FileManager.default.currentDirectoryPath, isDirectory: true)

let userProvidedPath: String? = nil // представим: аргумент не передали
let fallback = "LibraryCLI-data"    // временный дефолт для примера

let chosenPath = userProvidedPath ?? fallback
let resolved = URL(fileURLWithPath: chosenPath, relativeTo: cwdURL)

print(resolved.path) // например: /Users/alex/Projects/LibraryCLI/LibraryCLI-data

Заметьте: в этой лекции мы ещё не создаём директорию и не проверяем права. Мы только делаем важнейший подготовительный шаг: переводим ввод в “нормальную координату” — file URL.

5. Нюансы относительных путей: ., .., ~

Относительные пути могут включать элементы . и ... И если вы хоть раз пытались обработать их руками, вы знаете, что это заканчивается либо багом, либо философским принятием “пусть падает”. Мы всё-таки учимся делать так, чтобы не падало (или хотя бы падало осмысленно).

URL(fileURLWithPath:relativeTo:) уже умеет собирать корректный путь относительно базы, включая ... Давайте посмотрим на это в действии.

Пример: относительный путь с ..

import Foundation

let base = URL(fileURLWithPath: "/tmp/app/work", isDirectory: true)
let url = URL(fileURLWithPath: "../data/library.json", relativeTo: base)

print(url.path) // /tmp/app/data/library.json

Идея простая: вы не пытаетесь “понять” .., вы просто описываете намерение: “вот база, вот ввод”. Дальше система делает то, что она умеет лучше вашего split.

Отдельная важная оговорка из плана дня: в рамках этой лекции мы не вводим поддержку “магии шелла” вроде ~ (домашняя директория) и переменных окружения в строке пути. Если вы в терминале пишете ~/file.txt, оболочка часто разворачивает это до абсолютного пути до того, как ваше приложение увидит аргументы, но рассчитывать на это как на контракт — плохая идея. В нашем курсе мы либо вводим такое правило явно (позже), либо не обещаем поддержку.

6. Диагностика и схема потока данных

Когда вы делаете CLI устойчивым, очень помогает держать в голове “конвейер”: что мы получили, как преобразовали, и на каком шаге можем получить ошибку. Сейчас у нас конвейер пока короткий, но он уже дисциплинирует мышление.

Мини‑схема: от строки к file‑URL

Ниже — схема, которую стоит мысленно вспоминать каждый раз, когда вам хочется сделать path.split(separator: "/") (это как “хочется съесть печеньку в 2 ночи”: не запрещено, но последствия странные).

flowchart TD
    A["Строка от пользователя (String)"] --> B["CWD (String)"]
    B --> C["baseDir: URL(fileURLWithPath:isDirectory:)"]
    A --> D["URL(fileURLWithPath:relativeTo:)"]
    C --> D
    D --> E["resolvedURL.path -> операции FileManager"]

Фишка в том, что resolvedURL — это единый формат, с которым удобно жить дальше: создавать директории, проверять существование, читать/писать файлы (это будет в следующих днях курса).

Как понять, почему путь получился “не туда”

В CLI есть особый тип боли: “пользователь клянётся, что передал правильный путь, а вы видите, что всё улетело в другую папку”. И очень часто причина одна: CWD не тот, который человек предполагал.

Поэтому полезно на этапе “разрешения пути” иметь диагностический лог: что было базой, что было вводом, что стало результатом. И да — тот самый контекст #file / #line / #function тоже полезен, особенно когда у вас несколько мест, где пути собираются.

Пример: логируем resolve (используем существующий Logger)

import Foundation

func resolvePathForCLI(_ input: String, baseDir: URL, logger: any Logger) -> URL {
    let result = URL(fileURLWithPath: input, relativeTo: baseDir)
    logger.debug("resolve input=\(input) base=\(baseDir.path) -> \(result.path)")
    return result
}

Мы не создаём новый Logger и не меняем его контракт. Мы просто показываем, какие поля реально помогают: input, baseDir.path, result.path. Это тот минимум, который потом спасает время (и нервные клетки).

7. Типичные ошибки при работе с рабочей директорией и путями в CLI

Ошибка №1: считать, что относительный путь считается “от папки проекта”.
Это очень частая ловушка: человек запускает swift run ... из одной директории, потом из другой, потом IDE запускает из третьей, и внезапно “вчера работало”. В реальности относительный путь считается от CWD процесса. Если вам нужен предсказуемый корень — вы должны явно выбрать политику base‑директории и явно преобразовывать ввод в URL.

Ошибка №2: пытаться определить абсолютность пути ручными проверками и ветвлениями.
Появляется код в стиле if input.hasPrefix("/") { ... } else { ... }. Он ещё и “работает” на macOS, поэтому выглядит убедительно, как мошенник в костюме. Но это ломается на нюансах вроде .., лишних разделителей, и вообще на разных платформах. Гораздо проще и надёжнее: всегда использовать URL(fileURLWithPath:relativeTo:) и доверять системному API.

Ошибка №3: парсить пути через split(separator: "/") и склеивать обратно.
Сначала кажется, что это быстро. Потом выясняется, что бывают двойные слэши, бывают . и .., бывают расширения, бывают “папка vs файл”, а ещё бывают случаи, когда вы случайно добавили лишний / и получили странный путь. В итоге вы пишете свой “мини‑URL”, который хуже настоящего URL. Лучшее решение — вообще не начинать и собирать пути через URL и appendingPathComponent.

Ошибка №4: печатать и логировать не то представление пути (absoluteString вместо path).
absoluteString выглядит как “ну тоже строка, чего вы придираетесь”, но для файловых операций вам почти всегда нужен path. Если в логах у вас file:///tmp/..., а в FileManager вы передаёте это как путь — получится ошибка “нет такого файла”, и вы будете долго смотреть на экран с выражением “ну он же вот!”. Разделяйте: для ФС — path, для “посмотреть глазами” — можно absoluteString.

Ошибка №5: не логировать CWD при проблемах с путями.
Когда что-то пошло не так, очень хочется логировать “не удалось открыть файл”. Но без CWD эта фраза почти бесполезна: вы не понимаете, где именно программа “искала”. В диагностике проблем с путями CWD — это как координаты в навигаторе: без них вы можете спорить бесконечно, кто “не туда свернул”.

1
Задача
Swift SELF, 55 уровень, 2 лекция
Недоступна
Паспорт запуска
Паспорт запуска
1
Задача
Swift SELF, 55 уровень, 2 лекция
Недоступна
Разрешение пути
Разрешение пути
1
Задача
Swift SELF, 55 уровень, 2 лекция
Недоступна
Пакетное разрешение
Пакетное разрешение
1
Задача
Swift SELF, 55 уровень, 2 лекция
Недоступна
Диагностика базы
Диагностика базы
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ