Учим Ruby вместе! Урок 1
“Привет! Чем занимаешься?” – “Да так, одну штуку на руби программлю.” – “Это заразно :-)”
Вот такой диалог состоялся у меня сегодня с одним приятелем. Почему ruby?
Зачем учить Ruby
Этот вопрос касается тех, кто уже долго и успешно программирует на PHP. Вы освоились в одном языке, это здорово, но не надо останавливаться на достигнутом. Многие могут возразить, что они знают язык, ориентируются в нёи и уже изучили все грабли с ним. Я назову несколько причин для изучения Ruby.
- Любопытство. Мне, например, было очень интересно поработать с открытыми классами. Вот так вот взять и инжектировать свой метод в системный класс – по моему это здорово. А не будет ли путаницы? А как это сделать? Вообщем интересно всё новое.
- Ruby vs PHP. Т.к. я давно программлю на PHP, мне интересно чем же Ruby может похвастаться перед PHP/
- Скорость работы Ruby. На Ruby сделан твиттер (хотя в последнее время от него и отказались). Хочется в реальности проверить его производительность.
- Класс решаемых задач. Руби хорош для веб-приложений. А так ли это?
- Метапрограммирование. Пожалуй самая главная причина.
Как изучать Ruby. Учебный план.
Вот здесь моей первой ошибкой было то, что я начал изучать фреймворк не зная язык. Теперь я понял, что так делать не надо. Забыв на время о Ruby on Rails я начал изучать сам Ruby, благо в тикет-системе долгое время висела системная задача, которую на php решать было тяжело. Уж очень не хотелось давать ему права root 🙂 Итак план такой.
- Установка Ruby, настройка окружения.
- Общий синтаксис. Типы данных Ruby.
- Функции, классы, открытые классы. Атрибуты (аксессоры и мутаторы).
- Работа со строками, с массивами. Поиск и замена подстрок и т.д. Преобразование типов.
- Работа с файлами.
- Работа с системным окружением.
- Оформление приложения, работа с гемами (модулями).
- Работа с БД.
- Установка Ruby on Rails, создание первого приложения.
В этом посте я опубликую своё первое приложение на Ruby и приглашаю всех желающих к обсуждению. Указывайте на ошибки, предлагайте best practice, задавайте вопросы.
Давайте учить Ruby вместе!
Урок 1. Первое приложение на Ruby.
Задача стоит такая. Есть DNS сервер на хостинге и надо при вызове консольной утилиты на ruby добавлять зону для домена а также запись зоны в список зон (domains.list) и изменять одну запись в БД, куда прописывать этот домен. Настройки доступа к БД хранятся в php приложении, а конкретно в его INI файле. После всех действий надо перезагрузить DNS сервер (bind).
Рабочая среда для Ruby
В качестве IDE буду использовать RubyMine от JetBrains. Уж очень мне понравился их доклад на последнем ZFConf. Хоть он и был про phpStorm, но качество видно сразу. Руби ставим через RVM сначала для всех юзеров, потом настраиваем для рута и своего юзера.
Расширение открытых классов Ruby
Для работы с INI файлами в Ruby используем gem inifile. Но в нём есть небольшая проблема. В ZF INI файле можно спокойно использовать константы, и строки получаются такого вида:
1 |
includePaths[] = APPLICATION_PATH "/../vendors/Doctrine/" |
Вот от APPLICATION_INI то и сносит парсер гема. А конкретно то, что эта строка не подходит ни под один паттерн:
1 2 3 |
@rgxp_comment = %r/\A\s*\z|\A\s*[#{@comment}]/ @rgxp_section = %r/\A\s*\[([^\]]+)\]/o @rgxp_param = %r/\A([^#{@param}]+)#{@param}\s*"?([^"]*)"?\z/ |
Вот и ситуация для применения открытых классов. Заменим фунуию IniFile::parse на свою. Все дополнения я буду складывать в файл fucntion.rb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
class IniFile private # # call-seq # parse # # Parse the ini file contents. # def parse return unless File.file?(@fn) section = nil tmp_value = "" tmp_param = "" fd = (RUBY_VERSION >= '1.9' && @encoding) ? File.open(@fn, 'r', :encoding => @encoding) : File.open(@fn, 'r') while line = fd.gets line = line.chomp # mutline start # create tmp variables to indicate that a multine has started # and the next lines of the ini file will be checked # against the other mutline rgxps. if line =~ @rgxp_multiline_start then tmp_param = $1.strip tmp_value = $2 + "\n" # the mutline end-delimiter is found # clear the tmp vars and add the param / value pair to the section elsif line =~ @rgxp_multiline_end && tmp_param != "" then section[tmp_param] = tmp_value + $1 tmp_value, tmp_param = "", "" # anything else between multiline start and end elsif line =~ @rgxp_multiline_value && tmp_param != "" then tmp_value += $1 + "\n" # ignore blank lines and comment lines elsif line =~ @rgxp_comment then next # this is a section declaration elsif line =~ @rgxp_section then section = @ini[$1.strip] # otherwise we have a parameter elsif line =~ @rgxp_param then begin section[$1.strip] = $2.strip rescue NoMethodError raise Error, "parameter encountered before first section" end elsif line =~ %r/APPLICATION_/ then next else raise Error, "could not parse line '#{line}" end end # while ensure fd.close if defined? fd and fd end end |
Также я расширю класс String, чтобы можно было валидировать домены.
1 2 3 4 5 6 7 8 |
class String def valid_domain_name? domain_name = self.split(".") name = /(?:[A-Z0-9\-][0-9a-z.+][0-9a-z]+)+/.match(domain_name[0]).nil? tld = /(?:[A-Z]{2}|aero|ag|asia|at|be|biz|ca|cc|cn|com|de|edu|eu|fm|gov|gs|jobs|jp|in|info|me|mil|mobi|museum|ms|name|net|nu|nz|org|tc|tw|tv|uk|us|vg|ws)/.match(domain_name[1]).nil? (domain_name.count > 1 and name != false and tld != false) end end |
Исходники
Ну а теперь покажу вам собственно исходники.
index.rb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
#coding: utf-8 require "mysql2" require "socket" require "inifile" require "./functions.rb" # Хэш адресов серверных машин hosts = { :production => '83.168.22.1', :test => '84.22.11.1' } util = Util.new(hosts) util.releative_config_path='/site.com/application/config/application.ini' # Проверка параметров quit if (ARGV.count != 2) domain = ARGV[0] hostname = ARGV[1].split(".")[0] quit('Invalid domain name') if (not domain.valid_domain_name?) # Поиск компаниив БД result = Mysql2::Client.new(util.get_db_settings).query("SELECT id FROM `sites` WHERE `hostname` = '#{hostname}'") quit('Company not found') if result.count != 1 # Обновление её hostname rows = Array.new result.each{|row| rows << row} company_id = rows[0]['id'] result = Mysql2::Client.new(util.get_db_settings).query("UPDATE `dbname`.`sites` SET `domain` = '#{domain}' WHERE `dao_companies`.`id` =#{company_id};") # Добавление зоны bind_config_path = '/etc/bind' default_zone_file = bind_config_path + '/zones/DEFALT' new_zone_file = bind_config_path + "/zones/#{domain}.zone" zones_list_file = bind_config_path + "/domains.lst" quit('File with default zone does not exists') unless File.exist?(default_zone_file) quit('File with zones list does not exists') unless File.exist?(zones_list_file) zone = IO.read(default_zone_file).gsub('SERIAL',Time.now.strftime("%Y%m%d%S")).gsub('DOMAIN', domain) if not File.exist?(new_zone_file) then File.open(new_zone_file, "w") {|f| f.puts(zone) } else quit('Domain '+domain+' zone already exists!') end # Добавление зоны в список zone = "zone \"#{domain}\" { type master; file \"/etc/bind/zones/#{domain}.zone\"; };" if not IO.read(zones_list_file).include?(domain) then File.open(zones_list_file, "a") {|f| f.puts(zone) } end # Перезапуск сервисов (bind9) system("service bind9 restart") puts "Completed" |
Gemfile
В этом файле описываются зависимости проекта.
1 2 3 4 |
source :rubygems gem 'mysql2', '0.2.6' gem 'inifile' |
Ну и собственно включаемые функции.
functions.rb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 |
#coding: utf-8 class String def valid_domain_name? domain_name = self.split(".") name = /(?:[A-Z0-9\-][0-9a-z.+][0-9a-z]+)+/.match(domain_name[0]).nil? tld = /(?:[A-Z]{2}|aero|ag|asia|at|be|biz|ca|cc|cn|com|de|edu|eu|fm|gov|gs|jobs|jp|in|info|me|mil|mobi|museum|ms|name|net|nu|nz|org|tc|tw|tv|uk|us|vg|ws)/.match(domain_name[1]).nil? (domain_name.count > 1 and name != false and tld != false) end end class IniFile private # # call-seq # parse # # Parse the ini file contents. # def parse return unless File.file?(@fn) section = nil tmp_value = "" tmp_param = "" fd = (RUBY_VERSION >= '1.9' && @encoding) ? File.open(@fn, 'r', :encoding => @encoding) : File.open(@fn, 'r') while line = fd.gets line = line.chomp # mutline start # create tmp variables to indicate that a multine has started # and the next lines of the ini file will be checked # against the other mutline rgxps. if line =~ @rgxp_multiline_start then tmp_param = $1.strip tmp_value = $2 + "\n" # the mutline end-delimiter is found # clear the tmp vars and add the param / value pair to the section elsif line =~ @rgxp_multiline_end && tmp_param != "" then section[tmp_param] = tmp_value + $1 tmp_value, tmp_param = "", "" # anything else between multiline start and end elsif line =~ @rgxp_multiline_value && tmp_param != "" then tmp_value += $1 + "\n" # ignore blank lines and comment lines elsif line =~ @rgxp_comment then next # this is a section declaration elsif line =~ @rgxp_section then section = @ini[$1.strip] # otherwise we have a parameter elsif line =~ @rgxp_param then begin section[$1.strip] = $2.strip rescue NoMethodError raise Error, "parameter encountered before first section" end elsif line =~ %r/APPLICATION_/ then next else raise Error, "could not parse line '#{line}" end end # while ensure fd.close if defined? fd and fd end end def quit(message=nil) banner = " ============================ | DNS Addition tool | ============================ Usage: ruby ./index.rb domain.com olddomain.site.com" if not message.nil? then banner = message end puts banner exit end class Util attr_accessor :hosts, :releative_config_path, :environment def initialize(hosts =Array.new) self.hosts = hosts end # Получение локального IP-адреса def local_ip orig, Socket.do_not_reverse_lookup = Socket.do_not_reverse_lookup, true # turn off reverse DNS resolution temporarily UDPSocket.open do |s| s.connect '64.233.187.99', 1 s.addr.last end ensure Socket.do_not_reverse_lookup = orig end # Получение среды окружения def get_environment if @environment.nil? then hosts = self.hosts.invert if(hosts.include?(self.local_ip)) then @environment = hosts[self.local_ip] else @environment = 'development' end else @environment.to_s end end def get_config_path local_username = get_local_username '/home/'+local_username+'/sandbox'+self.releative_config_path end # Возвращает имя пользователя, в случае если утилита запущена через rvmsudo или напрямую def get_local_username if ENV['SUDO_USER'].nil? quit("Util should be run over rmvsudo, \r\nexample: rvmsudo ruby ./index.rb domain.ru some.subdomain.ru") else ENV['SUDO_USER'] end end def get_db_settings config = IniFile::load(self.get_config_path) section_name = self.get_environment.to_s + ' : bootstrap' quit('No suitable section in config file') unless config.has_section?(section_name) dsn = config.to_h[section_name]['resources.doctrinedata.connections.default.dsn'] # Parse dsn dsn.sub!('mysql://', '') arr = dsn.split('@') dbconfig = { :username => arr[0].split(':')[0], :password => arr[0].split(':')[1], :host => arr[1].split('/')[0], :database => arr[1].split('/')[1] } end end |
А как же PHP?
Этот пост не сводится к тому, что надо бросить PHP и начать изучать руби. PHP – самый популярный язык веб-программирования, на нём реализованы тысячи интересных вещей и алгоритмов, в т.ч. даже и нейросети. И я его люблю) За многие годы, можно сказать, что я с ним сроднился, несмотря на все его недостатки. Но это не значит, что не надо изучать для себя что-то новое.
Что почитать. Книги по Ruby
Мне в последнее время задают вопрос, по каким книжкам учить Ruby. Я сейчас читаю вот эту.
Эта книга – официальное руководство по динамическому языку программирования Ruby. Авторский состав воистину звездный: Дэвид Флэнаган – известнейший специалист в области программирования, автор ряда бестселлеров по JavaScript и Java; Юкихиро “Matz” Мацумото – создатель и ведущий разработчик Ruby.
В книге приведено детальное описание всех аспектов языка: лексической и синтаксической структуры Ruby, разновидностей данных и элементарных выражений, определений методов, классов и модулей. Кроме того, книга содержит информацию об API-функциях платформы Ruby.
Издание будет интересно опытным программистам, знакомящимся с новым для себя языком Ruby, а также тем, кто уже программирует на Ruby и хочет достичь более высокого уровня понимания и мастерства работы. Найти книжку можно на озоне или библио-глобусе.
Жду ваших комментов по коду и любые интересные идеи!)
В свое время делал выбор между Ruby и Python уже кодя на PHP (кстати про грабли тоже в блоге писал). Выбрал Python, по многим параметрам. Быстрее, старше, веб-фреймворков несколько, системных скриптов в Ubuntu на нем множество, Google его юзает вовсю (разработчик Python сейчас там работает). Ruby интересный конечно, но из практических соображений пришлось отказаться…
В данном конкретном случае нельзя расширять класс строк методом valid_domain_name?, т.к. это не совсем укладывается в парадигму ООП. Этот метод имеет узкую специализацию, поэтому, логичнее было бы вынести его в отдельный класс или модуль, либо же определять этот метод в конкретном инстансе строки, а не во всех строках подряд.
Ну и реально, код написан в php-style, здесь не видно “красоты” ruby 🙂
Ну дык до красоты пока не дошло 🙂