├── yq ├── operators │ ├── __init__.py │ ├── base.py │ ├── comma.py │ ├── extract.py │ ├── constant.py │ ├── match_error.py │ ├── sequence.py │ ├── subscript.py │ ├── comprehension.py │ ├── subsequence.py │ ├── dot.py │ └── projection.py ├── test │ ├── operator_test │ │ ├── __init__.py │ │ ├── match_error_test.py │ │ ├── sequence_test.py │ │ ├── comprehension_test.py │ │ ├── projection_test.py │ │ └── dot_test.py │ ├── output_test.py │ ├── run_functional_tests.py │ └── run_jq_tests.py ├── __init__.py ├── data │ ├── pipe.yml │ ├── comma.yml │ ├── dot.yml │ ├── dot_subscript.yml │ └── jq.txt ├── output.py ├── __main__.py └── parser.py ├── requirements.txt ├── .editorconfig ├── logging.conf ├── setup.py ├── .gitignore └── README.rst /yq/operators/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /yq/test/operator_test/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | PyYAML==3.10 2 | pyparsing==2.0.1 3 | -------------------------------------------------------------------------------- /yq/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | __author__ = 'Zoltan Nagy ' 4 | __version__ = '0.0.2' 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 4 -------------------------------------------------------------------------------- /yq/data/pipe.yml: -------------------------------------------------------------------------------- 1 | - filter: '.[] | .name' 2 | input: 3 | - name: YAML 4 | good: true 5 | - name: XML 6 | good: false 7 | output_list: [YAML, XML] -------------------------------------------------------------------------------- /yq/output.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | 3 | 4 | def output(o): 5 | if isinstance(o, dict) or isinstance(o, list): 6 | return yaml.dump(o) 7 | return str(o) 8 | 9 | -------------------------------------------------------------------------------- /yq/operators/base.py: -------------------------------------------------------------------------------- 1 | class Operator(object): 2 | def apply(self, data): 3 | return map(self._apply_item, data) 4 | 5 | def _apply_item(self, data): 6 | raise NotImplementedError() -------------------------------------------------------------------------------- /yq/operators/comma.py: -------------------------------------------------------------------------------- 1 | class Comma(object): 2 | def __init__(self, ops): 3 | self.ops = ops 4 | 5 | def apply(self, data): 6 | return [item for sublist in [op.apply(data) for op in self.ops] for item in sublist] 7 | -------------------------------------------------------------------------------- /yq/operators/extract.py: -------------------------------------------------------------------------------- 1 | class Extract(object): 2 | def apply(self, data): 3 | assert len(data) == 1 4 | item = data[0] 5 | if isinstance(item, dict): 6 | return item.values() 7 | return item 8 | 9 | -------------------------------------------------------------------------------- /yq/operators/constant.py: -------------------------------------------------------------------------------- 1 | from yq.operators.base import Operator 2 | 3 | 4 | class Constant(Operator): 5 | def __init__(self, value): 6 | self.value = value 7 | 8 | def apply(self, _): 9 | return self.value 10 | 11 | def __repr__(self): 12 | return str(self.value) 13 | 14 | -------------------------------------------------------------------------------- /yq/operators/match_error.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | 3 | 4 | class MatchError(ValueError): 5 | def __init__(self, operator, data, error): 6 | self.operator = operator 7 | self.data = data 8 | self.error = error 9 | self.message = 'Failed to apply "%s": %s. The data being processed was:\n%s' % (operator, error, yaml.dump(data)) 10 | 11 | def __str__(self): 12 | return self.message 13 | -------------------------------------------------------------------------------- /yq/operators/sequence.py: -------------------------------------------------------------------------------- 1 | from yq.operators.base import Operator 2 | 3 | 4 | class Sequence(Operator): 5 | def __init__(self, operators): 6 | self.operators = operators 7 | 8 | def apply(self, data): 9 | for operator in self.operators: 10 | data = operator.apply(data) 11 | return data 12 | 13 | def __repr__(self): 14 | return ''.join([repr(operator) for operator in self.operators]) 15 | -------------------------------------------------------------------------------- /yq/data/comma.yml: -------------------------------------------------------------------------------- 1 | - filter: ".foo, .bar" 2 | input: 3 | foo: 42 4 | bar: something else 5 | baz: true 6 | output_list: 7 | - 42 8 | - something else 9 | 10 | - filter: ".user, .projects[]" 11 | input: 12 | user: abesto 13 | projects: 14 | - yq 15 | - hs-snakelike 16 | output_list: 17 | - abesto 18 | - yq 19 | - hs-snakelike 20 | 21 | - filter: ".[4,2]" 22 | input: [a, b, c, d, e] 23 | output_list: [e, c] -------------------------------------------------------------------------------- /yq/data/dot.yml: -------------------------------------------------------------------------------- 1 | - filter: . 2 | input: Hello, World! 3 | output: Hello, World! 4 | 5 | - filter: .foo 6 | input: 7 | foo: 42 8 | bar: less interesting data 9 | output: 42 10 | 11 | - filter: .foo 12 | input: 13 | notfoo: true 14 | alsonotfoo: false 15 | output: null 16 | 17 | - skip: true 18 | comment: "The jq docs say this should work, but jq barfs on it, so we don't support it either" 19 | filter: ."foo" 20 | input: 21 | foo: 42 22 | output: 42 23 | -------------------------------------------------------------------------------- /yq/test/operator_test/match_error_test.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from yq.operators.dot import Dot 3 | from yq.operators.match_error import MatchError 4 | import yaml 5 | 6 | 7 | class MatchErrorTestCase(TestCase): 8 | def test_message(self): 9 | data = {'bar': ['baz']} 10 | exc = MatchError(Dot('foo'), data, 'error message') 11 | self.assertEqual('Failed to apply ".foo": error message. The data being processed was:\n%s' % yaml.dump(data), 12 | exc.message) 13 | -------------------------------------------------------------------------------- /yq/operators/subscript.py: -------------------------------------------------------------------------------- 1 | from yq.operators.base import Operator 2 | 3 | 4 | class Subscript(Operator): 5 | def __init__(self, indices): 6 | self.indices = indices 7 | 8 | def apply(self, data): 9 | retval = [] 10 | for i in self.indices: 11 | try: 12 | retval.append(data[0][i]) 13 | except IndexError: 14 | retval.append(None) 15 | return retval 16 | 17 | def __repr__(self): 18 | return '[%s]' % ','.join(self.indices) 19 | 20 | -------------------------------------------------------------------------------- /logging.conf: -------------------------------------------------------------------------------- 1 | [loggers] 2 | keys=root,simpleExample 3 | 4 | [handlers] 5 | keys=consoleHandler 6 | 7 | [formatters] 8 | keys=simpleFormatter 9 | 10 | [logger_root] 11 | level=DEBUG 12 | handlers=consoleHandler 13 | 14 | [logger_simpleExample] 15 | level=DEBUG 16 | handlers=consoleHandler 17 | qualname=simpleExample 18 | propagate=0 19 | 20 | [handler_consoleHandler] 21 | class=StreamHandler 22 | level=DEBUG 23 | formatter=simpleFormatter 24 | args=(sys.stdout,) 25 | 26 | [formatter_simpleFormatter] 27 | format=%(asctime)s - %(name)s - %(levelname)s - %(message)s 28 | datefmt= -------------------------------------------------------------------------------- /yq/test/operator_test/sequence_test.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from yq.operators.dot import Dot 3 | from yq.operators.sequence import Sequence 4 | 5 | 6 | class SequenceTestCase(TestCase): 7 | def test_dots(self): 8 | data = {'a': {'b': 'c'}} 9 | dot_a = Dot('a') 10 | dot_b = Dot('b') 11 | sequence = Sequence([dot_a, dot_b]) 12 | self.assertEqual(sequence.apply([data]), ['c']) 13 | 14 | def test_dots_repr(self): 15 | sequence = Sequence([Dot('foo'), Dot('bar')]) 16 | self.assertEqual(repr(sequence), '.foo.bar') -------------------------------------------------------------------------------- /yq/test/output_test.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from mock import patch 3 | 4 | from yq.output import output 5 | 6 | 7 | class OutputTest(TestCase): 8 | def test_output_string(self): 9 | self.assertEqual(output('foo'), 'foo') 10 | 11 | def test_output_int(self): 12 | self.assertEqual(output(1), '1') 13 | 14 | @patch('yq.output.yaml') 15 | def test_output_dict(self, mock_yaml): 16 | data = {'foo': 'bar'} 17 | self.assertEqual(output(data), mock_yaml.dump.return_value) 18 | mock_yaml.dump.assert_called_once_with(data) 19 | -------------------------------------------------------------------------------- /yq/operators/comprehension.py: -------------------------------------------------------------------------------- 1 | from yq.operators.base import Operator 2 | from yq.operators.match_error import MatchError 3 | 4 | 5 | class Comprehension(Operator): 6 | def __init__(self, op): 7 | self.op = op 8 | 9 | def _apply_item(self, data): 10 | if not isinstance(data, list): 11 | raise MatchError(self, data, 'tried to apply comprehension %s to non-array' % self) 12 | retval = [] 13 | for item in data: 14 | retval.append(self.op._apply_item(item)) 15 | return retval 16 | 17 | def __repr__(self): 18 | return '[%s]' % self.op 19 | -------------------------------------------------------------------------------- /yq/operators/subsequence.py: -------------------------------------------------------------------------------- 1 | from yq.operators.base import Operator 2 | 3 | 4 | class Subsequence(Operator): 5 | def __init__(self, low, hi): 6 | if low is None: 7 | self.low = low 8 | else: 9 | self.low = int(low) 10 | if hi is None: 11 | self.hi = None 12 | else: 13 | self.hi = int(hi) 14 | 15 | def _apply_item(self, data): 16 | try: 17 | return data[self.low:self.hi] 18 | except IndexError: 19 | return None 20 | 21 | def __repr__(self): 22 | return '[%s:%s]' % (self.low, self.hi) 23 | 24 | -------------------------------------------------------------------------------- /yq/operators/dot.py: -------------------------------------------------------------------------------- 1 | from yq.operators.base import Operator 2 | from yq.operators.match_error import MatchError 3 | 4 | 5 | class Dot(Operator): 6 | def __init__(self, key=''): 7 | self.key = key 8 | 9 | def _apply_item(self, data): 10 | if self.key == '': 11 | return data 12 | if not isinstance(data, dict): 13 | raise MatchError(self, data, 'tried to access field %s on a non-object' % self) 14 | try: 15 | return data[self.key] 16 | except KeyError: 17 | return None 18 | 19 | def __repr__(self): 20 | return '.%s' % self.key 21 | -------------------------------------------------------------------------------- /yq/__main__.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | import sys 3 | 4 | from yq import parser 5 | from yq.operators.match_error import MatchError 6 | from yq.output import output 7 | 8 | 9 | def main(op_str, input): 10 | op = parser.parse(op_str) 11 | data = yaml.load(input) 12 | try: 13 | for item in op.apply([data]): 14 | yield item 15 | except MatchError as ex: 16 | print ex 17 | 18 | 19 | if __name__ == '__main__': 20 | if len(sys.argv) < 2: 21 | print >> sys.stderr, 'Usage: {} '.format(sys.argv[0]) 22 | sys.exit(2) 23 | op = sys.argv[1] 24 | input = sys.stdin.read() 25 | for item in main(op, input): 26 | print output(item) 27 | -------------------------------------------------------------------------------- /yq/test/operator_test/comprehension_test.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from yq.operators.dot import Dot 3 | from yq.operators.match_error import MatchError 4 | from yq.operators.comprehension import Comprehension 5 | 6 | 7 | class SubscriptTestCase(TestCase): 8 | def test_simple(self): 9 | self.assertListEqual( 10 | Comprehension(Dot('foo'))._apply_item([{'foo': 3}, {'foo': 4, 'bar': 'baz'}]), 11 | [3, 4] 12 | ) 13 | 14 | def test_repr(self): 15 | self.assertEqual( 16 | repr(Comprehension(Dot('bar'))), 17 | '[.bar]' 18 | ) 19 | 20 | def test_not_a_list(self): 21 | with self.assertRaises(MatchError) as cm: 22 | Comprehension(None)._apply_item({1:2}) 23 | self.assertEqual(cm.exception.error, 'tried to apply comprehension [None] to non-array') 24 | 25 | -------------------------------------------------------------------------------- /yq/operators/projection.py: -------------------------------------------------------------------------------- 1 | from yq.operators.base import Operator 2 | from yq.operators.dot import Dot 3 | from yq.operators.match_error import MatchError 4 | 5 | 6 | class ProjectionItem(object): 7 | def __init__(self, key, op): 8 | self.key = key 9 | self.op = op 10 | 11 | 12 | class Projection(Operator): 13 | def __init__(self, items): 14 | self.items = items 15 | 16 | def _apply_item(self, data): 17 | retval = {} 18 | for item in self.items: 19 | retval[item.key] = item.op._apply_item(data) 20 | return retval 21 | 22 | def __repr__(self): 23 | str_items = [] 24 | for item in self.items: 25 | if isinstance(item.op, Dot) and item.op.key == item.key: 26 | str_items.append(item.key) 27 | else: 28 | str_items.append('%s: %s' % (item.key, item.op)) 29 | return '{' + ', '.join(str_items) + '}' 30 | -------------------------------------------------------------------------------- /yq/test/operator_test/projection_test.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from yq.operators.dot import Dot 3 | from yq.operators.match_error import MatchError 4 | from yq.operators.projection import Projection, ProjectionItem 5 | 6 | 7 | class ProjectionTestCase(TestCase): 8 | def simple_test(self): 9 | p = Projection([ProjectionItem('foo', Dot('foo')), ProjectionItem('blam', Dot('blam'))]) 10 | data = {'foo': 'bar', 'baz': 3, 'blam': 30} 11 | self.assertDictEqual(p._apply_item(data), {'foo': 'bar', 'blam': 30}) 12 | 13 | def test_simple_repr(self): 14 | p = Projection([ProjectionItem('foo', Dot('foo')), ProjectionItem('blam', Dot('blam'))]) 15 | self.assertEqual( 16 | repr(p), 17 | '{foo, blam}' 18 | ) 19 | 20 | def test_other_dot_repr(self): 21 | self.assertEqual( 22 | repr(Projection([ProjectionItem('foo', Dot('bar'))])), 23 | '{foo: .bar}' 24 | ) 25 | 26 | -------------------------------------------------------------------------------- /yq/data/dot_subscript.yml: -------------------------------------------------------------------------------- 1 | - filter: .[0] 2 | input: 3 | - name: YAML 4 | good: true 5 | - name: XML 6 | good: false 7 | output: 8 | name: YAML 9 | good: true 10 | 11 | - filter: .[2] 12 | input: 13 | - name: YAML 14 | good: true 15 | - name: XML 16 | good: false 17 | output: null 18 | 19 | - filter: .[2:4] 20 | input: abcdefghi 21 | output: cd 22 | 23 | - filter: .[:3] 24 | input: [a, b, c, d, e] 25 | output: [a, b, c] 26 | 27 | - filter: .[-2:] 28 | input: [a, b, c, d, e] 29 | output: [d, e] 30 | 31 | - filter: .["foo"] 32 | comment: Not an example in the jq docs, but hinted at in text. Also, it works with jq. 33 | input: 34 | foo: 42 35 | bar: baz 36 | output: 42 37 | 38 | - filter: .[] 39 | input: 40 | - name: YAML 41 | good: true 42 | - name: XML 43 | good: false 44 | output_list: 45 | - name: YAML 46 | good: true 47 | - name: XML 48 | good: false 49 | 50 | - filter: .[] 51 | input: 52 | a: 1 53 | b: 1 54 | output_list: 55 | - 1 56 | - 1 57 | -------------------------------------------------------------------------------- /yq/test/operator_test/dot_test.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from yq.operators.dot import Dot 3 | from yq.operators.match_error import MatchError 4 | 5 | 6 | class DotTest(TestCase): 7 | data = {'foo': 'bar'} 8 | 9 | def test_simple(self): 10 | dot = Dot('foo') 11 | self.assertEqual(dot._apply_item(self.data), 'bar') 12 | 13 | def test_repr(self): 14 | self.assertEqual(repr(Dot('foo')), '.foo') 15 | 16 | def test_empty_key(self): 17 | self.assertEqual(Dot('')._apply_item(self.data), self.data) 18 | self.assertEqual(Dot()._apply_item(self.data), self.data) 19 | 20 | def test_no_such_key(self): 21 | dot = Dot('gah') 22 | self.assertIsNone(dot._apply_item({'bar': 3})) 23 | 24 | def test_not_an_object(self): 25 | dot = Dot('bah') 26 | with self.assertRaises(MatchError) as cm: 27 | dot._apply_item([1,2,3]) 28 | self.assertEqual(cm.exception.error, 'tried to access field .bah on a non-object') 29 | 30 | def test_not_an_object_empty_dot(self): 31 | l = [1] 32 | self.assertListEqual(Dot().apply(l), l) -------------------------------------------------------------------------------- /yq/test/run_functional_tests.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import yaml 3 | import cli 4 | from os import listdir, path 5 | 6 | def functional_test(): 7 | def one(f): 8 | with open(f, 'r') as fd: 9 | suite = yaml.load(fd.read()) 10 | for idx, case in enumerate(suite): 11 | if 'skip' in case and case['skip']: 12 | continue 13 | yield (FunctionalTestCase(f, idx, case),) 14 | return map(one, listdir(path.join(path.dirname(path.dirname(__file__)), 'data'))) 15 | 16 | 17 | class FunctionalTestCase(object): 18 | def __init__(self, file, idx, case): 19 | self.case = case 20 | self.description = '%s case %d: %s' % (file, idx, case) 21 | 22 | def __call__(self): 23 | output = cli.main(self.case['filter'], yaml.dump(self.case['input'])) 24 | if 'output' in self.case: 25 | expected_list = [self.case['output']] 26 | else: 27 | expected_list = self.case['output_list'] 28 | for expected in expected_list: 29 | actual = output.next() 30 | print 'Actual: %s\nExpected: %s' % (actual, expected) 31 | assert actual == expected 32 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | from os import path, listdir 3 | from functools import partial 4 | from itertools import imap, ifilter 5 | from ast import parse 6 | from pip import __file__ as pip_loc 7 | 8 | if __name__ == '__main__': 9 | package_name = 'yq' 10 | 11 | f_for = partial(path.join, path.dirname(__file__), package_name) 12 | d_for = partial(path.join, path.dirname(path.dirname(pip_loc)), package_name) 13 | 14 | _data_join = partial(path.join, f_for('data')) 15 | _data_install_dir = partial(path.join, d_for('data')) 16 | 17 | get_vals = lambda var0, var1: imap(lambda buf: next(imap(lambda e: e.value.s, parse(buf).body)), 18 | ifilter(lambda line: line.startswith(var0) or line.startswith(var1), f)) 19 | 20 | with open(path.join(package_name, '__init__.py')) as f: 21 | __author__, __version__ = get_vals('__version__', '__author__') 22 | 23 | setup( 24 | name=package_name, 25 | author=__author__, 26 | version=__version__, 27 | description='Pure Python implementation of a subset of the features of jq for YAML documents', 28 | test_suite=package_name + '.tests', 29 | packages=find_packages(), 30 | package_dir={package_name: package_name}, 31 | data_files=[ 32 | (_data_install_dir(), map(_data_join, listdir(_data_join()))), 33 | ] 34 | ) 35 | -------------------------------------------------------------------------------- /yq/test/run_jq_tests.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | import json 3 | import cli 4 | 5 | 6 | def jq_test(): 7 | i = 0 8 | skipped = [] 9 | lines = [] 10 | for line in open('functional_tests/jq.txt', 'r'): 11 | line = line.strip() 12 | if not line: 13 | if lines: 14 | if lines[0] == 'skip': 15 | skipped.append('\n'.join(lines[1:])) 16 | else: 17 | yield (FunctionalTestCase('jq.txt', i, 18 | {'filter': lines[0], 19 | 'input': json.loads(lines[1]), 20 | 'output_list': lines[2:]}),) 21 | lines = [] 22 | i += 1 23 | elif line[0] != '#': 24 | lines.append(line) 25 | 26 | 27 | class FunctionalTestCase(object): 28 | def __init__(self, file, idx, case): 29 | self.case = case 30 | self.description = '%s case %d: %s' % (file, idx, case) 31 | 32 | def __call__(self): 33 | output = cli.main(self.case['filter'], yaml.dump(self.case['input'])) 34 | if 'output' in self.case: 35 | expected_list = [self.case['output']] 36 | else: 37 | expected_list = self.case['output_list'] 38 | for expected in expected_list: 39 | actual = output.next() 40 | print 'Actual: %s\nExpected: %s' % (actual, expected) 41 | expected = yaml.load(expected) 42 | assert actual == expected 43 | 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .venv 2 | ### PyCharm ### 3 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm 4 | 5 | *.iml 6 | 7 | ## Directory-based project format: 8 | .idea/ 9 | # if you remove the above rule, at least ignore the following: 10 | 11 | # User-specific stuff: 12 | # .idea/workspace.xml 13 | # .idea/tasks.xml 14 | # .idea/dictionaries 15 | 16 | # Sensitive or high-churn files: 17 | # .idea/dataSources.ids 18 | # .idea/dataSources.xml 19 | # .idea/sqlDataSources.xml 20 | # .idea/dynamic.xml 21 | # .idea/uiDesigner.xml 22 | 23 | # Gradle: 24 | # .idea/gradle.xml 25 | # .idea/libraries 26 | 27 | # Mongo Explorer plugin: 28 | # .idea/mongoSettings.xml 29 | 30 | ## File-based project format: 31 | *.ipr 32 | *.iws 33 | 34 | ## Plugin-specific files: 35 | 36 | # IntelliJ 37 | out/ 38 | 39 | # mpeltonen/sbt-idea plugin 40 | .idea_modules/ 41 | 42 | # JIRA plugin 43 | atlassian-ide-plugin.xml 44 | 45 | # Crashlytics plugin (for Android Studio and IntelliJ) 46 | com_crashlytics_export_strings.xml 47 | crashlytics.properties 48 | crashlytics-build.properties 49 | 50 | 51 | ### Python ### 52 | # Byte-compiled / optimized / DLL files 53 | __pycache__/ 54 | *.py[cod] 55 | 56 | # C extensions 57 | *.so 58 | 59 | # Distribution / packaging 60 | .Python 61 | env/ 62 | build/ 63 | develop-eggs/ 64 | dist/ 65 | downloads/ 66 | eggs/ 67 | lib/ 68 | lib64/ 69 | parts/ 70 | sdist/ 71 | var/ 72 | *.egg-info/ 73 | .installed.cfg 74 | *.egg 75 | 76 | # PyInstaller 77 | # Usually these files are written by a python script from a template 78 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 79 | *.manifest 80 | *.spec 81 | 82 | # Installer logs 83 | pip-log.txt 84 | pip-delete-this-directory.txt 85 | 86 | # Unit test / coverage reports 87 | htmlcov/ 88 | .tox/ 89 | .coverage 90 | .cache 91 | nosetests.xml 92 | coverage.xml 93 | 94 | # Translations 95 | *.mo 96 | *.pot 97 | 98 | # Django stuff: 99 | *.log 100 | 101 | # Sphinx documentation 102 | docs/_build/ 103 | 104 | # PyBuilder 105 | target/ 106 | -------------------------------------------------------------------------------- /yq/parser.py: -------------------------------------------------------------------------------- 1 | from pyparsing import * 2 | import yaml 3 | from yq.operators.comma import Comma 4 | from yq.operators.comprehension import Comprehension 5 | from yq.operators.constant import Constant 6 | from yq.operators.dot import Dot 7 | from yq.operators.extract import Extract 8 | from yq.operators.projection import Projection, ProjectionItem 9 | from yq.operators.sequence import Sequence 10 | from yq.operators.subscript import Subscript 11 | from yq.operators.subsequence import Subsequence 12 | 13 | 14 | def parse(str): 15 | return piped.parseString(str, True).asList()[0] 16 | 17 | 18 | def wtf(ts): 19 | import pdb; pdb.set_trace() 20 | 21 | 22 | key = lambda: Word(alphanums + '_') 23 | 24 | # Literal values 25 | literals = oneOf('true false null') | ('"'+Word(alphanums)+'"') | (Word(nums + '- .')).setName('literal') 26 | literals.setParseAction(lambda ts: Constant(yaml.load(ts[0]))) 27 | 28 | # Operations 29 | operation = Forward() 30 | 31 | emptyDot = Literal('.').setParseAction(lambda ts: Dot()) 32 | dot = (('.' + key()).setName('dot_str') | 33 | ('.["' + key() + '"]').setName('dot_subscript_str') | 34 | ('."' + key() + '"').setName('dot_quote_str')).setParseAction(lambda ts: Dot(ts[1])) 35 | 36 | extract = (Literal('.[]').setParseAction(Extract).setName('extract') | 37 | (dot + '[]').setParseAction(lambda ts: Sequence([ts[0], Extract()]))) 38 | 39 | subscript = ('[' + delimitedList(Word(nums)) + ']')\ 40 | .setParseAction(lambda ts: Subscript(map(int, ts[1:-1])))\ 41 | .setName('subscript') 42 | 43 | subsequence = ('[' + Optional(Word(nums + '-'), None) + ':' + Optional(Word(nums + '-'), None) + ']')\ 44 | .setParseAction(lambda ts: Subsequence(ts[1], ts[3]))\ 45 | .setName('subsequence') 46 | 47 | chainable = extract | dot | emptyDot | subscript | subsequence | literals 48 | chain = OneOrMore(chainable).setParseAction(lambda ts: Sequence(ts.asList())).setName('chain') 49 | 50 | comma = delimitedList(chain).setParseAction(Comma) 51 | 52 | projectionItem = ( 53 | (key() + ':' + chain).setParseAction(lambda ts: ProjectionItem(ts[0], ts[2])) | 54 | key().setParseAction(lambda ts: ProjectionItem(ts[0], Dot(ts[0]))) 55 | ).setName('projection_item') 56 | projection = ('{' + Optional(delimitedList(projectionItem, ',')) + '}').setParseAction(lambda ts: Projection(ts[1:-1])).setName('projection') 57 | 58 | comprehension = ('[' + operation + ']').setParseAction(lambda ts: Comprehension(ts[1])) 59 | 60 | operation << (comma | projection | comprehension | chain) 61 | 62 | piped = delimitedList(operation, '|').setParseAction(lambda ts: Sequence(ts.asList())) 63 | 64 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | yq 2 | == 3 | 4 | Pure Python implementation of a subset of the features of 5 | |jq|_ for ``YAML`` documents. 6 | 7 | **Status**: No active development planned, maintenance work only. This repository 8 | is superseded by https://github.com/kislyuk/yq in PyPI, starting with version ``2.0.0``. 9 | 10 | If you're looking for a way to do ``jq``-like filtering on ``YAML`` documents, 11 | you'll probably be better off using the ``yq`` provided by |y2j|_ or |kislyuk|_. They 12 | provides a wrapper around ``jq`` that transforms the input ``YAML`` into 13 | ``JSON``, calls out to ``jq``, then transforms the result back. This means it 14 | automatically supports the full feature set of ``jq``. 15 | 16 | If for some reason you need a pure python implementation, this repo is 17 | for you. 18 | 19 | Setup 20 | ----- 21 | 22 | :: 23 | 24 | pip install 'yq < 2.0.0' 25 | 26 | What's implemented? 27 | ------------------- 28 | 29 | Everything from the `Basic 30 | Filters `__ section 31 | of the ``jq`` manual is supported - at least ``yq`` acts the same way as 32 | ``jq`` in the examples there. Object construction also more or less 33 | works. 34 | 35 | Known limitations 36 | ----------------- 37 | 38 | The parsing technology used in this project (parser combinators) is 39 | not powerful enough to support parsing some of the more complex features of 40 | ``jq``. 41 | 42 | Hacking 43 | ------- 44 | 45 | :: 46 | 47 | virtualenv virtualenv 48 | . virtualenv/bin/activate 49 | pip install -r requirements.txt 50 | 51 | # Optionally install as a package: 52 | pip install . 53 | 54 | There are basic unit-tests for some of the operators, but the meat of 55 | the test suite are the functional tests defined in 56 | ``functional_tests/*.yml`` files and run by ``run_functional_tests.py``. 57 | 58 | The whole test suite of ``jq`` is in ``functional_tests/jq.txt``, run by 59 | ``run_jq_tests.py``. Sections starting with a line containing ``skip`` 60 | are, surprisingly, skipped - until that functionality is implemented. 61 | 62 | They're all started by ``nosetests`` 63 | 64 | Roadmap 65 | ------- 66 | 67 | Here are some steps that'd need to be taken to bring the feature set of 68 | this ``yq`` closer to that of ``jq``. 69 | 70 | - Work the same way as ``jq`` for the "Types and Values" section 71 | - Operators, functions 72 | - Check if operators can be refactored 73 | - Package, release 74 | - Conditionals, comparisons 75 | - "Advanced features", Assignment from the ``jq`` manual 76 | 77 | .. |jq| replace:: ``jq`` 78 | .. _jq: https://stedolan.github.io/jq/ 79 | .. |y2j| replace:: ``y2j`` 80 | .. _y2j: https://github.com/wildducktheories/y2j 81 | .. _kislyuk: https://github.com/kislyuk/yq 82 | .. |kislyuk| replace:: ``kislyuk/yq`` 83 | -------------------------------------------------------------------------------- /yq/data/jq.txt: -------------------------------------------------------------------------------- 1 | # Tests are groups of three lines: program, input, expected output 2 | # Blank lines and lines starting with # are ignored 3 | 4 | # 5 | # Simple value tests to check parser. Input is irrelevant 6 | # 7 | 8 | true 9 | null 10 | true 11 | 12 | false 13 | null 14 | false 15 | 16 | null 17 | 42 18 | null 19 | 20 | 1 21 | null 22 | 1 23 | 24 | -1 25 | null 26 | -1 27 | 28 | # FIXME: much more number testing needed 29 | 30 | {} 31 | null 32 | {} 33 | 34 | skip 35 | [] 36 | null 37 | [] 38 | 39 | skip 40 | {x: -1} 41 | null 42 | {"x": -1} 43 | 44 | # The input line starts with a 0xFEFF (byte order mark) codepoint 45 | # No, there is no reason to have a byte order mark in UTF8 text. 46 | # But apparently people do, so jq shouldn't break on it. 47 | skip 48 | . 49 | "byte order mark" 50 | "byte order mark" 51 | 52 | # We test escapes by matching them against Unicode codepoints 53 | # FIXME: more tests needed for weird unicode stuff (e.g. utf16 pairs) 54 | skip 55 | "Aa\r\n\t\b\f\u03bc" 56 | null 57 | "Aa\u000d\u000a\u0009\u0008\u000c\u03bc" 58 | 59 | skip 60 | . 61 | "Aa\r\n\t\b\f\u03bc" 62 | "Aa\u000d\u000a\u0009\u0008\u000c\u03bc" 63 | 64 | skip 65 | "inter\("pol" + "ation")" 66 | null 67 | "interpolation" 68 | 69 | skip 70 | @text,@json,([1,.] | @csv),@html,@uri,@sh,@base64 71 | "<>&'\"" 72 | "<>&'\"" 73 | "\"<>&'\\\"\"" 74 | "1,\"<>&'\"\"\"" 75 | "<>&'"" 76 | "%3c%3e%26'%22" 77 | "'<>&'\\''\"'" 78 | "PD4mJyI=" 79 | 80 | skip 81 | @uri 82 | "\u03bc" 83 | "%ce%bc" 84 | 85 | skip 86 | @html "\(.)" 87 | "" 88 | "<script>hax</script>" 89 | 90 | skip 91 | [.[]|tojson|fromjson] 92 | ["foo", 1, ["a", 1, "b", 2, {"foo":"bar"}]] 93 | ["foo",1,["a",1,"b",2,{"foo":"bar"}]] 94 | 95 | # 96 | # Dictionary construction syntax 97 | # 98 | 99 | skip 100 | {a: 1} 101 | null 102 | {"a":1} 103 | 104 | skip 105 | {a,b,(.d):.a,e:.b} 106 | {"a":1, "b":2, "c":3, "d":"c"} 107 | {"a":1, "b":2, "c":1, "e":2} 108 | 109 | skip 110 | {"a",b,"a$\(1+1)"} 111 | {"a":1, "b":2, "c":3, "a$2":4} 112 | {"a":1, "b":2, "a$2":4} 113 | 114 | # 115 | # Field access, piping 116 | # 117 | 118 | .foo 119 | {"foo": 42, "bar": 43} 120 | 42 121 | 122 | .foo | .bar 123 | {"foo": {"bar": 42}, "bar": "badvalue"} 124 | 42 125 | 126 | .foo.bar 127 | {"foo": {"bar": 42}, "bar": "badvalue"} 128 | 42 129 | 130 | .foo_bar 131 | {"foo_bar": 2} 132 | 2 133 | 134 | .["foo"].bar 135 | {"foo": {"bar": 42}, "bar": "badvalue"} 136 | 42 137 | 138 | ."foo"."bar" 139 | {"foo": {"bar": 20}} 140 | 20 141 | 142 | 143 | # 144 | # Multiple outputs, iteration 145 | # 146 | 147 | .[] 148 | [1,2,3] 149 | 1 150 | 2 151 | 3 152 | 153 | skip 154 | 1,1 155 | [] 156 | 1 157 | 1 158 | 159 | skip 160 | 1,. 161 | [] 162 | 1 163 | [] 164 | 165 | skip 166 | [.] 167 | [2] 168 | [[2]] 169 | 170 | skip 171 | [[2]] 172 | [3] 173 | [[2]] 174 | 175 | skip 176 | [{}] 177 | [2] 178 | [{}] 179 | 180 | skip 181 | [.[]] 182 | ["a"] 183 | ["a"] 184 | 185 | skip 186 | [(.,1),((.,.[]),(2,3))] 187 | ["a","b"] 188 | [["a","b"],1,["a","b"],"a","b",2,3] 189 | 190 | skip 191 | [([5,5][]),.,.[]] 192 | [1,2,3] 193 | [5,5,[1,2,3],1,2,3] 194 | 195 | skip 196 | {x: (1,2)},{x:3} | .x 197 | null 198 | 1 199 | 2 200 | 3 201 | 202 | # 203 | # Slices 204 | # 205 | 206 | skip 207 | [.[3:2], .[-5:4], .[:-2], .[-2:], .[3:3][1:], .[10:]] 208 | [0,1,2,3,4,5,6] 209 | [[], [2,3], [0,1,2,3,4], [5,6], [], []] 210 | 211 | skip 212 | [.[3:2], .[-5:4], .[:-2], .[-2:], .[3:3][1:], .[10:]] 213 | "abcdefghi" 214 | ["","","abcdefg","hi","",""] 215 | 216 | skip 217 | del(.[2:4],.[0],.[-2:]) 218 | [0,1,2,3,4,5,6,7] 219 | [1,4,5] 220 | 221 | skip 222 | .[2:4] = ([], ["a","b"], ["a","b","c"]) 223 | [0,1,2,3,4,5,6,7] 224 | [0,1,4,5,6,7] 225 | [0,1,"a","b",4,5,6,7] 226 | [0,1,"a","b","c",4,5,6,7] 227 | 228 | 229 | # 230 | # Variables 231 | # 232 | 233 | skip 234 | 1 as $x | 2 as $y | [$x,$y,$x] 235 | null 236 | [1,2,1] 237 | 238 | skip 239 | [1,2,3][] as $x | [[4,5,6,7][$x]] 240 | null 241 | [5] 242 | [6] 243 | [7] 244 | 245 | skip 246 | 42 as $x | . | . | . + 432 | $x + 1 247 | 34324 248 | 43 249 | 250 | skip 251 | 1 as $x | [$x,$x,$x as $x | $x] 252 | null 253 | [1,1,1] 254 | 255 | # [.,(.[] | {x:.},.),.,.[]] 256 | 257 | # 258 | # Builtin functions 259 | # 260 | 261 | skip 262 | 1+1 263 | null 264 | 2 265 | 266 | skip 267 | 1+1 268 | "wtasdf" 269 | 2.0 270 | 271 | skip 272 | 2-1 273 | null 274 | 1 275 | 276 | skip 277 | 2-(-1) 278 | null 279 | 3 280 | 281 | skip 282 | 1e+0+0.001e3 283 | "I wonder what this will be?" 284 | 20e-1 285 | 286 | skip 287 | .+4 288 | 15 289 | 19.0 290 | 291 | skip 292 | .+null 293 | {"a":42} 294 | {"a":42} 295 | 296 | skip 297 | null+. 298 | null 299 | null 300 | 301 | skip 302 | .a+.b 303 | {"a":42} 304 | 42 305 | 306 | skip 307 | [1,2,3] + [.] 308 | null 309 | [1,2,3,null] 310 | 311 | skip 312 | {"a":1} + {"b":2} + {"c":3} 313 | "asdfasdf" 314 | {"a":1, "b":2, "c":3} 315 | 316 | skip 317 | "asdf" + "jkl;" + . + . + . 318 | "some string" 319 | "asdfjkl;some stringsome stringsome string" 320 | 321 | skip 322 | "\u0000\u0020\u0000" + . 323 | "\u0000\u0020\u0000" 324 | "\u0000 \u0000\u0000 \u0000" 325 | 326 | skip 327 | 42 - . 328 | 11 329 | 31 330 | 331 | skip 332 | [1,2,3,4,1] - [.,3] 333 | 1 334 | [2,4] 335 | 336 | skip 337 | [10 * 20, 20 / .] 338 | 4 339 | [200, 5] 340 | 341 | skip 342 | 1 + 2 * 2 + 10 / 2 343 | null 344 | 10 345 | 346 | skip 347 | [16 / 4 / 2, 16 / 4 * 2, 16 - 4 - 2, 16 - 4 + 2] 348 | null 349 | [2, 8, 10, 14] 350 | 351 | skip 352 | 25 % 7 353 | null 354 | 4 355 | 356 | skip 357 | 49732 % 472 358 | null 359 | 172 360 | 361 | skip 362 | 1 + tonumber + ("10" | tonumber) 363 | 4 364 | 15 365 | 366 | skip 367 | [{"a":42},.object,10,.num,false,true,null,"b",[1,4]] | .[] as $x | [$x == .[]] 368 | {"object": {"a":42}, "num":10.0} 369 | [true, true, false, false, false, false, false, false, false] 370 | [true, true, false, false, false, false, false, false, false] 371 | [false, false, true, true, false, false, false, false, false] 372 | [false, false, true, true, false, false, false, false, false] 373 | [false, false, false, false, true, false, false, false, false] 374 | [false, false, false, false, false, true, false, false, false] 375 | [false, false, false, false, false, false, true, false, false] 376 | [false, false, false, false, false, false, false, true, false] 377 | [false, false, false, false, false, false, false, false, true ] 378 | 379 | skip 380 | [.[] | length] 381 | [[], {}, [1,2], {"a":42}, "asdf", "\u03bc"] 382 | [0, 0, 2, 1, 4, 1] 383 | 384 | skip 385 | map(keys) 386 | [{}, {"abcd":1,"abc":2,"abcde":3}, {"x":1, "z": 3, "y":2}] 387 | [[], ["abc","abcd","abcde"], ["x","y","z"]] 388 | 389 | skip 390 | [1,2,empty,3,empty,4] 391 | null 392 | [1,2,3,4] 393 | 394 | skip 395 | map(add) 396 | [[], [1,2,3], ["a","b","c"], [[3],[4,5],[6]], [{"a":1}, {"b":2}, {"a":3}]] 397 | [null, 6, "abc", [3,4,5,6], {"a":3, "b": 2}] 398 | 399 | # 400 | # User-defined functions 401 | # Oh god. 402 | # 403 | 404 | skip 405 | def f: . + 1; def g: def g: . + 100; f | g | f; (f | g), g 406 | 3.0 407 | 106.0 408 | 105.0 409 | 410 | skip 411 | def f: (1000,2000); f 412 | 123412345 413 | 1000 414 | 2000 415 | 416 | skip 417 | def f(a;b;c;d;e;f): [a+1,b,c,d,e,f]; f(.[0];.[1];.[0];.[0];.[0];.[0]) 418 | [1,2] 419 | [2,2,1,1,1,1] 420 | 421 | skip 422 | ([1,2] + [4,5]) 423 | [1,2,3] 424 | [1,2,4,5] 425 | 426 | skip 427 | true 428 | [1] 429 | true 430 | 431 | skip 432 | null,1,null 433 | "hello" 434 | null 435 | 1 436 | null 437 | 438 | skip 439 | [1,2,3] 440 | [5,6] 441 | [1,2,3] 442 | 443 | skip 444 | [.[]|floor] 445 | [-1.1,1.1,1.9] 446 | [-2, 1, 1] 447 | 448 | skip 449 | [.[]|sqrt] 450 | [4,9] 451 | [2,3] 452 | 453 | skip 454 | (add / length) as $m | map((. - $m) as $d | $d * $d) | add / length | sqrt 455 | [2,4,4,4,5,5,7,9] 456 | 2 457 | 458 | skip 459 | def f(x): x | x; f([.], . + [42]) 460 | [1,2,3] 461 | [[[1,2,3]]] 462 | [[1,2,3],42] 463 | [[1,2,3,42]] 464 | [1,2,3,42,42] 465 | 466 | # test closures and lexical scoping 467 | skip 468 | def id(x):x; 2000 as $x | def f(x):1 as $x | id([$x, x, x]); def g(x): 100 as $x | f($x,$x+x); g($x) 469 | "more testing" 470 | [1,100,2100.0,100,2100.0] 471 | 472 | # test backtracking through function calls and returns 473 | # this test is *evil* 474 | skip 475 | [[20,10][1,0] as $x | def f: (100,200) as $y | def g: [$x + $y, .]; . + $x | g; f[0] | [f][0][1] | f] 476 | 999999999 477 | [[110.0, 130.0], [210.0, 130.0], [110.0, 230.0], [210.0, 230.0], [120.0, 160.0], [220.0, 160.0], [120.0, 260.0], [220.0, 260.0]] 478 | 479 | # test recursion 480 | skip 481 | def fac: if . == 1 then 1 else . * (. - 1 | fac) end; [.[] | fac] 482 | [1,2,3,4] 483 | [1,2,6,24] 484 | 485 | # test stack overflow and reallocation 486 | # this test is disabled for now, it takes a realllllly long time. 487 | # def f: if length > 1000 then . else .+[1]|f end; f | length 488 | # [] 489 | # 1001 490 | 491 | skip 492 | reduce .[] as $x (0; . + $x) 493 | [1,2,4] 494 | 7 495 | 496 | # 497 | # Paths 498 | # 499 | 500 | skip 501 | path(.foo[0,1]) 502 | null 503 | ["foo", 0] 504 | ["foo", 1] 505 | 506 | skip 507 | path(.[] | select(.>3)) 508 | [1,5,3] 509 | [1] 510 | 511 | skip 512 | path(.) 513 | 42 514 | [] 515 | 516 | skip 517 | [paths] 518 | [1,[[],{"a":2}]] 519 | [[0],[1],[1,0],[1,1],[1,1,"a"]] 520 | 521 | skip 522 | [leaf_paths] 523 | [1,[[],{"a":2}]] 524 | [[0],[1,1,"a"]] 525 | 526 | skip 527 | ["foo",1] as $p | getpath($p), setpath($p; 20), delpaths([$p]) 528 | {"bar": 42, "foo": ["a", "b", "c", "d"]} 529 | "b" 530 | {"bar": 42, "foo": ["a", 20, "c", "d"]} 531 | {"bar": 42, "foo": ["a", "c", "d"]} 532 | 533 | skip 534 | map(getpath([2])), map(setpath([2]; 42)), map(delpaths([[2]])) 535 | [[0], [0,1], [0,1,2]] 536 | [null, null, 2] 537 | [[0,null,42], [0,1,42], [0,1,42]] 538 | [[0], [0,1], [0,1]] 539 | 540 | skip 541 | map(delpaths([[0,"foo"]])) 542 | [[{"foo":2, "x":1}], [{"bar":2}]] 543 | [[{"x":1}], [{"bar":2}]] 544 | 545 | skip 546 | ["foo",1] as $p | getpath($p), setpath($p; 20), delpaths([$p]) 547 | {"bar":false} 548 | null 549 | {"bar":false, "foo": [null, 20]} 550 | {"bar":false} 551 | 552 | skip 553 | delpaths([[-200]]) 554 | [1,2,3] 555 | [1,2,3] 556 | 557 | skip 558 | del(.), del(empty), del((.foo,.bar,.baz) | .[2,3,0]), del(.foo[0], .bar[0], .foo, .baz.bar[0].x) 559 | {"foo": [0,1,2,3,4], "bar": [0,1]} 560 | null 561 | {"foo": [0,1,2,3,4], "bar": [0,1]} 562 | {"foo": [1,4], "bar": [1]} 563 | {"bar": [1]} 564 | 565 | # 566 | # Assignment 567 | # 568 | skip 569 | .message = "goodbye" 570 | {"message": "hello"} 571 | {"message": "goodbye"} 572 | 573 | skip 574 | .foo = .bar 575 | {"bar":42} 576 | {"foo":42, "bar":42} 577 | 578 | skip 579 | .foo |= .+1 580 | {"foo": 42} 581 | {"foo": 43} 582 | 583 | skip 584 | .[] += 2, .[] *= 2, .[] -= 2, .[] /= 2, .[] %=2 585 | [1,3,5] 586 | [3,5,7] 587 | [2,6,10] 588 | [-1,1,3] 589 | [0.5, 1.5, 2.5] 590 | [1,1,1] 591 | 592 | skip 593 | [.[] % 7] 594 | [-7,-6,-5,-4,-3,-2,-1,0,1,2,3,4,5,6,7] 595 | [0,-6,-5,-4,-3,-2,-1,0,1,2,3,4,5,6,0] 596 | 597 | skip 598 | .foo += .foo 599 | {"foo":2} 600 | {"foo":4} 601 | 602 | skip 603 | .[0].a |= {"old":., "new":(.+1)} 604 | [{"a":1,"b":2}] 605 | [{"a":{"old":1, "new":2},"b":2}] 606 | 607 | skip 608 | def inc(x): x |= .+1; inc(.[].a) 609 | [{"a":1,"b":2},{"a":2,"b":4},{"a":7,"b":8}] 610 | [{"a":2,"b":2},{"a":3,"b":4},{"a":8,"b":8}] 611 | 612 | skip 613 | .[2][3] = 1 614 | [4] 615 | [4, null, [null, null, null, 1]] 616 | 617 | skip 618 | .foo[2].bar = 1 619 | {"foo":[11], "bar":42} 620 | {"foo":[11,null,{"bar":1}], "bar":42} 621 | 622 | # 623 | # Conditionals 624 | # 625 | 626 | skip 627 | [.[] | if .foo then "yep" else "nope" end] 628 | [{"foo":0},{"foo":1},{"foo":[]},{"foo":true},{"foo":false},{"foo":null},{"foo":"foo"},{}] 629 | ["yep","yep","yep","yep","nope","nope","yep","nope"] 630 | 631 | skip 632 | [.[] | if .baz then "strange" elif .foo then "yep" else "nope" end] 633 | [{"foo":0},{"foo":1},{"foo":[]},{"foo":true},{"foo":false},{"foo":null},{"foo":"foo"},{}] 634 | ["yep","yep","yep","yep","nope","nope","yep","nope"] 635 | 636 | 637 | # FIXME: define/test behaviour of 'if (.foo,.bar) then A else B end' 638 | 639 | skip 640 | [.[] | [.foo[] // .bar]] 641 | [{"foo":[1,2], "bar": 42}, {"foo":[1], "bar": null}, {"foo":[null,false,3], "bar": 18}, {"foo":[], "bar":42}, {"foo": [null,false,null], "bar": 41}] 642 | [[1,2], [1], [3], [42], [41]] 643 | 644 | skip 645 | .[] //= .[0] 646 | ["hello",true,false,[false],null] 647 | ["hello",true,"hello",[false],"hello"] 648 | 649 | skip 650 | .[] | [.[0] and .[1], .[0] or .[1]] 651 | [[true,[]], [false,1], [42,null], [null,false]] 652 | [true,true] 653 | [false,true] 654 | [false,true] 655 | [false,false] 656 | 657 | skip 658 | [.[] | not] 659 | [1,0,false,null,true,"hello"] 660 | [false,false,true,true,false,false] 661 | 662 | # Check numeric comparison binops 663 | skip 664 | [10 > 0, 10 > 10, 10 > 20, 10 < 0, 10 < 10, 10 < 20] 665 | {} 666 | [true,false,false,false,false,true] 667 | 668 | skip 669 | [10 >= 0, 10 >= 10, 10 >= 20, 10 <= 0, 10 <= 10, 10 <= 20] 670 | {} 671 | [true,true,false,false,true,true] 672 | 673 | # And some in/equality tests 674 | skip 675 | [ 10 == 10, 10 != 10, 10 != 11, 10 == 11] 676 | {} 677 | [true,false,true,false] 678 | 679 | skip 680 | ["hello" == "hello", "hello" != "hello", "hello" == "world", "hello" != "world" ] 681 | {} 682 | [true,false,false,true] 683 | 684 | skip 685 | [[1,2,3] == [1,2,3], [1,2,3] != [1,2,3], [1,2,3] == [4,5,6], [1,2,3] != [4,5,6]] 686 | {} 687 | [true,false,false,true] 688 | 689 | skip 690 | [{"foo":42} == {"foo":42},{"foo":42} != {"foo":42}, {"foo":42} != {"bar":42}, {"foo":42} == {"bar":42}] 691 | {} 692 | [true,false,true,false] 693 | 694 | # ugly complicated thing 695 | skip 696 | [{"foo":[1,2,{"bar":18},"world"]} == {"foo":[1,2,{"bar":18},"world"]},{"foo":[1,2,{"bar":18},"world"]} == {"foo":[1,2,{"bar":19},"world"]}] 697 | {} 698 | [true,false] 699 | 700 | # containment operator 701 | skip 702 | [("foo" | contains("foo")), ("foobar" | contains("foo")), ("foo" | contains("foobar"))] 703 | {} 704 | [true, true, false] 705 | 706 | # string operations 707 | skip 708 | [.[]|startswith("foo")] 709 | ["fo", "foo", "barfoo", "foobar", "barfoob"] 710 | [false, true, false, true, false] 711 | 712 | skip 713 | [.[]|endswith("foo")] 714 | ["fo", "foo", "barfoo", "foobar", "barfoob"] 715 | [false, true, true, false, false] 716 | 717 | skip 718 | [.[]|ltrimstr("foo")] 719 | ["fo", "foo", "barfoo", "foobar", "afoo"] 720 | ["fo","","barfoo","bar","afoo"] 721 | 722 | skip 723 | [.[]|rtrimstr("foo")] 724 | ["fo", "foo", "barfoo", "foobar", "foob"] 725 | ["fo","","bar","foobar","foob"] 726 | 727 | skip 728 | .[","] 729 | "a,bc,def,ghij,klmno" 730 | [1,4,8,13] 731 | 732 | skip 733 | .[", "] 734 | "a,bc,def,ghij,klmno" 735 | [] 736 | 737 | skip 738 | .[", "] 739 | "a,b,, c, d,ef, , ghi, jklmn, o" 740 | [4,7,13,15,20,27] 741 | 742 | skip 743 | [(index(","), rindex(","))] 744 | "a,bc,def,ghij,klmno" 745 | [1,13] 746 | 747 | skip 748 | [.[]|split(",")] 749 | ["a, bc, def, ghij, jklmn, a,b, c,d, e,f", "a,b,c,d, e,f,g,h"] 750 | [["a"," bc"," def"," ghij"," jklmn"," a","b"," c","d"," e","f"],["a","b","c","d"," e","f","g","h"]] 751 | 752 | skip 753 | [.[]|split(", ")] 754 | ["a, bc, def, ghij, jklmn, a,b, c,d, e,f", "a,b,c,d, e,f,g,h"] 755 | [["a","bc","def","ghij","jklmn","a,b","c,d","e,f"],["a,b,c,d","e,f,g,h"]] 756 | 757 | skip 758 | [.[] * 3] 759 | ["a", "ab", "abc"] 760 | ["aaa", "ababab", "abcabcabc"] 761 | 762 | skip 763 | [.[] / ","] 764 | ["a, bc, def, ghij, jklmn, a,b, c,d, e,f", "a,b,c,d, e,f,g,h"] 765 | [["a"," bc"," def"," ghij"," jklmn"," a","b"," c","d"," e","f"],["a","b","c","d"," e","f","g","h"]] 766 | 767 | skip 768 | [.[] / ", "] 769 | ["a, bc, def, ghij, jklmn, a,b, c,d, e,f", "a,b,c,d, e,f,g,h"] 770 | [["a","bc","def","ghij","jklmn","a,b","c,d","e,f"],["a,b,c,d","e,f,g,h"]] 771 | 772 | skip 773 | map(.[1] as $needle | .[0] | contains($needle)) 774 | [[[],[]], [[1,2,3], [1,2]], [[1,2,3], [3,1]], [[1,2,3], [4]], [[1,2,3], [1,4]]] 775 | [true, true, true, false, false] 776 | 777 | skip 778 | map(.[1] as $needle | .[0] | contains($needle)) 779 | [[["foobar", "foobaz"], ["baz", "bar"]], [["foobar", "foobaz"], ["foo"]], [["foobar", "foobaz"], ["blap"]]] 780 | [true, true, false] 781 | 782 | skip 783 | [({foo: 12, bar:13} | contains({foo: 12})), ({foo: 12} | contains({})), ({foo: 12, bar:13} | contains({baz:14}))] 784 | {} 785 | [true, true, false] 786 | 787 | skip 788 | {foo: {baz: 12, blap: {bar: 13}}, bar: 14} | contains({bar: 14, foo: {blap: {}}}) 789 | {} 790 | true 791 | 792 | skip 793 | {foo: {baz: 12, blap: {bar: 13}}, bar: 14} | contains({bar: 14, foo: {blap: {bar: 14}}}) 794 | {} 795 | false 796 | 797 | skip 798 | sort 799 | [42,[2,5,3,11],10,{"a":42,"b":2},{"a":42},true,2,[2,6],"hello",null,[2,5,6],{"a":[],"b":1},"abc","ab",[3,10],{},false,"abcd",null] 800 | [null,null,false,true,2,10,42,"ab","abc","abcd","hello",[2,5,3,11],[2,5,6],[2,6],[3,10],{},{"a":42},{"a":42,"b":2},{"a":[],"b":1}] 801 | 802 | skip 803 | (sort_by(.b) | sort_by(.a)), sort_by(.a, .b), sort_by(.b, .c), group_by(.b), group_by(.a + .b - .c == 2) 804 | [{"a": 1, "b": 4, "c": 14}, {"a": 4, "b": 1, "c": 3}, {"a": 1, "b": 4, "c": 3}, {"a": 0, "b": 2, "c": 43}] 805 | [{"a": 0, "b": 2, "c": 43}, {"a": 1, "b": 4, "c": 14}, {"a": 1, "b": 4, "c": 3}, {"a": 4, "b": 1, "c": 3}] 806 | [{"a": 0, "b": 2, "c": 43}, {"a": 1, "b": 4, "c": 14}, {"a": 1, "b": 4, "c": 3}, {"a": 4, "b": 1, "c": 3}] 807 | [{"a": 4, "b": 1, "c": 3}, {"a": 0, "b": 2, "c": 43}, {"a": 1, "b": 4, "c": 3}, {"a": 1, "b": 4, "c": 14}] 808 | [[{"a": 4, "b": 1, "c": 3}], [{"a": 0, "b": 2, "c": 43}], [{"a": 1, "b": 4, "c": 14}, {"a": 1, "b": 4, "c": 3}]] 809 | [[{"a": 1, "b": 4, "c": 14}, {"a": 0, "b": 2, "c": 43}], [{"a": 4, "b": 1, "c": 3}, {"a": 1, "b": 4, "c": 3}]] 810 | 811 | skip 812 | unique 813 | [1,2,5,3,5,3,1,3] 814 | [1,2,3,5] 815 | 816 | skip 817 | unique 818 | [] 819 | [] 820 | 821 | skip 822 | [min, max, min_by(.[1]), max_by(.[1]), min_by(.[2]), max_by(.[2])] 823 | [[4,2,"a"],[3,1,"a"],[2,4,"a"],[1,3,"a"]] 824 | [[1,3,"a"],[4,2,"a"],[3,1,"a"],[2,4,"a"],[4,2,"a"],[1,3,"a"]] 825 | 826 | skip 827 | [min,max,min_by(.),max_by(.)] 828 | [] 829 | [null,null,null,null] 830 | 831 | skip 832 | .foo[.baz] 833 | {"foo":{"bar":4},"baz":"bar"} 834 | 4 835 | 836 | skip 837 | .[] | .error = "no, it's OK" 838 | [{"error":true}] 839 | {"error": "no, it's OK"} 840 | 841 | skip 842 | [{a:1}] | .[] | .a=999 843 | null 844 | {"a": 999} 845 | 846 | skip 847 | to_entries 848 | {"a": 1, "b": 2} 849 | [{"key":"a", "value":1}, {"key":"b", "value":2}] 850 | 851 | skip 852 | from_entries 853 | [{"key":"a", "value":1}, {"key":"b", "value":2}] 854 | {"a": 1, "b": 2} 855 | 856 | skip 857 | with_entries(.key |= "KEY_" + .) 858 | {"a": 1, "b": 2} 859 | {"KEY_a": 1, "KEY_b": 2} 860 | 861 | skip 862 | map(has("foo")) 863 | [{"foo": 42}, {}] 864 | [true, false] 865 | 866 | skip 867 | map(has(2)) 868 | [[0,1], ["a","b","c"]] 869 | [false, true] 870 | 871 | skip 872 | keys 873 | [42,3,35] 874 | [0,1,2] 875 | 876 | skip 877 | [][.] 878 | 1000000000000000000 879 | null 880 | 881 | skip 882 | map([1,2][0:.]) 883 | [-1, 1, 2, 3, 1000000000000000000] 884 | [[1], [1], [1,2], [1,2], [1,2]] --------------------------------------------------------------------------------