Bash: навигация и файлы
Где я и что вокруг
pwd
ls
ls -la
ls -lh
ls -lt
cd /var/www
cd ../logs
cd ~
cd -
| Символ | Значение |
. | Текущая папка |
.. | Родительская папка |
~ | Домашняя папка пользователя |
/ | Корень файловой системы |
- (в cd) | Предыдущая папка |
Создание, копирование, удаление
mkdir logs
mkdir -p a/b/c
touch file.txt
cp file.txt backup.txt
cp -r src/ dist/
cp -a src/ dist/
mv old.txt new.txt
mv file.txt /tmp/
rm file.txt
rm -r folder/
rm -f file.txt
rm не спрашивает и не кладёт в корзину. Удалённое не восстановить. Особенно опасна связка rm -rf с переменной: если переменная пустая, rm -rf $DIR/ превращается в rm -rf /. Привычка, которая спасает: сначала ls на том же пути, и только потом rm.
mv — это и «переименовать», и «переместить». Разницы нет: файл просто получает новый путь.
Чтение файлов
cat file.txt
less file.txt
head -20 file.txt
tail -20 file.txt
tail -f storage/logs/laravel.log
wc -l file.txt
tail -f на логе — самая частая команда при отладке на сервере. Выход — Ctrl+C.
Bash: поиск и обработка текста
grep — поиск по содержимому
grep "error" app.log
grep -i "error" app.log
grep -r "TODO" app/
grep -n "error" app.log
grep -c "error" app.log
grep -v "debug" app.log
grep -A3 -B3 "error" app.log
grep -E "error|warning" app.log
grep на macOS: почему примеры из интернета не работают
В macOS предустановлен BSD grep, а подавляющее большинство примеров в сети написано под GNU grep из Linux. Команды выглядят одинаково, но часть ключей у BSD отсутствует — и строка, которая у автора статьи работала, на Mac падает с ошибкой.
$ grep --version
grep (BSD grep, GNU compatible) 2.6.0-FreeBSD
grep (GNU grep) 3.11
Чаще всего спотыкаются об один ключ — -P, то есть регулярные выражения в стиле Perl. Именно он стоит почти во всех однострочниках с \d, \w и \s:
$ grep -oP '\d+' app.log
grep: invalid option -- P
usage: grep [-abcdDEFGHhIiJLlMmnOopqRSsUVvwXxZz] ...
| Ключ | На macOS | Что делать |
-P (Perl-регулярки) | Нет, ошибка invalid option | Переписать на -E или поставить GNU-версию |
-E (расширенные) | Есть | Основной рабочий вариант |
-i -r -n -c -v -w -o | Есть | Работают одинаково |
-A -B -C (контекст) | Есть | Работают |
--include, --exclude | Есть | Работают |
Если ключ -P нужен ради \d и подобных сокращений, их почти всегда можно заменить на переносимые классы POSIX — они работают и на Mac, и на Linux:
grep -oP '\d+' app.log
grep -oP '\w+@\w+' users.txt
grep -oE '[[:digit:]]+' app.log
grep -oE '[[:alnum:]_]+@[[:alnum:]_]+' users.txt
| Сокращение Perl | Класс POSIX | Что означает |
\d | [[:digit:]] | Цифра |
\w | [[:alnum:]_] | Буква, цифра или подчёркивание |
\s | [[:space:]] | Пробел, табуляция, перенос строки |
\b | -w у самого grep | Граница слова |
Второй путь — поставить настоящий GNU grep через Homebrew. Он встанет рядом, под именем ggrep, чтобы не ломать системные скрипты:
brew install grep
ggrep -oP '\d+' app.log
grep -oP '\d+' app.log
Можно сделать GNU-версию основной, добавив её каталог в начало PATH — тогда grep будет означать GNU grep:
export PATH="/opt/homebrew/opt/grep/libexec/gnubin:$PATH"
Подменять системный grep стоит осознанно. Часть скриптов в системе рассчитывает на поведение BSD-версии. Безопаснее оставить ggrep отдельной командой, а в своих скриптах писать переносимо — на -E и классах POSIX. Тогда они одинаково пойдут и на Mac, и на сервере с Linux.
Та же история с другими утилитами. На macOS BSD-версии у sed, awk, date, xargs. Самое заметное: sed -i на Mac требует аргумент — sed -i '' 's/a/b/' file, иначе получите «invalid command code». GNU-версии ставятся тем же способом: brew install gnu-sed даёт gsed.
find — поиск файлов
find . -name "*.php"
find . -iname "*.PHP"
find . -type f -size +10M
find . -type d -name node_modules
find . -mtime -7
find . -name "*.log" -delete
find . -name "*.php" -exec grep -l "TODO" {} \;
find против grep. find ищет файлы — по имени, размеру, дате, типу. grep ищет внутри файлов — по содержимому. Их часто комбинируют: find отбирает файлы, grep смотрит, что внутри.
Обработка потока
sort file.txt
sort -u file.txt
sort -rn file.txt
uniq -c file.txt
cut -d: -f1 /etc/passwd
sed 's/old/new/g' f.txt
awk '{print $1, $3}' f
Bash: процессы, диск, сеть
Процессы
ps aux
ps aux | grep php
top
htop
kill 1234
kill -9 1234
pkill -f "artisan queue"
kill -9 — последнее средство. Процесс не успевает закрыть файлы, сбросить буферы и снять блокировки. Для воркера очереди это означает джоб, брошенный на середине. Сначала обычный kill, и только если не умер — -9.
Диск
df -h
du -sh /var/www
du -sh * | sort -rh
du -sh * | sort -rh | head -10
df — сколько свободно на диске. du — сколько занимает конкретная папка. Диск забился — начинай с df -h, потом du по подозрительным путям.
Сеть
ping example.com
lsof -i :8000
netstat -tulpn | grep 80
lsof -i :порт — первое, что запускают при «Address already in use».
curl: флаги, которые реально используют
Без флагов curl URL делает обычный GET и вываливает тело в терминал. Всё остальное поведение задаётся ключами, и почти всегда их комбинируют: типичная строка из скрипта выглядит как curl -sS -f -L URL. Разберём по группам.
| Флаг | Что делает | Когда нужен |
| Запрос |
-X МЕТОД | Задать метод: POST, PUT, DELETE, PATCH | Когда нужен не GET. С -d метод POST подставляется сам |
-d 'данные' | Тело запроса. Автоматически включает POST | Отправка JSON или формы |
-d @file.json | Тело из файла | Большой JSON не влезает в строку |
-H 'Заголовок: значение' | Добавить заголовок. Можно повторять | Content-Type, Authorization, X-API-Key |
-G | Переносит -d в строку запроса | GET с параметрами, которые нужно экранировать |
-u user:pass | Базовая авторизация | Простая защита, FTP |
-T файл | Загрузить файл на сервер (PUT или FTP) | Выкладка файла без формы |
| Вывод |
-o файл | Сохранить тело в указанный файл | Скачивание |
-O | Сохранить под именем из URL | Быстро скачать архив |
-s | Тихо: без индикатора прогресса | Скрипты, конвейеры. Иначе прогресс попадёт в вывод |
-S | Показывать ошибки даже в тихом режиме | Всегда в паре: -sS |
-i | Показать заголовки ответа вместе с телом | Посмотреть, что вернул сервер |
-I | Только заголовки, запрос методом HEAD | Проверить код ответа и редирект, не качая тело |
-v | Подробный лог: запрос, ответ, рукопожатие TLS | Разбор «почему не работает» |
-w 'формат' | Напечатать служебные значения после ответа | Код ответа, время, размер — для проверок |
| Поведение |
-L | Идти по редиректам | Без него на 301 получите пустое тело |
-f | Вернуть ненулевой код выхода при ошибке HTTP | В скриптах: иначе 500 считается успехом |
-k | Не проверять TLS-сертификат | Самоподписанный сертификат на тестовом стенде |
--connect-timeout N | Сколько ждать установки соединения | Чтобы скрипт не висел на мёртвом хосте |
--max-time N | Общий предел на всю операцию | Страховка в конвейерах и cron |
--retry N | Повторить при сетевой ошибке | Ненадёжный канал |
--compressed | Попросить сжатый ответ и распаковать | Большие JSON-ответы |
-A 'строка' | Свой User-Agent | Сервер отвечает по-разному разным клиентам |
-c / -b файл | Сохранить и отправить cookies | Сценарий с логином |
Откуда берутся «склеенные» флаги. Односимвольные ключи можно писать слитно: -sk это то же самое, что -s -k — тихий режим плюс отключённая проверка сертификата. Так же читаются -sS, -sSL, -fsSL. Последнее встречается в инструкциях по установке: тихо, падать при ошибке, показывать её, идти по редиректам.
-k отключает защиту, а не чинит сертификат. Он говорит curl: «не проверяй, кому принадлежит сертификат» — то есть снимает защиту от подмены. На тестовом стенде это нормально, в проде и в скриптах, которые ходят в чужие API, — нет. Правильное решение: подложить корневой сертификат через --cacert или починить цепочку на сервере.
Типовые вызовы, которые стоит помнить наизусть:
curl https://api.example.com/users
curl -s -o /dev/null -w '%{http_code}\n' https://example.com
curl -X POST https://api.example.com/orders \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer TOKEN' \
-d '{"total":"100.00"}'
curl -X POST https://api.example.com/orders \
-H 'Content-Type: application/json' \
-d @order.json
curl -X POST https://example.com/login \
-d 'email=a@b.com' -d 'password=secret'
curl -F 'file=@photo.jpg' -F 'title=Фото' https://example.com/upload
curl -L -C - -O https://example.com/big.zip
curl -v https://example.com 2>&1 | head -40
curl -s -o /dev/null -w 'dns=%{time_namelookup} connect=%{time_connect} total=%{time_total}\n' https://example.com
jq: читаемый JSON вместо простыни
API отвечает одной длинной строкой без переносов — читать её в терминале невозможно. jq форматирует JSON и умеет доставать из него нужное. Ставится одной командой: brew install jq на macOS, apt install jq на Debian и Ubuntu.
curl -s https://api.example.com/users | jq
curl -s https://api.example.com/user/1 | jq '.email'
TOKEN=$(curl -s -X POST https://api.example.com/login \
-d '{"email":"a@b.com","password":"secret"}' \
-H 'Content-Type: application/json' | jq -r '.token')
curl -s https://api.example.com/users | jq '.[] | {id, email}'
curl -s https://api.example.com/users | jq 'length'
curl -s https://api.example.com/orders | jq '.[] | select(.total > 1000)'
curl -s https://api.example.com/user/1 | jq '.profile.city // "не указан"'
curl -s https://api.example.com/users | jq -r '.[] | "\(.id)\t\(.email)"'
Связка, которую пишут чаще всего: curl -sS URL | jq. Тихий режим убирает индикатор прогресса, который иначе попал бы в конвейер и сломал разбор, -S оставляет сообщения об ошибках, а jq приводит ответ в читаемый вид.
Если jq ругается на разбор — почти всегда сервер вернул не JSON. Самое частое: HTML страницы ошибки или редирект, который вы не прошли. Посмотрите, что пришло на самом деле: curl -i URL | head -20. Второй по частоте случай — забытый -s, и в конвейер попал индикатор прогресса.
Если ставить jq некуда, минимальное форматирование есть в самом Python:
curl -s https://api.example.com/users | python3 -m json.tool
Bash: права доступа
Как читать rwx
$ ls -l
-rw-r--r-- 1 observer staff 1024 Oct 16 12:00 file.txt
drwxr-xr-x 5 observer staff 160 Oct 16 12:00 folder/
│└┬┘└┬┘└┬┘
│ │ │ └── остальные (others)
│ │ └───── группа (group)
│ └──────── владелец (user)
└────────── тип: - файл, d папка, l симлинк
| Буква | Число | Для файла | Для папки |
r read | 4 | Читать содержимое | Смотреть список файлов |
w write | 2 | Изменять | Создавать и удалять файлы внутри |
x execute | 1 | Запускать | Входить внутрь (cd) |
Откуда берутся числа вроде 755. Права складываются: rwx = 4+2+1 = 7, r-x = 4+1 = 5, rw- = 4+2 = 6. Три цифры — это владелец, группа, остальные. 755 = владельцу всё, остальным читать и заходить. 644 = владельцу читать-писать, остальным только читать.
chmod и chown
chmod 755 script.sh
chmod +x script.sh
chmod -R 775 storage/
chown www-data file
chown -R www-data:www-data /var/www/app
| Права | Для чего |
644 | Обычные файлы: конфиги, исходники |
755 | Папки и исполняемые скрипты |
775 | storage/ и bootstrap/cache/ в Laravel — пишет и владелец, и группа веб-сервера |
600 | Секреты: .env, приватные ключи |
chmod -R 777 — не решение, а дыра. Это «всем всё можно», включая запись чужими процессами. Проблема почти всегда в владельце, а не в правах: нужен chown -R www-data:www-data, а дальше хватит 775.
Bash: пайпы и перенаправление
Пайп — выход одной команды на вход другой
cat app.log | grep error | wc -l
ps aux | grep php | grep -v grep
du -sh * | sort -rh | head -5
git log --oneline | head -20
Символ | соединяет команды в конвейер: то, что первая напечатала, вторая получает на вход. Так из простых команд собираются сложные запросы.
Перенаправление в файл
command > file.txt
command >> file.txt
command 2> errors.txt
command > out.txt 2>&1
command > /dev/null 2>&1
| Поток | Номер | Что это |
stdin | 0 | Вход |
stdout | 1 | Обычный вывод |
stderr | 2 | Ошибки — отдельный поток |
Ошибки не идут в пайп. command | grep x фильтрует только stdout. Сообщение об ошибке пролетит мимо фильтра прямо в терминал. Чтобы поймать и его — command 2>&1 | grep x.
> затирает файл целиком, >> дописывает. Перепутать — потерять лог.
Цепочки команд
cmd1 && cmd2
cmd1 || cmd2
cmd1 ; cmd2
cmd &
git pull && composer install --no-dev && php artisan migrate --force
Почему &&, а не ;. С точкой запятой команды выполнятся все подряд, даже если git pull упал с конфликтом. С && цепочка остановится на первой ошибке — и ты не накатишь миграции на неполный код.
Git: от init до push
Начало работы
git init
git clone URL
git clone URL my-folder
git clone --depth 1 URL
git config --global user.name "Имя"
git config --global user.email "mail@example.com"
git config --list
Базовый цикл
git status
git add file.txt
git add .
git add -p
git commit -m "Описание"
git commit -am "Описание"
git push
git pull
git commit -am не видит новые файлы. Флаг -a берёт только то, что Git уже отслеживает. Только что созданный файл не попадёт в коммит, и это легко не заметить. Для новых файлов нужен явный git add.
История и просмотр
git log
git log --oneline
git log --oneline --graph --all
git log -5
git log -p file.txt
git log --author="Имя"
git diff
git diff --staged
git diff main..feature
git show HEAD
git blame file.txt
.gitignore
/vendor
/node_modules
/public/storage
/storage/*.key
.env
.env.backup
.phpunit.result.cache
.DS_Store
.gitignore не действует на уже отслеживаемые файлы. Закоммитил .env, потом добавил в ignore — он продолжит отслеживаться. Нужно убрать из индекса, оставив на диске:
git rm --cached .env
И помнить: из истории он при этом никуда не денется — для этого нужна чистка истории.
Git: отмена изменений
Что отменять и чем
| Задача | Команда |
| Откатить правки в файле | git restore file.txt |
| Убрать файл из индекса, правки оставить | git restore --staged file.txt |
| Поправить последний коммит | git commit --amend |
| Откатить коммит, сохранив изменения в индексе | git reset --soft HEAD~1 |
| Откатить коммит, изменения в рабочей папке | git reset HEAD~1 |
| Откатить коммит и выбросить изменения | git reset --hard HEAD~1 |
| Отменить коммит новым коммитом | git revert HASH |
| Удалить неотслеживаемые файлы | git clean -fd |
reset против revert. reset переписывает историю — коммита больше нет. revert создаёт новый коммит, который отменяет старый, — история сохраняется. Правило: в своей локальной ветке можно reset; в том, что уже запушено и видно другим, — только revert.
git reset --hard удаляет изменения безвозвратно. Незакоммиченная работа пропадёт без возможности вернуть — она нигде не сохранена. Если не уверен, сначала git stash: он спрячет изменения, и их можно будет достать обратно.
Спасательный круг — git reflog. Он помнит, где HEAD был раньше, даже после reset --hard по коммитам. Нашёл нужный хеш — git reset --hard HASH вернёт состояние. Работает только для закоммиченного.
Git: ветки и слияние
Работа с ветками
git branch
git branch -a
git branch feature-x
git switch feature-x
git switch -c feature-x
git checkout -b feature-x
git branch -d feature-x
git branch -D feature-x
switch и restore появились, чтобы разделить обязанности перегруженного checkout: он умел и переключать ветки, и восстанавливать файлы. Старый синтаксис работает, но новый понятнее.
Слияние и конфликты
git switch main
git merge feature-x
git status
git add file.txt
git commit
git merge --abort
Забытые маркеры конфликта. Если <<<<<<< или >>>>>>> останутся в коде, он закоммитится и сломается при запуске. Перед коммитом слияния: grep -rn "<<<<<<<" .
Rebase, squash и cherry-pick разобраны в Testing & DevOps → Git Advanced.
Git: remote и теги
Удалённые репозитории
git remote -v
git remote add origin URL
git remote set-url origin NEW_URL
git fetch
git pull
git pull --rebase
git push
git push -u origin feature-x
git push --force-with-lease
fetch против pull. fetch только скачивает изменения — ты можешь посмотреть их через git log origin/main и решить, что делать. pull сразу вливает в текущую ветку. Когда не уверен в состоянии ветки, безопаснее сначала fetch.
--force против --force-with-lease. Обычный --force затирает удалённую ветку безусловно — вместе с коммитами коллеги, которые тот запушил минуту назад. --force-with-lease сначала проверяет, что на сервере то, что ты видел последним, и откажется, если кто-то успел запушить. Привычка должна быть на вторую форму.
Теги
git tag
git tag v1.0.0
git tag -a v1.0.0 -m "Релиз"
git tag -a v1.0.0 HASH
git push origin v1.0.0
git push --tags
git tag -d v1.0.0
Теги не отправляются при обычном git push — их нужно пушить явно. Для релизов используют аннотированные (-a): в них есть автор, дата и сообщение.
Composer: install против update
Главное различие
| composer install | composer update |
| Читает | composer.lock | composer.json |
| Ставит | Точно те версии, что в lock | Самые свежие в рамках ограничений |
| Меняет lock | Нет | Да, перезаписывает |
| Где запускать | На проде, в CI, у новых разработчиков | Локально, осознанно |
| Воспроизводимость | Полная — у всех одинаково | Зависит от момента запуска |
Зачем нужен composer.lock. В composer.json написано "^7.5" — это «любая 7.x от 7.5». Поставишь сегодня — приедет 7.8, через месяц — 7.9 с поправленным поведением. Lock фиксирует точный коммит каждой библиотеки, и install воспроизводит ровно его. Поэтому lock-файл обязательно в Git: именно он гарантирует, что на проде и у коллеги соберётся то же самое.
composer update на проде — частая и дорогая ошибка. Она подтянет свежие версии, которых никто не тестировал, и перепишет lock прямо на сервере. На проде только:
composer install --no-dev --optimize-autoloader
Флаги для прода
composer install --no-dev --optimize-autoloader --no-interaction
composer update vendor/package
composer update --lock
--optimize-autoloader на проде даёт заметный прирост: вместо поиска файла по PSR-4 при каждом обращении к классу Composer собирает готовую карту «класс → путь».
Composer: версии и ограничения
Semver и операторы
Версия пишется как MAJOR.MINOR.PATCH, например 7.5.2. Правило семантического версионирования: MAJOR меняется при ломающих изменениях, MINOR — при новых совместимых возможностях, PATCH — при исправлениях.
| Запись | Что разрешает | Диапазон |
^7.5 | Не менять MAJOR — самый частый вариант | >= 7.5.0, < 8.0.0 |
~7.5 | Не менять MINOR | >= 7.5.0, < 8.0.0 |
~7.5.2 | Только PATCH | >= 7.5.2, < 7.6.0 |
7.5.* | Любой PATCH внутри 7.5 | >= 7.5.0, < 7.6.0 |
7.5.2 | Ровно эта версия | = 7.5.2 |
>=7.5 <8.0 | Явный диапазон | как написано |
* | Любая — так не делают | всё подряд |
^ против ~ — в чём подвох. Для записи из двух чисел они совпадают: ^7.5 и ~7.5 оба означают «до 8.0». Разница появляется с тремя: ^7.5.2 разрешит 7.9.0, а ~7.5.2 остановится на 7.5.x. Проще держать в голове так: ^ гибче, ~ консервативнее.
Фиксация точной версии мешает безопасности. "guzzlehttp/guzzle": "7.5.2" означает, что патчи с исправлением уязвимостей не приедут никогда. Для фиксации есть composer.lock — в composer.json держат диапазон, а воспроизводимость обеспечивает lock.
Composer: автозагрузка и скрипты
Секция autoload
{
"autoload": {
"psr-4": {
"App\\": "app/",
"Database\\Factories\\": "database/factories/"
},
"files": ["app/helpers.php"],
"classmap": ["database/seeders"]
},
"autoload-dev": {
"psr-4": { "Tests\\": "tests/" }
}
}
| Способ | Как работает |
psr-4 | Namespace ↔ папка. App\Services\Foo → app/Services/Foo.php. Основной способ |
files | Подключается всегда, при каждом запросе. Для функций-хелперов, у которых нет класса |
classmap | Сканирует папку и строит карту классов. Для кода без namespace |
composer dump-autoload
composer dump-autoload -o
Добавил files или новый namespace — нужен dump-autoload. Правка composer.json сама по себе ничего не меняет: карта автозагрузки лежит в vendor/composer/ и пересобирается только этой командой. Симптом — «Class not found» на файле, который очевидно существует.
Скрипты
{
"scripts": {
"test": "phpunit",
"lint": "pint --test",
"post-autoload-dump": [
"@php artisan package:discover --ansi"
],
"post-install-cmd": ["@php artisan optimize"]
}
}
composer test
composer run-script test
composer list
| Хук | Когда срабатывает |
post-install-cmd | После composer install |
post-update-cmd | После composer update |
post-autoload-dump | После пересборки автозагрузки — здесь Laravel ищет пакеты |
Скрипты удобны как единая точка входа: composer test работает одинаково на любой машине, независимо от того, где лежит бинарник phpunit.
Подводные камни
Bash
rm -rf с пустой переменной. rm -rf $DIR/ при незаданной $DIR превращается в rm -rf /. Всегда кавычки и проверка: rm -rf "${DIR:?переменная не задана}".
> вместо >>. Одна лишняя стрелка — и лог перезаписан вместо дописывания.
Ошибки не попадают в пайп. cmd | grep x фильтрует только stdout. Нужен cmd 2>&1 | grep x.
chmod 777 как решение любой проблемы с правами. Почти всегда настоящая причина — неверный владелец. Сначала chown, потом 775.
Git
git commit -am не берёт новые файлы. Только отслеживаемые. Новый файл тихо не попадёт в коммит.
.gitignore не влияет на уже отслеживаемое. Нужен git rm --cached. А из истории файл при этом не исчезнет.
push --force затирает чужую работу. Всегда --force-with-lease.
reset --hard съедает незакоммиченное без следа. reflog спасёт только то, что было в коммитах.
Маркеры конфликта в коммите. Перед завершением слияния: grep -rn "<<<<<<<" .
Composer
composer update на проде. Подтянет непротестированные версии и перепишет lock на сервере. Только install.
composer.lock не в Git. Тогда у всех разные версии, и «у меня работает» становится нормой.
vendor/ в Git. Десятки тысяч чужих файлов в истории, конфликты при каждом merge. Место vendor/ — в .gitignore.
Забытый dump-autoload. Добавил namespace или хелпер — получи «Class not found» на существующем файле.
--no-dev на машине разработчика. Снесёт phpunit и остальные dev-зависимости. Этот флаг только для прода и CI.