Обновить

Почему я перестал писать bash-скрипты и написал свой язык

Уровень сложностиСредний
Время на прочтение11 мин
Охват и читатели7.8K
Всего голосов 23: ↑21 и ↓2+23
Комментарии23

Комментарии 23

посчитать суммарное количество строк во всех .rs файлах проекта

Вроде как find <dir> -name "*.rs" -exec cat {} \; | wc -l , что тут ещё городить.

Именно об этом и речь в статье - я даже отдельно разбираю однострочник grep -l | xargs mv. Да, для конкретной задачи "посчитать строки" можно собрать конвейер из find/cat/wc. А завтра задача чуть другая — и снова гуглить, какой флаг у find для maxdepth, нужен ли + или \; в -exec.

Shik не про то, что bash не может. Bash может всё. Shik про то, чтобы выражать цепочки действий единообразно - одними и теми же правилами, без зоопарка утилит и флагов. Если ежедневная работа с этим связана - то конечно естественно их все знать. Но если нужно сделать что-то такое раз в неделю - гуглёж из раза в раз утомляет

Согласен, вопрос в том насколько часто тот же find используется просто при ежедневной работе в командной строке.

Сейчас проще писать скрипты с помощью AI помощников.

Справедливо конечно. Но процесс тот же: формулируешь промпт -> ждёшь -> запускаешь -> не работает -> уточняешь -> ждёшь снова. Иногда быстрее гугла - но это всё ещё цикл "попроси кого-то написать код за тебя и надейся, что он правильный".

Shik про другое - про то, чтобы ты сам мог написать скрипт за 30 секунд, понимая каждую строку, и не потратив на изучение языка несколько недель. Без посредников - не нужно выходить из терминала. Всё прямо тут, в REPL, через help, здесь и сейчас.

Но, замечу вот что - у этого языка вполне себе прямой посыл: "удобство использование в REPL и для планирования сценариев".
Например - у меня есть набор коммитов которые надо черри-пикнуть в ветку. И я возьму и спокойно напишу:

[:xfsc1 :fac3r :cx5nj :00dser] $>
  list.iterate fn [commit] shell "git cherry-pick {commit}"

Это не займёт у меня хоть какой-то когнитивной нагрузки, с учётом что язык знаком. Использовать ИИ, гуглить, пытаться написать это на bash или fish - да куда проще будет уже просто скопипастить пять раз `git cherry-pick {commit}` с конкретным коммитом. Вот для этого я и создал этот язык)

Да, если язык знаком, то да.

Мне bash-подобные скрипты органически не нравятся. Поэтому для таких задач я просто использую Go и AI-генерацию кода. Классический си-подобный синтаксис, всё просто и понятно. А для частых задач можно еще и в пользовательское меню или панель инструментов тотал коммандера добавить.

Как любитель bash, не удержусь ) Если там не терабайтные залежи, то:

grep -lR -- "- links" ./* | xargs -I {} cp {} ./topics/

grep -lRZ -- "- links" ./* | xargs -0 cp -t ./topics/

Как отдельный язык не думаю что взлетит.
Но как универсальная библиотека для других языков - почему нет?
Нечто похожее на встраиваемые SQLite или Lua.

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

Понятно с расширением синтаксиса SQL по типу T-SQL или PL/SQL.

Текстовые файлы идут из коробки, только кодировки предусмотреть.
Еще можно по бинарникам искать указывая hex-коды.

SQL-подобные запросы к файловой системе и структурированным данным — это довольно точно описывает Nushell! Там данные идут по пайпу как таблицы, можно фильтровать, сортировать, группировать — почти как SELECT/WHERE/ORDER BY, только в терминале. Думаю вам будет интересно посмотреть в его сторону)

Стоит ли целый отдельный язык для таких задач делать - вопрос спорный, имхо.

Уметь быстро парсить что-то через однострочник - будто необходимый для инженера/админа/etc навык, и обычно (если без экзотических окружений), стандартный набор утилит есть везде, их не нужно туда тащить и обновлять, в отличии от кастомных решений.

Для интерактивной разработки можно использовать ipython, там будут все подсказки нужные и адекватное редактирование в REPL (хотя в последних версиях Python его завезли).

# Вариант "правильный" — через высокоуровневое api pathlib
from pathlib import Path

for f in Path('.').iterdir():
    if f.is_file() and '- links' in f.read_text():
        f.rename(Path('topics') / f.name)

Плюс в том, что он уже хорошо читается и не нужно изобретать велосипед. Минус в том, что для использования в виде скрипта нужно еще забрать аргументы

Рядом с Rust постоянно ошивается ФП. Но зачем изобретать еще один язык, если есть nushell, например? Нравится ФП, изучи один популярный инструмент и используй:

ls | where type == file | filter { open --raw $in.name | str contains "your_text" } | get name | mv $in destination_directory/

P.S. Сам не использую nushell, т.к. живу в bash (и иногда в posix / sh) скриптах, это ИИ набросал но насколько я понимаю это то, что автор и хотел в одном из примеров. Выглядит вроде неплохо

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

Если кому-то поможет, для тестирования pipes, использую pipr

Благодарю за развёрнутый комментарий!
Да, Python достаточно хорошо читается, и вокруг него большая инфраструктура. Но мне, для этой задачи, не подходит его императивный стиль - вложенные циклы и условия вместо цепочки трансформаций, и плохой discoverability - в Shik достаточно набрать help file. чтобы увидеть все функции для работы с файлами...

По поводу Nushell - у них мощная идея, но это другая ниша.
Во-первых - оболочка. Это не значит что ей нельзя пользоваться совместно с bash, а что она в первую очередь должна быть удобна навигации по системе и вызова системных команд.
Во-вторых - в ней используется полностью отдельный DSL для написания сценариев только в терминале. Shik - Lisp-flavor с правилами аппликации как в Haskell, если человек знаком с этими языками, то я уверен, что овладение Shik на 100% займет меньше часа. Это именно то, что я хотел - мне просто было нужно что-то проще чем JS или Python, с максимально примитивными, но мощными правилами. Язык Nu же крайне специфичен, и пример сгенерённый ИИ идеально это демонстрирует - снова флаги, магические переменные и свойства, уже имеет deprecated синтаксис, вызов системных программ перемешан с конструкциями языка, так ещё и не работает

Переместить все файлы в topics, внутри которых есть строка "- links"
Переместить все файлы в topics, внутри которых есть строка "- links"

что на счет производительности?

Сделать язык супер-производительным у меня не было цели, однако там где можно и не муторно старался сделать оптимальней.

Замерил на проекте самого shik (~5500 строк Rust, 30 .rs файлов). Задача — подсчёт суммарного количества строк.

Shik:

file.glob :./src/**/*.rs $>
  list.map (file.read-lines #> list.len) $>
  list.sum $> print

Python:

from pathlib import Path
print(sum(len(f.read_text().splitlines()) for f in Path('./src').rglob('*.rs')))

Bash:

find ./src -name '*.rs' -exec cat {} + | wc -l

Результат (hyperfine --warmup 3 -N, macOS, Apple Silicon):

  • Shik:

    • Время: 4.4 ms

    • Память: 2.6 МБ

  • Bash:

    • Время: 9.1 ms

    • Память: 2.1 МБ

  • Python:

    • Время: 30.3 ms

    • Память: 12 МБ

Startup Rust-бинарника делает своё дело — нет тяжёлого интерпретатора, нет tracing GC - управление памятью через Rc/RefCell, все встроенные функции на нативном Rust, для glob поиска используется библиотека. На больших объёмах данных bash+coreutils скорее всего обгонит — потоковая обработка на C vs чтение файлов целиком в память. Но для типичных скриптовых задач (десятки-сотни файлов) разница не ощутима.

Но это актуально лишь для IO-bound операций, где всё решается нативным Rust кодом. Если какая-нибудь алгоритмическая задача, например, задачка с подсчётом суммы выигрыша на костях, то реализация на Shik в 10 раз медленней чем на Python(Python 30ms, Shik 323ms) - тут уже вступает в роль CPython и байткод, Shik же реализован как TreeWalk интерпретатор, без каких-то сложных оптимизаций.

Если интересно - вот реализация на Shik.

Подробнее про внутренности и процесс разработки — планирую отдельную статью.

Предъявы к bash-у обычно растут из:

  • попытки перенести знания синтаксиса из другого языка и удивляться, а чё это оно не работает

  • Ой, а зачем это так сделанно, вот мне же просто надо ...

  • Блин, дурацкие дефолты, короче пиши "set -oei blabla все буквы которые знаю" ну и pipefail до кучи

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

На вскидку:
pipefail: cmd1 | cmd2 (cmd1 завершается раньше, закрывает поток, но так и задуманно. cmd2 должен обработать то, что есть. exit code cmd1 не должен фэйлить скрипт)

set -e: не дефолт, т.к. есть много случаев где неуспех ещё не означает выход. if cmd ... и foo || bar - это ещё специальные случаи. например, set -e не спасёт вас если some_func || echo failed. some_func выполнит все команды всё равно. Наверное, это по духу ближе к С. игнорировать или явно обрабатывать ошибки - ваше личное дело.

"$file" vs file: объявление переменное vs взять её значение. например, в bash-е можно взять значение переменной не по имени ($file), а по значению другой переменной (${!file}).

if и "$file": кавычки тут не при чём. везде так или иначе мы составляем команду из переменных. а кто сказал что переменная - это всегда просто файл. вполне может быть что мы динамечески генерируем сам expression="-d $dirname && $foo -gt $2". а внутри if [[ $expression ]].

[] [[ ]] и тп: разные вещи. почему никто не говорит "ну C++, ну C#, какая разница?" и не ожидает что синтаксис одного будет работать в другом. есть sh, попроще, есть bash.
Найдите у себя файл '/usr/bin/['*
if [ -f /tmp/file ]; then - обычный запуск команды и всегда был. запустить программу "[' и передать аргументы -f, /tmp/file, ]. Я strace-ом смотрел, что bash перехватывает это и просто вызывает builtin. А [[]] - это bash-ский, расширенный вариант. у него больше возможностей.

grep -- "$somevar": "--" - это конец списка флагов. Случаи разные бывают. grep -rn $mainargs $extraargs -- $userinput (и всё может быть как в кавычках? так и намеренно без). А аргументы могут быть похожи на флаги.
Встречали же grep -A5 (когда короткий флаг и сразу значение без пробела).
grep -rn '- links' буквально читается как: запусти греп с флагами r, n и флагом " "(пробел) и аргумент к флагу-пробел links. ну и греп скажет grep: invalid option -- ' '

grep -q а не grep -s и тп: тут всё просто, запускаю какие-то команды, инструкции не читаю. Сижу в кабине пилота, тыкаю на разные кнопочки, куда-нибудь да прилечу.

Bash как и многие (почти все?) языки похожи друг на друга, но не отменяет того, что язык надо учить, прежде, чем на нём писать.

Всё верно, и спасибо за развёрнутый разбор - я прям углубил своё понимание bash.

Я не спорю с тем, что у каждого решения в bash есть причина. [ ] vs [[ ]], --, word splitting - всё это имеет логику и контекст. Статья не про то, что bash плохой и его надо чем-то заменить (это пытаются делать другие инструменты, такие как zsh/fish/nushell, от которых я заранее в статье открестился) - а про то, что его модель (запуск программ, их композиция через пайпинг, fork/exec) оптимизирована под одни задачи, а мне нужен инструмент под другие. Я перестал писать bash-скрипты - но не перестал им пользоваться.

Shik не пытается починить bash. Это отдельный язык, максимально практичный для своей задачи, который просто удобно использовать рядом с bash, особенно людям, которым нравится такой функциональный/декларативный стиль.

Удачи набрать код Shik по памяти :)

Они там в комментах ноют (и, местами, справедливо), но кто-то же должен Вам сказать:

Это очень красиво!

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

Дальше я тоже могу поныть - компактный базис эстетически прекрасен, но читается это не всегда замечательно, я бы добавил a = x как алиас let и := как алиас set, даже наплевав на чистоту концепции (читаться будет лучше), и да, заставить людей этим пользоваться ещё надо суметь, и да, лично я вообще не понимаю, как можно создать не ОО язык (хотя для скриптов ОО не нужно), но это нытьё. :)

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

Этим можно и нужно гордиться. Даже если оно умрёт.

Спасибо, you made my day.

Спасибо большое за такой тёплый ответ!

Статья имеет сильный уклон в практику и попытку "продать" этот язык - это моя ошибка. Комментарии вполне справедливые, с учётом того, на чём в статье я акцентирую внимание.

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

Меня сводит с ума простота Lisp - чистое вычисление, минимальное количество правил. Но так же привлекает структурность Haskell, что код пишется как таблица связей и трансформаций, которые работают по фундаментальным математическим правилам.

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

Этот язык - моё видение как Lisp можно сделать структурнее. Shik - это лишь первая итерация развития этого языка. В разработке так же находится другой язык - denshik. Он будет попыткой объединить dependent types с динамикой и простотой Lisp. Shik - это сокращение от denshik (денщик), его урезанная версия)

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации