Как стать автором
Поиск
Написать публикацию
Обновить
498.83
OTUS
Развиваем технологии, обучая их создателей

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

Уровень сложностиПростой
Время на прочтение4 мин
Количество просмотров1.6K

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

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

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

Теги:
Хабы:
Всего голосов 12: ↑7 и ↓5+5
Комментарии0

Публикации

Информация

Сайт
otus.ru
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Россия
Представитель
OTUS