Skip to content

Instantly share code, notes, and snippets.

@vitaly-t
Last active June 27, 2025 09:46
Show Gist options
  • Save vitaly-t/33e856b025632104e6477c384192a385 to your computer and use it in GitHub Desktop.
Save vitaly-t/33e856b025632104e6477c384192a385 to your computer and use it in GitHub Desktop.
Complete INI Parser for NodeJS
import {readFileSync} from 'node:fs';
/**
* Type of data produced by `parseIniFile`
*/
export type INI_Data<T> = { [name: string]: T | { [name: string]: T } };
/**
* Full section name, which consist of the section name itself, plus optional alias.
*
* In this implementation, when a section alias is specified, it is used for namespace
* resolution instead of the section name.
*/
export type INI_Section = { name: string, alias?: string };
/**
* Callback type for optional value converter used by `parseIniFile`.
*
* Note that `section` is `undefined` when it is global.
*/
export type INI_ConvertCB<T> = (cb: { key: string, value: string, section?: INI_Section }) => T;
/**
* Loads and parses an INI file, with an optional value-type converter.
*
* - section `[name]` namespaces are supported:
* - When a section appears multiple times, its inner values are extended.
* - Sections called `global` (case-insensitive) expose global variables.
* - Sections support aliasing: `[section "alias"]`, with the alias used as
* override for the section name, for namespace resolution.
* - each variable must be in the form of `name = value`
* - spaces surrounding `=`, `value` or section names are ignored
* - the `value` is taken until the end of line
* - lines that start with `;` or `#` are skipped
*/
export function parseIniFile<T = string>(iniFile: string, cb?: INI_ConvertCB<T>): INI_Data<T> {
const lines = readFileSync(iniFile, 'utf-8')
.replace(/\r/g, '')
.split('\n')
.map(a => a.trim())
.filter(f => f.length > 1 && f[0] !== ';' && f[0] !== '#');
const result: INI_Data<T> = {};
let root: any = result, section: INI_Section | undefined;
for (const a of lines) {
const m = a.match(/^\s*([\w$][\w.$]*)\s*=\s*(.*)/);
if (m) {
const key = m[1], value = m[2];
root[key] = typeof cb === 'function' ? cb({key, value, section}) : value;
} else {
const s = a.match(/\[\s*([\w$.-]+)\s*("(.*)")?\s*]/);
if (s) {
section = {name: s[1], alias: s[3]};
const name = section.alias || section.name; // alias overrides name
if (name.toLowerCase() === 'global') {
root = result;
section = undefined;
} else {
root = result[name] ??= {};
}
}
}
}
return result;
}
@vitaly-t
Copy link
Author

vitaly-t commented Jun 19, 2025

Limitations:

  • Section names with dots are supported as mere strings (no nesting sub-section logic)
  • Property names with dots are consumed as they are (no nesting sub-property logic)

@vitaly-t
Copy link
Author

vitaly-t commented Jun 26, 2025

UPDATE:

In the end though, I published this code in its own repo. Check that repo for the latest code, I'm no longer updating this gist.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment