Учим bash-скрипты, пишем Sokoban

Мне кажется, что на свете еще есть люди, которые хорошо знают несколько языков программирования, но при этом не пишут скриптов для bash, потому что скриптовый язык bash выглядит для них слишком странным. Чтобы доказать, что bash — это несложно, я написал игру Сокобан (или Грузчик, кому как нравится), и хочу рассказать, как она работает.



Коротко о главном


Переменные в bash

В bash нет типов данных, а еще при присваивании нельзя ставить лишних пробелов:
y=11 #правильно
B="" #и это тоже правильно
x = 3 #а это - неправильно

Чтобы прочитать значение переменной, перед ее именем нужно ставить знак доллара. Не возбраняется (а иногда и необходимо) заключать имя переменной в фигурые скобки, а затем перед открывающей скобкой все же ставить знак доллара. Арифметические операции (целочисленные) выполнять можно так:
r=$(( $x + $y )) #и снова обратите внимание на скобки и пробелы
r=$(( ${x} + ${y} )) #можно делать и так

Массивы в bash бывают только одномерные, инициализировать их не нужно. Можно обращаться к данным по индексу, а так же задавать пачками:
map[3]=4
map[${r}]="_"
map=( 1 2 3 4 5 6 ) #обратите внимание на пробелы


Ввод и вывод

Для вывода используется команда echo. Она поддерживает escape-последовательности, и это очень удобно.
echo "Hello, world!" #привет, мир!
echo -en "\E[3;3f Hello, world!" #курсор будет установлен по координатам (3;3), а затем будет выведен текст
echo "${x}" #переменные выводят вот так
echo -en "\E[${x};${y}f Hello!" #кстати, их можно использовать в качестве параметров escape-последовательностей

Для ввода используется команда read.
read B #ожидать ввода, сохранить результат в переменную B
read -n 1 B #ожидать ввода с клавиатуры одного символа
read -t 1 -n 1 B #ожидать в течении секунды ввода символа с клавиатуры


Управляющие конструкции

Конструкция if-then-else:
if [[ "$B" = "Q" ]] #сравнение строк, обратите внимание на пробелы и скобки
then
#команды тут
fi #конец сравнения

if [[ "$B" -eq 3 ]] #сравнение чисел
then
#команды
fi #конец сравнения

Конструкции while и case:
while ( [ "$B" != "Q" ] ) do #в скобках - условие, обратите внимание на пробелы и скобки
#команды
done

case "$B" in #аналог команды switch
  "W"   )  команда1;; #обратите внимание на скобку и пробелы
  "S"   )  команда2;;
  [Q-Z] ) команда3;; #можно использовать маски, похожие на регэкспы
esac #конец case

Цикл for:
for (( value=1 ; value<LIMIT; value++ )) do #где LIMIT - переменная
#команды
done


Интерактивная компьютерная игра Sokoban


#!/bin/bash
#Компьютерная игра Sokoban
#Определим карту как массив
map=( W W W W W W W W W W W W W W W W W W W W
W W W W W W W W W W W W W W W W W W W W
W W W W W _ _ _ W W W W W W W W W W W W
W W W W W = _ _ W W W W W W W W W W W W
W W W W W _ _ = W W W W W W W W W W W W
W W W _ _ = _ = _ W W W W W W W W W W W
W W W _ W _ W W _ W W W W W W W W W W W
W _ _ _ W _ W W _ W W W W W W _ _ o o W
W _ = _ _ = _ _ _ _ _ _ _ _ _ _ _ o o W
W W W W W _ W W W _ W _ W W W _ _ o o W
W W W W W _ _ _ _ _ W W W W W W W W W W
W W W W W W W W W W W W W W W W W W W W
W W W W W W W W W W W W W W W W W W W W
W W W W W W W W W W W W W W W W W W W W
W W W W W W W W W W W W W W W W W W W W
W W W W W W W W W W W W W W W W W W W W
W W W W W W W W W W W W W W W W W W W W
W W W W W W W W W W W W W W W W W W W W
W W W W W W W W W W W W W W W W W W W W
W W W W W W W W W W W W W W W W W W W W )

#И зададим начальные координаты грузчика
x=9
y=11
#За одно на всякий случай очистим переменную B, в которую будем сохранять ввод с клавиатуры
B=""

#установим счетчик цикла (а за одно размер игровой карты)
LIMIT=20

#Очистим экран
echo -en "\E[2J"

#И перейдем к главному циклу
while ( [ "$B" != "q" ] ) do

#Выведем карту на экран
for (( mx=1 ; mx<LIMIT; mx++ )) do
for (( my=1 ; my<LIMIT; my++ )) do
r=$(($mx*20+$my)) #у нас нет двумерных массивов, поэтому обойдемся одномерным
echo -en "\E[${mx};${my}f${map[${r}]}"
done
done

#За одно выведем всякую полезную информацию
echo -en "\E[22;2fWASD - move, Q - quit"
echo -en "\E[23;2fW - wall, X - hero, = and @ - chest, o - place for chest"
#И наконец - героя (чтобы курсор моргал в том месте, где он стоит)
echo -en "\E[${x};${y}fX\E[${x};${y}f" 

#Теперь очистим переменную для ввода с клавиатуры
B=""
#И прочитаем один символ
read -s -t 1 -n 1 B

#Сбросим переменные, в которые будем сохранять относительное перемещение грузчика
nx=0
ny=0

#Пришло время узнать, в какую сторону пользователь хочет переместить грузчика
case "$B" in
  [wW]   )  nx=$(( - 1));;
  [sS]   )  nx=$(( 1));;
  [aA]   )  ny=$(( - 1));;
  [dD]   )  ny=$(( 1));;
#На случай, если у кого-то нажат CAPS LOCK
  [qQ]   )  B="q";; 
esac

#Найдем координату клетки, на которую грузчик хочет перейти
r=$(( ($x + $nx) * $LIMIT + $y + $ny ))
#И сразу - следующую за ней
r2=$(( ($x + $nx + $nx) * $LIMIT + $y + $ny +$ny ))

#Если в этой клетке пусто, то
if [[ "${map[${r}]}" = "_" ]]
then
#Можно смело менять координаты
x=$(( $x + $nx ))
y=$(( $y + $ny ))
fi

#По местам для сундуков тоже можно ходить
if [[ "${map[${r}]}" = "o" ]] 
then
x=$(( $x + $nx ))
y=$(( $y + $ny ))
fi

#Ага, а что если ящик?
if [[ "${map[${r}]}" = "=" ]] 
then
#Если за ящиком пусто, то можно двигать
if [[ "${map[${r2}]}" = "_" ]] 
then
map[${r2}]="="
map[${r}]="_"
x=$(( $x + $nx ))
y=$(( $y + $ny ))
fi
#Если место для ящика свободно - тоже можно двигать
if [[ "${map[${r2}]}" = "o" ]] 
then
map[${r2}]="@"
map[${r}]="_"
x=$(( $x + $nx ))
y=$(( $y + $ny ))
fi
fi

#Столкнулись с ящиком, который стоит на месте
if [[ "${map[${r}]}" = "@" ]] 
then
#Если за ним пусто - значит, сдвинем ящик
if [[ "${map[${r2}]}" = "_" ]] 
then
map[${r2}]="="
map[${r}]="o"
x=$(( $x + $nx ))
y=$(( $y + $ny ))
fi
#Если за ним другое место - то тоже сдвинем
if [[ "${map[${r2}]}" = "o" ]] 
then
map[${r2}]="@"
map[${r}]="o"
x=$(( $x + $nx ))
y=$(( $y + $ny ))
fi
fi

#Возвращаемся к выводу на экран и опросу клавиатуры
done       
                    
#Пользователь нажал на Q - пора очистить экран от строительного мусора
echo -en "\E[2J"


Уфф! Вроде все. Можно скачать игру (не забудьте сделать sh-файл исполняемым командой chmod), а можно подробнее почитать про bash scripting.

С нетерпением жду появления 3D-движка или хотя бы аркадного платформера на bash. На JavaScript ведь можно...
+158
29 мая 2011, 19:14
258
sourcerer 139,8

комментарии (67)

+8
sl4mmer #
Воот во что можно погамать на плеере из плэйбоя!!!

А если серьезно + автору. Игра для примера — интересно, но кроме шуток, за счет скриптов можно очень здорово повысить эффективность работы. Пользователи *боксов подтвердят.
+4
sourcerer #
Именно, но часть моих знакомых отказывается иметь со скриптами дело по непонятным причинам.

*Вспомнил, как в 8 лет писал свою «Windows» на батнегах и ndos, когда из-за EGA-монитора не завелась Windows 3.11, и стер скупую мужскую слезу*
+1
VolCh #
Я вот не отказываюсь, но откладываю на «в следующий раз уж точно» уже несколько лет. То есть возникает какая-то задача для которой идеологически верно было бы, наверное, использовать язык шелла, но вот глядя на код вроде вашего, да ещё с пояснениями о важности пробелов и т. п. создаётся впечатление, что решать задачу в шелле придётся неоправданно долго из-за необходимости на практике столкнуться с кучей нюансов типа тех же пробелов. Да и вообще код как-то выглядит перегруженным «пунктуацией», назначение которой неочевидно по опыту разработки на других языках.
+1
tripiz #
Везет же автору с количеством свободного времени и сил :)
+1
sourcerer #
Спасибо за комплимент! Вот только сил и времени на все не хватает: есть идеи еще минимум на четыре статьи, да вот только писать их некогда (пока что).
0
4twilight #
Я джва года хочу такую игру…

P.S. а если серьезно, то хороший материал с точки зрения описания структуры и синтаксиса bash. Добавил в избранное.
–2
ramilexe #
надо в блог «ненормальное программирование»
+3
sourcerer #
Разве? Если бы на brainfuck или на чистых bat…
0
yktoo #
На Whitespace лучше.
0
bliznezz #
Есть же «оболочки».
0
equand #
bash? давайте на sh :D
+4
sl4mmer #
wget winetricks.org/winetricks

sh так sh — изучайте )
0
kost_bebix #
Спасибо огромное. Баш учить полностью лень, а вот статей «баш для тех, кто уже умеет программировать» не хватает. Впринципе, это и есть причина, почему толком баш не знаю.
+2
j1nn #
хороший способ выучить баш, не изучая его полностью — это написать на нем что-нибудь очень нужное:)
0
kost_bebix #
На очень нужное знаний хватает, ага.
0
printf #
Извиняюсь за очевидный вопрос, но в чем принципиальная разница?

if [ "$B" -eq 3 ]
if [[ "$B" -eq 3 ]]

Мне кажется, в данном случае неважно же.
0
nagato #
[ ] работает только с числами
разница будет, если, например, B=""
0
nagato #
Ой, про то, что только с числами, наврал.
+1
Livid #
Действительно наврали. А вот насчет пустой строки правда. Чтобы избежать сюрпризов при использовании [ приходится писать что-то в духе
if [ «x$B» -eq «xstringhere» ];…
+1
nagato #
Если сравнивать строки, то нужен =, -eq для чисел.
0
Livid #
Да, тут я уже наврал :)
0
mafet #
а я всю жизнь на пустоту сравнивал так:
if [ "$x" = "" ];
Это не правильно? не пойму, в чём тут подводный камень? вроде всего нормально работало.
0
Livid #
В общем-то правильно. Но есть оговорки. Например, примечание 18 в ABS 7.3 по ссылке автора: rus-linux.net/MyLDP/BOOKS/abs-guide/flat/abs-book.html#FTN.AEN2722

Цитирую
Как указывает S.C., даже заключение строки в кавычки, при построении сложных условий проверки, может оказаться недостаточным. [ -n "$string" -o "$a" = "$b" ] в некоторых версиях Bash такая проверка может вызвать сообщение об ошибке, если строка $string пустая. Безопаснее, в смысле отказоустойчивости, было бы добавить какой-либо символ к, возможно пустой, строке: [ «x$string» != x -o «x$a» = «x$b» ] (символ «x» не учитывается).
/Цитирую

Если говорить о кросс-шелловом скриптинге, то становится еще интереснее.
0
mafet #
хм. спасибо! у меня кстати был какой-то косяк с пустой строкой, но не помню на какой ОС это было и при каких обстоятельствах.
0
printf #
Так в .bat-файлах, кстати, делали. Синтаксис не помню уже, но смысл такой же, с добавлением символа.
+2
sourcerer #
[ — это команда, встроенная в bash, а [[ — это еще одно название test, при этом [[ — более универсальный вариант, не так ли? Соответственно, следует использовать [ там, где обязательна оптимизация по скорости и зависимостям. Я использовал [[ лишь для того, чтобы не путаться. А вообще, действительно не имеет значения.
+4
Livid #
Вообще-то нет. [[ это не обертка к test, это как раз башизм, причем гораздо менее портабельный. У него есть несколько преимуществ перед [ (который как раз test, в баше немного расширенный). Подробности, например, здесь: stackoverflow.com/questions/669452/is-preferable-over-in-bash-scripts
0
sourcerer #
Спасибо за информацию!
–1
ftp27 #
Помнится мне, что товарищ Sicness пару месяцев назад писал что то подобное :)
0
sourcerer #
Однако, в списке его топиков ничего такого нет.
+1
ftp27 #
Однако да ) Видно топик про это он решил не писать
0
Anexroid #
Спасибо. Давно думаю о том, чтобы bash поучить, да все руки не доходили да примеров достойных не было.
НЛО прилетело и опубликовало эту надпись здесь
+1
sourcerer #
Меня с детства по рукам били, когда я комментарии не писал. Шутка. Хотя…
0
Antigluk #
Не хватает какого-то «Game Over»-a) А так неплохо, да)
+1
sourcerer #
Хотите — добавьте по вкусу в главный цикл после каждого хода.

places=0
allmap=$(( $LIMIT * $LIMIT ))
for (( r=1 ; r<allmap ; r++ )) do
if [[ "${map[${r}]}" = "o" ]] 
then
places=$(( $qt + 1))
fi
done
if [[ "$places" -eq 0 ]] 
then
echo "You win!"
B="q"
fi


Можете даже несколько уровней добавить.
+1
Wizard999 #
А какие средства отладки есть?
+1
Antigluk #
echo?
0
Wizard999 #
Из wiki:

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

Вот такого хочется… А echo — это типа логи… Без отладчика очень сложно сделать что то большое и сложное. Особенно, тут же нет даже компилятора…
+4
Anexroid #
Ну не знаю. Нам наш препод по Си говорит, и я с ним согласен, с учетом большого опыта программирования и отладки PHP: Юзайте printf (echo), им можно отладить всё. Отладчики везде разные, больше времени потратите на знакомство с его функциями, а юзая printf можно отлаживать что угодно и где угодно.
+1
j1nn #
надо же, я так всегда думаю, а меня постоянно пытаются переубедить. и ладно бы на огромных проектах — нет, совсем нет.
+2
Antigluk #
Отладчик не нужен, согласен с Anexroid
0
Klaus #
хочется дополнить, что это справедливо в основном для скриптовых языков
0
Anexroid #
В основно, конечно, да. Просто потому, что для скриптов нет нормальных отладчиков. Но вообще, можно обойтись и без него и в компилируемых языках.
0
Klaus #
когда проект большой каждый раз ждать компиляции меня лично раздражало.
0
sourcerer #
Метод раз:1, 2
Метод два
+2
darkshine #
если в скрипте прописать «set -x», то при его запуске в stderr будут выводиться команды именно в том виде, в котором они выполняются. Таким образом, можно отслеживать какая из веток в операторе «if / then / else / fi» выполняется и какие значения переменных используются в выражениях.

вместо команды «set -x» можно просто вначале скрипта первой строкой поставить "!/bin/bash -x".
0
guglez #
либо можно просто выполнить bash -x ./script.sh
+1
yktoo #
> r=$(( $x + $y )) #и снова обратите внимание на скобки и пробелы

Внутри (( )) знак доллара вообще можно опустить и написать просто:

r=$((x+y))
0
Livid #
О! Спасибо, от моего внимания эта возможность как-то ускользнула.
0
romy4 #
в bash/sh конструкции if, for и т.д. — это команды, а не конструкции языка, потому после них обязательно нужен пробел. мне кажется, об этом необходимо упомянуть, чтобы понимать зачем так. вообще sh/bash чуть более, чем полностью состоит из команд.
0
romy4 #
только часть команд встроенные, а часть внешние как test и [ которые могут использоваться как и встроенные, так и отдельные программы /usr/bin/[
0
Livid #
На самом деле в баше не все так просто. Builtin-версии имеют кучку расширений (по сравнению с POSIX), которые могут быть и не доступны в системной версии. Поэтому /usr/bin/* почти никогда не используются в баше-специфичных скриптах. Для примера сравните man read и man bash (/^ *read \[)
+1
Beholder #
B — очень хорошее название для переменной для хранения символа с клавиатуры, да
+2
sourcerer #
B is for keyBoard.
0
KY05 #
Спасибо!

А как будет выглядеть Pac-Man?

п.с. «заодно»
0
sourcerer #
Если интересно, могу сделать специально для вас.
0
KY05 #
Специально для меня не надо, а для жителей Хабра, думаю, будет интересным.
+1
NoWeekOff #
Я имеет смысл для чего-то сложнее 5 строк использовать bash?

Более менее длинные скрипты пишу на ruby или python. Получается покрасивше и отлаживать проще. Проблем с наличием интерпретаторов в системе не испытывал.
0
murr #
Согласен, Ruby + Thor помогают просто решить множество проблем по автоматизации.
0
sourcerer #
Вы просто не пробовали писать для embedded-линуксов. Там python и ruby часто некуда впихнуть, а sh какой-никакой (чаще никакойbusybox) имеется.
0
NoWeekOff #
Это конечно да, но имхо сильно частный случай
0
sourcerer #
Разумеется, мощные скриптовые языки очень удобные. Разумеется, embedded — это частный случай. Но каждый опирается на свои возможности, так ведь?

Например: у меня есть несколько зверьков с embedded — пару роутеров, Palm и WinMo-девайс (правда, на нем linux живет в экспериментальных целях), и ни на одном нет python или ruby.

Кроме того, я активно использую KolibriOS, и полноценного порта python для нее пока что нет (впрочем, sh тоже нет). Почему не портирую сам? Python я не знаю, для меня must-have это lua, которую я уже портировал.
0
NoWeekOff #
> каждый опирается на свои возможности, так ведь?

Крайне верное замечание. Я как раз пришел к администрированию из программирования. Руки сами тянутся к любимому и знакомому.
0
Flack #
case-esac это жесть конечно.
0
sourcerer #
Мне кажется, это более-менее логичное решение для bash. По сравнению с другими, гораздо менее логичными.

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