diff --git a/CHANGELOG.md b/CHANGELOG.md
index ce8c75c..b036fdd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,50 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [3.1.0] - 2025-12-28
+
+### 🎉 Format System Refinement
+
+This release refines the output format system with explicit naming for legacy and modern formats.
+
+### Changed
+- 🔄 **Format parameter semantics** - Clarified format naming and behavior
+ - `Parser()` now explicitly defaults to `'legacy'` format (backward compatible)
+ - `Parser(output_format='legacy')` - OrderedDict with full command strings (backward compatible)
+ - `Parser(output_format='json')` - dict with hierarchical structure (modern, XPath enabled)
+ - `Parser(output_format='yaml')` - dict with hierarchical structure (modern, XPath enabled)
+- 🔄 **XPath support** - Now works with both 'json' and 'yaml' formats (any modern format)
+- 🔄 **Format validation** - Clear error messages for invalid format specifications
+
+### Technical Details
+
+**Breaking refinement** (minimal impact):
+- `output_format='json'` behavior changed from OrderedDict to dict with hierarchical structure
+- Since this feature was added hours ago (same day), no users are affected
+- Legacy behavior preserved via `output_format='legacy'` or `Parser()` (no params)
+
+**Migration:**
+```python
+# v3.0 code (still works)
+p = Parser() # Returns OrderedDict with full keys
+
+# v3.1 explicit legacy
+p = Parser(output_format='legacy') # Same as above
+
+# v3.1 modern formats (hierarchical structure, XPath enabled)
+p = Parser(output_format='json') # dict with hierarchy
+p = Parser(output_format='yaml') # dict with hierarchy
+```
+
+**Format comparison:**
+```python
+# Legacy format (OrderedDict with full keys)
+{'interface FastEthernet0/0': {'ip address 1.1.1.1': ''}}
+
+# Modern formats (dict with hierarchy)
+{'interface': {'FastEthernet0/0': {'ip': {'address': '1.1.1.1'}}}}
+```
+
## [3.0.0] - 2025-12-27
### 🎉 Major Release - Modernization
diff --git a/README.md b/README.md
index 68f08fa..8f204f8 100644
--- a/README.md
+++ b/README.md
@@ -28,13 +28,16 @@ shconfparser is a vendor independent library where you can parse the following f
- Table structure *`i.e. show ip interface`*
- Data *`i.e. show version`*
-YAML Format Output
+Modern Format (JSON/YAML) - Hierarchical Structure
-
+
+
+
+
-Tree Structure
+Legacy Format - OrderedDict with Full Keys
-
+
Table Structure
@@ -67,12 +70,12 @@ uv pip install shconfparser
### Basic Usage
-**Single show command with YAML format (recommended):**
+**Modern format (recommended - hierarchical structure with XPath):**
```python
from shconfparser.parser import Parser
-# Use YAML format for cleaner output and XPath support
-p = Parser(output_format='yaml')
+# Use modern format for cleaner output and XPath support
+p = Parser(output_format='json') # or 'yaml'
data = p.read('running_config.txt')
# Parse directly (no split needed for single show running command)
@@ -85,13 +88,16 @@ print(result.data) # 'R1'
```
-Alternative: JSON format (backward compatible)
+Alternative: Legacy format (backward compatible)
```python
-p = Parser() # Default is JSON format (OrderedDict)
+p = Parser() # Defaults to 'legacy' format
+# or explicitly: Parser(output_format='legacy')
data = p.read('running_config.txt')
tree = p.parse_tree(data)
print(p.dump(tree, indent=4))
+# Returns OrderedDict with full command strings as keys
+# Example: {'interface FastEthernet0/0': {...}}
```
@@ -99,7 +105,7 @@ print(p.dump(tree, indent=4))
```python
from shconfparser.parser import Parser
-p = Parser(output_format='yaml') # YAML format recommended
+p = Parser(output_format='json') # Modern format recommended
data = p.read('multiple_commands.txt') # Contains multiple show outputs
data = p.split(data) # Split into separate commands
data.keys()
@@ -216,40 +222,39 @@ print(match)
# {'Device ID': 'R2', 'Local Intrfce': 'Fas 0/0', ...}
```
-### Output Format Selection (New in 3.0!)
+### Output Format Selection
-Parse configurations to JSON (OrderedDict) or YAML-friendly dict structures:
+Parse configurations in legacy (OrderedDict) or modern (dict) hierarchical structures:
```python
from shconfparser.parser import Parser
-# Default: JSON format (OrderedDict - backward compatible)
-p = Parser()
+# Legacy format (backward compatible - OrderedDict with full keys)
+p = Parser() # Defaults to 'legacy'
+# or explicitly: Parser(output_format='legacy')
data = p.read('running_config.txt')
tree = p.parse_tree(data) # Returns OrderedDict
print(type(tree)) #
+# Example: {'interface FastEthernet0/0': {'ip address 1.1.1.1': ''}}
-# YAML format: cleaner hierarchical structure
-p = Parser(output_format='yaml')
+# Modern formats: JSON or YAML (hierarchical dict structure)
+p = Parser(output_format='json') # Hierarchical dict
+# or: Parser(output_format='yaml') # Same structure, different name
data = p.read('running_config.txt')
-tree_yaml = p.parse_tree(data) # Returns dict with nested structure
-print(type(tree_yaml)) #
+tree = p.parse_tree(data) # Returns dict
+print(type(tree)) #
+# Example: {'interface': {'FastEthernet0/0': {'ip': {'address': '1.1.1.1'}}}}
# Override format per call
-p = Parser() # Default is JSON
-tree_json = p.parse_tree(data) # OrderedDict
-tree_yaml = p.parse_tree(data, format='yaml') # dict
-
-# YAML structure example:
-# Input: "interface FastEthernet0/0" with nested config
-# JSON: {"interface FastEthernet0/0": {...}}
-# YAML: {"interface": {"FastEthernet0/0": {...}}}
+p = Parser() # Legacy by default
+tree_legacy = p.parse_tree(data) # OrderedDict
+tree_json = p.parse_tree(data, format='json') # dict
```
**Format Comparison:**
```python
-# JSON format (default) - preserves exact CLI structure
+# Legacy format - preserves exact CLI structure (OrderedDict)
{
"interface FastEthernet0/0": {
"ip address 1.1.1.1 255.255.255.0": "",
@@ -257,7 +262,7 @@ tree_yaml = p.parse_tree(data, format='yaml') # dict
}
}
-# YAML format - hierarchical and human-readable
+# Modern formats (json/yaml) - hierarchical and programmatic (dict)
{
"interface": {
"FastEthernet0/0": {
@@ -270,12 +275,12 @@ tree_yaml = p.parse_tree(data, format='yaml') # dict
}
```
-**Benefits of YAML format:**
+**Benefits of modern formats (json/yaml):**
- Cleaner hierarchy for nested configurations
- Better for programmatic access
-- Easier to convert to actual YAML files
+- XPath query support
+- Easier to convert to actual JSON/YAML files
- Natural structure for complex configs
-- Required for XPath queries
### XPath Queries (New in 3.0!)
diff --git a/asserts/img/sh_run_json.png b/asserts/img/sh_run_json.png
new file mode 100644
index 0000000..d6ebf1b
Binary files /dev/null and b/asserts/img/sh_run_json.png differ
diff --git a/docs/XPATH_GUIDE.md b/docs/XPATH_GUIDE.md
index a74f05c..1b0bf1b 100644
--- a/docs/XPATH_GUIDE.md
+++ b/docs/XPATH_GUIDE.md
@@ -28,16 +28,20 @@ XPath queries provide a powerful way to search and extract data from network con
### Requirements
-**XPath queries only work with YAML format:**
+**XPath queries work with modern formats (json or yaml):**
```python
-# ✅ Correct - YAML format required
-p = Parser(output_format='yaml')
+# ✅ Correct - JSON format (hierarchical dict)
+p = Parser(output_format='json')
tree = p.parse_tree(data)
result = p.xpath('/hostname')
-# ❌ Wrong - JSON format not supported
-p = Parser(output_format='json')
+# ✅ Correct - YAML format (hierarchical dict, same structure as json)
+p = Parser(output_format='yaml')
+result = p.xpath('/hostname')
+
+# ❌ Wrong - Legacy format not supported
+p = Parser() # Defaults to 'legacy'
result = p.xpath('/hostname') # Returns error
```
@@ -46,8 +50,8 @@ result = p.xpath('/hostname') # Returns error
```python
from shconfparser import Parser
-# Initialize parser with YAML format
-p = Parser(output_format='yaml')
+# Initialize parser with modern format (json or yaml)
+p = Parser(output_format='json') # or 'yaml' - both work
data = p.read('running_config.txt')
tree = p.parse_tree(data)
@@ -435,7 +439,7 @@ p = Parser(output_format='json')
result = p.xpath('/hostname') # Returns error
```
-**Solution:** Use `output_format='yaml'`
+**Solution:** Use `output_format='json'` or `output_format='yaml'` (modern formats)
### 2. No Attribute Selection
@@ -488,10 +492,10 @@ result = p.xpath('hostname') # Missing leading /
result = p.xpath('/hostname', context='invalid')
# result.error: "Invalid context 'invalid'. Must be 'none', 'partial', or 'full'"
-# JSON format error
-p = Parser(output_format='json')
+# Legacy format error
+p = Parser() # Defaults to 'legacy'
result = p.xpath('/hostname')
-# result.error: "XPath queries only work with output_format='yaml'..."
+# result.error: "XPath requires modern format (json/yaml)..."
```
## Performance Tips
diff --git a/pyproject.toml b/pyproject.toml
index 1deb783..041ee02 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "shconfparser"
-version = "3.0.0"
+version = "3.1.0"
description = "Network configuration parser that translates show command outputs into structured data"
readme = "README.md"
requires-python = ">=3.9"
diff --git a/shconfparser/__init__.py b/shconfparser/__init__.py
index 4630f30..314e7e3 100644
--- a/shconfparser/__init__.py
+++ b/shconfparser/__init__.py
@@ -41,7 +41,7 @@
from .tree_parser import TreeParser
from .xpath import XPath
-__version__ = "3.0.0"
+__version__ = "3.1.0"
__author__ = "Kiran Kumar Kotari"
__email__ = "kirankotari@live.com"
diff --git a/shconfparser/parser.py b/shconfparser/parser.py
index 16d071e..830e055 100644
--- a/shconfparser/parser.py
+++ b/shconfparser/parser.py
@@ -50,21 +50,32 @@ def __init__(
self,
log_level: int = logging.INFO,
log_format: Optional[str] = None,
- output_format: str = "json",
+ output_format: Optional[str] = None,
) -> None:
"""Initialize the Parser.
Args:
log_level: Logging level (default: INFO)
log_format: Custom log format string
- output_format: Default output format for parse_tree ('json' or 'yaml', default: 'json')
+ output_format: Output structure format
+ - None or 'legacy' (default): OrderedDict with full command strings
+ Example: {'interface FastEthernet0/0': {'ip address 1.1.1.1': ''}}
+ For backward compatibility. No XPath support.
+
+ - 'json': Hierarchical dict structure
+ Example: {'interface': {'FastEthernet0/0': {'ip': {'address': '1.1.1.1'}}}}
+ XPath support enabled. Clean programmatic access.
+
+ - 'yaml': Hierarchical dict structure (same as json)
+ Example: {'interface': {'FastEthernet0/0': {'ip': {'address': '1.1.1.1'}}}}
+ XPath support enabled. YAML-friendly output.
"""
# State for backward compatibility
self.data: TreeData = OrderedDict()
self.table: TableData = []
- # Output format configuration
- self.output_format: str = output_format
+ # Output format configuration (None defaults to 'legacy' for backward compatibility)
+ self.output_format: str = output_format if output_format is not None else "legacy"
# Logging
self.format: Optional[str] = log_format
@@ -104,16 +115,19 @@ def parse_tree(self, lines: List[str], format: Optional[str] = None) -> TreeData
Args:
lines: Configuration lines with indentation
- format: Output format ('json' or 'yaml'). If None, uses self.output_format
+ format: Output format ('legacy', 'json', or 'yaml'). If None, uses self.output_format
Returns:
- Nested OrderedDict (json format) or dict (yaml format) representing configuration hierarchy
+ - 'legacy': OrderedDict with full command strings as keys
+ - 'json' or 'yaml': dict with hierarchical structure
Example:
- >>> parser = Parser()
+ >>> parser = Parser() # Defaults to 'legacy'
>>> config = ['interface Ethernet0', ' ip address 1.1.1.1']
- >>> tree = parser.parse_tree(config) # Returns OrderedDict (JSON)
- >>> tree_yaml = parser.parse_tree(config, format='yaml') # Returns dict (YAML-friendly)
+ >>> tree = parser.parse_tree(config) # Returns OrderedDict with full keys
+
+ >>> parser = Parser(output_format='json')
+ >>> tree = parser.parse_tree(config) # Returns dict with hierarchy
"""
# Parse to OrderedDict first
ordered_tree = self.tree_parser.parse_tree(lines)
@@ -121,13 +135,23 @@ def parse_tree(self, lines: List[str], format: Optional[str] = None) -> TreeData
# Transform based on format
output_format = format if format is not None else self.output_format
- if output_format == "yaml":
- yaml_tree = self._tree_to_yaml_structure(ordered_tree)
- self.data = yaml_tree # type: ignore[assignment] # Store YAML format for xpath
- return yaml_tree
- else:
- self.data = ordered_tree # Store JSON format
+ # Validate format
+ valid_formats = {"legacy", "json", "yaml"}
+ if output_format not in valid_formats:
+ raise ValueError(
+ f"Invalid output_format '{output_format}'. "
+ f"Must be one of: {', '.join(sorted(valid_formats))}"
+ )
+
+ if output_format == "legacy":
+ # Legacy format: OrderedDict with full command strings
+ self.data = ordered_tree
return ordered_tree
+ else:
+ # Modern formats (json/yaml): dict with hierarchical structure
+ hierarchical_tree = self._tree_to_yaml_structure(ordered_tree)
+ self.data = hierarchical_tree # type: ignore[assignment]
+ return hierarchical_tree
def parse_tree_safe(self, lines: List[str]) -> TreeParseResult:
"""Parse tree structure with structured result.
@@ -398,11 +422,14 @@ def xpath(
query=query,
)
- # XPath only works with YAML format (dict, not OrderedDict)
- if self.output_format != "yaml":
+ # XPath only works with modern formats (json/yaml)
+ if self.output_format not in ("json", "yaml"):
return XPathResult(
success=False,
- error=f"XPath queries only work with output_format='yaml', current format is '{self.output_format}'",
+ error=(
+ f"XPath queries require modern format. Use output_format='json' or 'yaml'. "
+ f"Current format is '{self.output_format}' (OrderedDict with full command strings)."
+ ),
query=query,
)
diff --git a/tests/test_xpath.py b/tests/test_xpath.py
index 682b0e4..f277a40 100644
--- a/tests/test_xpath.py
+++ b/tests/test_xpath.py
@@ -131,15 +131,25 @@ def test_multiple_wildcards(self):
self.assertGreater(result.count, 0)
def test_xpath_on_json_format(self):
- """Test XPath on JSON format (should return error)."""
+ """Test XPath on JSON format works (modern hierarchical format)."""
parser = Parser(output_format="json")
lines = parser.read("data/shrun.txt")
parser.parse_tree(lines)
result = parser.xpath("/hostname")
- # XPath only works with YAML format
+ # XPath works with modern json format
+ self.assertTrue(result.success)
+ self.assertEqual(result.data, "R1")
+
+ def test_xpath_on_legacy_format(self):
+ """Test XPath on legacy format (should return error)."""
+ parser = Parser() # Defaults to legacy
+ lines = parser.read("data/shrun.txt")
+ parser.parse_tree(lines)
+ result = parser.xpath("/hostname")
+ # XPath doesn't work with legacy format
self.assertFalse(result.success)
self.assertIsNotNone(result.error)
- self.assertIn("yaml", result.error.lower())
+ self.assertIn("modern format", result.error.lower())
def test_predicate_exact_match(self):
"""Test predicate with exact identifier match."""