Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 29 additions & 8 deletions src/configparser.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,19 @@ const interpolation = require('./interpolation');
* @type {RegExp}
* @private
*/
const SECTION = new RegExp(/^\s*\[([^\]]+)]$/);
const SECTION = new RegExp(/^(?<indent>\s*)\[(?<sectionName>[^\]]+)]$/);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I switch the RegExp to use named groups, so that it's easier to reference them later in the code (vs referencing by index number)


/**
* Regular expression to match key, value pairs.
* @type {RegExp}
* @private
*/
const KEY = new RegExp(/^\s*(.*?)\s*[=:]\s*(.*)$/);
const KEY = new RegExp(/^(?<indent>\s*)(?<key>.*?)\s*[=:]\s*(?<value>.*)$/);

/**
* Regular expression to match second+ lines in a multiline value.
*/
const MULTILINE_VALUE = new RegExp(/^(?<indent>\s*)(?<value>.*?)$/);

/**
* Regular expression to match comments. Either starting with a
Expand All @@ -34,7 +39,6 @@ const LINE_BOUNDARY = new RegExp(/\r\n|[\n\r\u0085\u2028\u2029]/g);

const readFileAsync = util.promisify(fs.readFile);
const writeFileAsync = util.promisify(fs.writeFile);
const statAsync = util.promisify(fs.stat);
const mkdirAsync = util.promisify(mkdirp);

/**
Expand Down Expand Up @@ -247,23 +251,37 @@ ConfigParser.prototype.writeAsync = async function(file, createMissingDirs = fal

function parseLines(file, lines) {
let curSec = null;
let curIndent = null;
let curKey = null;
lines.forEach((line, lineNumber) => {
if(!line || line.match(COMMENT)) return;
let res = SECTION.exec(line);
if(res){
const header = res[1];
const header = res.groups.sectionName;
curIndent = res.groups.indent;
curSec = {};
this._sections[header] = curSec;
} else if(!curSec) {
throw new errors.MissingSectionHeaderError(file, lineNumber, line);
} else {
res = KEY.exec(line);
const multilineRes = MULTILINE_VALUE.exec(line);
if(res){
const key = res[1];
curSec[key] = res[2];
} else {
throw new errors.ParseError(file, lineNumber, line);
const key = res.groups.key;
curSec[key] = res.groups.value;
curKey = key;
return;
} else if (multilineRes) {
const indent = multilineRes.groups.indent;
// If current line is more indented than the section header,
// we know it's a multiline string, in which case we append
// the line to the last key's value.
if (indent.indexOf(curIndent) !== -1 && indent.length > curIndent.length) {
curSec[curKey] += '\n' + multilineRes.groups.value;
return;
}
}
throw new errors.ParseError(file, lineNumber, line);
}
});
}
Expand All @@ -279,6 +297,9 @@ function getSectionsAsString() {
for(key in keys){
if(!keys.hasOwnProperty(key)) continue;
let value = keys[key];
// For multiline values, add a tab indent for subsequent lines,
// and normalize the line boundary with *nix style line endings.
value = value.replace(LINE_BOUNDARY, '\n\t');
out += (key + '=' + value + '\n');
}
out += '\n';
Expand Down
74 changes: 67 additions & 7 deletions test/configparser.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
const fs = require('fs');
const os = require('os');
const path = require('path');

const expect = require('chai').expect;
const ConfigParser = require('../src/configparser');

Expand All @@ -14,7 +18,10 @@ describe('ConfigParser object', function(){
'interpolation',
'permissive_section:headers%!?',
'more_complex_interpolation',
'sec'
'sec',
'multiline_value',
'multiline_value_with_empty_first_line',
'indented_section_with_multiline_value'
]);
});

Expand All @@ -39,11 +46,11 @@ describe('ConfigParser object', function(){
});

it('should indicate if a section has a key', function(){
expect(config.hasKey('section1', 'idontknow')).to.equal(true);
expect(config.hasKey('section1', 'fakekey')).to.equal(false);
expect(config.hasKey('fake section', 'fakekey')).to.equal(false);
expect(config.hasKey('permissive_section:headers%!?', 'hello')).to.equal(true);
expect(config.hasKey('sec', 'key')).to.equal(true);
expect(config.hasKey('section1', 'idontknow')).to.equal(true);
expect(config.hasKey('section1', 'fakekey')).to.equal(false);
expect(config.hasKey('fake section', 'fakekey')).to.equal(false);
expect(config.hasKey('permissive_section:headers%!?', 'hello')).to.equal(true);
expect(config.hasKey('sec', 'key')).to.equal(true);
});

it('should get the value for a key in the named section', function(){
Expand Down Expand Up @@ -90,4 +97,57 @@ describe('ConfigParser object', function(){
expect(config.removeSection('new-section')).to.equal(true);
expect(config.hasSection('new-section')).to.equal(false);
});
});

it('should handle multiline strings correctly', function(){
expect(config.get('multiline_value', 'key')).to.equal('this is a\nstring that spans\nmultiple lines');
expect(config.get('multiline_value_with_empty_first_line', 'key')).to.equal('\nthis is a\nstring that spans\nmultiple lines');
expect(config.get('indented_section_with_multiline_value', 'key')).to.equal('this is yet another\nstring that spans\nmultiple lines');
});

});

describe('ConfigParser write', function() {
let tempFilePath;
let tempDirPath;

before(function() {
// Create temporary directory and file for testing purpose
tempDirPath = fs.mkdtempSync(path.join(os.tmpdir(), 'temp-'));
tempFilePath = path.join(tempDirPath, 'temp.ini');
});

after(function() {
// Delete the temporary file
fs.unlinkSync(tempFilePath);
});

it('should write to a file', function() {
const config = new ConfigParser();

// Modify the configparser
config.addSection('section1');
config.set('section1', 'key', 'new value');
config.set('section1', 'another_key', 'new value');

// Write the configparser to the file
config.write(tempFilePath);

// Read the file and verify the content
const fileContent = fs.readFileSync(tempFilePath, 'utf8');
expect(fileContent).to.equal('[section1]\nkey=new value\nanother_key=new value\n\n');
});

it('should handle multiline values', function() {
const config = new ConfigParser();
config.addSection('section1');
// Modify the configparser with multiline values
config.set('section1', 'multiline_key', 'this is a\nmultiline value\nwith three lines');

// Write the configparser to the file
config.write(tempFilePath);

// Read the file and verify the content
const fileContent = fs.readFileSync(tempFilePath, 'utf8');
expect(fileContent).to.equal('[section1]\nmultiline_key=this is a\n\tmultiline value\n\twith three lines\n\n');
});
});
18 changes: 17 additions & 1 deletion test/data/file.ini
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,20 @@ file=mytextfile.txt
[sec]
key = [value]
test = [something 1234 hello]
[wow] = [45]
[wow] = [45]

[multiline_value]
key=this is a
string that spans
multiple lines

[multiline_value_with_empty_first_line]
key=
this is a
string that spans
multiple lines

[indented_section_with_multiline_value]
key=this is yet another
string that spans
multiple lines