Parsing scutil output with perl
In Perl, you can use Marpa::R2, a Perl interface to Marpa, a general BNF parser.
Here is a quick example:
use 5.010;use strict;use warnings;use Data::Dumper;$Data::Dumper::Indent = 1;$Data::Dumper::Terse = 1;$Data::Dumper::Deepcopy = 1;use Marpa::R2;my $g = Marpa::R2::Scanless::G->new( { source => \(<<'END_OF_SOURCE'), :default ::= action => [ name, value] lexeme default = action => [ name, value] latm => 1 scutil ::= 'scutil' '> open' '> show' path '<dictionary>' '{' pairs '}' path ~ [\w/:\-]+ pairs ::= pair+ pair ::= name ':' value name ~ [\w]+ value ::= ip | mac | interface | signature | array | dict ip ~ octet '.' octet '.' octet '.' octet octet ~ [\d]+ mac ~ [a-z0-9][a-z0-9]':'[a-z0-9][a-z0-9]':'[a-z0-9][a-z0-9]':'[a-z0-9][a-z0-9]':'[a-z0-9][a-z0-9]':'[a-z0-9][a-z0-9] interface ~ [\w]+ signature ::= signature_item+ separator => [;] signature_item ::= signature_item_name '=' signature_item_value signature_item_name ~ [\w\.]+ signature_item_value ::= ip | mac dict ::= '<dictionary>' '{' pairs '}' array ::= '<array>' '{' items '}' items ::= item+ item ::= index ':' value index ~ [\d]+ :discard ~ whitespace whitespace ~ [\s]+END_OF_SOURCE} );my $input = <<EOI;scutil> open> show State:/Network/Service/0F70B221-EEF7-4ACC-96D8-ECBA3A15F132/IPv4<dictionary> { ARPResolvedHardwareAddress : 00:1b:c0:4a:82:f9 ARPResolvedIPAddress : 10.10.0.254 AdditionalRoutes : <array> { 0 : <dictionary> { DestinationAddress : 10.10.0.146 SubnetMask : 255.255.255.255 } 1 : <dictionary> { DestinationAddress : 169.254.0.0 SubnetMask : 255.255.0.0 } } Addresses : <array> { 0 : 10.10.0.146 } ConfirmedInterfaceName : en0 InterfaceName : en0 NetworkSignature : IPv4.Router=10.10.0.254;IPv4.RouterHardwareAddress=00:1b:c0:4a:82:f9 Router : 10.10.0.254 SubnetMasks : <array> { 0 : 255.255.255.0 }}EOIsay Dumper $g->parse( \$input, { trace_terminals => 0 } );
Output:
\[ 'scutil', 'scutil', '> open', '> show', [ 'path', 'State:/Network/Service/0F70B221-EEF7-4ACC-96D8-ECBA3A15F132/IPv4' ], '<dictionary>', '{', [ 'pairs', [ 'pair', [ 'name', 'ARPResolvedHardwareAddress' ], ':', [ 'value', [ 'mac', '00:1b:c0:4a:82:f9' ] ] ], [ 'pair', [ 'name', 'ARPResolvedIPAddress' ], ':', [ 'value', [ 'ip', '10.10.0.254' ] ] ], [ 'pair', [ 'name', 'AdditionalRoutes' ], ':', [ 'value', [ 'array', '<array>', '{', [ 'items', [ 'item', [ 'index', '0' ], ':', [ 'value', [ 'dict', '<dictionary>', '{', [ 'pairs', [ 'pair', [ 'name', 'DestinationAddress' ], ':', [ 'value', [ 'ip', '10.10.0.146' ] ] ], [ 'pair', [ 'name', 'SubnetMask' ], ':', [ 'value', [ 'ip', '255.255.255.255' ] ] ] ], '}' ] ] ], [ 'item', [ 'index', '1' ], ':', [ 'value', [ 'dict', '<dictionary>', '{', [ 'pairs', [ 'pair', [ 'name', 'DestinationAddress' ], ':', [ 'value', [ 'ip', '169.254.0.0' ] ] ], [ 'pair', [ 'name', 'SubnetMask' ], ':', [ 'value', [ 'ip', '255.255.0.0' ] ] ] ], '}' ] ] ] ], '}' ] ] ], [ 'pair', [ 'name', 'Addresses' ], ':', [ 'value', [ 'array', '<array>', '{', [ 'items', [ 'item', [ 'index', '0' ], ':', [ 'value', [ 'ip', '10.10.0.146' ] ] ] ], '}' ] ] ], [ 'pair', [ 'name', 'ConfirmedInterfaceName' ], ':', [ 'value', [ 'interface', 'en0' ] ] ], [ 'pair', [ 'name', 'InterfaceName' ], ':', [ 'value', [ 'interface', 'en0' ] ] ], [ 'pair', [ 'name', 'NetworkSignature' ], ':', [ 'value', [ 'signature', [ 'signature_item', [ 'signature_item_name', 'IPv4.Router' ], '=', [ 'signature_item_value', [ 'ip', '10.10.0.254' ] ] ], [ 'signature_item', [ 'signature_item_name', 'IPv4.RouterHardwareAddress' ], '=', [ 'signature_item_value', [ 'mac', '00:1b:c0:4a:82:f9' ] ] ] ] ] ], [ 'pair', [ 'name', 'Router' ], ':', [ 'value', [ 'ip', '10.10.0.254' ] ] ], [ 'pair', [ 'name', 'SubnetMasks' ], ':', [ 'value', [ 'array', '<array>', '{', [ 'items', [ 'item', [ 'index', '0' ], ':', [ 'value', [ 'ip', '255.255.255.0' ] ] ] ], '}' ] ] ] ], '}' ]
The solution with Marpa::R2 is actually a nice generic approach.However, I'm not so happy with the generated hash map, which is probably the toll one has to pay for the generic parser.
I've come up with the following code to get a more straight hash map:
use Data::Dumper;open(my $pipe, '-|', "scutil <<- end_scutil 2> /dev/nullopenshow State:/Network/Service/21AD96AA-AD28-4D5C-93C1-F343FD07DA60/IPv4closeend_scutil") or die $!;sub doParse { my ($type)=@_; my $map; my @arr; while(<$pipe>) { chomp; if ($type eq "dictionary") { if (m/^<dictionary> \{/) { $map=doParse("dictionary"); } elsif (m/\s*([^:]+) : <(.*)> \{/) { $map->{$1}=doParse($2); } elsif (m/\s*([^:]+) : ([^\}]+)$/) { $map->{$1}=$2; } elsif (m/\}$/) { return $map; } else { print STDERR "$type parse error on $_"; } } elsif ($type eq "array") { if (m/\s*(\d+) : <(.*)> \{/) { $arr[$1]=doParse($2); } elsif (m/\s*(\d+) : ([^\}]+)$/) { $arr[$1]=$2; } elsif (m/\}$/) { return \@arr; } else { print STDERR "$type parse error on $_"; } } } return $map;}print Dumper(doParse("dictionary"));1;__END__
With this input from scutil
<dictionary> { ARPResolvedHardwareAddress : 00:1e:8c:72:27:d2 ARPResolvedIPAddress : 192.168.1.10 AdditionalRoutes : <array> { 0 : <dictionary> { DestinationAddress : 192.168.1.232 SubnetMask : 255.255.255.255 } 1 : <dictionary> { DestinationAddress : 169.254.0.0 SubnetMask : 255.255.0.0 } } Addresses : <array> { 0 : 192.168.1.232 } ConfirmedInterfaceName : en0 InterfaceName : en0 NetworkSignature : IPv4.Router=192.168.1.10;IPv4.RouterHardwareAddress=00:1e:8c:72:27:d2 Router : 192.168.1.10 SubnetMasks : <array> { 0 : 255.255.255.0 }}
it produces this hashmap:
$VAR1 = { 'InterfaceName' => 'en0', 'Addresses' => [ '192.168.1.232' ], 'ARPResolvedHardwareAddress' => '00:1e:8c:72:27:d2', 'NetworkSignature' => 'IPv4.Router=192.168.1.10;IPv4.RouterHardwareAddress=00:1e:8c:72:27:d2', 'ARPResolvedIPAddress' => '192.168.1.10', 'AdditionalRoutes' => [ { 'SubnetMask' => '255.255.255.255', 'DestinationAddress' => '192.168.1.232' }, { 'DestinationAddress' => '169.254.0.0', 'SubnetMask' => '255.255.0.0' } ], 'Router' => '192.168.1.10', 'SubnetMasks' => [ '255.255.255.0' ], 'ConfirmedInterfaceName' => 'en0' };