Как стать автором
Обновить

Zsh: fucky new year

Время на прочтение8 мин
Количество просмотров19K
Прочитал пост habrahabr.ru/post/247161 и подумал: вот человек написал непонятную программу на bash, которая выводит «Happy new year». Но это ведь bash! Надо показать, что zsh не хуже, а даже намного лучше! И так, программа на zsh, выводящая «С новым годом!» (по‐русски!) со следующими ограничениями:
  1. Программа не должна использовать никакие сторонние программы. Ни base64, ни cat, ничего.
  2. Программа должна выводить текст по‐русски.
  3. Программа быть написана на ASCII, но не должна содержать ни одной буквы или цифры.
Не знаю, как бы я справлялся на bash, но с zsh всё проще:

У zsh есть параметры раскрытия переменных (Parameter Expansion Flags из man zshexpn): echo ${(#):-65} покажет вам латинскую букву «A». Работает с текущей локалью. В принципе, этого достаточно для написания нужной программы, но есть и другие знания, сильно облегчающие жизнь:

Во‐первых, существуют анонимные функции, благодаря чему не нужно выдумывать имена для функций (хотя функцию можно спокойно назвать даже +), а также можно получить дополнительный массив @ (и не один, но только один в одной области видимости).

Во‐вторых, везде, где zsh ожидает число, можно использовать арифметическое раскрытие (Arithmetic Expansion из того же man zshexpn, более подробно в секции ARITHMETIC EVALUATION в man zshmisc), что избавляет от написания $(()), $[], да и просто $. В том числе пример выше можно написать как V=0x41; echo ${(#):-V+(V-V)}, что применимо и к значениям внутри индексов (пригодится при использовании $@).

В‐третьих, процедуру вроде ${(#)} можно проделать с массивом, при этом (#) применится к каждому элементу массива.

В‐четвёртых, если вам нужно применить последовательно несколько преобразований, то вам не нужна временная переменная: ${${(#)@}// } вполне успешно преобразовывает массив арифметических выражений, данных в аргументах, в одну строку без пробелов (два преобразования: (#) и удаление пробелов). Вам не нужна временная переменная и для преобразований над строками: ${:-string} раскрывается в string, хотя никаких переменных здесь нет (вариант использовался выше). Bash и вообще все остальные оболочки так не могут.

Таким образом, получаем следующий код:
 1 (){__=$# } !;___=$[__<<__];____=$[__<<___];_____=$((___<<___))
 2 _______=$((__+(__<<(__+____))+(__<<(__+____))<<(__+____)))
 3 ______=$((_______+__+__<<____))
 4 \*(){(( ${@[-__]} < ______+(______-_______)+_____ )) && { <<< $@ ; <<< $(\* $[$@+____]) } }
 5 +(){<<< "${${(#)@}// }"}
 6 (){
 7     (){<<< "$@"} \
 8         "$(+ _______)" \
 9         "$(+ ${@[____]}-__ ${@[____]} ______ ${@[-__]}+__ ${@[____]}-___)" \
10         "$(+ ${@[__]}+__ ${@[____]} ${@[__]}+___ ${@[____]} ${@[____]}-___ '____<<(____-__)+__')" \
11 } $(\* $[______])
. Здесь в первой строке объявляем переменные со значениями 1, 2, 4, 8, во второй со значением 0x0421 (U+0421 это CYRILLIC CAPITAL LETTER ES), в третьей — 0x0432 (CYRILLIC SMALL LETTER VE). В четвёртой строчке рекурсивная функция, генерирующая последовательность чисел с шагом 4, в пятой — практически рассмотренная выше функция, превращающая массив арифметических выражений в строку без пробелов.

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

Кажется, задача решена. Запускаем:
env -i PATH= LANG=ru_RU.UTF-8 /bin/zsh -f script.zsh
*: command not found: cat
*: command not found: cat
*: command not found: cat
*: command not found: cat
*: command not found: cat
*: command not found: cat
*: command not found: cat
*: command not found: cat
*: command not found: cat
*: command not found: cat
*: command not found: cat
*: command not found: cat
*: command not found: cat
*: command not found: cat
+: command not found: cat
+: command not found: cat
+: command not found: cat
(anon): command not found: cat
. Ой, что‐то здесь не так: налицо нарушение первого условия. Всё дело в $NULLCMD: когда команды нет, а мы используем перенаправление, неявно подставляется значение этой переменной, по‐умолчанию равное cat. Решением может служить создание функции cat:
cat()
    while { read i } {
        echo $i
    }
(да, определение функции без фигурных скобок корректно, как и цикл с ними, но без do/done). Без eval такое сделать не получится, поэтому выполняемая строка должна иметь вид eval 'cat()while {read i} {echo $i}'. Небольшая проблемка тут в том, что <<< использовать нельзя, так что можно и не заморачиваться, а просто переписать всё с ещё одной переменной или функцией, содержащей/возвращающей echo. В итоговой программе это функция @:
(){__=$# } !;___=$[__<<__];____=$[__<<___];_____=$((___<<___))
______=$[_____<<____-___<<____+____]
________="${(#):-______+__}${(#):-______-__}${(#):-______+____}${(#):-______+_____+___+__}"
@() $________ $________
_______=$((__+(__<<(__+____))+(__<<(__+____))<<(__+____)))
______=$((_______+__+__<<____))
\*(){(( ${@[-__]} < ______+(______-_______)+_____ )) && { $(@) $@ ; $(@) $(\* $[$@+____]) } }
+() $(@) "${${(#)@}// }"
(){
    (){$(@) "$@"} \
        "$(+ _______)" \
        "$(+ ${@[____]}-__ ${@[____]} ______ ${@[-__]}+__ ${@[____]}-___)" \
        "$(+ ${@[__]}+__ ${@[____]} ${@[__]}+___ ${@[____]} ${@[____]}-___ '____<<(____-__)+__')"
} $(\* $[______])
Предпоследним шагом избавимся от кавычек где можно:
(){__=$# } !;___=$[__<<__];____=$[__<<___];_____=$((___<<___))
______=$[_____<<____-___<<____+____]
________=${(#):-______+__}${(#):-______-__}${(#):-______+____}${(#):-______+_____+___+__}
@() $________ $________
_______=$((__+(__<<(__+____))+(__<<(__+____))<<(__+____)))
______=$((_______+__+__<<____))
\*(){(( ${@[-__]} < ______+(______-_______)+_____ )) && { $(@) $@ ; $(@) $(\* $[$@+____]) } }
+() $(@) "${${(#)@}// }"
(){
    (){$(@) $@} \
        $(+ _______) \
        $(+ ${@[____]}-__ ${@[____]} ______ ${@[-__]}+__ ${@[____]}-___) \
        $(+ ${@[__]}+__ ${@[____]} ${@[__]}+___ ${@[____]} ${@[____]}-___ '____<<(____-__)+__')
} $(\* $[______])
Небольшая минификация, а то что‐то код больно понятный:
(){__=$# } !;___=$[__<<__];____=$[__<<___];_____=$[___<<___]
______=$[_____<<____-___<<____+____]
________=${(#):-______+__}${(#):-______-__}${(#):-______+____}${(#):-______+_____+___+__}
@()$________ $________
_______=$[__+(__<<(__+____))+(__<<(__+____))<<(__+____)]
______=$[_______+__+__<<____]
\*(){((${@[-__]}<______+(______-_______)+_____))&&{`@` $@ `\* $[$@+____]` }}
+()`@` "${${(#)@}// }"
(){(){`@` $@} `+ _______` `+ ${@[____]}-__ ${@[____]} ______ ${@[-__]}+__ ${@[____]}-___` `+ ${@[__]}+__ ${@[____]} ${@[__]}+___ ${@[____]} ${@[____]}-___ '____<<(____-__)+__'` } `\* $[______]`
. Запуск, напомню, env -i PATH= LANG=ru_RU.UTF-8 /bin/zsh -f script.zsh.

Вариант, не требующий юникодной локали (в нём собирается строчка eval LANG=C, а при этой локали ${(#)} выдаёт байты с заданным значением):
(){__=$# } !;___=$[__<<__];____=$[__<<___];_____=$[___<<___]
______=$[_____<<____-___<<____+____]
________=${(#):-______+__}${(#):-______-__}${(#):-______+____}${(#):-______+_____+___+__}
@()$________ $________
^()`@` ${(#):-$@[__]}${(#):-$@[___]}
''(){(($@<(_____<<____)))&&`@` ${(#)@}||^ $[($@)>>(____+___)|(_____<<____+_____<<(___+__))] $[($@)&(__<<(____+___)-__)|(_____<<____)]}
_______=$[__+(__<<(__+____))+(__<<(__+____))<<(__+____)]
______=$[_______+__+__<<____]
\*(){((${@[-__]}<${@[__]}))&&{`@` $[$@[-__]] `\* $@[__] $[$@[___]+____]` }}
+(){(($#))&&`@` $('' $@[__])$(+ $@[___,-__])}
/()`@` "${${(#)@}// }"
`() {/ $@[__] $@[-__]+____+__ $@[__]-____ $@[-___]-__} $(\* $[#________+(_____<<__)] $[#________])` \
    `() {/ $@[-__] $@[__]+__ $@[-__]+___ $@[___+__]-__} $(\* '(____<<____)+_____*___' '____<<____')`=${(#):-$[_____<<(___+__)+___+__]}
(){(){`@` $@} `+ _______` `+ ${@[____]}-__ ${@[____]} ______ ${@[-__]}+__ ${@[____]}-___` `+ ${@[__]}+__ ${@[____]} ${@[__]}+___ ${@[____]} ${@[____]}-___ '____<<(____-__)+__'` } `\* '______+(______-_______)+_____' ______`
.

Если добавить ограничения «никакого fork» и «никаких подчёркиваний», то будет задача поинтереснее. В этом случае придётся вспомнить ещё несколько трюков: во‐первых, попытка выполнения неизвестной команды завершается с кодом возврата 127, который и окажется в переменной $?. &>‐ при этом предотвратит печать сообщения об ошибке: функция /()+++++++&>- эквивалентна ?=127 (если бы, конечно, можно было присваивать этой переменной), если только у вас нет команды +++++++. ${#?} тогда окажется равной трём. Ещё, можно использовать /()''&>-: этот вариант всегда будет выдавать вам 126 в $?, поскольку выполнить каталог вы не можете (внимание: вы можете определить функцию с пустым именем, что сделает использование данного трюка невозможным). Последний трюк: функция /().&>- присвоит $? 1, так как здесь идёт попытка вызвать . без аргументов, а /(). ''&>-, т.к. . попытается найти файл с пустым именем в $PATH, и, разумеется, не сможет (если же вы добавите в $PATH файл вместо каталога, то получите тот же код возврата с сообщением «Это не каталог»: . ищет path_item/, и наличие / в конце приводит к такому результату).

Во‐вторых, переменная $! содержит PID последнего процесса, запущенного в фоне. Или 0, если ничего в фоне не запускалось (правда, последняя часть не описана в документации).

В‐третьих, хотя в статье, на которую я отвечаю этот трюк использовался, я его пока не задействовал: числа можно собирать простой конкатенацией строк. В том числе можно собирать их и в виде ${base}#${number}, что делает задачу просто элементарной для генерации: в $! имеем 0, в $@ имеем массив из одного элемента (1), число собираем в виде $[$@+$@]#$@$!$!$! соберёт 2#1000 или 8. Если же генерировать по какой‐либо причине не хочется, то в дело вступают анонимные функции с возможностью присвоить $@ нужные вам значения. Вот пример кода, который выполняет eval LANG=C:
/()+++&>-
//()''&>-
///().&>-
() {
    () {
        /
        () {
            /
            () {
                /
                ${(#):-$@[${##}<<${##}]+${##}}${(#):-$@[$#]+${##}<<${#?}}${(#):-$@[${#}-${##}]-${#?}}${(#):-$@[$#]-${##}-${##}} $@[${##}]
            } $@[${##}] ${##}$!$! $[${##}$!$!+${##}$!]
        } ${(#):-$@[$#]+${?[-${##}]}-${##}}${(#):-$@[${#}-${##}]+${?[${##}]}+${##}<<${?[${##}+${##}]}}${(#):-$@[$#]+${##}<<${#?}}${(#):-$@[$#]+${##}}=${(#):-$@[$#]-${#?}} $@[${##},${#?}]
    } $@ $[$@[$#]+${#}-${##}]$! $[$@[$#]+${#}]$!
} ${#?} $[${#?}+${#?}] $[${#?}<<(${#?}+${#?})]


Вот пример однострочника на Python, который, используя описанную выше технологию, соберёт echo С новым годом!:
#!/usr/bin/env python3.4
print(''.join(
    (
        ' '
        if i == next(iter(b' ')) else
        '${{(#):-$[$[$@+$@]#{:b}]}}'.format(i).replace('0', '$!').replace('1', '$@')
    )
    for i in 'echo С новым годом!'.encode('utf8')))


Итоговая программа, использующая генерацию для echo С новым годом! и ручную сборку eval LANG=C:
/()+++&>-
//()''&>-
///(). ''&>-
() {
    () {
        /
        () {
            /
            () {
                /
                ${(#):-$@[${##}<<${##}]+${##}}${(#):-$@[$#]+${##}<<${#?}}${(#):-$@[${#}-${##}]-${#?}}${(#):-$@[$#]-${##}-${##}} $@[${##}]
            } $@[${##}] ${##}$!$! $[${##}$!$!+${##}$!]
        } ${(#):-$@[$#]+${?[-${##}]}-${##}}${(#):-$@[${#}-${##}]+${?[${##}]}+${##}<<${?[${##}+${##}]}}${(#):-$@[$#]+${##}<<${#?}}${(#):-$@[$#]+${##}}=${(#):-$@[$#]-${#?}} $@[${##},${#?}]
    } $@ $[$@[$#]+${#}-${##}]$! $[$@[$#]+${#}]$!
} ${#?} $[${#?}+${#?}] $[${#?}<<(${#?}+${#?})]
:
() {
    ${(#):-$[$[$@+$@]#$@$@$!$!$@$!$@]}${(#):-$[$[$@+$@]#$@$@$!$!$!$@$@]}${(#):-$[$[$@+$@]#$@$@$!$@$!$!$!]}${(#):-$[$[$@+$@]#$@$@$!$@$@$@$@]} ${(#):-$[$[$@+$@]#$@$@$!$@$!$!$!$!]}${(#):-$[$[$@+$@]#$@$!$@$!$!$!$!$@]} ${(#):-$[$[$@+$@]#$@$@$!$@$!$!$!$!]}${(#):-$[$[$@+$@]#$@$!$@$@$@$@$!$@]}${(#):-$[$[$@+$@]#$@$@$!$@$!$!$!$!]}${(#):-$[$[$@+$@]#$@$!$@$@$@$@$@$!]}${(#):-$[$[$@+$@]#$@$@$!$@$!$!$!$!]}${(#):-$[$[$@+$@]#$@$!$@$@$!$!$@$!]}${(#):-$[$[$@+$@]#$@$@$!$@$!$!$!$@]}${(#):-$[$[$@+$@]#$@$!$!$!$@$!$@$@]}${(#):-$[$[$@+$@]#$@$@$!$@$!$!$!$!]}${(#):-$[$[$@+$@]#$@$!$@$@$@$@$!$!]} ${(#):-$[$[$@+$@]#$@$@$!$@$!$!$!$!]}${(#):-$[$[$@+$@]#$@$!$@$@$!$!$@$@]}${(#):-$[$[$@+$@]#$@$@$!$@$!$!$!$!]}${(#):-$[$[$@+$@]#$@$!$@$@$@$@$@$!]}${(#):-$[$[$@+$@]#$@$@$!$@$!$!$!$!]}${(#):-$[$[$@+$@]#$@$!$@$@$!$@$!$!]}${(#):-$[$[$@+$@]#$@$@$!$@$!$!$!$!]}${(#):-$[$[$@+$@]#$@$!$@$@$@$@$@$!]}${(#):-$[$[$@+$@]#$@$@$!$@$!$!$!$!]}${(#):-$[$[$@+$@]#$@$!$@$@$@$@$!$!]}${(#):-$[$[$@+$@]#$@$!$!$!$!$@]}
} ${#?}

(: нужна для того, чтобы гарантированно иметь 0 в $?). Если вы запустите strace, то увидите, что fork всё же есть: при запуске / сначала делается fork, а уж потом перебираются возможные местонахождения +++. То же самое выйдет с '', поэтому нужно заменить функцию / на ///, дающую абсолютно тот же самый результат, что и / (не //!), но без fork.
Теги:
Хабы:
Всего голосов 29: ↑25 и ↓4+21
Комментарии15

Публикации

Истории

Ближайшие события

Конференция «IT IS CONF 2024»
Дата20 июня
Время09:00 – 19:00
Место
Екатеринбург
Summer Merge
Дата28 – 30 июня
Время11:00
Место
Ульяновская область