Module: Yarsh::Multiline

Defined in:
lib/yarsh/multiline.rb

Overview

Multiline input support: decides when a line is syntactically incomplete (keep reading) and transforms a finished buffer so shell lines can live inside Ruby blocks/defs/classes: shell lines are rewritten into sh calls.

Defined Under Namespace

Classes: IdentifierSurround

Constant Summary collapse

INCOMPLETE_MESSAGES =
[
  /unexpected end-of-input/,
  /unterminated string/,
  /unterminated heredoc/,
  /unterminated regexp/,
  /can't find string ".*" anywhere before EOF/,
  /expected a `when` or `in` clause after `case`/,
  /expected an `end` to close the/
].freeze
STRUCTURAL_KEYWORDS =

Keywords that must never be treated as shell, even though some of them (break, next, return, times, ...) are also bash builtins.

%w[do end if else elsif when rescue ensure then begin
case class def module while until for unless
return break next yield].freeze
KEYWORD_REGEX =
Regexp.new("\\A(?:#{STRUCTURAL_KEYWORDS.join('|')})\\b")

Class Method Summary collapse

Class Method Details

.classify_line(line) ⇒ Object

:ruby - keep the line verbatim :shell - rewrite the line into a sh call :skip - drop the line (blank)



73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/yarsh/multiline.rb', line 73

def self.classify_line(line)
  stripped = line.strip
  return :skip if stripped.empty?
  # return :ruby if structural?(stripped)
  # return :shell if stripped.include?('-->') #FIXME: Should not be treated as a shell --> should allow multiline too
  # return :shell if shell_command && shell_command.call(stripped) #FIXME: pwd = Dir.pwd is evaluated as a shell command instead of a ruby
  # return :ruby if incomplete?(stripped)
  # return :ruby if compiles?(stripped)
  # #TODO: check what happend when it do not compile with another error message catched by incomplete? ?

  # :shell
  return :structural if structural?(stripped)

  # return :shell if !compiles?(stripped) && shell_command&.call(stripped)

  :yarsh
end

.compiles?(src) ⇒ Boolean

Returns:

  • (Boolean)


129
130
131
132
133
134
# File 'lib/yarsh/multiline.rb', line 129

def self.compiles?(src)
  RubyVM::InstructionSequence.compile(src)
  true
rescue SyntaxError
  false
end

.heredoc_delimiter(line) ⇒ Object



136
137
138
139
140
141
142
143
144
145
# File 'lib/yarsh/multiline.rb', line 136

def self.heredoc_delimiter(line)
  return nil if line.include?('-->')
  return nil unless line.include?('<<')

  m = line.match(/<<(-|~)?(['"])([^'"]+)\2(?:\s*\.\w+)?\s*\z/)
  return m[3] if m

  m = line.match(/<<(-|~)?([A-Za-z_]\w*)(?:\s*\.\w+)?\s*\z/)
  m && m[2]
end

.incomplete?(src) ⇒ Boolean

Returns:

  • (Boolean)


63
64
65
66
67
68
# File 'lib/yarsh/multiline.rb', line 63

def self.incomplete?(src)
  RubyVM::InstructionSequence.compile(src)
  false
rescue SyntaxError => e
  INCOMPLETE_MESSAGES.any? { |re| e.message =~ re }
end

.structural?(line) ⇒ Boolean

Returns:

  • (Boolean)


124
125
126
127
# File 'lib/yarsh/multiline.rb', line 124

def self.structural?(line)
  # line.strip.match?(KEYWORD_REGEX)
  Ripper.lex(line).any? { |token| STRUCTURAL_KEYWORDS.include?(token[2]) }
end

.transform(buffer, &shell_command) ⇒ Object



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
# File 'lib/yarsh/multiline.rb', line 91

def self.transform(buffer, &shell_command)
  heredoc_delim = nil
  is_structural = false
  print_binding = 'bind = binding'
  ident_surround = IdentifierSurround.new
  buffer.split("\n").map do |line|
    if heredoc_delim
      heredoc_delim = nil if line.strip == heredoc_delim
      next line
    end
    if (delim = heredoc_delimiter(line))
      heredoc_delim = delim
      next line
    end
    case classify_line(line, &shell_command)
    when :skip then nil
    when :structural
      next line if line.include? 'end'

      if is_structural
        line = ident_surround.surrond_with_bind(line, 'bind')
        print_binding = 'bind = binding + bind'
      end
      is_structural = true
      line + "\n" + print_binding
    when :shell then "__sh__(#{line.strip.inspect})"
    when :yarsh
      ident_surround.save_local_vars(line)
      "__yarsh__(#{line.strip.inspect}, #{is_structural ? 'bind' : 'nil'})"
    end
  end.join("\n")
end