Привет, Хабр!
Сегодня рассмотрим, как на базе Bash собрать свой собственный кастомный шелл — с автодополнением, историей, алиасами, логами, цветным prompt'ом, подсказками по sudo и возможностью расширения.
Минимальный REPL-интерпретатор на Bash
Начнём с базовой конструкции, которая делает из bash-а простой цикл чтения и выполнения команд:
#!/usr/bin/env bash
HISTORY_FILE="$HOME/.mybash_history"
touch "$HISTORY_FILE"
trap "echo; exit 0" SIGINT SIGTERM
while true; do
read -e -p "→ " CMD
echo "$CMD" >> "$HISTORY_FILE"
eval "$CMD"
done
HISTORY_FILE
— файл для сохранения истории между сессиями, trap
— ловим Ctrl+C и красиво выходим, read -e
— включает поддержку стрелок и автодополнения, eval "$CMD"
— исполняем введённую строку как Bash-команду.
Поддержка алиасов
Добавим свои алиасы и включим их поддержку:
shopt -s expand_aliases
alias ll='ls -la'
alias gs='git status'
shopt -s expand_aliases
— без него alias'ы в скрипте не работают. Дальше можно объявлять любые свои сокращения.
Добавим логирование команд
Хотим знать, кто и когда запускал какую команду:
LOGFILE="$HOME/.mybash_cmd.log"
log_command() {
echo "$(date "+%Y-%m-%d %H:%M:%S") | $1" >> "$LOGFILE"
}
log_command — простая функция, логирующая команду с временной меткой.
Используем её в цикле:
read -e -p "→ " CMD
log_command "$CMD"
eval "$CMD"
Измерение времени выполнения команды
Вариант с миллисекундами:
start=$(date +%s%3N)
eval "$CMD"
end=$(date +%s%3N)
echo "Команда выполнена за $((end - start)) мс"
date +%s%3N — время в миллисекундах. Считаем разницу до и после выполнения команды.
Подсказка на sudo при ошибке доступа
if eval "$CMD" 2>&1 | grep -iq "permission denied\|operation not permitted"; then
echo "Возможно, стоит попробовать: sudo $CMD"
fi
2>&1
— захватываем stderr. grep -iq
— проверяем сообщение об ошибке доступа, не учитывая регистр.
Цветной prompt
Пример синим цветом:
read -e -p $'\e[1;34m→\e[0m ' CMD
\e[1;34m — включаем синий цвет. \e[0m — сбрасываем в стандартный после символа prompt-а.
Лог piped-команд
if [[ "$CMD" == *"|"* ]]; then
echo "PIPE: $CMD" >> ~/.mybash_pipe.log
fi
Простая проверка на наличие pipe в команде и логирование её отдельно.
Используем PROMPT_COMMAND для хуков
export PROMPT_COMMAND='echo "[Hook] Снова в prompt-е"'
PROMPT_COMMAND — переменная, в которую можно вписать команду, исполняемую до показа prompt'а. Подходит для логов, счётчиков, метрик и вообще чего угодно.
Собираем всё воедино — финальный скрипт
#!/usr/bin/env bash
shopt -s expand_aliases
alias ll='ls -la'
alias gs='git status'
HISTORY_FILE="$HOME/.mybash_history"
LOGFILE="$HOME/.mybash_cmd.log"
PIPELOG="$HOME/.mybash_pipe.log"
touch "$HISTORY_FILE" "$LOGFILE" "$PIPELOG"
trap "echo; exit 0" SIGINT SIGTERM
log_command() {
echo "$(date "+%Y-%m-%d %H:%M:%S") | $1" >> "$LOGFILE"
}
while true; do
read -e -p $'\e[1;34m→\e[0m ' CMD
echo "$CMD" >> "$HISTORY_FILE"
log_command "$CMD"
if [[ "$CMD" == *"|"* ]]; then
echo "PIPE: $CMD" >> "$PIPELOG"
fi
start=$(date +%s%3N)
if ! eval "$CMD" 2> >(tee /tmp/mybash_err.log >&2); then
if grep -iq "permission denied\|operation not permitted" /tmp/mybash_err.log; then
echo "Возможно, стоит попробовать: sudo $CMD"
fi
fi
end=$(date +%s%3N)
echo "Команда выполнена за $((end - start)) мс"
done
Это уже вполне себе рабочая мини-оболочка, которая аккуратно собирает всё, что мы настроили раньше: автодополнение через read -e, история команд, которая не исчезает между сессиями, alias'ы как в нормальном shell'е, логирование всего подряд (включая пайпы), замеры времени выполнения и подсказки на тему «а не забыли ли вы sudo?». Всё это живёт в бесконечном цикле, превращая обычный bash-процесс в кастомизированный REPL, который реагирует на команды и делает это чуть умнее, чем дефолтный bash.
Что можно докрутить
Интеграция с gh, kubectl, helm и прочими DevOps-инструментами
Современные DevOps-интерфейсы — это, по сути, API-обёртки, которые отлично дружат с Bash. Можно превратить шелл в DevX-инструмент, просто завернув часто используемые вызовы в alias или функцию.
Примеры:
alias pr='gh pr list --limit 10'
alias logs='kubectl logs -f $(kubectl get pods | fzf)'
alias helmstatus='helm list --all-namespaces'
Или завести функции с аргументами:
ghissue() {
gh issue list --label "$1" --limit 5
}
Добавляем это в начало скрипта или подгружай через отдельный конфиг.
Поддержка JSON-вывода и jq
Почти все современные CLI-утилиты (docker, gh, kubectl, aws, gcloud) отдают данные в JSON. А jq — это grep/awk для JSON.
Примеры alias:
alias pods='kubectl get pods -o json | jq ".items[].metadata.name"'
alias ghactions='gh api repos/:owner/:repo/actions/runs | jq ".workflow_runs[].status"'
alias docker_ports='docker inspect $(docker ps -q) | jq \".. | .HostPort? // empty\"'
Можно даже динамически строить меню с select, fzf, gum, whiptail.
Запуск в Docker (изолированная среда + CI/CD)
Хочешь свой REPL внутри контейнера — для обучения, деплой-скриптов или как среду в CI?
Создай Dockerfile:
FROM bash:5.2
COPY mybash.sh /mybash.sh
RUN chmod +x /mybash.sh
CMD [\"/mybash.sh\"]
Собери и запусти:
docker build -t my-bash-repl .
docker run -it my-bash-repl
Теперь есть shell-движок в изолированной капсуле.
А вам приходилось делать что-то подобное? Делитесь в комментариях
Статья подготовлена в преддверии старта специализации "Administrator Linux". На странице специализации можно ознакомиться с подробной программой, а также посмотреть записи открытых уроков.
А в календаре мероприятий уже доступно расписание всех открытых уроков.