Вам нужно пройтись по всем файлам в папке, найти те, в которых есть определённая строка, и переместить их в другую директорию.
Казалось бы — 30 секунд работы. Вы открываете терминал, начинаете писать… и через пять минут уже гуглите “bash check if file contains string”, потому что grep -q vs grep -l vs grep -c — это три разных мира с тремя разными интерфейсами. А ещё нужно не забыть кавычки вокруг "$file", потому что в имени файла может быть пробел. И [ -f ] vs [[ -f ]] — два разных синтаксиса, оба валидных, оба с подводными камнями.
Или другая задача: посчитать суммарное количество строк во всех .rs файлах проекта. В голове это простая цепочка: “найди файлы → прочитай каждый → посчитай строки → сложи”. Но чтобы выразить эту мысль на bash, нужно собрать конвейер из find, xargs, wc и awk — четырёх разных утилит, каждая со своим набором флагов.
Я сталкивался с этим постоянно, изучал разные языки и оболочки — но всё не то.
Проблема не в инструментах — а в их фокусе
Bash — хороший командный интерпретатор. Fish — приятная интерактивная оболочка. Python — мощный универсальный язык.
Но ни один из них не спроектирован для задачи “быстро написать цепочку действий в терминале, в формате полёта мысли”.
У каждого инструмента есть своя основная задача, и скриптинг повседневных действий — не она:
Bash — создавался как оболочка для запуска программ. Скрипты — побочный эффект. Отсюда
[[ ]]vs[ ],$()vs обратные кавычки, и ворох несовместимых утилит с непредсказуемыми флагами.Fish — попытался исправить ситуацию — и во многом преуспел в плане интерактивности. Но он остаётся оболочкой, и его скриптовые конструкции всё ещё строятся вокруг запуска внешних команд, а не обработки данных.
Python — огромный язык с мощной стандартной библиотекой. Но для работы с файлами нужно выбирать между
os,shutilиpathlib— тремя модулями с разными API. А сам язык постоянно обрастает специальным синтаксисом: list comprehensions, generator expressions, walrus operator, match/case — каждая конструкция со своими правилами.Nushell, Elvish — переосмысленные оболочки со структурированными данными. Они решают проблему шире — заменяют собой bash/zsh целиком. Мне хватает bash/fish, мне лишь нужно иногда воспроизвести некий сценарий в удобном и лаконичном виде.
Все эти инструменты можно использовать для терминальных скриптов. Но ни один из них не проектировался специально для этого и только для этого. Каждый раз вы боретесь с языком и платформой, а не решаете задачу.
Shik — попытка создать язык, спроектированный именно для REPL сессий и автоматизированных сценариев.
Покажи, а не рассказывай
Задача: найти все файлы в текущей директории, содержащие строку “- links”, и переместить их в папку topics/.
Bash:
for file in *; do if [ -f "$file" ] && grep -q -- "- links" "$file"; then mv "$file" topics/ fi done
Кавычки вокруг "$file" — обязательны (пробелы в именах). grep -q — тихий режим, не -s (это подавление ошибок). -- — чтобы дефис в строке не стал флагом. Пять строк, четыре ловушки.
Fish:
for file in * if test -f $file; and grep -q "\- links" $file mv $file topics end end
Чище, но со своими занозами: императивный цикл, переменную объявляем без $ а используем с $, неочевидный ; перед and — и всё ещё вызов внешних программ (grep, mv), каждая со своими правилами.
Python:
# Вариант "в лоб" — с os и shutil import os, shutil for f in os.listdir('.'): if os.path.isfile(f): with open(f) as fh: if '- links' in fh.read(): shutil.move(f, 'topics/')
Три уровня вложенности, shutil.move вместо os.move — перемещение почему-то в другом модуле.
# Вариант "модный" — с 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)
Короче, но попробуйте написать это в REPL по памяти: Path('.').iterdir() — а может iterfiles()?, f.read_text() — а может f.read()? Path('topics') / f.name — деление как конкатенация путей? Опять мета-синтаксис? Это API, которое надо читать в доке и писать в IDE, а не угадать наощупь в терминале.
Shik:
file.glob :./* $> list.filter file.is-file $> list.filter (fn [f] file.read f $> string.has "- links") $> list.iterate (file.move :topics)
Четыре строки. Каждая — один шаг пайплайна. Никаких импортов, никаких кавычек вокруг переменных, никаких внешних утилит. Данные текут сверху вниз, слева направо.
Опытный возразит: grep -l "- links" * | xargs mv -t topics/ — одна строка. И будет прав. Но вопрос не в количестве символов, а в том, сможете ли вы написать эту строку по памяти и без опечаток? Какой флаг у grep для вывода имён файлов — -l или -L? xargs mv — в каком порядке аргументы? -t — это target? А если в именах файлов пробелы?
Эту проблему невозможно исправить полностью — любой язык надо изучать. Но Shik проектировался так, чтобы каждая конструкция работала по одним и тем же правилам. Чтобы каждое правило было универсальным. Чтобы после получаса использования максимум, за чем придётся заглянуть в мануал — «как называется функция» и «в каком порядке аргументы».
Ещё одна: посчитать суммарное количество строк во всех .rs файлах проекта:
file.glob :./src/**/*.rs $> list.map (file.read #> string.lines #> list.len) $> list.sum $> print
Если хочется понять, как это работает — давайте разберёмся.
Всё — функция
Дизайн Shik вырос из двух миров: Lisp и Haskell.
От Lisp — главный принцип: в языке нет ничего, кроме применения функций. + 1 2 — это не оператор сложения. Это вызов функции с именем +, которой переданы аргументы 1 и 2. Точно так же list.map, file.glob, string.upper — всё это просто имена функций. Точка в list.map — часть имени, а не обращение к модулю. Нет арифметических операторов, нет операторов сравнения, нет логических операторов — только вызовы функций.
От Haskell — синтаксис аппликации: пробел — это вызов функции.
file.glob :./src/**/*.rs
Это не специальная конструкция. Это буквально: “применить значение :./src/**/*.rs к функции file.glob”. Как f(x), но без скобок. Несколько аргументов — через пробел: + 1 2, list.at 0 lst, file.copy :dest :source.
И от Haskell же — автоматическое каррирование: если функция ожидает два аргумента, а вы передали один — результат не ошибка, а новая функция, ожидающая оставшийся аргумент. (+ 1) — готовая функция “прибавить один”. (file.write :out.txt) — функция “записать в out.txt”.
По сути, Shik — Lisp-подобный язык с правилами аппликации Haskell, адаптированный для набора в терминале. В нём ровно четыре “настоящих” оператора — и все они про аппликацию и композицию функций, каждый со своим приоритетом, от большего (самый липкий) к меньшему (самый скромный):
пробел — базовая аппликация:
f x#>— композиция:f #> gсоздаёт новую функциюfn [x] g (f x)$— аппликация с пониженным приоритетом:print $ + 1 2вместоprint (+ 1 2)$>— пайп, аппликация слева направо:x $> fвместоf x
Всё остальное в языке — включая if, let, while, < — это применение функций по тем же правилам.
В Shik ровно один механизм — аппликация функций. Ровно четыре оператора, и все про аппликацию. Вся «магия» маркирована — # и ’ — и работает по одним правилам. Сложное поведение достигается композицией простых правил, а не новым синтаксисом. Это сознательный выбор в духе Lisp: минимум правил — максимум комбинаций. Всё, чтобы язык можно было освоить язык максимально быстро
Слева направо
Когда пробел — это аппликация, а вложенные вызовы требуют скобок, цепочки быстро превращаются в кашу:
print (list.sum (list.map (fn [f] list.len (string.lines (file.read f))) (file.glob :./src/**/*.rs)))
Это тот самый пример подсчёта строк — но записанный “в лоб”, без пайпов. Чтобы понять что тут происходит, нужно найти самое глубокое вложение и разворачивать наружу. Типичная проблема Lisp-подобных языков.
Оператор $> решает это. Он берёт результат слева и передаёт последним аргументом в функцию справа — данные текут слева направо.
Ещё пара вещей, формирующих эргономику:
Всё — выражение: нет разделения на statements и expressions. Если выражение возвращает значение — его можно передать дальше.
Только данные и функции: Никаких методов, классов, скрытых свойств. Есть только данные и глобальные функции для работы с этими данными.
Сразу готов к бою:
file.,string.,list.,object.,shell.— доступны без импортов. Открыл REPL — и работаешь.Встроенная документация:
help list.покажет все функции для списков.help list.map— описание с примером.Удобство в абсолюте: каждая фича, что есть в языке в первую очередь должна быть удобной для своей цели, даже если для этого придётся нарушить общие конвенции (возможно даже Женевские), о чём позже.
Почти всё тестирование языка я проводил в примитивном REPL без поддержки стрелочек — они были добавлены лишь в самом конце. Если код удобно писать в таких спартанских условиях — синтаксис работает.
Синтаксис за пять минут
Краткая справочная карточка — минимум, чтобы читать и писать код на Shik.
Литералы:
; комментарий {* блочный комментарий *} 42 ; число "hello world" ; строка :hello ; тоже строка — для слов без пробелов (не нужны кавычки!) [1 2 3] ; список {:name :Alice :age 30} ; объект (словарь) true ; bool fn [arg] body ; функция — да, объявление функции выглядит как аппликация с двумя параметрами, но на деле это единая конструкция литерала
:symbol — сокращение для строки без пробелов. Мелочь, но набирать :src вместо "src" в терминале — заметно быстрее.
Переменные, функции, интерполяция:
let name :Alice let greet fn [name] "Hello, {name}!" print $ greet name ; Hello, Alice! let x 10 set x (+ x 5) ; мутация через set ;; string.+, file.glob - это не функции из родительского модуля, а одна функция. Точка - часть имени let string.dup fn [str] string.+ str str ;; Почти любые символы могут быть частью имени let $->евро (* 0.87) $->евро 100
let — привязка, где первый параметр это имя, а второй - значение. {выражение} — интерполяция прямо в строке.
Каррирование и порядок аргументов:
Каждая функция поддерживает частичное применение:
let lst [1 2 3 4] lst $> list.map (+ 1) ; [2 3 4 5] — прибавить 1 lst $> list.map (- 1) ; [0 1 2 3] — вычесть 1 lst $> list.map (* 2) ; [2 4 6 8] — умножить на 2 lst $> list.map (^ 2) ; [1 4 9 16] — возвести в квадрат
(+ 1), (- 1), (* 2), (^ 2) — единообразный паттерн. Не нужно писать fn [x] + x 1 — каррирование делает это за вас.
Но почему (- 1) означает “вычесть один”, а не “отнять от одного”? Да, это ересь, с которой я долго не мог согласиться сам. Но в Shik порядок аргументов — часть дизайна: первый аргумент — тот, который вы фиксируете при частичном применении. Для арифметики: первый — модификатор, второй — база. Все четыре строки выше читаются одинаково. Если бы - работал как “первый минус второй”, (- 1) означало бы “единица минус что-то”, и пришлось бы писать fn [x] - x 1 или вводить отдельный flip. Этот принцип работает единообразно для всех функций языка. Подробнее — в документации.
Абсолютность правил:
Каррирование и пайпинг работают с почти любой функцией, в том числе let:
let my-name-is (let me) my-name-is :Max print me ; Max my-name-is :Xam print me ; Xam ; Каждый вызов my-name-is - создаёт новую переменную с тем же именем (в Shik shadowing переменных повсеместен). Вот так вот, а что вы мне сделаете. Правила есть правила, и они - абсолютны ; Но куда более интересная фича - пайпинг в let ; Пишешь, пишешь такую длинную цепочку, и думаешь - вот бы сохранить в переменную. И пожалуйста: file.glob :./src/**/*.rs $> list.filter (file.read-lines #> list.len #> < 100) $> let big-files print big-files ; [ src/eval/error.rs src/eval/evaluator.rs src/eval/native_functions/bool.rs ]
Про “почти” - нельзя каррировать функции с динамическим числом аргументов: if, while, help, list.range, number.rand.
Композиция — склейка функций через #>:
let read-lines (file.read #> string.lines) read-lines :.gitignore ; ["target" "docs" "releases"]
file.read #> string.lines — новая функция: “прочитай, затем разбей на строки”. Записано в порядке выполнения.
Много-строчные функции и выражения
Тело функции может содержать не только одно выражение. Её можно расширить с помощью блока '():
let reverse fn [str] '( let reversed "" let append (string.push reversed) str $> string.iterate-backward append reversed ; последнее выражение - результат функции ) reverse :hello $> print ; olleh
Паттерн-матчинг:
let platform $ match "{shell.os}-{shell.arch}" { :Darwin-arm64 :macos-aarch64 :Darwin-x86_64 :macos-x86_64 :Linux-x86_64 :linux-x86_64 _ :unknown } match [1 2 3 4] { [] :empty [x y #rest] "first: {x}, rest: {rest}" } ; "first: 1, rest: [3 4]"
_ — wildcard. [x y #rest] — деструктуризация списка на голову и хвост.
Вызов внешних команд:
Shik — не оболочка, но умеет вызывать внешние программы через shell функции:
shell "git log --oneline -5" $> print ; Построчный вывод — для обработки в пайпе shell.lines "git branch" $> list.filter (string.has :feature) $> list.iterate print ; Проверки окружения shell.has :docker $> print ; true shell.env :HOME $> print ; /Users/max
shell возвращает stdout как строку, shell.lines — как список строк. Можно встраивать в любой пайплайн.
Подробнее можно узнать в документации к языку на GitHub.
Собираем вместе
До сих пор примеры были короткими. Вот задача побольше — из тех, что реально встречаются в работе.
Найти все файлы в проекте, содержащие TODO, и вывести отчёт — сколько в каком файле, отсортировано по убыванию:
let has-todo (file.read #> string.has :TODO) let count-todos (file.read #> string.lines #> list.filter (string.has :TODO) #> list.len) file.glob :./src/**/*.rs $> list.filter has-todo $> list.map (fn [f] [f (count-todos f)]) $> list.sort (fn [[_ a] [_ b]] - a b) $> list.iterate (fn [[file n]] print "{file}: {n} TODOs")
Семь строк. Разберём:
has-todoиcount-todos— две функции, собранные из композиции (#>) существующих. Никаких лямбд, просто склейка: “прочитай файл → проверь есть ли TODO” и “прочитай → разбей на строки → отфильтруй с TODO → посчитай”.list.filter has-todo— отсеиваем файлы без TODO. Каррирование:has-todoуже полностью готовая функция, не нужно оборачивать.list.map (fn [f] [f (count-todos f)])— для каждого файла создаём пару[имя, количество].list.sort (fn [[_ a] [_ b]] - a b)— сортируем по второму элементу пары. Деструктуризация прямо в аргументах лямбды.list.iterate— печатаем каждую строку отчёта.
Бекап утилита:
А вот скрипт, на котором я отлаживал весь язык — бекап и развёртывание конфигурационных файлов между системами:
let home shell.home let make-path fn [dir] "{home}/.config/{dir}" ;; let$ — вариант `let` с деструктуризацией. let$ [KITTY-PATH FISH-PATH] [(make-path :kitty) (make-path :fish)] let HOME-FILES [:.ghci :.gitconfig] let FISH-FILES [ :fish_plugins :functions/start.fish :functions/setup-tabs.fish :functions/gr.fish :functions/set-theme.fish ] let KITTY-FILES $ file.list KITTY-PATH let make-copier fn [files from dest] fn [] '( files $> list.iterate fn [file] '( print "Copy: {from}/{file} -> {dest}/{file}" file.copy "{dest}/{file}" "{from}/{file}" ) ) ;; Sync let sync-fish $ make-copier FISH-FILES FISH-PATH :fish let sync-kitty $ make-copier KITTY-FILES KITTY-PATH :kitty let sync-home $ make-copier HOME-FILES home :home ;; Install let install-fish $ make-copier FISH-FILES :fish FISH-PATH let install-kitty $ make-copier KITTY-FILES :kitty KITTY-PATH let install-home $ make-copier HOME-FILES :home home ;; Run ; [:shik :backup.shk :sync :home] -> [:sync :home] let options $ list.drop 2 shell.args let mode $ list.head options let options $ list.tail options ; sync/install по-умолчанию let targets (if (list.empty? options) [:fish :home :kitty] options) targets $> list.map (+ "{mode}-" #> var.get) $> list.iterate fn.invoke
Самый интересный момент — последние три строки. Разберём по шагам:
list.map (+ "{mode}-")— еслиmode="sync", аtargets=[:fish :home], получаем["sync-fish" "sync-home"]. Каррированная функция+приклеивает префикс к каждому имени.list.map var.get— берём строку"sync-fish"и получаем значение переменнойsync-fish, с помощьюvar.get. Это обращение к текущему пространству имён как к объекту!list.iterate fn.invoke— теперь наш список это набор функций.fn.invoke- это просто вызов функции без передачи в неё аргументов.
Строки превращаются в имена переменных, имена — в функции, функции — вызываются. Shik сознательно жертвует строгостью и плодит анти-паттерны ради гибкости — это компромисс, и весьма весёлый.
Как это выглядит в жизни:

Как попробовать
Shik — проект в активной разработке (v0.7.1). Он уже пригоден для использования, но будьте готовы к шероховатостям. Это не production-ready инструмент — это рабочий прототип, которым я пользуюсь каждый день. Tree-walk интерпретатор написан на Rust — startup мгновенный, все функции написаны на самом Rust и оптимизированы, большое внимание уделено недопущению RAM бомб в замыканиях, ибо сборщика мусора нет — а для скриптов он, скорее, и не нужен.
Установка:
# Через cargo (нужен Rust toolchain) cargo install shik # macOS / Linux curl --proto '=https' --tlsv1.2 -LsSf https://github.com/pungy/shik/releases/latest/download/shik-installer.sh | sh # Windows (PowerShell) powershell -ExecutionPolicy ByPass -c "irm https://github.com/pungy/shik/releases/latest/download/shik-installer.ps1 | iex"
shik # REPL shik script.shk # запуск файла
В REPL работает help — встроенная документация по всем модулям и функциям.
В разработке и будет реализовано, по приоритету:
Shebang (
#!/usr/bin/env shik) для запуска скриптов напрямую;Регулярные выражения;
Разделитель
,для нескольких выражений в одной строке;Работа с сетью;
Шорткат функции (ленивые функции) -
list.sort #(- #1 #2)вместоlist.sort (fn [a b] - a b);Парсинг JSON;
Пользовательская обработка ошибок:
try/catchили аналог, чтобы вы могли перехватить ошибку в скрипте и обработать её сами;Многопоточность/асинхронность - как минимум запуск не-блокирующих операций;
Вместо заключения
Shik не заменит вам bash — он не оболочка и не пытается ей быть. Не заменит python для создания telegram ботов.
Но если вам раз в пару дней надо написать скрипт для файловой, текстовой или сценарной рутины — переместить, отфильтровать, переименовать, собрать отчёт — и каждый раз вы тратите больше времени на гугление флагов find и xargs, чем на саму задачу; или же вам кажется, что для этого подойдёт декларативный язык — попробуйте.
Shik на GitHub, с подробной документацией: github.com/pungy/shik
Комментарии (21)

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

PunGy Автор
08.04.2026 09:31Справедливо конечно. Но процесс тот же: формулируешь промпт -> ждёшь -> запускаешь -> не работает -> уточняешь -> ждёшь снова. Иногда быстрее гугла - но это всё ещё цикл "попроси кого-то написать код за тебя и надейся, что он правильный".
Shik про другое - про то, чтобы ты сам мог написать скрипт за 30 секунд, понимая каждую строку, и не потратив на изучение языка несколько недель. Без посредников - не нужно выходить из терминала. Всё прямо тут, в REPL, через
help, здесь и сейчас.Но, замечу вот что - у этого языка вполне себе прямой посыл: "удобство использование в REPL и для планирования сценариев".
Например - у меня есть набор коммитов которые надо черри-пикнуть в ветку. И я возьму и спокойно напишу:[:xfsc1 :fac3r :cx5nj :00dser] $> list.iterate fn [commit] shell "git cherry-pick {commit}"Это не займёт у меня хоть какой-то когнитивной нагрузки, с учётом что язык знаком. Использовать ИИ, гуглить, пытаться написать это на bash или fish - да куда проще будет уже просто скопипастить пять раз `git cherry-pick {commit}` с конкретным коммитом. Вот для этого я и создал этот язык)

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

mihail-v-n34
08.04.2026 09:31Как любитель bash, не удержусь ) Если там не терабайтные залежи, то:
grep -lR -- "- links" ./* | xargs -I {} cp {} ./topics/

Garykom
08.04.2026 09:31Как отдельный язык не думаю что взлетит.
Но как универсальная библиотека для других языков - почему нет?
Нечто похожее на встраиваемые SQLite или Lua.Имхо еще лучше адаптировать язык SQL для работы с файловой системой и данными в них.
Включая содержимое файлов, с подключаемыми модулями-плагинами для разных форматов.Понятно с расширением синтаксиса SQL по типу T-SQL или PL/SQL.
Текстовые файлы идут из коробки, только кодировки предусмотреть.
Еще можно по бинарникам искать указывая hex-коды.
PunGy Автор
08.04.2026 09:31SQL-подобные запросы к файловой системе и структурированным данным — это довольно точно описывает Nushell! Там данные идут по пайпу как таблицы, можно фильтровать, сортировать, группировать — почти как SELECT/WHERE/ORDER BY, только в терминале. Думаю вам будет интересно посмотреть в его сторону)

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

Helltraitor
08.04.2026 09:31Для интерактивной разработки можно использовать 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

PunGy Автор
08.04.2026 09:31Благодарю за развёрнутый комментарий!
Да, Python достаточно хорошо читается, и вокруг него большая инфраструктура. Но мне, для этой задачи, не подходит его императивный стиль - вложенные циклы и условия вместо цепочки трансформаций, и плохой discoverability - в Shik достаточно набратьhelp file.чтобы увидеть все функции для работы с файлами...
По поводу Nushell - у них мощная идея, но это другая ниша.
Во-первых - оболочка. Это не значит что ей нельзя пользоваться совместно с bash, а что она в первую очередь должна быть удобна навигации по системе и вызова системных команд.
Во-вторых - в ней используется полностью отдельный DSL для написания сценариев только в терминале. Shik - Lisp-flavor с правилами аппликации как в Haskell, если человек знаком с этими языками, то я уверен, что овладение Shik на 100% займет меньше часа. Это именно то, что я хотел - мне просто было нужно что-то проще чем JS или Python, с максимально примитивными, но мощными правилами. Язык Nu же крайне специфичен, и пример сгенерённый ИИ идеально это демонстрирует - снова флаги, магические переменные и свойства, уже имеет deprecated синтаксис, вызов системных программ перемешан с конструкциями языка, так ещё и не работает
Переместить все файлы в topics, внутри которых есть строка "- links"

classx
08.04.2026 09:31что на счет производительности?

PunGy Автор
08.04.2026 09:31Сделать язык супер-производительным у меня не было цели, однако там где можно и не муторно старался сделать оптимальней.
Замерил на проекте самого shik (~5500 строк Rust, 30 .rs файлов). Задача — подсчёт суммарного количества строк.
Shik:
file.glob :./src/**/*.rs $> list.map (file.read-lines #> list.len) $> list.sum $> printPython:
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.
Подробнее про внутренности и процесс разработки — планирую отдельную статью.
-

jsirex
08.04.2026 09:31Предъявы к 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 как и многие (почти все?) языки похожи друг на друга, но не отменяет того, что язык надо учить, прежде, чем на нём писать.
PunGy Автор
08.04.2026 09:31Всё верно, и спасибо за развёрнутый разбор - я прям углубил своё понимание bash.
Я не спорю с тем, что у каждого решения в bash есть причина.
[ ]vs[[ ]],--, word splitting - всё это имеет логику и контекст. Статья не про то, что bash плохой и его надо чем-то заменить (это пытаются делать другие инструменты, такие как zsh/fish/nushell, от которых я заранее в статье открестился) - а про то, что его модель (запуск программ, их композиция через пайпинг, fork/exec) оптимизирована под одни задачи, а мне нужен инструмент под другие. Я перестал писать bash-скрипты - но не перестал им пользоваться.Shik не пытается починить bash. Это отдельный язык, максимально практичный для своей задачи, который просто удобно использовать рядом с bash, особенно людям, которым нравится такой функциональный/декларативный стиль.
unreal_undead2
Вроде как find <dir> -name "*.rs" -exec cat {} \; | wc -l , что тут ещё городить.
PunGy Автор
Именно об этом и речь в статье - я даже отдельно разбираю однострочник
grep -l | xargs mv. Да, для конкретной задачи "посчитать строки" можно собрать конвейер из find/cat/wc. А завтра задача чуть другая — и снова гуглить, какой флаг у find для maxdepth, нужен ли+или\;в -exec.Shik не про то, что bash не может. Bash может всё. Shik про то, чтобы выражать цепочки действий единообразно - одними и теми же правилами, без зоопарка утилит и флагов. Если ежедневная работа с этим связана - то конечно естественно их все знать. Но если нужно сделать что-то такое раз в неделю - гуглёж из раза в раз утомляет
unreal_undead2
Согласен, вопрос в том насколько часто тот же find используется просто при ежедневной работе в командной строке.