Кастомный шелл на bash: мини-интерпретатор с поддержкой pipe, history и alias

от автора

Привет, Хабр!

Сегодня рассмотрим, как на базе 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». На странице специализации можно ознакомиться с подробной программой, а также посмотреть записи открытых уроков.

А в календаре мероприятий уже доступно расписание всех открытых уроков.


ссылка на оригинал статьи https://habr.com/ru/articles/898508/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *