Repository: d0c-s4vage/pfp
Pull request #91 (2019-10-24 14:08:37)

Adds console script for pfp
Example:

```
pfp -t ~/templates/png.bt --keep test.png
```

Help output:

```
usage: pfp [-h] -t TEMPLATE [-k] input

Run pfp on input data using a specified 010 Editor template for parsing

positional arguments:
  input                 The input data stream or file to parse. Use '-' for
                        piped data

optional arguments:
  -h, --help            show this help message and exit
  -t TEMPLATE, --template TEMPLATE
                        The template to parse with
  -k, --keep            Keep successfully parsed data on error
```


Patch:
diff --git a/docs/source/fields.rst b/docs/source/fields.rst
index 54eb477..919f66f 100644
--- a/docs/source/fields.rst
+++ b/docs/source/fields.rst
@@ -27,9 +27,30 @@ until the main dom is built again.
 Printing
 """"""""
 
-Use the :any:`pfp.fields.Field._pfp__show()` method to return a
+Use the :any:`pfp.fields.Field._pfp__show` method to return a
 pretty-printed representation of the field.
 
+Full Field Paths
+""""""""""""""""
+
+Use the :any:`pfp.fields.Field._pfp__path` method to fetch the full path
+of the field. E.g. in the template below, the ``inner`` field would have
+a full path of ``root.nested1.nested2.inner``, and the second element of
+the ``array`` field would have a full path of ``root.nested1.nested2.array[1]``:
+
+.. code-block:: c
+
+    struct {
+        struct {
+            struct {
+                char inner;
+                char array[4];
+            } nested2;
+            int some_int;
+        } nested1;
+        int some_int2;
+    } root;
+
 Structs
 ^^^^^^^
 
@@ -40,7 +61,7 @@ Field Reference Documentation
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
 .. autoclass:: pfp.fields.Field
-   :members: _pfp__name, _pfp__parent, _pfp__build, _pfp__parse, _pfp__watchers, _pfp__watch_fields, _pfp__width, _pfp__set_value, _pfp__show
+   :members: _pfp__name, _pfp__parent, _pfp__build, _pfp__parse, _pfp__watchers, _pfp__watch_fields, _pfp__width, _pfp__set_value, _pfp__show, _pfp__path
 
 .. autoclass:: pfp.fields.Array
    :members: width, field_cls, raw_data
diff --git a/pfp/__main__.py b/pfp/__main__.py
new file mode 100644
index 0000000..abceeed
--- /dev/null
+++ b/pfp/__main__.py
@@ -0,0 +1,61 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+
+"""
+Run pfp on input data using a specified 010 Editor template for parsing
+"""
+
+
+import argparse
+import os
+import pfp
+import sys
+
+
+def parse_args(argv):
+    parser = argparse.ArgumentParser("pfp", description=__doc__)
+
+    parser.add_argument(
+        "-t", "--template",
+        required=True,
+        help="The template to parse with",
+    )
+
+    parser.add_argument(
+        "-k", "--keep",
+        default=False,
+        action="store_true",
+        help="Keep successfully parsed data on error",
+    )
+
+    parser.add_argument(
+        "input",
+        type=argparse.FileType("rb"),
+        default=sys.stdin,
+        help="The input data stream or file to parse. Use '-' for piped data",
+    )
+
+    return parser.parse_args(argv[1:])
+
+
+def main(argv=None):
+    """Main function for this script
+
+    :argv: TODO
+    :returns: TODO
+    """
+    if argv is None:
+        argv = sys.argv
+
+    args = parse_args(argv)
+    dom = pfp.parse(
+        template_file=args.template,
+        data=args.input,
+        keep_successful=args.keep,
+    )
+    print(dom._pfp__show())
+
+
+if __name__ == "__main__":
+    main(sys.argv)
diff --git a/pfp/fields.py b/pfp/fields.py
index bf9670c..6ad101a 100644
--- a/pfp/fields.py
+++ b/pfp/fields.py
@@ -260,6 +260,36 @@ class Field(object):
         if stream is not None:
             self._pfp__parse(stream, save_offset=True)
 
+    # see #51 - fields should have a method of returning the full path of its name
+    def _pfp__path(self):
+        """Return the full pathname of this field. E.g. given
+        the template below, the ``a`` field would have a full
+        path of ``root.nested.a``
+
+        .. code-block:: c
+
+            struct {
+                struct {
+                    char a;
+                } nested;
+            } root;
+        """
+        curr = self
+        res = []
+        while curr is not None:
+            # don't show the meta __root name in the path
+            if curr._pfp__name == "__root" and curr._pfp__parent is None:
+                break
+
+            res.append(curr._pfp__name)
+
+            if isinstance(curr._pfp__parent, Array):
+                curr = curr._pfp__parent._pfp__parent
+            else:
+                curr = curr._pfp__parent
+
+        return ".".join(reversed(res))
+
     def _pfp__snapshot(self, recurse=True):
         """Save off the current value of the field
         """
diff --git a/setup.py b/setup.py
index ab026ad..cb0fec7 100644
--- a/setup.py
+++ b/setup.py
@@ -33,5 +33,8 @@ setup(
         "Programming Language :: Python :: 2",
         "Programming Language :: Python :: 3",
     ],
+    entry_points={
+        "console_scripts": ["pfp = pfp.__main__:main"]
+    },
     packages=["pfp", "pfp.native", "pfp.fuzz"],
 )


Test patch:
diff --git a/tests/test_struct_union.py b/tests/test_struct_union.py
index 02dd282..fa6839d 100644
--- a/tests/test_struct_union.py
+++ b/tests/test_struct_union.py
@@ -22,6 +22,34 @@ class TestStructUnion(utils.PfpTestCase):
     def tearDown(self):
         pass
 
+    def test_field_path(self):
+        dom = self._test_parse_build(
+            "Abbbb4141414142424242",
+            """
+                struct {
+                    struct {
+                        struct {
+                            char inner;
+                            char array[4];
+                        } nested2;
+                        int some_int;
+                    } nested1;
+                    int some_int2;
+                } root;
+            """,
+        )
+        self.assertEqual(dom.root._pfp__path(), "root")
+        self.assertEqual(dom.root.some_int2._pfp__path(), "root.some_int2")
+        self.assertEqual(dom.root.nested1._pfp__path(), "root.nested1")
+        self.assertEqual(dom.root.nested1.some_int._pfp__path(), "root.nested1.some_int")
+        self.assertEqual(dom.root.nested1.nested2._pfp__path(), "root.nested1.nested2")
+        self.assertEqual(dom.root.nested1.nested2.inner._pfp__path(), "root.nested1.nested2.inner")
+        self.assertEqual(dom.root.nested1.nested2.array._pfp__path(), "root.nested1.nested2.array")
+        self.assertEqual(dom.root.nested1.nested2.array[0]._pfp__path(), "root.nested1.nested2.array[0]")
+        self.assertEqual(dom.root.nested1.nested2.array[1]._pfp__path(), "root.nested1.nested2.array[1]")
+        self.assertEqual(dom.root.nested1.nested2.array[2]._pfp__path(), "root.nested1.nested2.array[2]")
+        self.assertEqual(dom.root.nested1.nested2.array[3]._pfp__path(), "root.nested1.nested2.array[3]")
+    
     def test_struct(self):
         dom = self._test_parse_build(
             "abcddcba",