Объектно-ориентированное программирование на bash

Стоит сразу сказать, что оболочка bash не имеет возможностей создания объектов и работы с ними. Тем не менее, можно написать скрипты таким образом, чтобы скрипт был достаточно сильно похож на объектно-ориентированную программу, и реализовать таким образом объектно-ориентированное программирование на bash.

Давайте посмотрим, как можно написать скрипты, которые похожи на программу с объектами. Для этого нам надо сэмулировать методы и свойства объектов. И единственная возможность это сделать — это использование функций.

Я буду употреблять термины, связанные с объектами, но вы при чтении имейте ввиду, что это всё только эмуляция, хитрая подмена.

В первую очередь надо определиться с названием класса. Пусть это будет класс «obj». А объект мы назовем «myobject». В скрипте объект будет создаваться следующим образом:

obj myobject

Это чем-то похоже на другие языки, где вы сначала пишете название типа переменной, а затем название переменной. Пусть в нашем скрипте это будет тоже так же, чтобы было больше похоже на другие языки.

При создании объекта нам по логике вещей необходимо вызвать конструктор экземпляра объекта. И этим конструктором будем именно функция obj. Для лучшего вида программы сделаем следующее: вынесем obj в отдельный файл, и для сходства с другими языками назовем его «obj.h». Это будет заголовочный файл, мы будем его подключать. Этот заголовочный файл будет иметь всего одну функцию: obj

obj(){

}

И пока отложим эту функцию. Наша программа тем временем выглядит вот так:

. obj.h
obj myobject

Давайте добавим в программу работу с методами и свойствами объекта.

. obj.h
obj myobject

# вызов метода
myobject.sayHello

# работа со свойствами объекта
myobject.fileName = "file1"

И добавим для красоты еще один объект — stdout, это будет объект, реализующий вывод на экран.

stdout.printString "Property value is:"
stdout.printValue myobject.fileName

Для него нам тоже понадобится заголовочный файл, назовем его «system.h». Теперь наша программа выглядит так:

. obj.h
. system.h

obj myobject

myobject.sayHello

myobject.fileName = "file1"

stdout.printString "value is"
stdout.printValue myobject.fileName

Теперь надо всё это заставить корректно работать. Давайте для каждого класса создадим файл, назовем их тем же именем, что и заголовочные файлы, только окончание пусть будет «.class». У нас будет два файла: «obj.class» и «system.class». Эти файлы в нашей программе фигурировать не будут, они будут использоваться посредством заголовочных файлов, которые мы используем. Начнем с простого. Реализуем объект stdout, два метода которого мы используем. Один метод выводит текстовую строку, указанную в качестве аргумента, а второй метод выводит значение указанного свойства объекта.

Вот сам файл «system.class»:

stdout.printValue(){
    echo $($@)
}

stdout.printString(){
    echo $@
}

Первый метод, как вы видите, вычисляет переданное выражение и выводит значение. Второй метод просто выводит то, что ему было передано в качестве аргумента, и больше ничего не делает. В названиях функций можно использовать точку, поэтому мы можем описать псевдометоды псевдообъекта похоже на то, как принято описывать методы объекта. Для нашего объекта «myobject» класса «obj» всё будет не так просто. Системный поток вывода у нас один, поэтому несколько его экземпляров мы создавать не будем. Один есть — и уже достаточно, мы будем его использовать прямо в таком виде. Теперь надо написать содержимое заголовочного файла «system.h». Он будет очень простым, как и положено заголовочному файлу, состоящим из одной строчки:

. system.class

Здесь мы просто включаем в нашу итоговую программу содержимое файла «system.class», всё просто.

А вот с классом «obj» всё сложнее, надо реализовать использование нескольких экземпляров класса. Как это сделать, посмотрим чуть позже, а пока просто реализуем сам класс «obj». С методами класса всё понятно. А вот со свойствами не так всё просто, так как если в именах функций использовать точку можно, то в именах переменных нельзя. И тут нам придется еще раз схитрить. Мы будем использовать для хранения свойств объекта массив, а для обозначения названия свойства, которое там будет храниться — переменные, названные именами свойств. А для простоты работы со свойствами напишем одну общую функцию, получающую имя свойства, и либо задающую его значение, если указан знак «равно», либо выводящий текущее значение.

obj_properties=()

# properties IDs
fileName=0
fileSize=1

obj.property(){
    if [ "$2" == "=" ]
    then
        obj_properties["$1"]="$3"
    else
        echo ${obj_properties["$1"]}
    fi
}

Тут есть один момент: для разных классов нельзя использовать разные значения индексов со свойствами, имеющими одинаковые названия, поскольку это может привести к некорректной работе скрипта.

И теперь для нашего объекта опишем работу со свойством «fileName»:

obj.fileName(){
    if [ "$1" == "=" ]
    then
        obj.property fileName = "$2"
    else
        obj.property fileName
    fi
}

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

Вот всё содержимое файла «obj.h»:

obj_properties=()

# properties IDs
fileName=0
fileSize=1

obj.method1(){
    echo obj method1 called
}

obj.property(){
    if [ "$2" == "=" ]
    then
        obj_properties["$1"]="$3"
    else
        echo ${obj_properties["$1"]}
    fi
}

obj.fileName(){
    if [ "$1" == "=" ]
    then
        obj.property fileName = "$2"
    else
        obj.property fileName
    fi
}

Теперь надо сделать еще одно — написать заголовочный файл для нашего класса «obj». Ведь нам, как вы помните, надо иметь возможность работать с несколькими экземплярами класса. Он будет тоже очень короткий, хотя и длиннее, чем «system.h»:

obj(){
    . <(sed "s/obj/$1/g" obj.class)
}

Это конструктор класса для экземпляра объекта, который указан в качестве аргумента. Эта строчка делает следующее: в текущее место включается содержимое файла "obj.class", но перед вставкой название класса в этом файле заменяется именем экземпляра класса. Соответственно, свойства объекта и методы можно таким образом изолировать для каждого экземпляра класса. Собственно, вот и всё. У меня вся реализация "ООП" на баше уложилась в 800 байт. Напоследок содержимое всех файлов.

example.sh

. obj.h
. system.h

# create class object
obj myobject

# use object methods
myobject.sayHello

myobject.fileName = "file1"

system.stdout.printString "value is"
system.stdout.printValue myobject.fileName

system.h

. system.class

system.class

system.stdout.printValue(){
    echo $($@)
}

system.stdout.printString(){
    echo $@
}

obj.h

obj(){
    . <(sed "s/obj/$1/g" obj.class)
}

obj.class

obj_properties=()

# properties IDs
fileName=0
fileSize=1

obj.method1(){
    echo obj method1 called
}

obj.property(){
    if [ "$2" == "=" ]
    then
        obj_properties["$1"]="$3"
    else
        echo ${obj_properties["$1"]}
    fi
}

obj.fileName(){
    if [ "$1" == "=" ]
    then
        obj.property fileName = "$2"
    else
        obj.property fileName
    fi
}

Теперь вы можете использоваться в bash ООП, это не единственный вариант реализации и это, как вы видите, совсем несложно.

[wysija_form id="2"]

Объектно-ориентированное программирование на bash: 9 комментариев

  1. #kstn

    Тут есть один момент: для разных классов нельзя использовать разные значения индексов со свойствами, имеющими одинаковые названия, поскольку это может привести к некорректной работе скрипта.

    А почему тогда не обозвать бы их не fileName и fileSize, a obj_fileName и obj_fileSize соответственно?

    1. mnorin Автор записи

      Хорошая мысль, согласен. Тогда действительно, для каждого объекта будет свой индивидуальный набор свойств с уникальными именами. В итоге будет как-то так:
      obj.fileName(){
      if [ «$1» == «=» ]
      then
      obj.property obj_fileName = $2
      else
      obj.property obj_fileName
      fi
      }
      При этом доступ к свойству будет так же выполняться как
      myobject.fileName = «file1»

      1. #kstn

        Развил идею здесь — http://kstn-debian.livejournal.com/16601.html (разумеется со ссылкой на исходный пост).

        Если вкратце — здесь есть ошибка — $1, $2, итд надо окавычивать, иначе
        myobject.fileName = «very long argument with spases»
        присвоит тольок «very»

        А вообще идея классная, спасибо запользую.

        1. mnorin Автор записи

          Вообще эта статья «Just for fun», так что не знаю, насколько в практике это можно использовать. И да, разделение на .h и .class сделано для большего уподобления. Отличная мысль насчет деструктора :)

          1. #kstn

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

          2. #kstn

            А на тему практического применения — https://gitorious.org/back-in-a-minute . Это тоже тот еще джастфорфан (да, я в курсе, что писать что-то больше 100 строк на баше это бред, но убедить автора перейти на что-то другое у меня так и не получилось).

            1. mnorin Автор записи

              Как же, как же. Такая ссылка у меня в закладках есть. Правда еще не смотрел, что там внутри. Иногда требуется и больше 100 строк, если это, например, инсталлер типа такого с деинсталляцией и разными проверками. Я, честно говоря, не думаю, что скрипты больше 100 строк — бред. Игра в крестики-нолики на два терминала, например, у меня заняла примерно 3,5 килобайта и 179 строк. Но скриптовый язык bash — это все-таки язык сценариев командной оболочки и некоторые вещи, которые очень просто сделать во многих языках программирования, делать может быть придется нетривиальными способами. И, опять же, скорость работы на больших объемах обрабатываемых данных. Иногда приходится оптимизировать миллисекунды, чтобы на больших объемах стало более или менее заметно. Например, думать, какие скобки использовать. Я о баше могу долго говорить, так что на этом комментарий, пожалуй, закончу :)

              1. #kstn

                В рамках «таки оставить последнее слово за собой» — 100 строк — это, ЕМНИП, из «книги с альпакой», самое начало, что если ваш скрипт больше 100 строк, то это повод задуматься — а не разбить ли его на несколько файлов (в случае с перлом — модулей). Оно, конечно, не константа, но в большинстве случаев — верно.

                1. mnorin Автор записи

                  Я привык мерить размер bash-скриптов одним килобайтом :)

Обсуждение закрыто.