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, относительный — зависит.
Давайте зафиксируем это в маленькой табличке. Здесь нет магии, только здравый смысл.
| Ввод пользователя | Тип по смыслу | От чего зависит результат |
|---|---|---|
|
абсолютный | ни от чего (в пределах одной ФС) |
|
относительный | от CWD процесса |
|
относительный | от CWD процесса |
|
относительный | от CWD процесса |
Почему это важно именно для CLI? Потому что CLI часто запускают “не из той папки”. Сегодня вы стоите в /Users/me/Projects, завтра — в /tmp, послезавтра CI запускает вас вообще в какой‑нибудь служебной директории. Если вы не контролируете интерпретацию относительных путей, приложение будет “иногда работать”, а это, как известно, худший вид неработы.
3. URL для файловых путей
На этом дне курса мы рассматриваем URL не как “ссылку на сайт”, а как тип для адреса в файловой системе (file URL). И это не снобизм, а практическая защита от багов: URL умеет корректно склеивать пути, добавлять компоненты и расширения, а также хранить информацию “это директория или файл”.
FileManager.default.currentDirectoryPath отдаёт строку, но дальнейшая работа с файловыми путями удобнее в URL, потому что:
- URL явно отделяет “путь” от “текстового представления”
- URL даёт безопасные операции построения пути без ручной конкатенации
- 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 — это как координаты в навигаторе: без них вы можете спорить бесконечно, кто “не туда свернул”.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ