diff --git a/cznicinfo/cli.py b/cznicinfo/cli.py
index 696ce61425cf24192a3f29461004bbb8bbc62a39..b16b9ddaafe3aa63ee55ac86cd6dcca8953020cc 100644
--- a/cznicinfo/cli.py
+++ b/cznicinfo/cli.py
@@ -6,7 +6,8 @@ Usage: cznicinfo <command> [<args>...]
 
 Commands:
   list        List available projects
-  show        Show project information
+  show        Show project(s) information
+  find        Find projects using regexp
   versions    Show versions of project and its packages
 
 Options:
@@ -18,7 +19,7 @@ from docopt import docopt
 import sys
 
 from . import __version__
-from . import commands
+from . import commands  # noqa: F401 (dynamic import)
 from . import exception
 from .util import log
 
@@ -56,7 +57,6 @@ def cznicinfo(*cargs):
         print(log.T.bold_yellow(str(ex)))
         return ex.exit_code
 
-
     return 0
 
 
diff --git a/cznicinfo/commands/find.py b/cznicinfo/commands/find.py
new file mode 100644
index 0000000000000000000000000000000000000000..d6d78f958a588af0ff5aaf29224c0b0d0482059e
--- /dev/null
+++ b/cznicinfo/commands/find.py
@@ -0,0 +1,44 @@
+"""
+Find projects using regexp
+
+Usage: cznicinfo find [-h] [-f pretty|yaml|list] <query>...
+
+Arguments:
+    <query>  can be either:
+             * python regex to match again relevant project attributes
+             * ATTR:REGEX string where ATTR is a project attribute
+               to match REGEX against. Lists kind of supported.
+
+Options:
+    -f, --format=<fmt>    Output format:
+                          'pretty', 'yaml' or 'list' [default: pretty]
+"""
+
+from docopt import docopt
+
+from cznicinfo import infocore
+# from cznicinfo.util import log
+
+
+def run_command(*args, **kwargs):
+    cargs = docopt(__doc__)
+    fmt = cargs['--format']
+
+    info = infocore.get_info()
+    projs = infocore.filter_projects(info, cargs['<query>'])
+
+    if fmt == 'list':
+        infocore.pretty_list_projects(projs)
+    elif fmt == 'yaml':
+        infocore.pretty_print_data(projs)
+    else:
+        # default --format is pretty print
+        first = True
+        for proj in projs:
+            if first:
+                first = False
+            else:
+                print()
+            infocore.pretty_print_project(proj)
+
+    return 0
diff --git a/cznicinfo/commands/list.py b/cznicinfo/commands/list.py
index 2556de8b4addeb22b4e8eab10dacec1e633c85aa..d0987f285033e75d8a6fcf837c2aed6049cf2210 100644
--- a/cznicinfo/commands/list.py
+++ b/cznicinfo/commands/list.py
@@ -4,17 +4,15 @@ List available projects
 Usage: cznicinfo list [-h]
 """
 
-from docopt import docopt
+# from docopt import docopt
 
-from cznicinfo import infoparse
-from cznicinfo.util import log
+from cznicinfo import infocore
 
 
 def run_command(*args, **kwargs):
-    cargs = docopt(__doc__)
+    # cargs = docopt(__doc__)
 
-    info = infoparse.get_info();
-    for proj in info['projects']:
-        print("%s: %s" % (proj['short-name'], proj['full-name']))
+    info = infocore.get_info()
+    infocore.pretty_list_projects(info)
 
     return 0
diff --git a/cznicinfo/commands/show.py b/cznicinfo/commands/show.py
index e2f36bf65424760b73d45400f2c6b3b49ba81405..c76aefc065c6a690faf503545fb4a5a1ed0c7e3d 100644
--- a/cznicinfo/commands/show.py
+++ b/cznicinfo/commands/show.py
@@ -9,7 +9,7 @@ Options:
 
 from docopt import docopt
 
-from cznicinfo import infoparse
+from cznicinfo import infocore
 from cznicinfo.util import log
 
 
@@ -17,8 +17,8 @@ def run_command(*args, **kwargs):
     cargs = docopt(__doc__)
     fmt = cargs['--format']
 
-    info = infoparse.get_info();
-    projs, not_found = infoparse.get_projects_by_name(info, cargs['<project>'])
+    info = infocore.get_info()
+    projs, not_found = infocore.get_projects_by_name(info, cargs['<project>'])
 
     if not_found:
         nf_str = ", ".join(not_found)
@@ -26,7 +26,7 @@ def run_command(*args, **kwargs):
         return 4
 
     if fmt == 'yaml':
-        infoparse.pretty_print_data(projs)
+        infocore.pretty_print_data(projs)
     else:
         # default --format is pretty print
         first = True
@@ -35,6 +35,6 @@ def run_command(*args, **kwargs):
                 first = False
             else:
                 print()
-            infoparse.pretty_print_project(proj)
+            infocore.pretty_print_project(proj)
 
     return 0
diff --git a/cznicinfo/commands/versions.py b/cznicinfo/commands/versions.py
index 21d84060a061d51b1f57e27814234d0da8108e54..62255df14d71e8969f162c1f38cc811810fc8e8c 100644
--- a/cznicinfo/commands/versions.py
+++ b/cznicinfo/commands/versions.py
@@ -6,7 +6,7 @@ Usage: cznicinfo versions [-h] <project>...
 
 from docopt import docopt
 
-from cznicinfo import infoparse
+from cznicinfo import infocore
 from cznicinfo import versions
 from cznicinfo.util import log
 
@@ -14,9 +14,9 @@ from cznicinfo.util import log
 def run_command(*args, **kwargs):
     cargs = docopt(__doc__)
 
-    info = infoparse.get_info();
+    info = infocore.get_info()
 
-    projs, not_found = infoparse.get_projects_by_name(info, cargs['<project>'])
+    projs, not_found = infocore.get_projects_by_name(info, cargs['<project>'])
 
     if not_found:
         nf_str = ", ".join(not_found)
diff --git a/cznicinfo/exception.py b/cznicinfo/exception.py
index a479b75a685c817717ac761c3c626d7043e3c887..fecdc9eaddc7ce398c45a2510ca7eb72b4a7ed3c 100644
--- a/cznicinfo/exception.py
+++ b/cznicinfo/exception.py
@@ -31,3 +31,8 @@ class DuplicateInfoItem(InvalidInfoStructure):
 
 class DuplicateInfoProjects(InvalidInfoStructure):
     msg_fmt = "Duplicate info projects: %(item)s"
+
+
+class InvalidProjectFilter(CZNICInfoException):
+    msg_fmt = "Invalid filter: %(why)"
+    exit_code = 12
diff --git a/cznicinfo/infoparse.py b/cznicinfo/infocore.py
similarity index 62%
rename from cznicinfo/infoparse.py
rename to cznicinfo/infocore.py
index 83099f36faf98f02a0d69a62cc148d1f3543e588..b9491da0fa580b91d6b37cadd8be5a2d9767e0fa 100644
--- a/cznicinfo/infoparse.py
+++ b/cznicinfo/infocore.py
@@ -1,6 +1,9 @@
 import yaml
 import os
+import re
 from collections import defaultdict
+from collections.abc import Iterable
+from functools import partial
 
 from cznicinfo.util import log
 from cznicinfo import exception
@@ -11,6 +14,13 @@ DEFAULT_RELEASE = {
     'branch': 'master'
 }
 
+
+# XXX: if thise file gets big, consider splitting:
+# * cznicinfo.info.core   (parsing)
+# * cznicinfo.info.query  (querying)
+# * cznicinfo.info.print  (pretty printing & other output)
+
+
 def get_info_fn(module_path=None):
     if module_path:
         path = module_path
@@ -32,7 +42,7 @@ def load_info(info_fn=None):
 
 def parse_projects(info):
     if 'projects' not in info:
-        raise exception.MissingRequiredInfoItem(item='projects list' % item)
+        raise exception.MissingRequiredInfoItem(item='projects list')
 
     unique_items = ['short-name', 'full-name']
     unique_lists = defaultdict(set)
@@ -41,7 +51,8 @@ def parse_projects(info):
         for item_name in unique_items:
             item_value = proj.get(item_name)
             if not item_value:
-                raise exception.MissingRequiredInfoItem(item='projects[%d].%s' % (i, item_name))
+                raise exception.MissingRequiredInfoItem(
+                        item='projects[%d].%s' % (i, item_name))
             if item_value in unique_lists[item_name]:
                 item_str = 'projects[%d].%s = %s' % (i, item_name, item_value)
                 raise exception.DuplicateInfoProjects(item=item_str)
@@ -71,15 +82,78 @@ def get_projects_by_name(info, names):
         for proj in info['projects']:
             if (proj['short-name'] == pname or
                proj['full-name'] == pname):
-                 found_projs.append(proj)
-                 found = True
-                 break
+                found_projs.append(proj)
+                found = True
+                break
         if not found:
             not_found.append(pname)
 
     return found_projs, not_found
 
 
+def split_query(query):
+    attr, _, rex = query.partition(':')
+    if rex:
+        return attr, rex
+    return None, attr
+
+
+def filter_projects(info, queries):
+    projs = info.get('projects', [])
+    qs = list(map(split_query, queries))
+    filter_wrapper = partial(_match_project, qs)
+    filtered_projs = list(filter(filter_wrapper, projs))
+    return filtered_projs
+
+
+def match_project_by_attr_rex(project, attr, rex):
+    val = project.get(attr)
+    if val is None:
+        return False
+    if isinstance(val, str):
+        if not re.search(rex, val):
+            return False
+    elif isinstance(val, Iterable):
+        # collection matches if any item of collection matches
+        found = False
+        for e in val:
+            if re.search(rex, e):
+                found = True
+                break
+        if not found:
+            return False
+    else:
+        raise exception.InvalidProjectFilter(
+            why=("Can only filter strings but '%s' is %s"
+                 % (attr, type(rex).__name__)))
+    return True
+
+
+def match_project_by_smart_rex(project, rex):
+    if re.search(rex, project['short-name']):
+        return True
+    if re.search(rex, project['full-name']):
+        return True
+    # TODO: match more stuff like URLs
+    return None
+
+
+def _match_project(queries, project):
+    for attr, rex in queries:
+        if attr:
+            m = match_project_by_attr_rex(project, attr, rex)
+        else:
+            m = match_project_by_smart_rex(project, rex)
+        if not m:
+            return False
+    return True
+
+
+def pretty_list_projects(projects_info):
+    for proj in projects_info:
+        print("%s: %s" % (proj['short-name'], proj['full-name']))
+
+
 def pretty_print_data(d):
     print(yaml.dump(d))
 
@@ -130,5 +204,3 @@ def pretty_print_project(project_info, indent=2):
                     n=cp['name'],
                     v=cp['build-status-url'],
                     spc=space, t=log.T))
-    # optional scalars
-    release_style = p.get('release-style')
diff --git a/cznicinfo/versions.py b/cznicinfo/versions.py
index 9859347d489de09c9807e9c93debbb4e5de1b41b..8858bd9c95f3c2eb977b63a11f49a5b42503c256 100644
--- a/cznicinfo/versions.py
+++ b/cznicinfo/versions.py
@@ -2,14 +2,14 @@ def get_project_versions_list(proj):
     vl = []
     upstream = proj.get('upstream')
     if upstream:
-       nvchecker_options = upstream.get('nvchecker-options')
-       if nvchecker_options:
-           base_nvopts = nvchecker_options
-           for rls in proj.get('releases', []):
-               nvopts = base_nvopts.copy()
-               if 'branch' in rls:
-                   nvopts['branch'] = rls['branch']
-               vl.append((rls['release'], nvopts))
+        nvchecker_options = upstream.get('nvchecker-options')
+        if nvchecker_options:
+            base_nvopts = nvchecker_options
+            for rls in proj.get('releases', []):
+                nvopts = base_nvopts.copy()
+                if 'branch' in rls:
+                    nvopts['branch'] = rls['branch']
+                vl.append((rls['release'], nvopts))
     return vl