Ignore file lines in git

For example, you have this code and you want to ignore in git lines which contain username and password values:

def ssh_connect
  username = "root" # ignore this line
  password = "password" # ignore this line too
  ip = "192.168.1.1"
  # ...
end

To ignore some special lines in git you need to:

  1. Create a .gitattributes in your repository (remember that .gitattributes will be committed into the repository, if you don’t want that, add it to .git/info/attributes)
  2. Add *.rb filter=ignoreline (run filter named ignoreline on all *.rb files)
  3. Now, we need to define ignoreline filter in .gitconfig
  4. git config --global filter.ignoreline.clean "sed '/#ignoreline$/'d" (delete theses lines)
  5. git config --global filter.ignoreline.smudge cat (do nothing when pulling file from the repository)

And then, you can ignore some lines using #ignoreline comment:

def ssh_connect
  username = "root" #ignoreline
  password = "password" #ignoreline
  ip = "192.168.1.1"
  # ...
end

Happy hacking!

Write simple scheme interpreter on ruby

TL;DR: github repo

Every developer has a moment in his life where he wants to write his own programming language. In this article, I want to show you how to do this for a simple lisp compiler.

Why scheme and lisp?

Firstly, lisp is very simple for create and for understanding. Lisp (LISt Processor) is a family of languages which is based on the idea of S-expressions. S-Expression are needed for data representation and may consist of atoms (numbers, symbols, boolean expressions) or are the expression of the form (x . y) where x and y are s-expressions. This expression may be formed as lists ((1 . ( 2 . 3)) this equals (1 2 3)) and trees (((1 . (2 . 3)) . (4 . 5))).

Secondly, after creating interpreter you can better understand the language (the author fully understood the environment idea). Also you can understand the main idea of compilers and interpreters.

Now let’s begin our journey into the world of compilers and interpreters so that we can write a simple scheme interpreter.

Main idea

Our language will contain two parts: a parser which translates the string to AST and eval function. This function will take the AST with envariement value and will return the result of the code.

Schematically, it looks like this:

code(string) => parse function => AST => eval function => result

First step. Parser.

To begin, let’s define what we want to get. For example, we have a string '(+ 1 1 1)'. What should our parser return? What kind of data structure should we receive? I think, that an array would be correct.

Let write simple test code:

program = '(+ 1 1 1)'
lisp = Lisp.new
lisp.parse(program) == [:+, 1, 1, 1]

As you can see, this is a simple code, so I just displayed parse method code:

class Lisp
  def parse(program)
    tokenize(program)
  end

  def tokenize(chars)
    chars
      .gsub(/\s\s+/, ' ')
      .gsub('(', ' ( ')
      .gsub(')', ' ) ')
      .split(' ')
      .map{ |token| atom(token) }
      .compact
  end

  def atom(token)
    if token[/\.\d+/]
      token.to_f
    elsif token == '(' || token == ')'
      nil
    elsif token[/\d+/]
      token.to_i
    else
      token.to_sym
    end
  end
end

As you know, in lisp you can write your code with nested operators, for example - (+ (* 2 2) (- 5 3)). And this code will return 6.

When we use our parser for this code, the result is not quite what we need, so let’s update our test code:

program = '(+ (* (1 2) 3) 4)'
lisp = Lisp.new
lisp.parse(program) == [:+, [:*, [1, 2], 3], 4]

As you might guess, the most obvious way to fix our code is to call parse method in recursion and all array elements from '(' to ')'. We will move to a nested array. The code will be look loke this:

class Lisp
  def parse(program)
    read_from_tokens(tokenize(program))
  end

  def tokenize(chars)
    chars
      .gsub(/\s\s+/, ' ')
      .gsub('(', ' ( ')
      .gsub(')', ' ) ')
      .split(' ')
  end

  def read_from_tokens(tokens)
    return if tokens.empty?

    token = tokens.shift

    if '(' == token
      list = []

      while tokens.first != ')'
        list << read_from_tokens(tokens)
      end
      tokens.shift

      list
    elsif ')' == token
      raise 'unexpected )'
    else
      atom(token)
    end
  end

  def atom(token)
    if token[/\.\d+/]
      token.to_f
    elsif token[/\d+/]
      token.to_i
    else
      token.to_sym
    end
  end
end

We did it! Let’s start to make eval method.

Eval method

As I said earlier, our interpreter consists of two parts: a parser and eval function.

The eval function will take two arguments: an expression, exp, that we want to evaluate, and an environment, env, in which to evaluate it. An environment is a mapping from variable names to their values. By default, eval will use a instance value that includes the names for a bunch of standard things.

let’s implement @env variable with car, cdr and cons functions:

env = {
  :car  => lambda { |*list| list[0] },
  :cdr  => lambda { |*list| list.drop 1 },
  :cons => lambda { |(e, cell), _| [e] + cell },
}

The next step is to make eval function which will look for a match on the first element of the input array

class Lisp
  def initialize(ext = {})
    @env = {
      :car   => lambda { |*list| list[0] },
      :cdr   => lambda { |*list| list.drop 1 },
      :cons  => lambda { |(e, cell), _| [e] + cell },
    }.merge(ext)
  end

  def eval(exp, env = @env)
    env[exp.first].(exp[1..-1])
  end

  # ...
end

Now we have a problem: what will happen when the first element of the array will be not be a symbol (integer for example) and what will be happen when we have nested functions? I think we can add a check to the element type like this:

class Lisp
  def initialize(ext = {})
    @env = {
      :car   => lambda { |*list| list[0] },
      :cdr   => lambda { |*list| list.drop 1 },
      :cons  => lambda { |(e, cell), _| [e] + cell },
    }.merge(ext)
  end

  def eval(exp, env = @env)
    if exp.is_a? Numeric
      exp
    elsif exp.is_a? Symbol
      env[exp]
    else
      code = eval(exp[0], env)
      args = exp[1..-1].map{ |arg| eval(arg, env) }
      code.(*args)
    end
  end

  # ...
end

Some (eg arithmetic), we can easily add to env variable, and some do not. Therefore, we need to extend the checking in eval function. We will add a check for function name. For example, the code below will demonstrate quote and if functions:

def eval(exp, env)
  # ...
  elsif exp[0] == :quote
    exp[1..-1]
  elsif exp[0] == :if
    _, test, conseq, alt = exp
    exp = eval(test, env) ? conseq : alt
    eval(exp, env)
  # ...
end

The next step is to initialize define and lambda functions. In scheme define function syntax is as follows:

(define name
  (expression))

And our code must create a new key-value pair in the env hash:

def eval(exp, env)
  # ...
  elsif exp[0] == :define
    _, var, e = exp
    env[var] = eval(e, env)
  # ...
end

The last function, lambda in scheme will have this syntax:

(lambda (arg1, arg2, ...)
  (block of code))

The first thing that comes to mind is to return the new lambda object with a new value inside env that will serve our code:

def eval(exp, env)
  # ...
  elsif exp[0] == :lambda
    _, params, e = exp
    lambda { |*args| self.eval(e, env.merge(Hash[params.zip(args)])) }
  # ...
end

As you can see, we made basic functionality of the our interpreter. I will leave learning how to do the implement the arithmetic methods and other methods such as true, false, list, etc up to the reader.

REPL

In main REPL have really simple idea repl takes single user inputs, evaluates them, and returns the result to the user. And all this is happens in an infinite loop:

def repl(prompt = 'lisp >> ')
  while true
    print prompt
    program = gets

    p eval(parse(program))
  end
end

As a result, you should get something like this:

lisp >> (define pi 3.14)
3.14
lisp >> (define circle-area (lambda (r) (* pi (* r r))))
#<Proc:0x007fa6140c6cc0@lib/rlisp.rb:86 (lambda)>
lisp >> (circle-area 11)
379.94

Conclusions

Right now, we have a simple scheme interpreter. It is easy to expand since we wrote a simple repl, and considered the basic idea of the interpreter. In this article, we did not consider such important concepts as macros, multithreading, code optimization, work with the system, and much more. These concepts will be discussed in future articles.

Further reading

Get request with custom locale in your tests

Sometimes in tests you have to set special locale for get request. For example you want to test selecting locale language. For this you have to set locale in 'HTTP_ACCEPT_LANGUAGE' variable in params hash.

it 'your test description' do
  rackenv = {'HTTP_ACCEPT_LANGUAGE' => 'ru,en'}
  get '/', {}, rackenv

  ...
end

Memory profiler tools for ruby

Memory profiler tools for ruby It so happened that I was need know how much ruby objects created for each call sidekiq perform_async method (mperham/sidekiq#2288). That’s why I decided to create list with tools which helps you to know all required information.

1. Ruby GC

Sam Saffron has written great post, in which he talk about ruby garbage collector and wonderful GC.start method which returns data hash:

2.2.0:001 > GC.stat
# => {:count=>21, :heap_allocated_pages=>236, :heap_sorted_length=>237, :heap_allocatable_pages=>0, :heap_available_slots=>96193, :heap_live_slots=>95354, … }

Also author mentions a ObjectSpace module that lets you know information about object/memory management. For example you can easily know general number of created objects. For this you need write this code:

2.2.0:002 > ObjectSpace.count_objects[:TOTAL]
# => 96193

2. Ruby Memory Profiler

Also Sam wrote great tool (ruby-memory-profiler) which provide easily display all required information for any code in block. For this you will need call something like this (I called this code in my sidekiq reduce allocation PR):

MemoryProfiler.report{ 100.times{ HardWorker.perform_async } }.pretty_print

3. ruby-prof

General features:

  • display different reports;
  • fast;
  • easily profiles rails apps;

How to use:

require 'ruby-prof'

# Profile the code
RubyProf.start
# [code to profile]
RubyProf.pause
# [other code]
RubyProf.resume
# [code to profile]
result = RubyProf.stop

Conclusions

Profiling Ruby isn’t as difficult as it seems at first glance. There are a lot of great libraries. The only thing I was missing — profiling the code from a file, but I’m sure it will soon add (maybe it will add myself ☺ ).

Convert string to color hash

So it happens that sometimes you need to convert string to color hash. I think that Digest::MD5 class is easiest way for this. For this you need call hexdigest method with necessary string.

Digest::MD5.hexdigest('My string')[0..5] # => 'a537d0'

And if you will create rgb color you need convert hex to decimal with to_i(16) method.

Digest::MD5.hexdigest(worker)[0..5]
  .scan(/../)
  .map{ |color| color.to_i(16) }

История vim

Я свято верю, что перед тем, как изучать новую технологию, необходимо узнать, зачем и в каких условиях она была создана. Думаю, данные знания позволяют в будущем не только не забивать гвозди микроскопом, но и не спрашивать, почему они так плохо забиваются. Поэтому первую запись из цикла статей, связанных с vim-ом, я бы хотел начать с истории его становления и небольшого описания его функционала.

Представьте себе: 70тые годы, расцвет unix машин, на данный момент существует только текстовый редактор ed, который является командно-ориентированным, а за ним появляется редактор ex. Во времена первого никто не слышал о мониторах, поэтому использовали для ввода телетайпы, из-за чего ed имеет командно-ориентированный интерфейс. А ex, в свою очередь, являлся улучшеным ed. Поэтому в нем появились такие штуки как экранное редактирование и работа с несколькими файлами, что для тех времен является прорывом. В это время многие программисты начинают писать свои редакторы, и один из них, Билл Джой, придумывает логичную и простую концепцию поверх редактора ex. Во первых, было бы логично воспринимать текст как объект, к которому применяются команды редактирования. Во вторых, использование unix идеологии о простоте и взаимности комманд. Так, в 1976 году, появляется первая версия текстового редактора vi, сокращено от (visual). Давайте рассмотрим каждую из концепций отдельно.

Во первых, человек часто мыслит абстрактно, поэтому мы никогда не воспринимает набор последовательных символов как непосредственно набор последовательных символов. Наш разум создает абстракции, такие как слова, предложения, параграфы или абзацы и тому подобные. Так почему бы нам не реализовать эту же идею в текстовом редакторе? Думаю, этим же вопросом задался Билл Джой, и поэтому vi оперирует не просто с набором символов, как многие другие текстовые редакторы, а оперирует непосредственно объектами-абстракциями: словами, строками, параграфами.

Допустим, мы смогли реализовать такую идею внутри редактора, но теперь встает вопрос, что делать с этими объектами? Да собственно то же самое, что мы, доблестные воины клавиатуры и текста, делаем с ними каждый день - изменять их посредством передачи в функций. Кстати, часть функций можно позаимствовать из unix-а, например, sort и ему подобные. Поэтому на данный момент наше взаимодействие с редактором можно описать простым набором функций вида: [комманда] [объект].

А теперь самое интересное, как мы реализуем такое количество комманд и способов выбора объектов, и при этом нам надо как то печатать обычный текст. Думаю, многие понимают, что использовать все клавиши для комманд и для набора текста - не реально вообще никак. А тем более на такой клавиатуре, которую использовали в терминалах того времени.

old unix keybord

Кстати, небольшой оффтоп, вы никогда не задумывались, откуда пошло, что символ ‘~’ в nix like системах обозначает home дирикторию? Посмотрите внимательно на клавиатуру выше, а именно на клавишу home ;)

Так вот, тут-то мы и приходим к модальному интерфейсу. Что же это значит? Это значит, что существует несколько режимов, в которых различные клавиши выполняют различные комманды. В vi было целых 2 режима: режим вставки, в котором пользователь мог набирать текст, и коммандный режим, в котором пользователь, посредством нажатия комбинаций клавиш, работает с текстом.

Вся прелесть данного текстового редактора была не только в его инновациях, но и в его открытости, что позволило в 1991 году Браму Мооленару создать первую версию vim, которая расшифровывается как Vi improved. В нем было очень много нового, например, визуальный режим, макросы, работа с кучей файлов одновременно, итеграция с ОС и так далее.

Так что же это все дает нам, обычным программистам? Во первых, мы можем приобщиться к истории, а если серьезно - ввиду того, что раньше не было мыши, то все комманды печатались на клавиатуре, и никакие Васи не тыкали мышкой в кнопочки на экране. Ну а так как тыкаться мышкой в интерфейсе дольше, чем использовать хоткеи, то мы получаем довольно сильный профит в скорости. Во вторых, интерфейс не перегружен излишней функциональностью, как во многих IDE или текстовых редакторах. Предлагаю вам сравнить скриншоты vim-а и какого-нибудь eclipse.

Vim Eclipse

Сказать по правде, я давно хотел спросить, вы часто тыкаете во все эти кнопки в cтроке наверху?

В третьих, вы получаете необычный, и оттого интересный, удобный, после привыкания, способ взаимодействия с текстом. Подумайте сами, вы взаимодействуете с текстом как с объектом, а не как с набором символов, что позволяет вам выбирать не просто последовательность символов, а целые слова, параграфы, линии, блоки текста и оперировать с ними как захотите.

Но для начала стоит вернуться к режимам. В vim-е их 4: нормальный, в котором вы посредством хоткеев выполняете операции над текстом; коммандный, в котором вы можете вводить комманды как в консоли(и, кстати, выполнять консольные комманды тоже); режим вставки, в котором вы можете набирать текст и оперировать им, как в обычном текстовом редакторе, а-ля sublime; ну и последний, визуальный, в котором вы можете выбирать текст для дальнейшего взаимодействия с ним. Стоит отметить, что визуальных режимов 3 - обычный, в котором вы выбираете текст посимвольно, строчный, в котором вы вы выбираете текст построчно, и блочный, в этом режиме вы задаете 2 точки прямоугольника, весь текст в котором выделяется и пригоден для взаимодействия. ( Сказать по правде, есть еще один режим, режим эмуляции ex редактора. Вызывается он с помощью Q, обязательно с шифтом, после чего вы попадаете в отдельное окно, в котором вы можете вводить комманды без префикса :. Чтобы выйти из этого режима достаточно набрать visual или vi. Для справки советую набрать :h Ex-mode в vim. )

Исходя из всего, что я написал выше - вам придется полностью изменить свое отношение к взаимодействию с текстом, что, в свою очередь, позволит вам увеличить вашу скорость редактирования/поиска текста. Скажу сразу, это непривычно, но с другой стороны, это как изучение нового языка программирования. Вы можете оставаться в собственной зоне комфорта, а можете сломать свой мозг, изучив какой-нибудь lisp или haskell. Тут то же самое, вы можете всю жизнь использовать привычный sublime, а можете попробовать что-то новое. Ну а в следующей записи мы более детально обсудим редактор и рассмотрим, как же он может облегчить нам жизнь.

Seed файл и вы

Совсем недавно, на работе, потребовалось мне заполнить новый проект данными для дальнейшего тестирования и разработки. Конечно же, данные должны быть в любом виде, и первое, о чем я подумал, был seed файл, поэтому сегодня мы поговорим именно о нем. Как всем известно, данный файл служит для генерации данных в рельсовых приложениях. Вы пишите скрипт, выполняете rake db:seed и радуетесь жизни. В моем случае данные были типовыми, а именно, нужно было сгенерировать пользователей, посты и комментарии к этим постам. Я думаю все прекрасно понимают, как все взаимосвязанно, поэтому на этом останавливаться не вижу особого смысла.

Обычная практика многих людей - задать одинаковые данные для всех типов данных и наплодить их с десяток. Смотрится это обычно как-то так:

user = {
  name:  'Jon'
  email: 'my@email.org'
  password: '12345678',
  password_confirmation: '12345678'  
  }

post = {
  title:  'My Post'
  body:   'My body'
  }

comment = { body: 'comment' }

10.times do
  my_user = User.create(user)
  my_post = my_user.create_post(post)
  my_post.create_comment(user, comment)
end

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

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

Для того, чтобы наш воображаемый мир существовал, нам, естественно, нужны пользователи. И наша цель - создать абсолютно разных пользователей, не похожих друг на друга. Конечно же, первое, что всплывает в голову - замечательный гем faker, который поможет нам генерировать произвольные имена и почтовые адресса для наших пользователей. Но при всем при этом, не будем забывать про нашего админа. Так же, давайте зададим рандомное количество записей в интервале от 18 до 25 штук (числа, как вы догадались, могут быть абсолютно любые):

user = {
  name:  admin
  email: admin@my_app.com
  password: '12345678',
  password_confirmation: '12345678'  
  }

rnd = Random.new
user_count = rnd.rand(18..23)
User.create(user)

user_count.times do
  user[:name]  = Faker::Name.name
  user[:email] = Faker::Internet.email  
  User.create(user)
end

Cобственно я уверен, faker поможет вам сгенерировать почти любую информацию, стоит только открыть доки. Ну а если вам не угодил этот гем, то существует достаточно много других data генераторов.

Не думаю, что тут что-то было сложно, поэтому пререйдем к постам. Сказать по правде, в нашем проекте посты состояли из строго заданных кусков html-a, поэтому тут ничего не оставалось, кроме как делать в лоб. Единственный момент, мы будем выбирать произвольно пользователя, чтобы от его имени создавать наш пост:

posts = [
  {
    title:  'My first Post'
    body:   'My body'
  },
  {
    title:  'My second Post'
    body:   'My body'    
  },
  # Еще какое-то количество данных для постов ...
]

def rnd_user(count, rnd)
  random_user_id = rnd.rand(1..(count))
  User.find(random_user_id)
end

posts.each do |post|
  rand_user = rnd_user user_count, rnd

  post[:user_id] = rand_user.id
  created_post = rand_user.create_post(post)
end

Настало время самого интересного и забавного, комментарии. В данном проекте мы использовали гем acts_as_commentable_with_threading. Он содержит 2ух уровневую структуру комментариев, поэтому работы нам немного прибавилось. Чтобы создать комментарий, нам необходимы 3 значения: пост, где будет этот комментарий, пользователь, оставивший комментарий, и непосредственно сам текст комментария. Смотрится все это примерно так:

post.build_comment(user_id, body)

Ну а для “подкомментария” нам так же необходимо знать родительский комментарий, от которого ветка и пойдет, т.е. создание подобного комментария будет выглядеть примерно так:

child_comment = post.build_comment(user_id, body)
child_comment.move_to_child_of(comment)

А теперь давайте создадим от 10 до 21 главных комментариев и до 9ти дочерних для каждого главного, при этом каждый комментарий будет оставлять рандомный пользователь:

posts.each do |post|
  rand_user = rnd_user user_count, rnd

  post[:user_id] = rand_user.id
  created_post = rand_user.create_post(post)

  rnd.rand(10..21).times do
    rand_user = rnd_user user_count, rnd
    comment = created_post.build_comment(rand_user.id, 'Comment body')
    comment.save!

    rnd.rand(9).times do
      rand_user = rnd_user user_count, rnd
      child_comment = created_post.build_comment(rand_user.id, 'Comment body')
      child_comment.save!
      child_comment.move_to_child_of(comment)
    end
  end
end

Хм, рандомное количество комментариев мы сделали, пользователей тоже разных назначили, но вот незадача, у нас body каждого комментария одно и тоже, а именно 'Comment body'.Что же делать и как нам быть? Раз уж мы договорились создать подобие “живого” приложения, то и комментарии у нас должны быть разные и тоже живые. Первое, что приходит в голову, - опять использовать массив данных, но я слишков ленив (да и не путь самурая это), чтобы все это набирать, пусть даже копипастить и тем более придумывать. Второе, что приходит на ум, генерировать рандомную строчку текста. Да, идея не плохая, как минимум, нам придется писать меньше кода, и он по-любому будет всегда разный. Но есть одно но: мы пытаемся достигнуть абсолютной правдоподобности, а строки вида 'skjafnskdjn ksajdnf' нам точно не подойдут как комментарии. Поэтому нам на помощь приходит отличное решение - гем raingrams.

Что же такого может этот гем, спросите Вы? На самом деле, ничего особенного, Вы просто скармливаете ему текст, а он, в свою очередь, разбивает его на куски и рандомно выдает обратно. В чем плюсы? Да, они не отличаются от банальной генерации строки, единственное и очевидное отличие - генерируемый текст будет логичен в пределах строки.

В документации достаточно подробно описано, как гем ставится и настраивается, но я бы хотел уделить внимание 2ум подводным камням, с которыми мы столкнулись:

  • Во первых, гем не поддерживает русский язык. Скажем так, он его не видит. Поэтому, если для Вас важен русский язык, используйте наш форк, в котором исправлен этот косяк.

  • Ну а второй момент, в старых версиях существовал метод train_with_url, в который передавалась ссылка, а он уже все парсил и выдавал конечный результат. К сожалению, в свежих версиях этот метод был убран, причем убран очень хитро. Если быть точным, то автор просто вырезал часть этого метода, а вторую забыл(а может, решил стебануться над простыми парнями как мы, этого я, к сожалению, не знаю :) ).

А теперь, используя полученные знания, перепишем наш метод. В качестве текста для raingrams мы будем использовать комментарии из пикабу, которые предварительно распарсим:

model = QuadgramModel.build do |model|
  doc = Nokogiri::HTML(open('http://pikabu.ru/story/v_den_programmista_pro_logiku_pikabu_685289'))
  doc.search('div.comment_desc').each do |div|
    model.train_with_text(div.inner_text)
    model.refresh
  end
end

# ....

posts.each do |post|
  rand_user = rnd_user user_count, rnd

  post[:user_id] = rand_user.id
  created_post = rand_user.create_post(post)

  rnd.rand(10..21).times do
    rand_user = rnd_user user_count, rnd
    comment = created_post.build_comment(rand_user.id, model.random_sentence)
    comment.save!

    rnd.rand(9).times do
      rand_user = rnd_user user_count, rnd
      child_comment = created_post.build_comment(rand_user.id, model.random_sentence)
      child_comment.save!
      child_comment.move_to_child_of(comment)
    end
  end
end

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

Выглядит здорово. Да, может, код не самый чистый, и в целом скрипт слишком часто обращается к базе, но согласитесь, наше творение имитирует реальную активность пользователей. Не идеально, конечно, но все же. Думаю, на этом можно было бы закончить рассказ, но остался последний момент, который хотелось бы осветить и исправить в нашем скрипте.

Как думаете, где еще нам придется создавать пользователей (и не только их), которых мы создали в самом начале? Правильно, в тестах, надо же на чем-то тестировать приложение. Так почему бы нам не убить 2ух зайцев и не заменить ручную генерацию, как это было в начале статьи, на старую добрую фабричную? Так как в нашем проекте мы используем гем fabrication, то и пример будет с ним. Вы также можете использовать любую другуюю фабрику, которая вам по вкусу.

Для начала определим нашего пользователя и администратора:

Fabricator(:user) do
  email Faker::Internet.email
  name Faker::Name.name
  password '12345678'
  password_confirmation '12345678'
end

Fabricator(:admin) do
  email 'admin@my_app.org'
  name 'admin'
  password '12345678'
  password_confirmation '12345678'
end

Ну а теперь, воспользуемся нашей новосозданной фабрикой для избавления от лишнего кода в seed файле:

user_count = rnd.rand(18..23)
Fabricate(:admin)

user_count.times { Fabricate(:user) }

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

На этом, пожалуй, я закончу наши эксперименты. Как видите, простора для фантазии осталось еще много и также осталось много идей для рефакторинга. В любом случае, данный пример явно показывает, что к любой, сколь скучной она не была бы, задаче всегда можно применить творческий подход и неплохо развлечься :)

IRB и все все все

Думаю, каждый, кто так или иначе связан с Ruby и тем более с Rails, хоть раз в жизни использовал irb. Возможности интерактивного ruby шела безграничны, поэтому сегодня я бы хотел поговорить об улучшении или кастомизации, если вам так угодно, вашего irb.

Но для начала давайте посмотрим документацию, а именно откроем модуль IRB в стандартной библиотеке ruby. “Что интересного тут есть?”, спросите вы? Думаю стоит начать с самого начала, с запуска. Если вы просто наберете в консоли irb, то запустите шел с вашей текущей версией руби. Но если у вас RVM(хотя, сказать по правде я не уверен, что это работает только с RVM), то вы можете выбрать среду для запуска из всех тех, что у вас установлены. Например вот так, я, при активном MRI 2.0, могу запустить у себя jruby:

jruby in irb

Дальше, думаю, следует обсудить ключи. Их много, они разные. В документации они все есть. Самые интересные, на мой взляд, - -d включающий дебаг мод(аналогично ruby -d) и ключ -I path, загружающий указанную директорию.

Ну а теперь самое интересное: поговорим о конфигурации вашего irb. Для этого необходимо создать *rc аналогичный .bashrc или .vimrc. Существует несколько разновидностей именования этого файла, например: .irbrc, irb.rc, _irbrc или же $irbrc.

Думаю, для начала, следует указать конфигурационные значения, например, увеличить количество сохраняемых команд и записывать их в отдельный файл:

IRB.conf[:SAVE_HISTORY] = 1000
IRB.conf[:HISTORY_FILE] = "#{ENV['HOME']}/.irb_history"

Так же можно указать кучу других настроек, но я бы не стал писать эту статью, если бы хотел рассказать только это. Все веселье начинается тогда, когда приходит понимание того, что данный файл является обычным *.rb файлом, который загружается при старте вашего irb.

Думаю, все догадались, что дальше будет? :)

Предлагаю написать, ради развлечения, метод, который возвращал бы все локальные методы объекта. Выглядеть он будет примерно так:

# .irbrc
class Object
  def local_methods
    (methods - Object.instance_methods).sort
  end
end

Как он будет работать, спросите вы? Да все просто, берете объект и вызываете на нем данный метод:

Using local_mathods method in irb

Думаю, вы заметили цвета, которых не хватает в дефолтном irb?

Да, все верно, так же можно подключать гемы, и да, есть гемы, которые добавляют ярких цветов в ваш irb, например, wirble. Достаточно написать такой код:

require 'rubygems'
require 'wirble'

Wirble.init
Wirble.colorize

И ваш интерактивный шел заиграет новыми красками :)

А если вам нравится pry, то вам никто не запрещает его добавить, в последующем вызывая его, просто набрав pry во время irb сессии:

#.irbrc
require 'pry'

# irb
2.0.0p247 :015 > pry
[1] pry(main)> 

Ну а выглядеть это будет как-то так: Using pry in irb

Но самое полезное, что можно сделать, это забыть построчный ввод кода. “Как?”, спросите вы? Начну издалека: мне очень нравится vim :) Поэтому, смотря vimcasts-ы, я узнал то, что перевернуло мой мир, а именно: любой текстовый редактор можно вызывать прямо из irb.

Делается это очень просто: нужно добавить гем interactive_editor. Данный гем позволяет вызывать любой текстовый редактор из вашего irb, например, набрав vim - откроется vim, где вы сможете набрать любой код, который выполнится после сохранения файла и закрытия редактора.

Using vim in irb

Собственно то же самое будет работать с sublime, textmate, emacs и дургими текстовыми редакторами.

Ну и на последок стоит рассказать про сессии в irb. Если вы используете vim или когда то использовали его, то вам знакомо такое понятие как буфер, которое чем-то похоже на сессии в irb. Ну а если нет, то краткая справка: сессия - некий сеанс интерактивного шела. Для того, что бы посмотреть список всех сессий в irb, достаточно набрать jobs:

2.0.0p247 :012 > jobs
 => #0->irb on main (#<Thread:0x007fcb810bcda0>: running) 

Ну а если вам надо создать новую сессию, то просто наберите irb:

2.0.0p247 :013 > irb
2.0.0p247 :001 > jobs
 => #0->irb on main (#<Thread:0x007fcb810bcda0>: stop)
#1->irb#1 on main (#<Thread:0x007fcb819b7bf8>: running) 

Как видите, irb создал новую сессию и переключился в нее. Но что делать, когда вы хотите удалить или изменить сессию? Для этого есть методы kill <number_session> и fg <number_session> соответственно:

2.0.0p247 :002 > jobs
 => #0->irb on main (#<Thread:0x007fcb810bcda0>: stop)
#1->irb#1 on main (#<Thread:0x007fcb819b7bf8>: stop)
#2->irb#2 on main (#<Thread:0x007fcb81319238>: running) 
2.0.0p247 :003 > fg 1
 => #<IRB::Irb: @context=#<IRB::Context:0x007fcb819b7a40>, @signal_status=:IN_EVAL, @scanner=#<RubyLex:0x007fcb819b74c8>> 
2.0.0p247 :009 > jobs
 => #0->irb on main (#<Thread:0x007fcb810bcda0>: stop)
#1->irb#1 on main (#<Thread:0x007fcb819b7bf8>: running)
#2->irb#2 on main (#<Thread:0x007fcb81319238>: stop) 
2.0.0p247 :010 > kill 2
 => [2] 
2.0.0p247 :011 > jobs
 => #0->irb on main (#<Thread:0x007fcb810bcda0>: stop)
#1->irb#1 on main (#<Thread:0x007fcb819b7bf8>: running) 
2.0.0p247 :012 > kill 1
 => #<IRB::Irb: @context=#<IRB::Context:0x007fcb8185b2c8>, @signal_status=:IN_EVAL, @scanner=#<RubyLex:0x007fcb8110d5e8>> 
2.0.0p247 :014 > jobs
 => #0->irb on main (#<Thread:0x007fcb810bcda0>: running) 

Ruby и special/predefined variables

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

Выглядел примерно так:

"test string in irb".match /irb/
puts $&
#=> "irb"

Так как до этого я не часто встречался с подобными “глобальными” переменными, пример заинтересовал меня и захотелось выяснить, что же это за переменные. Первым делом, я решил узнать, как они называются и где их можно найти. Спустя несколько минут, стало ясно, что это так называемые “special variables”. Не долго думая и открыв google, просмотрев пару тройку результатов, стало ясно, что кроме списка этих переменных ничего особо нет. Это не сильно меня обрадовало и стало понятно, что пришло время открыть репозитарий ruby и начать искать в нем. Так же мне очень помогла одна небезизвестная книга. Как оказалось, ruby создает несколько специальных переменных, в зависимости от откружения, в котором запускаются программы, или в зависимости от действий, которые были выполены ранее. Кстати, это не совсем глобальные перменные, в чем легко можно убедиться, рассмотрев простой пример:

def test
  "test string in irb".match /test/
  puts "$& in test method #{$&}"
end

"test string in irb".match /irb/
puts "$& in main #{$&}"
#=> "irb"

test
#=> "test"

Как видно из примера, в каждом scope (main и метода), “глобальная” переменная отличается. Любой адекватный человек спросит: как такое, тысяча чертей, возможно? На самом деле все довольно просто, но, для полного понимания, начать придется с основ. Как многие знают, начиная с верисии 1.9 в ruby была добавлена виртуальная машина или YARV или же yet another ruby virtual machine, называйте как хотите, суть одна и та же. Смысл в том, что каждый раз, при запуске, YARV так же создает особый стек, для локальных переменных. В этом стеке указываются абсолютно все локальные переменные, свои для каждого scope. Разделение scope-ов происходит с помощью специальной точки или указателя - environment point (далее EP). Так же, в стеке, перед каждой EP, создается специальная переменная svar, которая как раз и указывает на таблицу специальных символов. Именно из-за этого для каждого scope могут быть свои значения специальных символов, что мы видели в примере выше. Но самое интересное, что у обычного блока и у места, где он будет вызван, scope одинаковый, в чем можно легко убедиться благодаря такому примеру:

"test string in irb".match /irb/

1.times do
  "test string in irb".match /test/
  puts "$& in block #{$&}"
end

puts "$& in main #{$&}"

> ruby test.rb
>> "$& in block test"
>> "$& in block test"
>> "$& in main test"

На самом деле это логичное поведение, ибо замыкания никто не отменял. Как я уже говорил, таких переменных много, но расскажу я о самых интересных(естественно для себя):


$&

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

$1 $2 $3 …

Думаю, многим знакомая похожая переменная из регулярных выражений. Хотя, кого я обманываю? Это та же самая перменная, которая хранит совпадения из скобок:

"test string in irb".match /(irb)/
puts $1
#=> "irb"
$~

Содержит объект класса MatchData, соответствующий последнему совпадению.

"test string in irb".match /(irb)/
puts $~
#=> #<MatchData "irb" 1:"irb">
puts $~.to_s
#=> "irb"
puts $~.to_a
#=> ["irb", 'irb']
$+

Содержит значение последней круглой скобки из последнего совпадения:

"test string in irb".match /irb/
puts $~
#=> #<MatchData "irb" 1:"irb">
puts $+
#=> nil
"test in irb".match /(test) (in) (irb)/
puts $~
#=> #<MatchData "test in irb" 1:"test" 2:"in" 3:"irb">
puts $+
#=> "irb"
$`

Содержит все то, что не совпало в последнем регулярном выражении:

"test string in irb".match /irb/
puts $`
#=> "test string in"
$!

Содержит последнее вызванное исключение:

1 / 0 rescue $!
#=> #<ZeroDivisionError: divided by 0>
$@

Ну а эта переменная содержит массив со всеми trace stack-ами из последнего исключения:

1 / 0 rescue $@
#=> ["<main>:4:in `/'", "<main>:4:in `/'", "(irb):98:in `irb_binding'", ... ]
$*

Эта переменная равносильна переменной ARGV, думаю этим все сказанно.

$$

Переменная возвращает номер процесса, под которым выполняется скрипт.

$$
#=> 33630
puts `ps aux | grep irb`
#=> anton           33630   0.0  0.3  2470520  24084 s008  S+    2Jan14   0:01.24 irb

.


Так где же определены эти переменные в исходном коде ruby? Как оказалось, все не так сложно, как кажется. Определенны эти переменные в файле parse.y примерно на 7950-той строке (да да, файл не очень большой, всего 11.5к строк кода). Для тех, кто не в курсе, parse.y - грамматический файл интерпритатора, благодаря которому происходит разбиение написанного вами кода на токены (лексемы/указатели), которые в последующем преобразуются в AST структуру, а затем в YARV структуру, ну а дальше в машинный код, который в последующем и будет выполняется. Как не трудно заметить, case функция ищет совпадение символа “$” и специальных символов (блок case), после чего передает их функции set_yylval_name:

7965: case '~':                /* $~: match-data */
7966: case '*':                /* $*: argv */
7967: case '$':                /* $$: pid */
7968: case '?':                /* $?: last status */
7969: case '!':                /* $!: error string */
7970: case '@':                /* $@: error position */
7971: case '/':                /* $/: input record separator */
7972: case '\\':               /* $\: output record separator */
7973: case ';':                /* $;: field separator */
7974: case ',':                /* $,: output field separator */
7975: case '.':                /* $.: last read line number */
7976: case '=':                /* $=: ignorecase */
7977: case ':':                /* $:: load path */
7978: case '<':                /* $<: reading filename */
7979: case '>':                /* $>: default output handle */
7980: case '\"':                /* $": already loaded files */
7981:   tokadd('$');
7982:   tokadd(c);
7983:   goto gvar;

-------

7997: gvar:
7998: set_yylval_name(rb_intern3(tok(), tokidx, current_enc));
7999: return tGVAR

И в завершение, следует упомянуть особый файл - English.rb, в котором прописаны алиасы для специальных переменных, благодаря чему можно использовать данные переменные намного понятнее, нежели чем использование $$, $& и так далее:

"waterbuffalo" =~ /buff/
print $", $', $$, "\n"

# With English:

require "English"

"waterbuffalo" =~ /buff/
print $LOADED_FEATURES, $POSTMATCH, $PID, "\n"