NAME
File::SOPS::Format::INI - INI format handler for SOPS
VERSION
version 0.003
SYNOPSIS
use File::SOPS::Format::INI;
# Parse an INI document (encrypted or plaintext)
my ($data, $metadata) = File::SOPS::Format::INI->parse($ini_string);
# Serialize a tree plus its metadata back to an INI document
my $ini = File::SOPS::Format::INI->serialize(
data => $data,
metadata => $metadata,
);
# The tree is exactly two levels deep: section -> key
$data->{db}{host};
# A section's comments live under the empty key, as a list
$data->{db}{''}; # [ File::SOPS::Comment, ... ]
DESCRIPTION
INI format handler for SOPS documents. Reproduces what the reference implementation does with an ini file, which is what Go's gopkg.in/ini.v1 does: this handler's grammar is that library's, measured against sops 3.13.3 rather than read out of its source.
The tree is exactly two levels deep -- section, then key -- because that is all the format can express. A value outside any section belongs to the section named DEFAULT, which is where sops puts it too.
The line
[name]opens a section. The name runs to the last]on the line. An empty name is refused, as sops refuses it.A line whose first non-blank character is
;or#is a comment. Consecutive comment lines are one leaf, whose text is the block with the first line's marker stripped and every later line kept verbatim.Anything else is
key = value. Both=and:are delimiters and the first one found wins. The key may be quoted with backticks or"""; everything before the delimiter is otherwise trimmed and taken as it stands.The value is read the way go-ini reads it, which is not "the rest of the line": a leading
"""or backtick quotes it, possibly across several lines; otherwise a trailing backslash continues it onto the next line, an unquoted#or;starts an inline comment, and a matching pair of surrounding quotes is stripped.A blank line is skipped and does not end a comment block.
A comment at the end of the file is dropped, because it has no following node to attach to. sops drops it too.
Where a comment lives, and why it is not where Format::ENV puts one
Measured against sops 3.13.3, with a comment in every position an ini document has: a comment leaf authenticates under the AAD of its section -- db:, never db::. sops attaches a comment to the node that follows it, and both a comment above a key and a comment above the section header itself end up in that section's branch with the section's own path.
So a section's comments live under the empty key of that section, as a list:
{
db => {
'' => [ File::SOPS::Comment->new(text => 'a note') ],
host => 'localhost',
},
}
The empty key is reserved for it. Nothing collides: sops refuses an empty key name outright (exit 2), so an ini document cannot carry one as data.
File::SOPS::Format::ENV keeps its comments under the empty key of the document, which produces the AAD : -- the same thing for a flat format and a different thing here. See docs/adr/0047.
Where this handler differs from sops, and why
Sections and keys are written sorted. sops writes them in document order. The MAC's encrypt side hashes leaves in
sort keysorder at every level, so a document this library writes has to be in that order or it fails its own digest. Reading takes its order from the document, which is what "parse_in_document_order" is for.A section's comments are written before its header, as a block, because the tree is a Perl hash and comment position is gone before any emitter sees it. Measured: comment position is in no digest -- every comment line in a sops-written file moved to the top still reads at exit 0 -- and a comment above a section header is read back into that same section.
DEFAULTis written with an explicit[DEFAULT]header. sops writes the implicit section headerless and first. Writing it in its sorted position is what keeps the sorted order above true for every section name; go-ini creates its implicit emptyDEFAULTbeside the explicit one and sops reads the result at exit 0, measured.A value the format cannot carry unchanged is refused rather than written -- see below.
A boolean, a null and an integral float are written as the bytes the digest covers, not as sops writes them. docs/adr/0035, unchanged here.
The quoting, and the values that are refused
go-ini's writer quotes a value in three cases and its reader undoes rather more than that, so the two do not always agree. Measured, sops 3.13.3, on unencrypted values -- the only ones whose text reaches the file as itself:
value sops wrote sops -d
a#b `a#b` a#b round trips
a;b `a;b` a;b round trips
line1<LF>line2 """line1<LF>line2""" the two lines
a`b """a`b""" a`b round trips
" leading" " leading" kept
"" "" LOSSY: read back as the empty string
'quoted' 'quoted' LOSSY: read back as quoted
"quoted" "quoted" LOSSY: read back as quoted
"""x""" """x""" LOSSY: read back as x
The last four are files sops writes at exit 0 and then refuses to read (MAC mismatch, exit 51), because its reader strips a matching pair of surrounding quotes that its writer never put there.
This handler refuses such a value instead, and it asks the question the way docs/adr/0030 asks it in the dotenv handler: the value is quoted with this format's writer, read back with this format's reader, and refused when the two disagree. A test for a character would be a second spelling of the rule and would drift from it.
The encrypted slot never reaches this: an ENC[...] string is base64 plus []:,=, which the writer leaves bare and the reader returns unchanged.
An unencrypted leaf is written as its digest bytes
docs/adr/0035, and it is the dotenv handler's rule line for line -- the two were measured together. The ini store has no type syntax, so an unencrypted leaf's digest input is the literal text of its line, and this handler writes exactly File::SOPS::Encrypted->value_to_bytes for it. That is where a boolean becomes True, an undef the empty string, and 1.0 the token 1: three values sops writes in a display form and then cannot read back (k124, k125, k137).
What this handler cannot carry
Nesting. The format is two levels deep. sops does not refuse a deeper tree -- measured, it writes
inner = [{host ENC[...]}], a Go struct dump that is not a document any more. This handler refuses it.A top-level scalar. sops refuses one itself:
Section values should always be TreeBranches, exit 4.A duplicate section name. sops keeps both as separate branches -- a document it writes can contain two
[db]blocks -- and a Perl hash cannot.A duplicate key in one section. sops keeps the last silently; this handler refuses rather than dropping a line the caller wrote. No sops-written document can contain one, because go-ini already collapsed it on the way in.
A section named
sops. That is where the metadata goes. sops refuses it too, exit 203.Bytes that are not valid UTF-8. This distribution's boundary is characters and its emitters encode unconditionally (docs/adr/0003), so bytes that carry no meaning as text are refused rather than double-encoded later.
parse
my ($data, $metadata) = File::SOPS::Format::INI->parse($ini_string);
Class method. Parses an INI document -- encrypted or plaintext -- and returns the data as a HashRef of sections and a File::SOPS::Metadata object built from the [sops] section, or undef where the document has none.
The tree is two levels deep. A value written outside any section is in the section DEFAULT. A section's comments are a list under its empty key.
Dies on a line that is not a comment, a section header or a key = value pair; on a section opened twice; on a key set twice inside one section; on an empty section or key name; and on bytes that are not valid UTF-8.
parse_in_document_order
my $ordered = File::SOPS::Format::INI->parse_in_document_order($content);
Reparses $content for its key order only and returns the document as a HashRef whose sections, and whose keys within each section, iterate in the order the file writes them, with the [sops] section removed. Returns nothing when the text cannot be read that way.
This is the half of MAC verification that only a format handler can supply: an ini document sops wrote is in document order, and without this the digest would be taken in sorted key order and every such file whose keys were not already sorted would fail to verify. The values are undef -- only the shape is read, never a value -- and each section's comment sequence is present with the right length so that the shape matches "parse"'s. The order comes from a tied hash, which is the whole of what "order preserving" means for a format whose parser has none. See docs/adr/0036.
serialize
my $ini = File::SOPS::Format::INI->serialize(
data => \%data,
metadata => $metadata_obj,
);
Class method. "emit" plus the flat metadata in a [sops] section, appended at the end as sops writes it.
The data parameter must be a HashRef of sections and metadata a File::SOPS::Metadata object. Dies through "emit" if the data carries a section named sops, which is where the metadata goes.
emit
my $ini = File::SOPS::Format::INI->emit(\%data);
Class method. Turns a tree into an INI document without a metadata section: each section's comments, then its [name] header, then its key = value lines padded to the section's longest key -- sections in sorted order, keys sorted within each.
This is the emitter "serialize" uses and the one File::SOPS::decrypt_file and File::SOPS::edit write plaintext with, so it is on the wire path. In particular the sorted order is not a formatting preference -- see "Where this handler differs from sops, and why".
An encrypted leaf is written verbatim. Everything else is written as File::SOPS::Encrypted->value_to_bytes, quoted for the format, and refused where that quoting does not round-trip (see "The quoting, and the values that are refused").
Dies on a section that is not a HashRef, on a nested value, on an unblessed reference, on a comment in a value slot, on a section named sops, and on anything in a section's empty key that is not a comment. A blessed leaf is written as its stringification, which is what the digest covers for it -- where File::SOPS::Format::YAML and File::SOPS::Format::JSON refuse one, because their emitters would write it as something else (docs/adr/0008). Here document and digest cannot disagree: they are the same call.
detect_content
if (File::SOPS::Format::INI->detect_content($encrypted)) { ... }
Class method. True when $content is an encrypted INI document: every line is a comment, a section header or a key = value pair, and one of the sections is [sops].
Both halves are needed, and this answers for encrypted content only -- a plaintext .ini carries no metadata at all, and a line of key: value scans as an ini pair whatever document it came from. Plaintext is recognised by its file name.
format_name
Returns 'ini'.
file_extensions
Returns a list of file extensions: ('ini').
detect
if (File::SOPS::Format::INI->detect($filename)) {
# File is an INI file based on extension
}
Class method to detect if a filename is an INI file based on extension.
Returns true if the filename ends with .ini (case-insensitive).
SEE ALSO
File::SOPS - Main SOPS interface
File::SOPS::Format::ENV - the dotenv handler, which shares this one's flat metadata, type rule and order-preserving reparse
File::SOPS::Format::ENV::Ordered - the tied hash both of them carry that reparse's key order in, one per section here
File::SOPS::Metadata::Flat - the flat metadata encoding, here with an empty prefix
File::SOPS::Comment - the comment leaf
docs/adr/0022 - the flat metadata encoding, and why INI escapes its metadata and not its data
docs/adr/0035 - what an unencrypted leaf is written as
docs/adr/0036 - the order-preserving parse, and the contract above
docs/adr/0041 - the comment leaf, and why it is not in the digest
docs/adr/0047 - this handler: the whole document measured, the comment AAD, and the four questions the ticket left open
SUPPORT
Issues
Please report bugs and feature requests on GitHub at https://github.com/Getty/p5-file-sops/issues.
CONTRIBUTING
Contributions are welcome! Please fork the repository and submit a pull request.
AUTHOR
Torsten Raudssus <getty@cpan.org>
COPYRIGHT AND LICENSE
This software is copyright (c) 2026 by Torsten Raudssus.
This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.