Учим Ruby вместе! Урок 1
// 20 мая, 2011 // Ruby, Веб-разработка
«Привет! Чем занимаешься?» — «Да так, одну штуку на руби программлю.» — «Это заразно :-)»
Вот такой диалог состоялся у меня сегодня с одним приятелем. Почему 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 файле можно спокойно использовать константы, и строки получаются такого вида:
includePaths[] = APPLICATION_PATH "/../vendors/Doctrine/"
Вот от APPLICATION_INI то и сносит парсер гема. А конкретно то, что эта строка не подходит ни под один паттерн:
@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
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, чтобы можно было валидировать домены.
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
#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
В этом файле описываются зависимости проекта.
source :rubygems gem 'mysql2', '0.2.6' gem 'inifile'
Ну и собственно включаемые функции.
functions.rb
#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 🙂
Ну дык до красоты пока не дошло 🙂