jgrep-1.5.4/0000755000175000017500000000000013770044645012502 5ustar gabstergabsterjgrep-1.5.4/jgrep.gemspec0000644000175000017500000000271213770044645015160 0ustar gabstergabster######################################################### # This file has been automatically generated by gem2tgz # ######################################################### # -*- encoding: utf-8 -*- # stub: jgrep 1.5.4 ruby lib Gem::Specification.new do |s| s.name = "jgrep".freeze s.version = "1.5.4" s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version= s.require_paths = ["lib".freeze] s.authors = ["P Loubser".freeze, "Dominic Cleal".freeze, "R.I. Pienaar".freeze] s.date = "2020-09-10" s.description = "Compare a list of json documents to a simple logical language and returns matches as output".freeze s.email = ["ploubser@gmail.com".freeze, "dominic@cleal.org".freeze, "rip@devco.net".freeze] s.executables = ["jgrep".freeze] s.extra_rdoc_files = ["CHANGELOG.markdown".freeze, "README.markdown".freeze] s.files = ["CHANGELOG.markdown".freeze, "COPYING".freeze, "README.markdown".freeze, "Rakefile".freeze, "bin/jgrep".freeze, "lib/jgrep.rb".freeze, "lib/parser/parser.rb".freeze, "lib/parser/scanner.rb".freeze, "spec/Rakefile".freeze, "spec/spec_helper.rb".freeze, "spec/unit/jgrep_spec.rb".freeze, "spec/unit/parser_spec.rb".freeze, "spec/unit/scanner_spec.rb".freeze] s.homepage = "https://github.com/ploubser/JSON-Grep".freeze s.licenses = ["Apache-2.0".freeze] s.rubygems_version = "3.2.0.rc.2".freeze s.summary = "Filter JSON documents with a simple logical language".freeze end jgrep-1.5.4/spec/0000755000175000017500000000000013770044645013434 5ustar gabstergabsterjgrep-1.5.4/spec/unit/0000755000175000017500000000000013770044645014413 5ustar gabstergabsterjgrep-1.5.4/spec/unit/scanner_spec.rb0000644000175000017500000000527313770044645017412 0ustar gabstergabsterrequire File.dirname(__FILE__) + "/../spec_helper" module JGrep describe Scanner do describe "#get_token" do it "should identify a '(' token" do scanner = Scanner.new("(") token = scanner.get_token expect(token).to eq(["(", "("]) end it "should identify a ')' token" do scanner = Scanner.new(")") token = scanner.get_token expect(token).to eq([")", ")"]) end it "should identify an 'and' token" do scanner = Scanner.new("and ") token = scanner.get_token expect(token).to eq(%w[and and]) end it "should identify a '&&' token" do scanner = Scanner.new("&& ") token = scanner.get_token expect(token).to eq(%w[and and]) end it "should identify an 'or' token" do scanner = Scanner.new("or ") token = scanner.get_token expect(token).to eq(%w[or or]) end it "should identify a " || " token" do scanner = Scanner.new("|| ") token = scanner.get_token expect(token).to eq(%w[or or]) end it "should identify an 'not' token" do scanner = Scanner.new("not ") token = scanner.get_token expect(token).to eq(%w[not not]) end it "should identify an '!' token" do scanner = Scanner.new("!") token = scanner.get_token expect(token).to eq(%w[not not]) end it "should identify a statement token" do scanner = Scanner.new("foo.bar=bar") token = scanner.get_token expect(token).to eq(["statement", "foo.bar=bar"]) end it "should identify a statement token with escaped parentheses" do scanner = Scanner.new("foo.bar=/baz\\(gronk\\)quux/") token = scanner.get_token expect(token).to eq(["statement", "foo.bar=/baz\\(gronk\\)quux/"]) end it "should identify a complex array statement" do scanner = Scanner.new("[foo=bar and bar=foo]") token = scanner.get_token expect(token).to eq(["statement", [["statement", "foo=bar"], %w[and and], ["statement", "bar=foo"]]]) end it "should fail if expression terminates with 'and'" do scanner = Scanner.new("and") expect do scanner.get_token end.to raise_error("Class name cannot be 'and', 'or', 'not'. Found 'and'") end it "should identify a '+' token" do scanner = Scanner.new("+foo") token = scanner.get_token expect(token).to eq(["+", "foo"]) end it "should identify a '-' token" do scanner = Scanner.new("-foo") token = scanner.get_token expect(token).to eq(["-", "foo"]) end end end end jgrep-1.5.4/spec/unit/parser_spec.rb0000644000175000017500000001204413770044645017247 0ustar gabstergabsterrequire File.dirname(__FILE__) + "/../spec_helper" module JGrep describe Parser do describe "#parse" do it "should parse statements seperated by '='" do parser = Parser.new("foo.bar=bar") expect(parser.execution_stack).to eq([{"statement" => "foo.bar=bar"}]) end it "should parse statements seperated by '<'" do parser = Parser.new("foo.bar<1") expect(parser.execution_stack).to eq([{"statement" => "foo.bar<1"}]) end it "should parse statements seperated by '>'" do parser = Parser.new("foo.bar>1") expect(parser.execution_stack).to eq([{"statement" => "foo.bar>1"}]) end it "should parse statements seperated by '<='" do parser = Parser.new("foo.bar<=1") expect(parser.execution_stack).to eq([{"statement" => "foo.bar<=1"}]) end it "should parse statements seperated by '>='" do parser = Parser.new("foo.bar>=1") expect(parser.execution_stack).to eq([{"statement" => "foo.bar>=1"}]) end it "should parse statement sperated by '!='" do parser = Parser.new("foo.bar!=1") expect(parser.execution_stack).to eq([{"not" => "not"}, {"statement" => "foo.bar=1"}]) end it "should parse a + token" do parser = Parser.new("+foo") expect(parser.execution_stack).to eq([{"+" => "foo"}]) end it "should parse a - token" do parser = Parser.new("-foo") expect(parser.execution_stack).to eq([{"-" => "foo"}]) end it "should parse a correct 'and' token" do parser = Parser.new("foo.bar=123 and bar.foo=321") expect(parser.execution_stack).to eq([{"statement" => "foo.bar=123"}, {"and" => "and"}, {"statement" => "bar.foo=321"}]) end it "should not parse an incorrect and token" do expect do Parser.new("and foo.bar=1") end.to raise_error("Error at column 12. \n Expression cannot start with 'and'") end it "should parse a correct 'or' token" do parser = Parser.new("foo.bar=1 or bar.foo=1") expect(parser.execution_stack).to eq([{"statement" => "foo.bar=1"}, {"or" => "or"}, {"statement" => "bar.foo=1"}]) end it "should not parse an incorrect and token" do expect do Parser.new("or foo.bar=1") end.to raise_error("Error at column 11. \n Expression cannot start with 'or'") end it "should parse a correct 'not' token" do parser = Parser.new("! bar.foo=1") expect(parser.execution_stack).to eq([{"not" => "not"}, {"statement" => "bar.foo=1"}]) parser = Parser.new("not bar.foo=1") expect(parser.execution_stack).to eq([{"not" => "not"}, {"statement" => "bar.foo=1"}]) end it "should not parse an incorrect 'not' token" do expect do Parser.new("foo.bar=1 !") end.to raise_error("Error at column 10. \nExpected 'and', 'or', ')'. Found 'not'") end it "should parse correct parentheses" do parser = Parser.new("(foo.bar=1)") expect(parser.execution_stack).to eq([{"(" => "("}, {"statement" => "foo.bar=1"}, {")" => ")"}]) end it "should fail on incorrect parentheses" do expect do Parser.new(")foo.bar=1(") end.to raise_error("Error. Missing parentheses '('.") end it "should fail on missing parentheses" do expect do Parser.new("(foo.bar=1") end.to raise_error("Error. Missing parentheses ')'.") end it "should parse correctly formatted compound statements" do parser = Parser.new("(foo.bar=1 or foo.rab=1) and (bar.foo=1)") expect(parser.execution_stack).to eq([{"(" => "("}, {"statement" => "foo.bar=1"}, {"or" => "or"}, {"statement" => "foo.rab=1"}, {")" => ")"}, {"and" => "and"}, {"(" => "("}, {"statement" => "bar.foo=1"}, {")" => ")"}]) end it "should parse complex array statements" do parser = Parser.new("[foo.bar=1]") expect(parser.execution_stack).to eq([{"statement" => [["statement", "foo.bar=1"]]}]) end it "should not parse failed complex array statements" do expect do Parser.new("[foo.bar=1 or]") end.to raise_error("Class name cannot be 'and', 'or', 'not'. Found 'or'") end it "should not allow nested complex array statements" do expect do Parser.new("[foo.bar=1 and [foo.bar=1]]") end.to raise_error("Error at column 27\nError, cannot define '[' in a '[...]' block.") end it "should parse complex, compound array statements" do parser = Parser.new("[foo.bar=1 and foo.rab=2] and !(foo=1)") expect(parser.execution_stack).to eq( [ {"statement" => [["statement", "foo.bar=1"], %w[and and], ["statement", "foo.rab=2"]]}, {"and" => "and"}, {"not" => "not"}, {"(" => "("}, {"statement" => "foo=1"}, {")" => ")"} ] ) end end end end jgrep-1.5.4/spec/unit/jgrep_spec.rb0000644000175000017500000002117013770044645017062 0ustar gabstergabsterrequire File.dirname(__FILE__) + "/../spec_helper" module JGrep describe JGrep do describe "#validate_expression" do it "should be true for valid expressions" do expect(JGrep.validate_expression("bob=true")).to be(true) end it "should return errors for invalid ones" do expect(JGrep.validate_expression("something that is invalid")).to start_with("Error") end end describe "#jgrep" do it "should return a valid json document" do result = JGrep.jgrep("[{\"foo\":1}]", "foo=1") expect(result).to eq([{"foo" => 1}]) end it "should fail on an invalid json document" do STDERR.expects(:puts).with("Error. Invalid JSON given") JGrep.jgrep("[foo:]", "foo=1") end it "should return '[]' if value is not present in document" do result = JGrep.jgrep("[{\"bar\":1}]", "foo=1") expect(result).to eq([]) end it "should correctly return 'null' if a null value is present in the document" do result = JGrep.jgrep("[{\"foo\":null}]", "foo=null") expect(result).to eq([{"foo" => nil}]) end it "should return the origional json document if no expression is given" do result = JGrep.jgrep("[{\"foo\":\"bar\"}]", "") expect(result).to eq([{"foo" => "bar"}]) end it "should filter on the origional json document if not expression is given and a filter is given" do result = JGrep.jgrep("[{\"foo\":\"bar\"}]", "", "foo") expect(result).to eq(["bar"]) end it "should support starting from a subdocument" do doc = ' {"results": [ {"foo":"bar"}, {"foo":"baz"} ] } ' JGrep.verbose_on results = JGrep.jgrep(doc, "foo=bar", nil, "results") expect(results).to eq([{"foo" => "bar"}]) end end describe "#format" do it "should correctly format integers" do result1, result2 = JGrep.format("1", 1) expect(result1.is_a?(Integer)).to eq(true) expect(result2.is_a?(Integer)).to eq(true) end it "should correctly format floating point numbers" do result1, result2 = JGrep.format("1.1", 1.1) expect(result1.is_a?(Float)).to eq(true) expect(result2.is_a?(Float)).to eq(true) end it "should not format strings" do result1, result2 = JGrep.format("foo", "bar") expect(result1.is_a?(String)).to eq(true) expect(result2.is_a?(String)).to eq(true) end it 'should not format strings with a single [^\d\.] character' do result1, result2 = JGrep.format("2012R2", "2008R2") expect(result1).to be_a(String) expect(result2).to be_a(String) end end describe "#has_object?" do it "should compare on a '=' operator" do result = JGrep.has_object?({"foo" => 1}, "foo=1") expect(result).to eq(true) end it "should compare on a '<=' operator" do result = JGrep.has_object?({"foo" => 1}, "foo<=0") expect(result).to eq(false) end it "should compare on a '>=' operator" do result = JGrep.has_object?({"foo" => 1}, "foo>=0") expect(result).to eq(true) end it "should compare on a '<' operator" do result = JGrep.has_object?({"foo" => 1}, "foo<1") expect(result).to eq(false) end it "should compare on a '>' operator" do result = JGrep.has_object?({"foo" => 1}, "foo>0") expect(result).to eq(true) end it "should compare based on regular expression" do result = JGrep.has_object?({"foo" => "bar"}, "foo=/ba/") expect(result).to eq(true) end it "should compare true booleans" do result = JGrep.has_object?({"foo" => true}, "foo=true") expect(result).to eq(true) result = JGrep.has_object?({"foo" => false}, "foo=true") expect(result).to eq(false) end it "should compare true booleans" do result = JGrep.has_object?({"foo" => false}, "foo=false") expect(result).to eq(true) result = JGrep.has_object?({"foo" => true}, "foo=false") expect(result).to eq(false) end end describe "#is_object_in_array?" do it "should return true if key=value is present in array" do result = JGrep.is_object_in_array?([{"foo" => 1}, {"foo" => 0}], "foo=1") expect(result).to eq(true) end it "should return false if key=value is not present in array" do result = JGrep.is_object_in_array?([{"foo" => 1}, {"foo" => 0}], "foo=2") expect(result).to eq(false) end end describe "#has_complex?" do it "should return true if complex statement is present in an array" do result = JGrep.has_complex?({"foo" => ["bar" => 1]}, [["statement", "foo.bar=1"]]) expect(result).to eq(true) end it "should return false if complex statement is not present in an array" do result = JGrep.has_complex?({"foo" => ["bar" => 1]}, [["statement", "foo.bar=0"]]) expect(result).to eq(false) end end describe "#eval_statement" do it "should return true if if document matches logical expression" do result = JGrep.eval_statement({"foo" => 1, "bar" => 1}, [{"statement" => "foo=1"}, {"and" => "and"}, {"statement" => "bar=1"}]) expect(result).to eq(true) end it "should return true if if document matches logical expression array" do result = JGrep.eval_statement({"foo" => ["bar" => 1]}, [{"statement" => [["statement", "foo.bar=1"]]}]) expect(result).to eq(true) end it "should return false if if document doesn't match logical expression" do result = JGrep.eval_statement({"foo" => 1, "bar" => 1}, [{"statement" => "foo=0"}, {"and" => "and"}, {"statement" => "bar=1"}]) expect(result).to eq(false) end end describe "#filter_json" do it "should return the correct values if there is a single filter" do result = JGrep.filter_json([{"foo" => 1, "bar" => 1}], "foo") expect(result).to eq([1]) end it "should return the correct values if there are multiple filters" do result = JGrep.filter_json([{"foo" => 1, "foo1" => 1, "foo2" => 1}], %w[foo2 foo1]) expect(result).to eq([{"foo2" => 1, "foo1" => 1}]) end it "should return an empty set if the filter has not been found and there is only 1 filter" do result = JGrep.filter_json([{"foo" => 1}], "bar") expect(result).to eq([]) end it "should not return a structure containing a key if that key is not specified in the document" do result = JGrep.filter_json([{"foo" => 1}], %w[foo bar]) expect(result).to eq([{"foo" => 1}]) end end describe "#validate_filters" do it "should validate correct single filter" do result = JGrep.validate_filters("foo") expect(result).to be_nil end it "should not validate if a single filter contains an invalid field" do expect do JGrep.validate_filters("and") end.to raise_error "Invalid field for -s filter : 'and'" end it "should correctly validate an array of filters" do result = JGrep.validate_filters(%w[foo bar]) expect(result).to be_nil end it "should not validate if an array of filters contain an illegal filter" do expect do JGrep.validate_filters(%w[foo or]) end.to raise_error "Invalid field for -s filter : 'or'" end end describe "#dig_path" do it "should return the correct key value for a hash" do result = JGrep.dig_path({"foo" => 1}, "foo") expect(result).to eq(1) end it "should return the correct value for any value that is not a hash or an array" do result = JGrep.dig_path(1, "foo") expect(result).to eq(1) end it "should return the correct value for a subvalue in an array" do result = JGrep.dig_path([{"foo" => 1}, {"foo" => 2}], "foo") expect(result).to eq([1, 2]) end it "should return the correct value if a wildcard is specified" do result = JGrep.dig_path([{"foo" => {"bar" => 1}}], "foo.*") expect(result).to eq([[{"bar" => 1}]]) end it "should return the correct value if the path contains a dot seperated key" do result = JGrep.dig_path({"foo.bar" => 1}, "foo.bar") expect(result).to eq(1) result = JGrep.dig_path({"foo" => {"foo.bar" => 1}}, "foo.foo.bar") expect(result).to eq(1) end end end end jgrep-1.5.4/spec/spec_helper.rb0000644000175000017500000000026513770044645016255 0ustar gabstergabsterrequire "rubygems" require "rspec" require "rspec/mocks" require "mocha" require File.dirname(__FILE__) + "/../lib/jgrep" RSpec.configure do |config| config.mock_with :mocha end jgrep-1.5.4/spec/Rakefile0000644000175000017500000000036113770044645015101 0ustar gabstergabsterrequire "spec_helper.rb" require "rake" require "rspec/core/rake_task" desc "Run JGrep tests" RSpec::Core::RakeTask.new(:test) do |t| t.pattern = "unit/*_spec.rb" t.rspec_opts = "--format s --color --backtrace" end task default: :test jgrep-1.5.4/lib/0000755000175000017500000000000013770044645013250 5ustar gabstergabsterjgrep-1.5.4/lib/parser/0000755000175000017500000000000013770044645014544 5ustar gabstergabsterjgrep-1.5.4/lib/parser/scanner.rb0000644000175000017500000001025713770044645016527 0ustar gabstergabstermodule JGrep class Scanner attr_accessor :arguments, :token_index def initialize(arguments) @token_index = 0 @arguments = arguments end # Scans the input string and identifies single language tokens def get_token return nil if @token_index >= @arguments.size begin case chr(@arguments[@token_index]) when "[" return "statement", gen_substatement when "]" return "]" when "(" return "(", "(" when ")" return ")", ")" when "n" if (chr(@arguments[@token_index + 1]) == "o") && (chr(@arguments[@token_index + 2]) == "t") && ((chr(@arguments[@token_index + 3]) == " ") || (chr(@arguments[@token_index + 3]) == "(")) @token_index += 2 return "not", "not" else gen_statement end when "!" return "not", "not" when "a" if (chr(@arguments[@token_index + 1]) == "n") && (chr(@arguments[@token_index + 2]) == "d") && ((chr(@arguments[@token_index + 3]) == " ") || (chr(@arguments[@token_index + 3]) == "(")) @token_index += 2 return "and", "and" else gen_statement end when "&" if chr(@arguments[@token_index + 1]) == "&" @token_index += 1 return "and", "and" else gen_statement end when "o" if (chr(@arguments[@token_index + 1]) == "r") && ((chr(@arguments[@token_index + 2]) == " ") || (chr(@arguments[@token_index + 2]) == "(")) @token_index += 1 return "or", "or" else gen_statement end when "|" if chr(@arguments[@token_index + 1]) == "|" @token_index += 1 return "or", "or" else gen_statement end when "+" value = "" i = @token_index + 1 begin value += chr(@arguments[i]) i += 1 end until (i >= @arguments.size) || (chr(@arguments[i]) =~ /\s|\)/) @token_index = i - 1 return "+", value when "-" value = "" i = @token_index + 1 begin value += chr(@arguments[i]) i += 1 end until (i >= @arguments.size) || (chr(@arguments[i]) =~ /\s|\)/) @token_index = i - 1 return "-", value when " " return " ", " " else gen_statement end end rescue NoMethodError raise "Error. Expression cannot be parsed." end private def gen_substatement @token_index += 1 returnval = [] while (val = get_token) != "]" @token_index += 1 returnval << val unless val[0] == " " end returnval end def gen_statement current_token_value = "" j = @token_index begin if chr(@arguments[j]) == "/" begin current_token_value << chr(@arguments[j]) j += 1 if chr(@arguments[j]) == "/" current_token_value << "/" break end end until (j >= @arguments.size) || (chr(@arguments[j]) =~ /\//) else begin current_token_value << chr(@arguments[j]) j += 1 if chr(@arguments[j]) =~ /'|"/ begin current_token_value << chr(@arguments[j]) j += 1 end until (j >= @arguments.size) || (chr(@arguments[j]) =~ /'|"/) end end until (j >= @arguments.size) || (chr(@arguments[j]) =~ /\s|\)|\]/ && chr(@arguments[j - 1]) != '\\') end rescue raise "Invalid token found - '#{current_token_value}'" end if current_token_value =~ /^(and|or|not|!)$/ raise "Class name cannot be 'and', 'or', 'not'. Found '#{current_token_value}'" end @token_index += current_token_value.size - 1 ["statement", current_token_value] end # Compatibility with 1.8.7, which returns a Fixnum from String#[] def chr(character) character.chr unless character.nil? end end end jgrep-1.5.4/lib/parser/parser.rb0000644000175000017500000000765513770044645016402 0ustar gabstergabstermodule JGrep class Parser attr_reader :scanner, :execution_stack def initialize(args) @scanner = Scanner.new(args) @execution_stack = [] parse end # Parse the input string, one token at a time a contruct the call stack def parse(substatement = nil, token_index = 0) p_token = nil if substatement c_token, c_token_value = substatement[token_index] else c_token, c_token_value = @scanner.get_token end parenth = 0 until c_token.nil? if substatement token_index += 1 n_token, n_token_value = substatement[token_index] else @scanner.token_index += 1 n_token, n_token_value = @scanner.get_token end next if n_token == " " case c_token when "and" unless (n_token =~ /not|statement|\(|\+|-/) || (scanner.token_index == scanner.arguments.size) raise "Error at column #{scanner.token_index}. \nExpected 'not', 'statement' or '('. Found '#{n_token_value}'" end raise "Error at column #{scanner.token_index}. \n Expression cannot start with 'and'" if p_token.nil? raise "Error at column #{scanner.token_index}. \n #{p_token} cannot be followed by 'and'" if %w[and or].include?(p_token) when "or" unless (n_token =~ /not|statement|\(|\+|-/) || (scanner.token_index == scanner.arguments.size) raise "Error at column #{scanner.token_index}. \nExpected 'not', 'statement', '('. Found '#{n_token_value}'" end raise "Error at column #{scanner.token_index}. \n Expression cannot start with 'or'" if p_token.nil? raise "Error at column #{scanner.token_index}. \n #{p_token} cannot be followed by 'or'" if %w[and or].include?(p_token) when "not" unless n_token =~ /statement|\(|not|\+|-/ raise "Error at column #{scanner.token_index}. \nExpected 'statement' or '('. Found '#{n_token_value}'" end when "statement" if c_token_value.is_a? Array raise "Error at column #{scanner.token_index}\nError, cannot define '[' in a '[...]' block." if substatement parse(c_token_value, 0) end if c_token_value.is_a?(String) && c_token_value =~ /!=/ c_token_value = c_token_value.gsub("!=", "=") @execution_stack << {"not" => "not"} end if !n_token.nil? && !n_token.match(/and|or|\)/) raise "Error at column #{scanner.token_index}. \nExpected 'and', 'or', ')'. Found '#{n_token_value}'" end when "+" if !n_token.nil? && !n_token.match(/and|or|\)/) raise "Error at column #{scanner.token_index}. \nExpected 'and', 'or', ')'. Found '#{n_token_value}'" end when "-" if !n_token.nil? && !n_token.match(/and|or|\)/) raise "Error at column #{scanner.token_index}. \nExpected 'and', 'or', ')'. Found '#{n_token_value}'" end when ")" if !n_token.nil? && n_token !~ /|and|or|not|\(/ raise "Error at column #{scanner.token_index}. \nExpected 'and', 'or', 'not' or '('. Found '#{n_token_value}'" end parenth += 1 when "(" unless n_token =~ /statement|not|\(|\+|-/ raise "Error at column #{scanner.token_index}. \nExpected 'statement', '(', not. Found '#{n_token_value}'" end parenth -= 1 else raise "Unexpected token found at column #{scanner.token_index}. '#{c_token_value}'" end unless n_token == " " || substatement @execution_stack << {c_token => c_token_value} end p_token = c_token c_token = n_token c_token_value = n_token_value end return if substatement raise "Error. Missing parentheses ')'." if parenth < 0 raise "Error. Missing parentheses '('." if parenth > 0 end end end jgrep-1.5.4/lib/jgrep.rb0000644000175000017500000002116113770044645014705 0ustar gabstergabsterrequire "parser/parser.rb" require "parser/scanner.rb" require "rubygems" require "json" module JGrep @verbose = false @flatten = false def self.verbose_on @verbose = true end def self.flatten_on @flatten = true end # Parse json and return documents that match the logical expression # Filters define output by limiting it to only returning a the listed keys. # Start allows you to move the pointer indicating where parsing starts. # Default is the first key in the document heirarchy def self.jgrep(json, expression, filters = nil, start = nil) errors = "" begin JSON.create_id = nil json = JSON.parse(json) json = [json] if json.is_a?(Hash) json = filter_json(json, start).flatten if start result = [] if expression == "" result = json else call_stack = Parser.new(expression).execution_stack json.each do |document| begin result << document if eval_statement(document, call_stack) rescue Exception => e # rubocop:disable Lint/RescueException if @verbose require "pp" pp document STDERR.puts "Error - #{e} \n\n" else errors = "One or more the json documents could not be parsed. Run jgrep -v for to display documents" end end end end puts errors unless errors == "" return result unless filters filter_json(result, filters) rescue JSON::ParserError STDERR.puts "Error. Invalid JSON given" end end # Validates an expression, true when no errors are found else a string representing the issues def self.validate_expression(expression) Parser.new(expression) true rescue $!.message end # Strips filters from json documents and returns those values as a less bloated json document def self.filter_json(documents, filters) result = [] if filters.is_a? Array documents.each do |doc| tmp_json = {} filters.each do |filter| filtered_result = dig_path(doc, filter) unless (filtered_result == doc) || filtered_result.nil? tmp_json[filter] = filtered_result end end result << tmp_json end else documents.each do |r| filtered_result = dig_path(r, filters) unless (filtered_result == r) || filtered_result.nil? result << filtered_result end end end result.flatten if @flatten == true && result.size == 1 result end # Validates if filters do not match any of the parser's logical tokens def self.validate_filters(filters) if filters.is_a? Array filters.each do |filter| if filter =~ /=|<|>|^and$|^or$|^!$|^not$/ raise "Invalid field for -s filter : '#{filter}'" end end elsif filters =~ /=|<|>|^and$|^or$|^!$|^not$/ raise "Invalid field for -s filter : '#{filters}'" end nil end # Correctly format values so we can do the correct type of comparison def self.format(kvalue, value) if kvalue.to_s =~ /^\d+$/ && value.to_s =~ /^\d+$/ [Integer(kvalue), Integer(value)] elsif kvalue.to_s =~ /^\d+\.\d+$/ && value.to_s =~ /^\d+\.\d+$/ [Float(kvalue), Float(value)] else [kvalue, value] end end # Check if the json key that is defined by statement is defined in the json document def self.present?(document, statement) statement.split(".").each do |key| if document.is_a? Hash if document.value?(nil) document.each do |k, _| document[k] = "null" if document[k].nil? end end end if document.is_a? Array rval = false document.each do |doc| rval ||= present?(doc, key) end return rval end document = document[key] return false if document.nil? end true end # Check if key=value is present in document def self.has_object?(document, statement) key, value = statement.split(/<=|>=|=|<|>/) if statement =~ /(<=|>=|<|>|=)/ op = $1 else op = statement end tmp = dig_path(document, key) tmp = tmp.first if tmp.is_a?(Array) && tmp.size == 1 tmp, value = format(tmp, (value.gsub(/"|'/, "") unless value.nil?)) # rubocop:disable Style/FormatString # Deal with null comparison return true if tmp.nil? && value == "null" # Deal with booleans return true if tmp == true && value == "true" return true if tmp == false && value == "false" # Deal with regex matching if !tmp.nil? && tmp.is_a?(String) && value =~ /^\/.*\/$/ tmp.match(Regexp.new(value.delete("/"))) ? (return true) : (return false) end # Deal with everything else case op when "=" return tmp == value when "<=" return tmp <= value when ">=" return tmp >= value when ">" return tmp > value when "<" return tmp < value end end # Check if key=value is present in a sub array def self.is_object_in_array?(document, statement) document.each do |item| return true if has_object?(item, statement) end false end # Check if complex statement (defined as [key=value...]) is # present over an array of key value pairs def self.has_complex?(document, compound) field = "" tmp = document result = [] fresult = [] compound.each do |token| if token[0] == "statement" field = token break end end field = field[1].split(/=|<|>/).first field.split(".").each_with_index do |item, _| tmp = tmp[item] return false if tmp.nil? next unless tmp.is_a?(Array) tmp.each do |doc| result = [] compound.each do |token| case token[0] when "and" result << "&&" when "or" result << "||" when /not|\!/ result << "!" when "statement" op = token[1].match(/.*<=|>=|=|<|>/) left = token[1].split(op[0]).first.split(".").last right = token[1].split(op[0]).last new_statement = left + op[0] + right result << has_object?(doc, new_statement) end end fresult << eval(result.join(" ")) # rubocop:disable Security/Eval (fresult << "||") unless doc == tmp.last end return eval(fresult.join(" ")) # rubocop:disable Security/Eval end end # Evaluates the call stack en returns true of selected document # matches logical expression def self.eval_statement(document, callstack) result = [] callstack.each do |expression| case expression.keys.first when "statement" if expression.values.first.is_a?(Array) result << has_complex?(document, expression.values.first) else result << has_object?(document, expression.values.first) end when "+" result << present?(document, expression.values.first) when "-" result << !present?(document, expression.values.first) when "and" result << "&&" when "or" result << "||" when "(" result << "(" when ")" result << ")" when "not" result << "!" end end eval(result.join(" ")) # rubocop:disable Security/Eval end # Digs to a specific path in the json document and returns the value def self.dig_path(json, path) index = nil path = path.gsub(/^\./, "") if path =~ /(.*)\[(.*)\]/ path = $1 index = $2 end return json if path == "" if json.is_a? Hash json.keys.each do |k| if path.start_with?(k) && k.include?(".") return dig_path(json[k], path.gsub(k, "")) end end end path_array = path.split(".") if path_array.first == "*" tmp = [] json.each do |j| tmp << dig_path(j[1], path_array.drop(1).join(".")) end return tmp end json = json[path_array.first] if json.is_a? Hash if json.is_a? Hash return json if path == path_array.first return dig_path(json, path.include?(".") ? path_array.drop(1).join(".") : path) elsif json.is_a? Array if path == path_array.first && (json.first.is_a?(Hash) && !json.first.keys.include?(path)) return json end tmp = [] json.each do |j| tmp_path = dig_path(j, (path.include?(".") ? path_array.drop(1).join(".") : path)) tmp << tmp_path unless tmp_path.nil? end unless tmp.empty? return index ? tmp.flatten[index.to_i] : tmp end elsif json.nil? return nil else return json end end end jgrep-1.5.4/bin/0000755000175000017500000000000013770044645013252 5ustar gabstergabsterjgrep-1.5.4/bin/jgrep0000755000175000017500000000751013770044645014312 0ustar gabstergabster#!/usr/bin/env ruby require "jgrep" require "optparse" @options = {flat: false, start: nil, field: [], slice: nil} def print_json(result) if @options[:flat] puts(result.first.to_json) else result = result.first if @options[:stream] puts(JSON.pretty_generate(result)) end end def do_grep(json, expression) if @options[:field].empty? result = JGrep.jgrep(json, expression, nil, @options[:start]) result = result.slice(@options[:slice]) if @options[:slice] exit 1 if result == [] print_json(result) unless @options[:quiet] == true elsif @options[:field].size > 1 JGrep.validate_filters(@options[:field]) result = JGrep.jgrep(json, expression, @options[:field], @options[:start]) result = result.slice(@options[:slice]) if @options[:slice] exit 1 if result == [] print_json(result) unless @options[:quiet] == true else JGrep.validate_filters(@options[:field][0]) result = JGrep.jgrep(json, expression, @options[:field][0], @options[:start]) result = result.slice(@options[:slice]) if @options[:slice] exit 1 if result == [] if result.is_a?(Array) && !(result.first.is_a?(Hash) || result.flatten.first.is_a?(Hash)) unless @options[:quiet] == true result.map {|x| puts x unless x.nil?} end else print_json(result) unless @options[:quiet] == true end end end begin OptionParser.new do |opts| opts.banner = "Usage: jgrep [options] \"expression\"" opts.on("-s", "--simple [FIELDS]", "Display only one or more fields from each of the resulting json documents") do |field| raise "-s flag requires a field value" if field.nil? @options[:field].concat(field.split(" ")) end opts.on("-c", "--compact", "Display non pretty json") do @options[:flat] = true end opts.on("-n", "--stream", "Display continuous output from continuous input") do @options[:stream] = true end opts.on("-f", "--flatten", "Makes output as flat as possible") do JGrep.flatten_on end opts.on("-i", "--input [FILENAME]", "Specify input file to parse") do |filename| @options[:file] = filename end opts.on("-q", "--quiet", "Quiet; don't write to stdout. Exit with zero status if match found.") do @options[:quiet] = true end opts.on("-v", "--verbose", "Verbose output") do JGrep.verbose_on end opts.on("--start [FIELD]", "Where in the data to start from") do |field| @options[:start] = field end opts.on("--slice [RANGE]", "A range of the form 'n' or 'n..m', indicating which documents to extract from the final output") do |field| range_nums = field.split("..").map(&:to_i) @options[:slice] = range_nums.length == 1 ? range_nums[0] : Range.new(*range_nums) end end.parse! rescue OptionParser::InvalidOption => e puts e.to_s.capitalize exit 1 rescue Exception => e # rubocop:disable Lint/RescueException puts e exit 1 end begin expression = nil # Identify the expression from command line arguments ARGV.each do |argument| if argument =~ /<|>|=|\+|-/ expression = argument ARGV.delete(argument) end end expression = "" if expression.nil? # Continuously gets if inputstream in constant # Load json from standard input if tty is false # else find and load file from command line arugments if @options[:stream] raise "No json input specified" if STDIN.tty? while json = gets do_grep(json, expression) end elsif @options[:file] json = File.read(@options[:file]) do_grep(json, expression) elsif !STDIN.tty? json = STDIN.read do_grep(json, expression) else raise "No json input specified" end rescue Interrupt STDERR.puts "Exiting..." exit 1 rescue SystemExit exit e.status rescue Exception => e # rubocop:disable Lint/RescueException STDERR.puts "Error - #{e}" exit 1 end jgrep-1.5.4/Rakefile0000644000175000017500000000032013770044645014142 0ustar gabstergabsterrequire 'rspec/core/rake_task' RSpec::Core::RakeTask.new(:spec) desc "Run rubycop style checks" task :rubocop do sh("rubocop -f progress -f offenses lib spec bin") end task :default => [:rubocop, :spec] jgrep-1.5.4/README.markdown0000644000175000017500000001225413770044645015207 0ustar gabstergabsterJGrep is a command line tool and API for parsing JSON documents based on logical expressions. ### Installation: jgrep is available as a gem: gem install jgrep ### JGrep binary usage: jgrep [expression] -i foo.json or cat "foo.json" | jgrep [expression] ### Flags: -s, --simple [FIELDS] : Greps the JSON and only returns the value of the field(s) specified -c, --compat : Returns the JSON in its non-pretty flat form -n, --stream : Specify continuous input -f, --flatten : Flatten the results as much as possible -i, --input [FILENAME] : Target JSON file to use as input -q, --quiet : Quiet; don't write to stdout. Exit with zero status if match found. -v, --verbose : Verbose output that will list a document if it fails to parse --start FIELD : Starts the grep at a specific key in the document --slice [RANGE] : A range of the form 'n' or 'n..m', indicating which documents to extract from the final output ### Expressions: JGrep uses the following logical symbols to define expressions. 'and' : - [statement] and [statement] Evaluates to true if both statements are true 'or' : - [statement] and [statement] Evaluates true if either statement is true 'not' : - ! [statement] - not [statement] Inverts the value of statement '+' - +[value] Returns true if value is present in the json document '-' - -[value] Returns true if value is not present in the json doument '(' and ')' - (expression1) and expression2 Performs the operations inside the perentheses first. ### Statements: A statement is defined as some value in a json document compared to another value. Available comparison operators are '=', '<', '>', '<=', '>=' Examples: foo.bar=1 foo.bar>0 foo.bar<=1.3 ### Complex expressions: Given a json document, {"foo":1, "bar":null}, the following are examples of valid expressions Examples: +foo ... returns true -bar ... returns false +foo and !(foo=2) ... returns true !(foo>=2 and bar=null) or !(bar=null) ... returns true ### CLI missing an expression: If JGrep is executed without a set expression, it will return an unmodified JSON document. The -s flag can still be applied to the result. ### In document comparison: If a document contains an array, the '[' and ']' operators can be used to define a comparison where statements are checked for truth on a per element basis which will then be combined. Example: [foo.bar1=1 and foo.bar2=2] on [ { "foo": [ { "bar1":1 }, { "bar2":2 } ] }, { "foo": [ { "bar1":0 }, { "bar2":0 } ] } ] will return [ { "foo": [ { "bar1": 1 }, { "bar2": 2 } ] } ] **Note**: In document comparison cannot be nested. ### The -s flag: The s flag simplifies the output returned by JGrep. Given a JSON document [{"a":1, "b":2, "c":3}, {"a":3, "b":2, "c":1}] a JGrep invocation like cat my.json | jgrep "a=1" -s b will output 1 The s flag can also be used with multiple field, which will return JSON as output which only contain the specified fields. **Note**: Separate fields by a space and enclose all fields in quotes (see example below) Given: [{"a":1, "b":2, "c":3}, {"a":3, "b":2, "c":1}] a JGrep invocation like cat my.json | jgrep "a>0" -s "a c" will output [ { "a" : 1, "c" : 3 }, { "a" : 3, "c" : 1 } ] ### The --start flag: Some documents do not comply to our expected format, they might have an array embedded deep in a field. The --start flag lets you pick a starting point for the grep. An example document can be seen here: {"results": [ {"name":"Jack", "surname":"Smith"}, {"name":"Jill", "surname":"Jones"} ] } This document does not comply to our standard but does contain data that can be searched - the _results_ field. We can use the --start flat to tell jgrep to start looking for data in that field:
$ cat my.json | jgrep --start results name=Jack -s surname
Smith
### The --slice flag Allows the user to provide an int or range to slice an array of results with, in particular so a single element can be extracted, e.g. $ echo '[{"foo": {"bar": "baz"}}, {"foo": {"bar":"baz"}}]' | jgrep "foo.bar=baz" --slice 0 { "foo": { "bar": "baz" } } ### The --stream flag With the --stream or -n flag, jgrep will process multiple JSON inputs (newline separated) until standard input is closed. Each JSON input will be processed as usual, but the output immediately printed. ### JGrep Gem usage: require 'jgrep' json = File.read("yourfile.json") expression = "foo=1 or bar=1" JGrep::jgrep(json, expression) sflags = "foo" JGrep::jgrep(json, expression, sflags) jgrep-1.5.4/COPYING0000644000175000017500000002611413770044645013541 0ustar gabstergabster Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2011 P.Loubser Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. jgrep-1.5.4/CHANGELOG.markdown0000644000175000017500000000171113770044645015535 0ustar gabstergabster# Changelog ## 1.5.4 * Include missing fixes for Ruby 2.7.0 deprecations ## 1.5.3 * Fix Ruby 2.7.0 deprecation warnings * Bump Rspec version in Gemfile ## 1.5.2 * Fixed an issue where strings like 2012R2 would get parsed as floats ## 1.5.1 * Now handles escaped parens when tokenising statements ## 1.5.0 * Dropped support for Ruby 1.8.3 * Added support for modern Ruby versions (Tested up to 2.4.0) * Added utility method to validate expressions ## 1.4.1 * Fix binary exit code to be 1 when no matches are found (Mickaël Canévet) ## 1.4.0 * Expressions support matching true/false booleans (Boyan Tabakov) * `--slice` option added to jgrep to get array elements (Jon McKenzie) * `-i` option to read file supported without a TTY (Jon McKenzie) * `-n` streaming option from 1.3.2 reinstated * Performance fix: string splitting replaced with character access * Performance fix: regexes replaced with simpler string methods * Tests fixed and enabled on Travis CI