Прочитал пост habrahabr.ru/post/247161/ и подумал: вот человек написал непонятную программу на bash, которая выводит «Happy new year». Но это ведь bash! Надо показать, что zsh не хуже, а даже намного лучше! И так, программа на zsh, выводящая «С новым годом!» (по‐русски!) со следующими ограничениями:
- Программа не должна использовать никакие сторонние программы. Ни base64, ни cat, ничего.
- Программа должна выводить текст по‐русски.
- Программа быть написана на 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
.
Автор: ZyXI