Я уже более десяти лет веду собственные дотфайлы и за это время написал множество скриптов оболочки. В этой статье я решил поделиться с вами теми из них, которые сам применяю регулярно.

Буфер обмена

copy и pasta — это простые обёртки для менеджеров системного буфера обмена вроде pbcopy в macOS и xclip в Linux. Я использую их постоянно.

# Общие примеры.
run_some_command | copy
pasta > file_from_my_clipboard.txt

# Копирование содержимого файла.
copy < file.txt

# Открытие пути файла из буфера обмена.
vim "$(pasta)"

# Декодирование base64 из буфера обмена.
pasta | base64 --decode

pastas выводит текущее состояние буфера обмена в stdout и затем при каждом изменении буфера также выводит новую версию. Этот инструмент я использую где-то раз в неделю.

# Общий пример.
pastas > everything_i_copied.txt

# Скачивает содержимое каждой ссылки, которую я копирую в буфер обмена.
pastas | wget -i -

cpwd копирует адрес текущего каталога в буфер обмена. По сути, это pwd | copy. Я часто так делаю, когда хочу использовать текущий каталог в другой вкладке терминала. То есть я копирую адрес в одной вкладке и перехожу по нему через cd в другой. Пригождается где-то раз в день.

Управление файлами

mkcd foo создаёт каталог и переходит в него через cd. По сути, это команда mkdir foo && cd foo. Я этим скриптом пользуюсь постоянно — почти при каждом создании каталога.

tempe создаёт временный каталог и переключается на него. По существу, это команда cd "$(mktemp -d)". Её я часто использую, когда нужно проделать какую-то временную работу, чтобы потом вручную всё не подчищать. Вот пара типичных примеров:

# Скачивание и извлечение файла.
tempe
wget 'https://example.com/big_file.tar.xz'
tar -xf big_file.tar.xz
# ...какие-то действия с файлом...

# Написание быстрого одноразового скрипта для экспериментов.
tempe
vim foo.py
python3 foo.py

trash a.txt b.png отправляет a.txt и b.png в корзину. Работает в macOS и Linux. Этим скриптом я пользуюсь каждый день, причём явно чаще, чем rm. Полезная штука — защищает от случайного удаления файлов.

mksh позволяет быстро создавать скрипты оболочки. К примеру, mksh foo.sh создаёт foo.sh, делает его исполняемым с помощью chmod u+x, добавляет префиксы Bash и открывает скрипт в редакторе (в моём случае Vim). Пригождается раз в несколько дней. Многие из перечисленных в этой статье скриптов были созданы с помощью mksh.

Интернет

serveit запускает на порту localhost:8000 статический файловый сервер, предоставляющий доступ к файлам текущего каталога. По сути, это команда python3 -m http.server 8000, только в этом случае скрипт обрабатывает сценарии, когда Python не установлен, используя вместо него другие программы. Мне этот инструмент пригождается несколько раз в неделю. Если вы не веб-разработчик, то вам он может оказаться не так полезен.

getsong использует yt-dlp для скачивания музыки в максимальном доступном качестве с таких ресурсов, как YouTube или SoundCloud. Например, getsong https://www.youtube.com/watch?v=dQw4w9WgXcQ скачает указанный ролик в виде трека. Я пользуюсь этим скриптом несколько раз в неделю. Обычно для скачивания саундтреков к играм.

getpod также с помощью yt-dlp скачивает какие-нибудь записи для проигрывателя подкастов. Пригождается несколько раз в месяц.

getsubs скачивает английские субтитры к видео. Этот скрипт хорош тем, что сначала ищет «официальные» субтитры и только потом использует сгенерированные. Иногда я считываю субтитры вручную, иногда выполняю getsubs https://video.example/foo | ollama run llama3.2 "Summarize this", а иногда использую этот скрипт просто для генерации описания к видео, которое не хочу сохранять на ПК. Пригождается раз в несколько дней.

wifi offwifi on и wifi toggle помогают с управлением WiFi. wifi toggle я обычно использую, когда возникают траблы с интернетом. Благо, в этих целях пригождается он где-то всего раз в месяц.

url "$my_url" парсит URL-адрес на составляющие части. Использую примерно раз в месяц для получения информации об URL, зачастую просто потому, что не хочу кликать по ссылке с трекером.

url 'https://evil.example/track-user-link?url=https%3A%2F%2Furl-i-want-to-visit.example&track=06f8582a-91e6-4c9c-bf8e-516884584aba#cookie=123'
# original: https://evil.example/track-user-link?url=https%3A%2F%2Furl-i-want-to-visit.example&track=06f8582a-91e6-4c9c-bf8e-516884584aba#cookie=123
# protocol: https
# hostname: evil.example
# path: /track-user-link
# query: url=https%3A%2F%2Furl-i-want-to-visit.example&track=06f8582a-91e6-4c9c-bf8e-516884584aba
# - url https://url-i-want-to-visit.example
# - track 06f8582a-91e6-4c9c-bf8e-516884584aba
# hash: cookie=123

Обработка текста

line 10 выводит строку 10 из stdin. Например, cat some_big_file | line 10 выводит 10 строку файла. Думаю, такая возможность должна быть встроенной — как команды head и tail. Использую её где-то раз в месяц.

scratch открывает временный буфер Vim. По сути, это алиас для $EDITOR $(mktemp). Пригождается почти каждый день для оперативной обработки текста или для написания временной заметки.

straightquote преобразует «умные кавычки» в “прямые”. Вообще, меня этот нюанс не особо волнует, но иногда такие кавычки просачиваются в код, с которым я работаю. Кроме того, их изменение в прямые позволяет чуть уменьшить размер файла, что порой весьма актуально. Мне эта функция пригождается как минимум раз в неделю.

markdownquote добавляет > перед каждой строкой. Пользуюсь этим скриптом в Vim постоянно. Я выбираю нужный фрагмент и выполняю :'<,'>!markdownquote, чтобы сделать выбранный текст цитатой. Пригождается где-то раз в неделю.

length foo возвращает 3 (пожалуй, нужно просто использовать wc -c.)

jsonformat получает JSON из stdin и в красивом виде выводит через stdout. Использую этот скрипт несколько раз в год.

uppered и lowered преобразуют строки в нижний и верхний регистр. Например, echo foo | uppered возвращает FOO. Пригождается раз в неделю.

nato bar возвращает Bravo Alfa Romeo. Обычно использую при общении со службой поддержки, когда нужно вывести длинную строку из букв и цифр, что за всю жизнь мне приходилось делать всего несколько раз. Но иногда выручает!

u+ 2025 возвращает ñ, LATIN SMALL LETTER N WITH TILDE. Быстрый способ поиска строки Юникода. Этот скрипт пригождается редко — может, раз в месяц.

snippets foo выводит содержимое ~/.config/evanhahn-snippets/foo. Я использую snippet arrow в качестве snippet recruiter в качестве быстрого ответа рекрутёрам “not interested”snippet lorem для вывода блока “Lorem ipsum” и ещё несколько других. В неделю пригождается пару раз.

Лаунчеры REPL

Вдохновлённый интерактивной оболочкой REPL в Ruby, я создал несколько лаунчеров:

  • iclj запускает Clojure REPL,

  • ijs запускает Deno REPL (или Node REPL, если Deno отсутствует),

  • iphp запускает PHP REPL,

  • ipy запускает Python REPL,

  • isql запускает оболочку SQLite (алиас для sqlite3 :memory:).

Дата и время

hoy выводит текущую дату в формате ISO, например, 2020-04-20. Им я пользуюсь постоянно, так как люблю добавлять в начало названий файлов текущую дату.

timer 10m запускает таймер на 10 минут, затем воспроизводит рингтон и отправляет системное уведомление (подробнее в notify ниже). Я также регулярно использую команду bb timer 5m, чтобы запустить пятиминутный таймер в фоновом режиме (подробнее о bb ниже). В целом этот скрипт пригождается мне почти каждый день в качестве эффективного инструмента отслеживания времени.

rn выводит текущее время и дату, используя date и cal. Пригождается где-то раз в неделю. Вот пример его выдачи:

4:20PM on Wednesday, October 22, 2025

    September 2025
Su Mo Tu We Th Fr Sa
    1  2  3  4  5  6
 7  8  9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30

Аудио, видео и изображения

ocr my_image.png извлекает из изображения текст и выводит его в stdout. К сожалению, работает только в macOS, но я планирую это исправить. (Вот отдельная статья, посвящённая этому скрипту).

boop (алиас, не скрипт оболочки). Если команда была выполнена успешно, издаёт мажорный звук, в противном случае — минорный. Я применяю его, например, так: run_the_tests ; boop, чтобы понять, успешно ли завершились тесты. Он также пригождается при выполнении длительных команд, так как об их завершении уведомлений не поступает. Использую его постоянно.

sfx foo, по сути, просто воспроизводит ~/.config/evanhahn-sfx/foo.ogg. Применяется в описанных выше boop и timer.

tunes воспроизводит аудио из файла с помощью mpv. Использую постоянно через команду --shuffle ~/music.

pix показывает изображение с помощью mpv. Пригождается несколько раз в неделю для просмотра фото.

radio — небольшая обёртка для моих любимых станций radio lofi и radio salsa. Использую несколько раз в месяц.

speak считывает данные из stdin, удаляет всё форматирование Markdown и передаёт их в механизм речевого синтеза (say в macOS и espeak-ng в Linux). Пользуюсь этим несколько раз в месяц, обычно, когда нет возможности вычитать собственный текст вслух.

shrinkvid — обёртка ffmpeg, немного сжимающая видео. Пригождается где-то раз в месяц.

removeexif удаляет из JPEG данные EXIF. Пользуюсь этой штукой редко, отчасти, потому что она ��е удаляет EXIF из файлов других форматов вроде PNG. Но я рассчитываю однажды её доработать, поэтому пока держу на вооружении.

tuivid — этот скрипт позволяет смотреть видео в терминале, но я его почти не использую. Хотя при всей своей странности он мне нравится.

Управление процессами

each — мой ответ xargs и find ... -exec, которые я нахожу сложными в работе. Например, ls | each 'du -h {}' выполняет du -h для каждого файла в каталоге. Пользуюсь этим решением я редко, но регулярно мучаюсь с xargs, так что это неплохая альтернатива.

running foo аналогичен ps aux | grep foo, но намного читабельнее (лично для меня) — просто выделенный фиолетовым PID и команда.

murder foo or murder 1234 — это обёртка вокруг kill, которая отправляет kill -15 $PID, недолго ожидает, отправляет kill -2, ожидает, отправляет kill -1, и ещё раз ожидает, прежде чем в завершение отправить kill -9. Если я хочу остановить программу, то сначала прошу её об этом вежливо и уже потом грублю. Пригождается несколько раз в месяц.

waitfor $PID — ожидает завершения PID и только потом продолжает выполнение. Также не позволяет системе уйти в режим сна. Я этим скриптом пользуюсь где-то раз в месяц, когда:

# Хочу запустить что-либо только после завершения другого процесса.
waitfor 1234 ; something_else

# Запустил длительный процесс и хочу знать, когда он завершится.
waitfor 1234 ; notify 'process 1234 is done'

bb my_command подобна my_command &, только выполняет команду в глубоком фоновом режиме, совершенно не требуя вашего внимания. Пригождается, когда вам нужно запустить демона или длительный процесс, дальнейшее выполнение которого вы отслеживать не хотите. Чаще всего я применяю этот макрос в виде bb ollama serve и bb timer 5m. В целом пригождается где-то раз в день.

prettypath выводит $PATH, но с разделением строк пробелами для повышения читабельности. Использую этот скрипт в основном при отладке проблем с $PATH, которые случаются редко. Зато, когда такое происходит, он очень выручает.

tryna my_command выполняет my_command, пока та не завершится успешно. trynafail my_command, напротив, выполняет my_command, пока та не провалится. Пригождается редко, но в некоторых случаях помогает. tryna wget ... будет пытаться скачать указанный элемент. trynafail npm test останавливает тесты, когда те начинают проваливаться.

Быстрый доступ

emoji помогает находить эмодзи. Например, emoji cool выводит:

?
?
?
?
?

httpstatus выводит все коды HTTP-состояния. httpstatus 204 выводит 204 No Content. Так как я веб-разработчик, этот скрипт пригождается мне несколько раз в месяц, избавляя от необходимости искать ответ в сети.

alphabet просто выводит английский алфавит в верхнем и нижнем регистре. Кстати, использую я этот инструмент на удивление часто (где-то раз в месяц). Вот его вывод:

abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ

Управление системой

theme 0 меняет тему системы на тёмную, а theme 1 — на светлую. Причём этот скрипт меняет не только тему ОС, но и темы Vim, Tmux и терминала. Пригождается минимум раз в день.

sleepybear отправляет систему в сон. Работает в macOS и Linux. Использую несколько раз в неделю.

ds-destroy рекурсивно удаляет все файлы .DS_Store в каталоге. Мне не нравится, что macOS захламляет ими папки. Пользуюсь этим скриптом редко, но при необходимости он весьма выручает.

Всякая всячина

catbin foo — это, по сути, cat "$(which foo)". Позволяет просматривать исходный код файла, указанного в PATH (к примеру, я его использовал для написания этой статьи). Пригождается пару раз в месяц.

notify отправляет уведомление от ОС. Применял его в некоторых других скриптах (см. выше). Плюс примерно раз в месяц использую его как-то так:

run_some_long_running_process ; notify 'all done'

uuid выводит UUID v4. Применяю где-то раз в месяц.

А какие скрипты часто используете вы? Поделитесь

Я здесь перечислил лишь те, которыми много пользуюсь сам. Надеюсь, некоторые из них пригодятся и вам.  

Комментарии (9)


  1. evgenyk
    02.11.2025 09:08

    Спасибо! Много интересных идей.


  1. zoydik
    02.11.2025 09:08

    Скрипт для скачивания аудио реально хорош


  1. ic10
    02.11.2025 09:08

    Для меня большая проблема с личными скриптами -- запомнить их имена. Иногда то, что они вообще существует :)

    Я придумал лайфхак, который может кому-то будет полезен -- имитация пространства имен. Допустим есть какая-то область деятельности, скажем, управление iptables. Называем все свои скрипты/алиасы через

    iptables.action

    И теперь, когда вы наберете iptables.<TAB>, шелл покажет для автодополнения все ваши скрипты/алиасы и не надо их все запоминать.

    Ну и также для других областей, или все в один неймспейс загнать вроде my.<TAB>


  1. Alex_Sage
    02.11.2025 09:08

    Вывод в rn красиво выглядит.
    Я на работе всплывашку с таким календарем добавил для сайта, скопировал из своей утилиты. Теперь осталось только для консоли её переписать, чтобы календари прямо везде были ))


  1. aragaer
    02.11.2025 09:08

    Когда увидел упоминание bb, то ожидал, что дальше будет babashka. Из волшебных скриптов у меня есть

    • xscr -- scrot + xclip + удалить файл (т.е. скриншот сразу в буфер обмена)

    • mpvs -- mpv на содержимое буфера обмена (обычно ссылка на ютуб)

    • ecc -- emacslient -c

    • gpg-check, gpg-unlock -- проверка того, что пассфраза закеширована в агенте и соответственно "запросить пассфразу, чтобы ее закешировать"

    • pyshell -- проверяет наличие .venv, если нет, то создает, а потом создает subshell и в нем этот venv активирует (аналог pipenv shell)

    • tmux-attach (и алиас ta) -- написанная на кложе (собственно bb) обвязка для tmux


    1. Newpson
      02.11.2025 09:08

      xscr -- scrot + xclip + удалить файл (т.е. скриншот сразу в буфер обмена)

      вам не кажется расточительным создавать файл лишь ради того, чтобы через мгновение удалить? или он создаётся в tmpfs?


  1. ThingCrimson
    02.11.2025 09:08

    Спасибо за интересные примеры скриптописательства; и большое спасибо за идею выстроить из этого добра некую свою экосистему! Поставил себе в todo перешерстить свои скрипты с подобным подходом.


  1. 0x00FA7A55
    02.11.2025 09:08

    Вот мой скриптик чтоб из двух ямликов вытаскивать только разницу. Бывает полезен когда, например, делаешь манифесты для fluxcd и хочешь чтоб там был только минимальный набор инфы.

    Скрытый текст
    #!/usr/bin/env python3
    """
    Compare two YAML files and generate a patch with differences.
    Usage: ./get-yaml-diff.py default-values.yaml custom-values.yaml
    """
    
    import sys
    import yaml
    from typing import Any
    
    
    def find_differences(base: dict[str, Any], custom: dict[str, Any], path: str = "") -> dict[str, Any]:
        """Recursively find differences between two dictionaries."""
        differences: dict[str, Any] = {}
        keys = set(base) | set(custom)
    
        for key in sorted(keys):
            full_path = f"{path}.{key}" if path else key
    
            if key not in base:
                differences[full_path] = {'default': None, 'custom': custom[key]}
            elif key not in custom:
                differences[full_path] = {'default': base[key], 'custom': None}
            else:
                base_val, custom_val = base[key], custom[key]
    
                if isinstance(base_val, dict) and isinstance(custom_val, dict):
                    differences.update(find_differences(base_val, custom_val, full_path))
                elif base_val != custom_val:
                    differences[full_path] = {'default': base_val, 'custom': custom_val}
    
        return differences
    
    
    def create_patch(differences: dict[str, Any]) -> dict[str, Any]:
        """Convert flat differences dictionary to nested patch structure."""
        patch: dict[str, Any] = {}
    
        for dotted_path, info in differences.items():
            value = info['custom']
            if value is None:
                continue
    
            current = patch
            parts = dotted_path.split('.')
            for key in parts[:-1]:
                current = current.setdefault(key, {})
            current[parts[-1]] = value
    
        return patch
    
    
    def load_yaml(filename: str) -> dict[str, Any]:
        """Load YAML file and return its content as a dictionary."""
        try:
            with open(filename, 'r') as f:
                return yaml.safe_load(f) or {}
        except FileNotFoundError:
            print(f"Error: File '{filename}' not found.", file=sys.stderr)
            sys.exit(1)
        except yaml.YAMLError as e:
            print(f"Error parsing YAML file '{filename}': {e}", file=sys.stderr)
            sys.exit(1)
    
    
    def main():
        """Load YAML files, compute differences, and print patch."""
        if len(sys.argv) != 3:
            print(f"Usage: {sys.argv[0]} default-values.yaml custom-values.yaml", file=sys.stderr)
            sys.exit(1)
    
        default_file, custom_file = sys.argv[1], sys.argv[2]
    
        default_values = load_yaml(default_file)
        custom_values = load_yaml(custom_file)
    
        differences = find_differences(default_values, custom_values)
        patch = create_patch(differences)
    
        print(yaml.dump(patch, allow_unicode=True, sort_keys=False, default_flow_style=False, indent=2))
    
    
    if __name__ == "__main__":
        main()


  1. UnknownUserMax
    02.11.2025 09:08

    Какие-то сомнительные скрипты. Будто человек только-только начал разбираться в командах консоли.