Helpex - Trao đổi & giúp đỡ Đăng nhập

Cách triển khai Ruby Hash Giống như Ngữ pháp trong Parslet, Phần 1

Trước khi bạn có thể hiểu cách xây dựng trình phân tích cú pháp bằng cách sử dụng parslet, bạn cần hiểu lý do tại sao bạn có thể muốn. Trong trường hợp của tôi, tôi có một thư viện tên là rundoc , nó cho phép bất kỳ ai viết tài liệu có thể "chạy". Ví dụ: ai đó có thể viết tài liệu có cái này:

```
:::>> $ rails -v
```

Sau đó, trong đầu ra tài liệu của bạn, bạn sẽ nhận được kết quả này:

```
$ rails -v
Rails 5.2.0
```

Lưu ý: Nếu muốn, bạn có thể chuyển sang phần tiếp theo để xem hướng dẫn.

Mặc dù bề ngoài điều này có vẻ không ấn tượng lắm - nếu bạn có tài liệu cần được cập nhật thường xuyên, nó có thể tiết kiệm rất nhiều thời gian sao chép và dán kết quả đầu ra. Quan trọng hơn, nó có thể bắt lỗi và hồi quy mà bạn có thể không bắt được theo cách thủ công.

Tôi sử dụng thư viện này để giữ một số tài liệu trong trình phát triển Heroku, chẳng hạn như bài viết bắt đầu với Rails 5 . Khi một phiên bản mới của Rails hoặc Ruby được phát hành, tôi có thể cập nhật tài liệu, "chạy nó" và sau đó đảm bảo rằng đầu ra trong tài liệu khớp hoàn toàn với đầu ra mà một người nào đó sử dụng các lệnh tương tự sẽ nhận được. Nếu một lệnh không thành công, quá trình tạo không thành công, vì vậy tài liệu cũng hoạt động như một bài kiểm tra.

Lợi ích bổ sung khác của cách tiếp cận này là cho người đọc. Bằng cách đảm bảo tính nhất quán của đầu ra các lệnh, người đọc có thể phát hiện tốt hơn khi có điều gì đó không ổn. Tuy nhiên, về dự án của tôi là đủ, bài viết này là về việc xây dựng một trình phân tích cú pháp. Tại sao rundoccần parslet?

Khi tôi viết, rundoclần đầu tiên tôi thực hiện nó bằng cách sử dụng regexes. Họ bắt đầu đơn giản, nhưng sau đó ngày càng trở nên phức tạp hơn, đây là những gì họ trông như bây giờ:

INDENT_BLOCK       = '(?<before_indent>(^\s*$\n|\A)(^(?:[ ]{4}|\t))(?<indent_contents>.*)(?<after_indent>[^\s].*$\n?(?:(?:^\s*$\n?)*^(?:[ ]{4}|\t).*[^\s].*$\n?)*))'
GITHUB_BLOCK       = '^(?<fence>(?<fence_char>~|`){3,})\s*?(?<lang>\w+)?\s*?\n(?<contents>.*?)^\g<fence>\g<fence_char>*\s*?\n'
CODEBLOCK_REGEX    = /(#{GITHUB_BLOCK})/m
COMMAND_REGEX      = ->(keyword) {
                         /^#{keyword}(?<tag>(\s|=|-|>)?(=|-|>)?)\s*(?<command>(\S)+)\s+(?<statement>.*)$/
                        }

Điều này cho phép đăng ký một "lệnh", chẳng hạn như lệnh $này sau đó nhận một đầu vào, một chuỗi. Nó khá linh hoạt, nhưng gần đây tôi muốn cho phép một ngữ pháp phức tạp hơn, một cái gì đó sẽ cho phép tôi sử dụng các đối số từ khóa như cú pháp:

```
:::>> background.start(command: "rails server", name: "server")
```

Mặc dù tôi có thể đã triển khai điều này trong regex, nhưng có vẻ như tôi đã sử dụng sai công cụ cho công việc và tôi cần một trình phân tích cú pháp đầy đủ tính năng. Nhập Parslet.

Thay vì cố gắng viết một regex để thống trị tất cả, hãy parsletcho phép tôi xây dựng từng phần ngữ pháp của mình. Những phần nhỏ này sau đó sẽ khớp với nhau để tạo nên những ngữ pháp ngày càng phức tạp hơn. Đầu ra của bước phân tích cú pháp này là một "cây" cú pháp lồng nhau sâu sắc mà chúng ta sẽ thấy trong một chút.

Parslet sau đó có khái niệm về một "máy biến áp" có thể được sử dụng để biến các cây được phân tích cú pháp phức tạp thành bất kỳ loại mã Ruby nào, chẳng hạn như các đối tượng giá trị mà chúng ta muốn. Khi bạn kết hợp trình phân tích cú pháp và trình biến đổi, bạn có thể viết ngôn ngữ nhỏ của riêng mình với bất kỳ quy tắc cú pháp nào bạn muốn. Điều này là hoàn hảo cho một cái gì đó như rundoc.

Trong khi có các thư viện phân tích cú pháp khác trong Ruby, tôi đã chọn phân tích cú pháp phần lớn vì số lượng ví dụ và tài liệu mà nó có. Các trang web tài liệu là rất tốt, và có những ví dụ trực tiếp trong repo . Vấn đề nhỏ duy nhất của tôi là readme không được hiển thị trong đánh dấu trong GitHub , nhưng tôi có thể vượt qua nó.

Tôi cũng đã xem xét treetopviên đá quý, nhưng tôi không thể tiến bộ nhiều. Tôi đã điều tra raccthư viện, thật tuyệt nếu bạn quen thuộc với yacccú pháp (mà tôi thì không). Yacc là cú pháp mà Ruby sử dụng để triển khai ngữ pháp của riêng nó . Nếu bạn muốn đi xuống cái hố Rabbit đó, Aaron đã giới thiệu cuốn sách của O'Reilly trên Yacc, cuốn này có vẻ hay nhưng cần đọc nhiều hơn mức tôi sẵn sàng đưa vào dự án này.

Bây giờ bạn đã biết vấn đề của tôi và bộ công cụ của tôi, đã đến lúc hướng dẫn! Tôi viết bài này, không phải với tư cách là một chuyên gia về phân tích cú pháp (hoặc parsletthậm chí), mà thực sự để thuyết phục bản thân rằng tôi hiểu những gì tôi đã làm. Khi bạn có thể giải thích cách bạn đã làm điều gì đó với người khác, thì bạn thực sự hiểu nó. Không cần thêm quảng cáo, đây là hướng dẫn.

Hướng dẫn thiết lập

Trong hướng dẫn phân tích cú pháp này, chúng tôi sẽ xây dựng một ngữ pháp có thể đọc trong một băm kiểu Ruby 1.9 với các khóa ký hiệu và giá trị chuỗi. Một cái gì đó như thế này:

{hello: "world", iam: "Schneems"}

Đó có vẻ là một mục tiêu dễ dàng, nhưng như Caral Sagan đã từng nói, nếu bạn muốn tạo ra một chiếc bánh quy bơ từ đầu, trước tiên bạn sẽ phải phát minh ra vũ trụ.

Làm thế nào để điều này liên quan đến rundoc? Hãy nhớ cách tôi muốn cú pháp phù hợp với các args từ khóa? Tôi sẽ phải triển khai cùng một cú pháp đó để cú pháp chữ băm của Ruby hoạt động trong Parslet.

Tôi thường không phát triển bằng cách sử dụng các phương pháp TDD thuần túy (tức là viết bài kiểm tra trước, sau đó vượt qua), nhưng tôi nhận thấy khi triển khai ngữ pháp, việc viết bài kiểm tra và sau đó thực hiện nó lần thứ hai sẽ dễ dàng hơn nhiều.

Để đạt được điều đó, tôi đã thực hiện một bài kiểm tra thất bại và mã của tôi tất cả trong một tệp:

require 'parslet'

class MyParser < Parslet::Parser
end

class MyTransformer < Parslet::Transform
end

require 'minitest/autorun'
require 'parslet/convenience'

class MyParserTest < Minitest::Test
  def test_parses_a_comma
    input = %Q{,}
    parser = MyParser.new.comma
    tree = parser.parse_with_debug(input)
    refute_equal nil, tree
  end
end

Tôi có thể chạy trực tiếp tệp này bằng cách sử dụng $ ruby example.rbvà xem các bài kiểm tra của tôi có vượt qua hay không.

Bài kiểm tra thất bại đầu tiên của tôi

Nếu bạn muốn xem thành phẩm trước, bạn có thể chuyển đến mã nguồn để đến ứng dụng phân tích cú pháp mẫu của tôi .

Trong đoạn mã trên, commaphương thức trên MyParser.newđề cập đến một "quy tắc" trong phân tích cú pháp mà tôi chưa triển khai. Thử nghiệm này không thành công với:

NoMethodError: undefined method `comma' for #<MyParser:0x00007ff5dc074d18>
    example.rb:14:in `test_parses_a_comma'

Trong parslet, bạn thêm quy tắc bằng cách sử dụng ruletừ khóa, đặt tên ký hiệu cho quy tắc, sau đó xác định quy tắc bên trong một khối. Đây là quy tắc khớp với dấu phẩy của chúng tôi:

class MyParser < Parslet::Parser
  rule(:comma) { str(",") }
end

Bên trong khối, từ khóa strđề cập đến một đối sánh chuỗi theo nghĩa đen. Trong trường hợp này, chúng tôi chỉ chuyển bằng một dấu phẩy duy nhất, vì vậy nó khớp với dấu phẩy trong chuỗi đầu vào của chúng tôi. Khá đơn giản. Các bài kiểm tra hiện đã vượt qua.

Khi xây dựng ngữ pháp, tôi thấy dễ nhất là bắt đầu với những phần rất nhỏ và xây dựng từ đó. Chúng ta có thể sử dụng lại những phần nhỏ đó để giúp xây dựng những ngữ pháp phức tạp hơn.

Mặc dù điều này khớp với một dấu phẩy duy nhất, nó sẽ không khớp nếu chúng tôi thêm một bài kiểm tra để có khoảng trắng xung quanh nó:

def test_parses_a_comma_with_spaces
  input = %Q{ , }
  parser = MyParser.new.comma
  tree = parser.parse_with_debug(input)
  refute_equal nil, tree
end

Bây giờ kiểm tra không thành công:

Expected ",", but got " " at line 1 char 1.
F

Finished in 0.001192s, 838.9262 runs/s, 838.9262 assertions/s.

  1) Failure:
MyParserTest#test_parses_a_comma_with_spaces [example.rb:19]:
Expected nil to not be equal to nil.

Làm cách nào chúng ta có thể cập nhật ngữ pháp để cho phép khoảng trắng? Ngoài ra str, cũng có một matchtừ khóa sẽ đối sánh qua regex. Một không gian có thể được khớp như thế này:

rule(:spaces) { match('\s').repeat(1) }

Ở đây regex \ssẽ khớp với bất kỳ ký tự khoảng trắng nào. Cuộc gọi đếnrepeat(1) says that it must be repeated at least once, but has no upper bound.

This means it will match `(một dấu cách) và` `(6 dấu cách) nhưng không phải là` `(không có dấu cách).

Mặc dù đây là một quy tắc hữu ích, chúng tôi cũng muốn kết hợp trường hợp chúng tôi không có khoảng trắng ngoài trường hợp chúng tôi có. Để thực hiện điều đó, chúng ta có thể thêm một spaces?quy tắc, quy tắc đó sử dụng spacesquy tắc và thêm vào đó:

 rule(:spaces?) { spaces.maybe }

Bên trong khối, lệnh gọi spacessử dụng quy tắc đã xác định trước đó của chúng tôi. Các maybephương pháp được cung cấp bởi parslet và chỉ ra rằng nếu nó phù hợp với spacesquy định, tuyệt vời. Ngoài ra nếu nó không phù hợp với quy tắc đó, cũng không sao.

Chúng ta có thể kết hợp tất cả những điều này lại với nhau để giúp các bài kiểm tra của chúng ta vượt qua bằng cách cập nhật commaquy tắc, đây là toàn bộ nội dung:

class MyParser < Parslet::Parser
  rule(:spaces)  { match('\s').repeat(1) }
  rule(:spaces?) { spaces.maybe }
  rule(:comma)   { spaces? >> str(',') >> spaces? }
end

Trong khi chúng tôi hiểu spaces?str(','), >>nhà điều hành này đang làm gì? Tôi không biết thuật ngữ của nó, nhưng tôi thầm đặt tên cho nó là "và sau đó." Tôi đọc quy tắc này là "Khớp các khoảng trắng (nếu có), sau đó khớp rõ ràng một chuỗi ',' và sau đó khớp với các khoảng trắng (nếu có)". Bây giờ chúng ta có một quy tắc, hãy đảm bảo rằng các bài kiểm tra của chúng ta vượt qua:

Finished in 0.001970s, 1015.2284 runs/s, 1015.2284 assertions/s.

2 runs, 2 assertions, 0 failures, 0 errors, 0 skips

Tuyệt quá!

Bây giờ chúng ta đã có một dấu phẩy, ngôn ngữ của chúng ta có thể sử dụng những gì khác? Làm thế nào về một chuỗi.

Đây là bài kiểm tra:

  def test_parses_a_string
    input = %Q{"hello world"}
    parser = MyParser.new.string
    tree = parser.parse_with_debug(input)
    refute_equal nil, tree
  end

Nó không thành công, làm thế nào nó sẽ vượt qua? Trước tiên, hãy xem cấu trúc của chuỗi của chúng ta. Nó có một ký tự trích dẫn "sau đó nó có các ký tự khác, những ký tự đó có thể được lặp lại và chúng không thể bao gồm một trích dẫn khác. Sau đó, cuối cùng, chuỗi kết thúc bằng một câu trích dẫn khác ". Điều này được biểu diễn như thế nào trong cú pháp phân tích cú pháp?

rule(:string) {
  str('"') >> (
    str('"').absent? >> any
  ).repeat.as(:string) >> str('"')
}

Bạn đã nhìn thấy tất cả mọi thứ ở đây trước, ngoại trừ absent?, anyas. Các absent?kiểm tra phương pháp cho việc thiếu của nhân vật đó. Các anytừ khóa sẽ phù hợp, tốt, bất cứ điều gì. Ký anytự được viết tắt cho match('.'). Sự kết hợp của str("'").absent? >> anylà kiểm tra từng ký tự để xem nó không chứa a "và sau đó nó sẽ khớp với bất kỳ ký tự nào khác.

Làm gì as? Đây là cách chúng ta nói với parslet rằng chúng ta đang xử lý một phần quan trọng trong ngữ pháp của mình. Mặc dù tôi không nhất thiết phải biết có bao nhiêu dấu cách xung quanh dấu phẩy, nhưng tôi có thể muốn biết nội dung của một chuỗi. Đó là lý do tại sao tôi đã thêm as(:string).

Khi bạn chạy các bài kiểm tra, bạn sẽ thấy rằng nó vượt qua. Tuy nhiên, tôi muốn tiến thêm một bước nữa và thực sự xác minh định dạng của cây đã phân tích cú pháp (thay vì nói "not nil"). Để làm điều đó, tôi sẽ thay đổi bài kiểm tra:

  def test_parses_a_string
    input = %Q{"hello world"}
    parser = MyParser.new.string
    tree = parser.parse_with_debug(input)
    expected = {string: "hello world"}
    assert_equal expected, tree
  end

Trong parslet, mỗi mã astạo ra một hàm băm (và có thể là một Mảng). Khi chúng ta tiếp tục, bạn sẽ thấy rằng chúng sẽ được lồng vào nhau rất sâu. Trong khi trình phân tích cú pháp xây dựng một cây, một "máy biến áp" lấy một cây làm đầu vào và đơn giản hóa nó để làm cho nó nhỏ hơn.

Để hiểu, chúng tôi sẽ cố gắng thêm ngữ pháp cho hàm băm của Ruby. Cuối cùng, chúng tôi muốn phân tích cú pháp  {hello: "world" }. Tuy nhiên, để bắt đầu, chúng ta có thể so khớp một tập hợp con của cái này, chỉ là phần bên trong hello: "world". Điều này cũng tương tự với args từ khóa.

Trước khi bắt đầu, chúng ta hãy suy nghĩ về cú pháp của điều này. Khóa là bất kỳ ký tự nào không phải là một :hoặc một khoảng trắng, theo sau là một :ký tự. Giá trị là một chuỗi, nhưng trong tương lai, nó có thể là một số hoặc một mảng. Chúng ta cũng có thể có nhiều khóa và nhiều giá trị được phân tách bằng dấu phẩy.

Đây là một tính năng phức tạp, hãy bắt đầu với các phần nhỏ hơn và hướng tới phần lớn hơn. Tôi đã đề cập rằng một giá trị có thể là những thứ khác với một chuỗi, để cho phép điều này sau này, chúng ta có thể thêm một valuequy tắc:

rule(:value) { string }

Nếu tồn tại quy tắc "số" thì chúng ta có thể sử dụng |toán tử để chỉ định rằng giá trị có thể là string | number(chuỗi hoặc số). Chúng tôi sẽ không làm điều đó ở đây, nhưng biết đó là lý do tại sao tôi đã đặt trước một valuequy tắc, ngay cả khi nó không giống như nó đang làm gì cả.

Tiếp theo, tôi muốn một keytrình phân tích cú pháp. Đây là một bài kiểm tra:

def test_key
  input = %Q{ hello: }
  parser = MyParser.new.key
  tree = parser.parse_with_debug(input)
  refute_equal nil, tree
end

Quy tắc chính cần cho phép một khoảng trắng ở đầu và cuối. Bạn sẽ nhớ rằng khóa là sự lặp lại của các ký tự không :hoặc dấu cách và kết thúc bằng dấu hai chấm  :,. Đây là những gì tôi nghĩ ra:

rule(:key) {
  spaces? >> (
    str(':').absent? >> match('\s').absent? >> any
  ).repeat.as(:key) >> str(':') >> spaces?
}

Điều này giúp các bài kiểm tra vượt qua. Lưu ý rằng vì chúng tôi sẽ quan tâm đến nội dung của key, nó được đặt tên ở đây bằng cách sử dụng astừ khóa.

Một hàm băm có thể có các cặp khóa-giá trị lặp lại. Trước khi chúng tôi khớp toàn bộ một chuỗi, hãy nhóm một khóa và một giá trị lại với nhau. Bắt đầu với một bài kiểm tra:

def test_parses_a_key_value_pair
  input = %Q{ hello: "world" }
  parser = MyParser.new.key_value
  tree = parser.parse_with_debug(input)
  refute_equal nil, tree
end

Nó không thành công, hãy làm cho nó vượt qua với một key_valuequy tắc:

rule(:key_value) {
  (
    key >> value.as(:val)
  ).as(:key_value) >> spaces?
}

Chúng tôi thực sự gần hoàn thành ngữ pháp của mình, nhưng trước khi chúng tôi làm, tôi muốn xem xét kết quả đầu ra của cây cho khóa / giá trị này. Nó trông như thế này:

{ :key_value => { :key => "hello", :val => { :string => "world" }}}

Hàm băm này phần nào có ý nghĩa đối với tôi. Chúng tôi có một khóa cấp cao nhất key_valuevà khóa đó trỏ đến một hàm băm khác. Hàm băm này có một keykhóa trỏ đến một "hello"chuỗi và một valkhóa trỏ đến một hàm băm khác  {:string=>"world"},. Mặc dù nó không quá phức tạp, nhưng chúng ta có thể làm cho kết quả của việc này đơn giản hơn bằng cách sử dụng một máy biến áp.

Đó là tất cả cho bài đăng hôm nay, hãy điều chỉnh trong ngày mai khi chúng ta sẽ thảo luận về máy biến áp, các quy tắc lặp lại và tạo cấu trúc dữ liệu thực sự đầu tiên của chúng ta. 

17 hữu ích 0 bình luận 6.0k xem chia sẻ

Có thể bạn quan tâm

loading