NAME

Config::NINI - NINI configuration format parser

SYNOPSIS

use Config::NINI qw( nini_load_file nini_parse_data );

# Load and parse a NINI file
my $data   = nini_load_file('config.nini');
my $config = nini_parse_data( $data );

# or

my $dl = []; # debug locations
my $data   = nini_load_file('config.nini', { DL => $dl } );
my $config = nini_parse_data( $data, { DL => $dl, DEBUG => 1 } );

# Access configuration
my $value    = $config->{section_name}{key_name};
my $arrayref = $config->{section_name}{array_key};
my $hashref  = $config->{section_name}{subsection_name};

DESCRIPTION

Config::NINI is a Perl module for parsing and loading configuration files in the NINI format (Not INI). NINI extends traditional INI file concepts with hierarchical sections, inheritance, array values, and dynamic file inclusion.

Specification, examples, notes on NINI format can be found at:

https://github.com/cade-vs/NINI
https://github.com/cade-vs/NINI/blob/master/nini-spec-19.txt
https://github.com/cade-vs/NINI/blob/master/nini-examples-19.txt

Key Features

INSTALLATION

Place Config::NINI in your Perl module search path (@INC). Typically:

/usr/lib/perl/
/usr/local/lib/perl/
~/perl5/lib/perl5/

Or reference it directly:

use lib '/path/to/lib';
use Config::NINI;

QUICK START

Configuration file example (config.nini):

= Database
    host      example.com
    port      5432
    user      admin
    password  secret

= Database Cache
    & Database
    ttl       3600
    enabled   1

Perl script:

my $config = nini_parse_data( nini_load_file( 'config.nini' ), {} );

print $config->{Database}{host};              # example.com
print $config->{'Database Cache'}{host};      # inherited: example.com

FUNCTIONS

nini_load_file( FILENAME, OPTIONS )

Loads a NINI configuration file from disk, processing includes and returning raw data as array reference.

nini_parse_data( DATA, OPTIONS )

Parses processed configuration data into hierarchical hash structure.

NINI FORMAT SPECIFICATION

Overview

NINI files consist of:

File structure:

=Section_Name
    key1    value1
    key2[,] v1, v2, v3
    key3[|] v1 | v2 | v3
    & Parent_Section

=Section_Name  Sub_Section_Name
    & Other Sub_Section
    key4 value4
    text[]  those are space delimited 6 words

Blank lines and comment lines (starting with #) are ignored.

Comments

Lines starting with # are treated as comments and ignored entirely.

Lines with content are treated as-is; there is no support for trailing/inline comments. Unless line begins with '#', the '#' symbol is verbatim and has no special meaning.

# port number 5566
port  #5566

# anywhere in-between
url https://www.gocomics.com/extras/first-calvin-and-hobbes-comic-strip#rulz

SECTIONS

Regular Sections

Marked with = (equals sign). Create new hierarchical branch.

= Top_Level_Section

= Top_Level_Section Sub_Section
  (continues in parent section by default)

= Another_Top_Level

Whitespace-separated path components create hierarchy:

= level1 level2 level3

Creates Perl structure:

%hash = (
  level1 => {
    level2 => {
      level3 => { ... }
    }
  }
);

Abstract Sections

Marked with * (asterisk). Used for dynamic array-indexed branches.

* items           # Creates branch at index 0
key1 value1

* items           # Creates branch at index 1
key2 value2

Creates structure:

items => {
  *0 => { key1 => 'value1' },
  *1 => { key2 => 'value2' }
}

Auto-indexing with * wildcard in path:

= data *
name Widget A

Expands * to next available integer index. Integer indexes are not required to be continuous. If there are 1, 5, 16 the next auto-index will be 17 etc.

Section Modifiers

Delete section contents using ! operators:

Example:

= Section
key1 value1
branch_name branch_data

= Section
!!
# only key1 left

KEYS AND VALUES

Key Syntax

[+|-]KEY[ARRAY][WHITESPACE]VALUE

Components:

Examples:

key1                    # Scalar, value = '1'
key2 hello world        # Scalar, value = 'hello world'
key3 [,] a,b,c          # Array, comma-separated
key4 [|] x|y|z          # Array, pipe-separated
key5 [ ] space separated # Array, space-separated
+key6 append_value      # Add operation (modifier)

Delimiters

Array delimiter specified in [X] where X is:

Quote Handling

Value Parsing

Whitespace handling:

Examples:

key1     value with spaces
key2 "  quoted value  "
key3 "#hash_included"

Results:

key1 => 'value with spaces'
key2 => '  quoted value  '
key3 => '#hash_included'

ARRAYS

Array Declaration

Arrays specified with delimiter in brackets:

array_name [DELIMITER] item1 DELIMITER item2 DELIMITER item3

Returns Perl array reference.

Access in code:

my @items = @{$config->{section}{array_name}};
my $count = scalar @{$config->{section}{array_name}};
my $first = $config->{section}{array_name}->[0];

Whitespace-Separated Arrays

Using space as implicit delimiter:

array_name[] one two three four

Results in:

array_name => ['one', 'two', 'three', 'four']

Single-Character Delimiters

All delimiters are single-character:

ports[,] 8080,8081,8082,8443
formats[|] json|xml|csv|yaml
paths[:] /etc:/opt:/home

Quote Handling in Arrays

Quotes included in array delimiters:

items[,] "first item", 'second item', third

Results in:

items => ['first item', 'second item', 'third']

INHERITANCE

Basic Inheritance

Sections can inherit from other sections using & operator.

= Base
key1 value1
key2 value2

= Derived_Section
& Base
key3 value3

Derived Section inherits key1 and key2 from Base Section.

Result:

'Derived_Section' => {
  key1 => 'value1',  # inherited
  key2 => 'value2',  # inherited
  key3 => 'value3'   # own
}

Inheritance Levels

Multiple & specifiers control inheritance depth:

Selective Inheritance

= Parent
    key1 value1
    sub_branch sub_data

= Child
    & Parent                # Inherits key1 only
    && Parent               # Inherits sub_branch only
    &&& Parent              # Inherits both

Inherited Branch Structure

When inheriting branches (&&), nested structures are deep-copied:

= Source
    nested parent_value

= Target
    && Source               # Copies entire nested structure

Resulting structure:

Target => {
  nested => { ... }     # Deep copy, modifications don't affect Source
}

Inheritance Loops

Since inheritance is a deep copy, loops are allowed but may lead to not obvious results.

= A
    asd 999
    & A
    # no-op, silent, warning if debug

= B
    qwe 333
    & A
    # ok

= A
    & B
    # still ok since deep copy, now both A and B will have asd and qwe keys

INCLUDE DIRECTIVES

File Inclusion

Include other NINI files with @include:

@include filename.nini
@include /path/to/config.nini

Syntax variations:

@include filename.nini
@include path/to/file.nini
@include /absolute/path/file.nini

Specify search directories in OPTIONS:

my $config = nini_parse_data(
  nini_load_file('main.nini', {
    DIRS => [
      '/etc/app',
      '/opt/app/config',
      '/home/user/.app'
    ]
  }),
  {}
);

If @include uses relative path, directories are searched in order. First match wins. Absolute paths (starting with /) used directly.

Include Context

When including a file, the context will be forced to be the root section. When exit included file, the previous secont will be restored. This is needed so that all keys at the beginning of the included file will not be injected into the current section of the base file and all keys after the include will not be attached into the last section of the include file:

= Mid_Section
    # all leading keys of included.nini will be forced to be in the root
    @include included.nini
    # key1 will be into "Mid_Section", not in the last open section in
    # included.nini
    key1 value1

This behaviour was selected to match close the "what you (probably) expect is what you get" :)

Loop Detection

Circular includes are detected and prevented:

# file1.nini
@include file2.nini

# file2.nini
@include file1.nini

Attempting to load file1.nini will error:

nini: error: file load: loop detected for file [file1.nini]

Include Failure Handling

If included file not found:

PATH RESOLUTION

Path Navigation

Paths are whitespace-separated section names:

= level1 level2 level3
key value

Creates:

level1/level2/level3/key => value

Path Syntax

Valid path elements:

[A-Za-z0-9:._\/-]+       # Alphanumeric with special chars
*                        # Auto-index wildcard
!                        # Delete key modifier
!!                       # Delete branch modifier
!!!                      # Delete all modifier

Deleting Path Elements

Single ! deletes matching keys:

= section
    # Remove all keys in section, mostly for debug or temporary purpose
    !

Double !! deletes matching branches:

= section
    # Remove all branches
    !!

Triple !!! deletes both:

= section
    # Remove everything
    !!!

Auto-Indexing with *

Wildcard * finds next available numeric index:

= items  *
    name First

= items  *
    name Second

Sections become:

'items' => {
  1 => { name => 'First' },
  2 => { name => 'Second' }
}

OPTIONS AND DEBUG TRACKING

Load Options

nini_load_file OPTIONS hash:

Example:

my @locations;
my $data = nini_load_file('app.nini', {
  DIRS => ['/etc', '/home/user'],
  DL   => \@locations
});

# @locations now contains file:line information for each line

Parse Options

nini_parse_data OPTIONS hash:

Example:

my @locations;
my $config = nini_parse_data($data, {
  DEBUG => 1,
  DL    => \@locations
});

Debug Information

Accessing debug info:

for my $origin (@{$config->{section}{':origin'}}) {
  print "Defined at: $origin\n";
}

if (exists $config->{':warn'}) {
  for my $warning (@{$config->{':warn'}}) {
    warn "Config warning: $warning\n";
  }
}

Error Messages

Error messages include context:

nini: error: file load: loop detected for file [FILE]
nini: error: syntax error in data [LINE] at LOCATION
nini: error: unrecognisable data [LINE] at LOCATION
nini: error: cannot inherit parent branch into current at LOCATION

EXAMPLES

Simple Configuration

File: database.nini

= Database
host        localhost
port        5432
user        dbuser
password    secure_pass
options[,] ssl,compression,timeout

Perl:

my $cfg = nini_parse_data( nini_load_file( 'database.nini' ) );

print $cfg->{Database}{host};      # localhost
print $cfg->{Database}{port};      # 5432

my @opts = @{$cfg->{Database}{options}};
# @opts = ('ssl', 'compression', 'timeout')

Hierarchical Configuration

File: app.nini

= Application
    name    MyApp
    version 1.0

= Application Database
    host    db.example.com
    port    5432

= Application Cache
    enabled 1
    ttl     3600

Perl:

my $cfg = nini_parse_data( nini_load_file('app.nini') );

print $cfg->{Application}{name};
print $cfg->{Application}{Database}{host};
print $cfg->{Application}{Cache}{enabled};

Inheritance

File: servers.nini

= Default Server
    user          ubuntu
    ssh_key_file  /home/ubuntu/.ssh/id_rsa
    timeout       30

= Production Web Server
  & Default Server
  hostname      prod-web-01
  ip_address    192.168.1.100
  cpu_count     8

= Staging Web Server
  & Default Server
  hostname      staging-web-01
  ip_address    192.168.1.101
  cpu_count     4

Perl:

my $cfg = nini_parse_data( nini_load_file('servers.nini') );

my $prod = $cfg->{'Production Web Server'};
print $prod->{user};        # Inherited: ubuntu
print $prod->{hostname};    # Own: prod-web-01
print $prod->{timeout};     # Inherited: 30

Arrays and Delimiters

File: services.nini

= Services
    tcp_ports[,] 22,80,443,8080
    http_methods[|] GET|POST|PUT|DELETE
    workers[] worker1 worker2 worker3

Perl:

my $cfg = nini_parse_data( nini_load_file('services.nini') );

my @tcp = @{ $cfg->{Services}{tcp_ports} };
# @tcp = ('22', '80', '443', '8080')

my @methods = @{$cfg->{Services}{http_methods}};
# @methods = ('GET', 'POST', 'PUT', 'DELETE')

my @w = @{ $cfg->{Services}{workers} };
# @w = ('worker1', 'worker2', 'worker3')

File Inclusion

File: main.nini

= Application
    name MyApp

    @include database.nini
    @include services.nini

File: database.nini

= Application Database
    host localhost
    port 5432

File: services.nini

= Application Services
    api_port 8000

Perl:

my $cfg = nini_parse_data(
  nini_load_file('main.nini', {
    DIRS => ['.']
  }),
  {}
);

print $cfg->{Application}{name};
print $cfg->{Application}{Database}{host};
print $cfg->{Application}{Services}{api_port};

Abstract Sections and Auto-Indexing

File: items.nini

inventory *
      name      Laptop
      quantity  5
      price     1200

inventory *
      name      Mouse
      quantity  25
      price     50

inventory *
      name      Monitor
      quantity  10
      price     350

Perl:

my $cfg = nini_parse_data(
  nini_load_file('items.nini'),
  {}
);

# Access by numeric index
for my $i (0..2) {
  my $key = "$i";
  my $item = $cfg->{inventory}{$key};
  print "$item->{name}: $item->{quantity}\n";
}

Output:

Laptop: 5
Mouse: 25
Monitor: 10

Debug Information

File: config.nini

= Section1
    key1 value1

= Section2
    & NonExistent

Perl:

my @locations;
my $cfg = nini_parse_data(
  nini_load_file('config.nini', { DL => \@locations }),
  { DEBUG => 1, DL => \@locations }
);

# Check for warnings
if (exists $cfg->{':warn'}) {
  for my $w (@{$cfg->{':warn'}}) {
    print "Warning: $w\n";
  }
}

Output:

Warning: cannot find path to inherit [NonExistent] at config.nini line 5

ERROR HANDLING

Fatal Errors

Parser dies on:

Use eval to catch:

my $cfg;
eval {
  $cfg = nini_parse_data(
    nini_load_file('config.nini'),
    {}
  );
};

if ($@) {
  die "Failed to load config: $@\n";
}

Non-Fatal Issues

Warnings collected in :warn key:

Access warnings:

if (exists $config->{':warn'}) {
  for my $warning (@{$config->{':warn'}}) {
    warn "Config issue: $warning\n";
  }
}

Common Issues and Solutions

Value contains # character

Solution: Quote the value

key1 "#important"       # Value is: #important
key2 "URL: http://..."  # Value is: URL: http://...

Array parsing fails

Solution: Ensure separator is single character and matches usage

correct:   items [,] a,b,c
wrong:     items [,] a, b, c   # Spaces included in values
solution:  items [,] a, b, c   # Use proper format

Include file not found

Solution: Verify DIRS paths or use absolute paths

Relative:  @include config.nini
Absolute:  @include /etc/app/config.nini

Inheritance path not found

Solution: Ensure section exists and is defined before use

= Parent Section
key1 value1

= Child Section
& Parent Section
key2 value2

DEPENDENCIES

NINI uses dclone() from Storable module.

AUTHOR

Copyright: 2026 (c) Vladi Belperchinov-Shabanski "Cade" cade@noxrun.com

LICENSE

This module is distributed under the terms of the GNU General Public License, version 2 (GPLv2). See http://www.gnu.org/licenses/gpl-2.0.html for details.