Skip to main content

Network configuration parser that translates show command outputs into structured data

Project description

Show Configuration Parser (shconfparser)

License: MIT Tests codecov Downloads GitHub issues open CodeQL PyPI

🚀 Version 3.0 - Modern Python library (3.8+) with uv support! See docs/ for guides.

Introduction

Show configuration parser (shconfparser) is a Python library for parsing network device configurations. This library examines the config and breaks it into a set of parent and clild relationships.

shconfparser is a vendor independent library where you can parse the following formats:

  • Tree structure i.e. show running
  • Table structure i.e. show ip interface
  • Data i.e. show version

Modern Format (JSON/YAML) - Hierarchical Structure

show run to modern YAML format structure

show run to modern JSON format structure

Legacy Format - OrderedDict with Full Keys

show run to legacy format structure

Table Structure

show cdp neighbour to table structure

Key Features

Zero Dependencies - Uses only Python standard library
Fast - Modern tooling with uv package manager support
🔒 Type Safe - Full type hints and py.typed marker
🎯 Vendor Independent - Works with any network device configuration
📊 Multiple Formats - Parse trees, tables, and unstructured data
📄 Format Flexibility - Output as JSON or YAML structures
🔍 XPath Queries - NSO-style queries with context tracking (NEW!)
🧪 Well Tested - 80%+ code coverage, tested on Python 3.8-3.13

Quick Start

Installation

pip install shconfparser

Faster with uv:

curl -LsSf https://astral.sh/uv/install.sh | sh
uv pip install shconfparser

Basic Usage

Modern format (recommended - hierarchical structure with XPath):

from shconfparser.parser import Parser

# 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)
tree = p.parse_tree(data)
print(p.dump(tree, indent=2))

# Query with XPath
result = p.xpath('/hostname')
print(result.data)  # 'R1'
Alternative: Legacy format (backward compatible)
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': {...}}

Multiple show commands in one file:

from shconfparser.parser import Parser

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()
# odict_keys(['running', 'version', 'cdp_neighbors', 'ip_interface_brief'])

# Now parse each command separately
data['running'] = p.parse_tree(data['running'])

headers = ['Device ID', 'Local Intrfce', 'Holdtme', 'Capability', 'Platform', 'Port ID']
data['cdp_neighbors'] = p.parse_table(data['cdp_neighbors'], header_names=headers)

print(p.dump(data['running'], indent=2))
Alternative: Access internal properties
p = Parser()
p.read('multiple_commands.txt')
p.split(p.r.data)

# Access split data from internal property
data = p.s.shcmd_dict
data['running'] = p.parse_tree(data['running'])
print(p.dump(data['running'], indent=4))

Usage Examples

Check Library Version

import shconfparser
print(shconfparser.__version__)  # '3.0.0'

Parse Tree Structure (show running-config)

from shconfparser.parser import Parser

p = Parser()

# Single command file - parse directly
data = p.read('running_config.txt')
tree = p.parse_tree(data)  # No split() needed

# Access nested configuration
print(p.dump(tree['interface FastEthernet0/0'], indent=2))
# {
#   "ip address 1.1.1.1 255.255.255.0": null,
#   "duplex auto": null,
#   "speed auto": null
# }

Parse Table Structure (show cdp neighbors)

# Single command file
p = Parser()
data = p.read('cdp_neighbors.txt')

# Parse table directly (no split needed)
headers = ['Device ID', 'Local Intrfce', 'Holdtme', 'Capability', 'Platform', 'Port ID']
cdp_data = p.parse_table(data, header_names=headers)

# Access as list of dictionaries
for neighbor in cdp_data:
    print(f"{neighbor['Device ID']} on {neighbor['Local Intrfce']}")
# Output: R2 on Fas 0/0

Parse Unstructured Data (show version)

# Single command file
p = Parser()
data = p.read('show_version.txt')

# Parse show version output directly
version_data = p.parse_data(data)  # No split() needed

# Search for specific information
import re
for line in version_data.keys():
    if re.search(r'IOS.*Version', line):
        print(line)
# Output: Cisco IOS Software, 3700 Software (C3725-ADVENTERPRISEK9-M), Version 12.4(25d)...

Search in Tree

# Search for all interfaces
pattern = r'interface\s+\w+.*'
matches = p.search.search_all_in_tree(pattern, tree)

for key, value in matches.items():
    print(value)
# interface FastEthernet0/0
# interface FastEthernet0/1

Search in Table

# Find specific device in CDP table
pattern = r'R\d+'
match = p.search.search_in_table(pattern, cdp_data, 'Device ID')
print(match)
# {'Device ID': 'R2', 'Local Intrfce': 'Fas 0/0', ...}

Output Format Selection

Parse configurations in legacy (OrderedDict) or modern (dict) hierarchical structures:

from shconfparser.parser import 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))  # <class 'collections.OrderedDict'>
# Example: {'interface FastEthernet0/0': {'ip address 1.1.1.1': ''}}

# 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 = p.parse_tree(data)  # Returns dict
print(type(tree))  # <class 'dict'>
# Example: {'interface': {'FastEthernet0/0': {'ip': {'address': '1.1.1.1'}}}}

# Override format per call
p = Parser()  # Legacy by default
tree_legacy = p.parse_tree(data)  # OrderedDict
tree_json = p.parse_tree(data, format='json')  # dict

Format Comparison:

# Legacy format - preserves exact CLI structure (OrderedDict)
{
    "interface FastEthernet0/0": {
        "ip address 1.1.1.1 255.255.255.0": "",
        "duplex auto": ""
    }
}

# Modern formats (json/yaml) - hierarchical and programmatic (dict)
{
    "interface": {
        "FastEthernet0/0": {
            "ip": {
                "address": "1.1.1.1 255.255.255.0"
            },
            "duplex": "auto"
        }
    }
}

Benefits of modern formats (json/yaml):

  • Cleaner hierarchy for nested configurations
  • Better for programmatic access
  • XPath query support
  • Easier to convert to actual JSON/YAML files
  • Natural structure for complex configs

XPath Queries (New in 3.0!)

Query YAML-formatted configurations using NSO-style XPath with optional context tracking:

from shconfparser.parser import Parser

# XPath requires YAML format
p = Parser(output_format='yaml')
data = p.read('running_config.txt')
tree = p.parse_tree(data)

# Simple queries
result = p.xpath('/hostname')
print(result.data)  # 'R1'

# Wildcards - find all interface duplex settings
result = p.xpath('/interface/*/duplex')
print(result.matches)  # ['auto', 'auto']
print(result.count)    # 2

# Predicates with slashes (network interface names)
result = p.xpath('/interface[FastEthernet0/0]/duplex')
print(result.data)  # 'auto'

# Recursive search - find anywhere in tree
result = p.xpath('//duplex')
print(result.matches)  # ['auto', 'auto']

# Predicate wildcards
result = p.xpath('/interface[FastEthernet*]/ip/address')
print(result.data)  # '1.1.1.1 255.255.255.0'

Context Options - Solve the "which interface?" problem:

# Problem: Can't identify source with wildcards
result = p.xpath('/interface/*/duplex')
print(result.matches)  # ['auto', 'auto'] - Which interface?

# Solution 1: context='none' (default - just values)
result = p.xpath('/interface/*/duplex', context='none')
print(result.matches)  # ['auto', 'auto']

# Solution 2: context='partial' (from wildcard match point)
result = p.xpath('/interface/*/duplex', context='partial')
print(result.matches)
# [{'FastEthernet0/0': {'duplex': 'auto'}},
#  {'FastEthernet0/1': {'duplex': 'auto'}}]

# Solution 3: context='full' (complete tree hierarchy)
result = p.xpath('/interface/*/duplex', context='full')
print(result.matches)
# [{'interface': {'FastEthernet0/0': {'duplex': 'auto'}}},
#  {'interface': {'FastEthernet0/1': {'duplex': 'auto'}}}]

# Path tracking (always available)
result = p.xpath('/interface/*/speed')
print(result.paths)
# [['interface', 'FastEthernet0/0', 'speed'],
#  ['interface', 'FastEthernet0/1', 'speed']]

XPath Features:

  • ✅ Absolute paths: /interface/FastEthernet0/0/duplex
  • ✅ Recursive search: //duplex (find anywhere)
  • ✅ Wildcards: /interface/*/duplex
  • ✅ Predicates: /interface[FastEthernet0/0]
  • ✅ Predicate wildcards: /interface[FastEthernet*]
  • ✅ Context tracking: See which match came from where
  • ✅ Path tracking: result.paths shows path components

XPathResult Structure:

result = p.xpath('//duplex')
print(result.success)   # True
print(result.data)      # First match: 'auto'
print(result.matches)   # All matches: ['auto', 'auto']
print(result.count)     # Number of matches: 2
print(result.query)     # Original query: '//duplex'
print(result.paths)     # Path to each match
print(result.error)     # Error message if failed

# Boolean check
if result:
    print(f"Found {result.count} matches")

Note: XPath queries only work with output_format='yaml'. JSON format (OrderedDict) preserves exact CLI structure and should use traditional dict navigation.

Alternative: Using Individual Components

For advanced users who need granular control
from shconfparser import Reader, ShowSplit, TreeParser, TableParser

# For multiple show commands
reader = Reader('multiple_commands.txt')
splitter = ShowSplit()
data = splitter.split(reader.data)  # Split only if multiple commands

# Use specific parsers
tree_parser = TreeParser()
table_parser = TableParser()

running = tree_parser.parse(data['running'])
cdp = table_parser.parse(data['cdp_neighbors'], header_names=headers)

💡 Remember: Use split() only when your file contains multiple show commands. For single command files, parse directly.

📖 For more examples, see docs/ folder.

Documentation

📚 Complete documentation: docs/README.md

For Users

Guide Description
Usage Examples Detailed parsing examples (tree, table, data)
API Reference Complete API documentation
Migration Guide Upgrade from v2.x to v3.0
Python Compatibility Python version support

For Contributors

Guide Description
Quick Start 5-minute contributor setup
Contributing Guide How to contribute
Architecture System design and structure
Business Standards Quality and compliance standards

Support

Getting Help

Frequently Asked Questions

Q: What Python versions are supported?
A: Python 3.8-3.13 are fully tested and supported.

Q: Does this work with my network vendor?
A: Yes! shconfparser is vendor-independent and works with any hierarchical configuration format.

Q: Are there any dependencies?
A: No runtime dependencies - uses only Python standard library.

Q: How do I migrate from v2.x?
A: The API is backward compatible. Just run pip install --upgrade shconfparser. See Migration Guide for details.

Community

  • 🌟 Star us on GitHub
  • 🤝 Contribute: See CONTRIBUTING.md
  • 📊 CI/CD: Automated testing on Python 3.8-3.13 across Ubuntu, macOS, Windows

License

MIT License © 2016-2025 Kiran Kumar Kotari

License: MIT

Special thanks to all contributors

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

shconfparser-3.1.1.tar.gz (1.8 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

shconfparser-3.1.1-py3-none-any.whl (27.9 kB view details)

Uploaded Python 3

File details

Details for the file shconfparser-3.1.1.tar.gz.

File metadata

  • Download URL: shconfparser-3.1.1.tar.gz
  • Upload date:
  • Size: 1.8 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.21 {"installer":{"name":"uv","version":"0.9.21","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for shconfparser-3.1.1.tar.gz
Algorithm Hash digest
SHA256 87d0cf33e25ac0f6176715c780adf382cfc981048b07d8eb003d104f880a5c77
MD5 2fd22a4b2f527323d7c479dbbb5c0029
BLAKE2b-256 672a09ddba3808d1000a654257ea1bfb7db516fe720ad947062b324df292e33b

See more details on using hashes here.

File details

Details for the file shconfparser-3.1.1-py3-none-any.whl.

File metadata

  • Download URL: shconfparser-3.1.1-py3-none-any.whl
  • Upload date:
  • Size: 27.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.21 {"installer":{"name":"uv","version":"0.9.21","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for shconfparser-3.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 9a4da79eed0b1872008317be97706b45f25efb58fc3b076a9d1b17546b7c6271
MD5 e3d5cf58287b8d537ffdbecc9c71279d
BLAKE2b-256 b864b3ba60408b790afd6ca409579ea2994af3586c879bfaef97995bf576aa7c

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page