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

Казалось бы — 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, адаптированный для набора в терминале. В нём ровно четыре “настоящих” оператора — и все они про аппликацию и композицию функций, каждый со своим приоритетом, от большего (самый липкий) к меньшему (самый скромный):

  1. пробел — базовая аппликация: f x

  2. #> — композиция: f #> g создаёт новую функцию fn [x] g (f x)

  3. $ — аппликация с пониженным приоритетом: print $ + 1 2 вместо print (+ 1 2)

  4. $> — пайп, аппликация слева направо: 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

Самый интересный момент — последние три строки. Разберём по шагам:

  1. list.map (+ "{mode}-") — если mode = "sync", а targets = [:fish :home], получаем ["sync-fish" "sync-home"]. Каррированная функция + приклеивает префикс к каждому имени.

  2. list.map var.get — берём строку "sync-fish" и получаем значение переменной sync-fish, с помощью var.get. Это обращение к текущему пространству имён как к объекту!

  3. 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)


  1. unreal_undead2
    08.04.2026 09:31

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

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


    1. PunGy Автор
      08.04.2026 09:31

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

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


      1. unreal_undead2
        08.04.2026 09:31

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


  1. sci_nov
    08.04.2026 09:31

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


    1. 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}` с конкретным коммитом. Вот для этого я и создал этот язык)


      1. sci_nov
        08.04.2026 09:31

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


  1. NeoCode
    08.04.2026 09:31

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


  1. mihail-v-n34
    08.04.2026 09:31

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

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


    1. artopp
      08.04.2026 09:31

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


  1. Garykom
    08.04.2026 09:31

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

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

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

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


    1. PunGy Автор
      08.04.2026 09:31

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


  1. d3d14
    08.04.2026 09:31

    Сейчас все такие скрипты пишет ИИ.


  1. Xelld
    08.04.2026 09:31

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

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


  1. 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


    1. 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"
      Переместить все файлы в topics, внутри которых есть строка "- links"


  1. classx
    08.04.2026 09:31

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


    1. 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 $> 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.

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


  1. lkik
    08.04.2026 09:31

    Guile scheme


  1. 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 как и многие (почти все?) языки похожи друг на друга, но не отменяет того, что язык надо учить, прежде, чем на нём писать.


    1. PunGy Автор
      08.04.2026 09:31

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

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

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


  1. JuriM
    08.04.2026 09:31

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