Compare commits

..

8 Commits

Author SHA1 Message Date
Dominik Dzienia 56b40de56c 1.3.0 2 years ago
Dominik Dzienia 2d466826fc Support parsing into JSON 2 years ago
Dominik Dzienia 133ed73568 1.2.0 2 years ago
Dominik Dzienia 6bf0f19d3d Support for onlyAppend mode 2 years ago
Dominik Dzienia 57caea14cb 1.1.0 2 years ago
Dominik Dzienia 2fcea26577 Parser accepts simple unquoted vars with spaces 2 years ago
Dominik Dzienia fbff65b29d 1.0.1 2 years ago
Dominik Dzienia 818d7f8f82 Adds documentation 2 years ago

@ -1,4 +1,18 @@
The command line tool to read, manipulate and update .env files.
The command line tool and library to read, manipulate and update .env files.
## Features
* by default, preserves white spaces, line breaks and comments
* supports reading and updating .env files
* can parse .env file into JSON format
* when parsing checks format validity and report error line number
* can read and write to stdin/stdout to be used in shell as a part of commands pipeline
* support multiple input files for updates as well as list of arguments at end of command
* supports single and double quoted values
* support comment lines and comments at end of variables
* can do smart append to preserve logical groups of variables
* can beautify (normalize) file layout
* command line can work in resilient mode, ignoring parsing errors
## Setup
@ -16,6 +30,16 @@ $ cat result.txt
value1
```
By default, new keys are added at end of resulting file.
This can be changed by options:
* `--onlyUpdate` - prevents new keys to be appended, only existing keys are updated
* `--smartAppend` - finds most similar group of variables and appends new variables between them, preserving alphabetical order.
To disable updating (overwriting) existing keys, and only append (supplement with) new keys, use `--onlyAppend`
To operate on files, specify input file(s) with `-i` and output file with `-o`. To modify first input file use `-m`.
### Modify files
```bash
@ -23,9 +47,9 @@ $ dotenv-tool -h
```
```
Usage: dotenv-tool [options] [command] [paramsToSet]
Usage: dotenv-tool [options] [command] [paramsToSet...]
Tool to read and update .env files
Tool to read, parse and update .env files
Arguments:
paramsToSet space separated list of additional envs to set, in format key=value (default: "")
@ -35,11 +59,17 @@ Options:
-i, --files <filePaths...> Input file(s)
-o, --outputFile <filePath> Output file
-m, --modify Modify first input file
-b, --beautify Beautifies resulting env file
-u, --onlyUpdate Only updates existing values, without appending new values
-a, --onlyAppend Only append new values, without updating existing values
-p, --smartAppend Smart appends variables depending on their names
-r, --resilient Ignore files that cannot be read during update
-s, --silent Mute all messages and errors
-h, --help display help for command
Commands:
get [options] <key> Returns given variable from env file (if specified)
parse [options] Parses and returns env file as JSON
```
### Read prop from file
@ -60,3 +90,88 @@ Options:
-f, --file <filePath> Input file to parse (if not given, stdio is used)
-h, --help display help for command
```
Also accept `-o` flag.
### Parse .env file/input as JSON
```bash
$ dotenv-tool parse -h
```
```
Usage: dotenv-tool parse [options]
Parses and returns env file as JSON
Options:
-f, --file <filePath> Input file to parse (if not given, stdio is used)
-n, --normalize Normalize keys in JSON (lowercase them)
-h, --help display help for command
```
Parse also accepts `-b` and `-o` flags.
## API
Internally, library parses into and manipulates on array of tokens `Token[]`:
* `VariableToken` holding contents of line with variable
* `SimpleToken` - for new lines, white space lines or comment lines.
To parse .env string, use `parseMultiLine`, to update list of tokens use `update`, to convert token list back into .env string use `stringifyTokens`.
```ts
import { parseMultiLine } from 'dotenv-tool'
const tokens = parseMultiLine("VAR1=value1\nVAR2=value2");
const updatedTokens = update(tokens, { name: 'VAR1', value: 'new value', comment: ' # optional comment' });
console.log(stringifyTokens(updatedTokens))
```
### update
```ts
function update(tokens: Token[], updateWith: VariableToUpdate[], config?: Config): Token[];
```
For convenience, `update` can get list of updates (`updateWith` argument) in three different formats:
* **Tuple**
`[string, string | null]` - with key on position `0` and value on position `1`
* **Simple object**
`Variable` type - contains key, value and optional comment
* **Token**
`VariableToken` type - useful when updating with result of parsing other .env strings
```ts
update(tokens, [['VAR1', 'new value'], ['VAR2', null]])
update(tokens, [ { name: 'VAR1', value: 'new value' }])
update(tokens, [
{
token: TokenType.VARIABLE,
name: 'VAR1',
value: 'new value',
beginning: '',
equals: ' = ', // extra space around =
quote: '"', // value will be inside double quotes
ending: ' ' // extra white space at end of line
comment: ''
}
])
```
If new value is `null` - **variable will be deleted**, if it is empty string or `undefined` - it will be left empty (`VAR=`)
### Configuration
Both `update` as well as `stringifyTokens` take `config` parameter:
```ts
type Config = {
beautify?: boolean; // if true, output will be beautified/normalized. Default: false
enforceNewLineEnd?: boolean; // if true, output will have ensured \n character at end. Default: true
noUpdate?: boolean; // if true, only new values will be appended, but existing one wont be updated
modifyMode?: ModifyMode; // decide if new keys should or should not be added, and where. Default: ModifyMode.APPEND
};
```

4
package-lock.json generated

@ -1,12 +1,12 @@
{
"name": "dotenv-tool",
"version": "1.0.1",
"version": "1.3.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "dotenv-tool",
"version": "1.0.1",
"version": "1.3.0",
"license": "MIT",
"dependencies": {
"commander": "^11.0.0",

@ -1,7 +1,7 @@
{
"name": "dotenv-tool",
"version": "1.0.0",
"description": "Tool to read and update .env files",
"version": "1.3.0",
"description": "Tool to read, parse and update .env files",
"repository": {
"type": "git",
"url": "https://gitea.dzienia.pl/shared/dotenv-tool.git"

@ -0,0 +1,121 @@
import { mockProcessExit, mockProcessStderr, mockProcessStdout } from 'jest-mock-process';
import { makeProgram } from './binlib';
import { Command } from 'commander';
import mock from 'mock-fs';
import fs from 'fs';
let mockStdout: jest.SpyInstance;
let mockStderr: jest.SpyInstance;
let mockExit: jest.SpyInstance;
let program: Command;
let stdinContents = '';
function getContents() {
return stdinContents;
}
jest.mock('./binutils.ts', () => ({
stdinToString: jest.fn(() => getContents()),
}));
beforeEach(() => {
mockStdout = mockProcessStdout();
mockStderr = mockProcessStderr();
program = makeProgram();
program.exitOverride();
mockExit = mockProcessExit();
mock({
'.env': 'ALA=makota',
'ugly.env': '########\n\n SOME = value \n\n\n ANOTHER= "value with space"',
'first.env': 'SERVER_PORT=80\nSERVER_HOST=localhost',
'second.env': 'SERVER_PORT=8080\nSERVER_PASSWORD=secret\nCLIENT_HOST=192.168.4.42',
'third.env': 'CLIENT_PORT=3000',
'broken.env': 'CLI ENT_PORT=3000',
'read-only.env': mock.file({
content: 'READ=only',
mode: 0o444,
}),
});
stdinContents = '';
});
afterEach(() => {
mockStdout.mockRestore();
mockStderr.mockRestore();
mockExit.mockRestore();
mock.restore();
});
describe('Standard utils', () => {
it('outputs help', () => {
expect(() => {
program.parse(['node', 'test', '--help']);
}).toThrow();
expect(mockStdout).toHaveBeenCalledWith(expect.stringContaining('Usage: dotenv-tool [options] [command] [paramsToSet...]'));
});
it('outputs version', () => {
expect(() => {
program.parse(['node', 'test', '-v']);
}).toThrow(
expect.objectContaining({
message: expect.stringMatching(/[0-9]+\.[0-9]+\.[0-9]+/),
}),
);
});
});
describe('Read command', () => {
it('reads from empty stdin', () => {
stdinContents = '';
program.parse(['node', 'test', 'get', 'TEST']);
expect(mockExit).toHaveBeenCalledWith(1);
expect(mockStderr).toBeCalledTimes(1);
expect(mockStderr).toHaveBeenNthCalledWith(1, expect.stringMatching(/Variable TEST not found in stdin/));
});
it('reads from invalid stdin', () => {
stdinContents = 'junks';
program.parse(['node', 'test', 'get', 'TEST']);
expect(mockExit).toHaveBeenCalledWith(1);
expect(mockStderr).toBeCalledTimes(2);
expect(mockStderr).toHaveBeenNthCalledWith(1, expect.stringMatching(/Parsing stdin failed/));
expect(mockStderr).toHaveBeenNthCalledWith(2, expect.stringMatching(/PARSING FAILED/));
});
it('reads from correct stdin', () => {
stdinContents = 'TEST=works';
program.parse(['node', 'test', 'get', 'TEST']);
expect(mockExit).toBeCalled();
expect(mockStdout).toHaveBeenCalledWith('works');
});
it('writes result to file', () => {
stdinContents = 'TEST=works';
program.parse(['node', 'test', 'get', '-o', 'test.txt', 'TEST']);
expect(mockExit).toBeCalled();
expect(fs.readFileSync('test.txt').toString()).toEqual('works');
});
it('reads from correct file', () => {
program.parse(['node', 'test', 'get', '-f', '.env', 'ALA']);
expect(mockExit).toBeCalled();
expect(mockStdout).toHaveBeenCalledWith('makota');
});
});
describe('Silent flag', () => {
it('reads from empty stdin', () => {
stdinContents = '';
program.parse(['node', 'test', '-s', 'get', 'TEST']);
expect(mockExit).toHaveBeenCalledWith(1);
expect(mockStderr).toBeCalledTimes(0);
});
it('reads from invalid stdin', () => {
stdinContents = 'junks';
program.parse(['node', 'test', '-s', 'get', 'TEST']);
expect(mockExit).toHaveBeenCalledWith(1);
expect(mockStderr).toBeCalledTimes(0);
});
});

@ -0,0 +1,121 @@
import { mockProcessExit, mockProcessStderr, mockProcessStdout } from 'jest-mock-process';
import { makeProgram } from './binlib';
import { Command } from 'commander';
import mock from 'mock-fs';
import fs from 'fs';
let mockStdout: jest.SpyInstance;
let mockStderr: jest.SpyInstance;
let mockExit: jest.SpyInstance;
let program: Command;
let stdinContents = '';
function getContents() {
return stdinContents;
}
jest.mock('./binutils.ts', () => ({
stdinToString: jest.fn(() => getContents()),
}));
beforeEach(() => {
mockStdout = mockProcessStdout();
mockStderr = mockProcessStderr();
program = makeProgram();
program.exitOverride();
mockExit = mockProcessExit();
mock({
'.env': 'ALA=makota',
'ugly.env': '########\n\n SOME = value \n\n\n ANOTHER= "value with space"',
'first.env': 'SERVER_PORT=80\nSERVER_HOST=localhost',
'second.env': 'SERVER_PORT=8080\nSERVER_PASSWORD=secret\nCLIENT_HOST=192.168.4.42',
'third.env': 'CLIENT_PORT=3000',
'broken.env': 'CLI ENT_PORT=3000',
'read-only.env': mock.file({
content: 'READ=only',
mode: 0o444,
}),
});
stdinContents = '';
});
afterEach(() => {
mockStdout.mockRestore();
mockStderr.mockRestore();
mockExit.mockRestore();
mock.restore();
});
describe('Parse command', () => {
it('reads from empty stdin', () => {
stdinContents = '';
program.parse(['node', 'test', 'parse']);
expect(mockStdout).toBeCalledTimes(2);
expect(mockStdout).toHaveBeenNthCalledWith(1, "{\"status\":\"ok\",\"values\":{}}");
});
it('reads from invalid stdin', () => {
stdinContents = 'junks';
program.parse(['node', 'test', 'parse']);
expect(mockExit).toHaveBeenCalledWith(1);
expect(mockStdout).toBeCalledTimes(2);
expect(mockStdout).toHaveBeenNthCalledWith(1, expect.stringMatching(/\"status\":\"parse_error\"/));
expect(mockStdout).toHaveBeenNthCalledWith(1, expect.stringMatching(/\"errorLine\":1/));
});
it('reads from correct stdin', () => {
stdinContents = 'TEST=works';
program.parse(['node', 'test', 'parse', 'TEST']);
expect(mockExit).toBeCalled();
expect(mockStdout).toHaveBeenCalledWith("{\"status\":\"ok\",\"values\":{\"TEST\":\"works\"}}");
});
it('writes result to file', () => {
stdinContents = 'TEST=works';
program.parse(['node', 'test', 'parse', '-o', 'test.txt']);
expect(mockExit).toBeCalled();
expect(fs.readFileSync('test.txt').toString()).toEqual("{\"status\":\"ok\",\"values\":{\"TEST\":\"works\"}}");
});
it('writes error to file', () => {
stdinContents = 'junks';
program.parse(['node', 'test', 'parse', '-o', 'test.txt']);
expect(mockExit).toBeCalled();
const fileCnt = fs.readFileSync('test.txt').toString();
expect(fileCnt).toMatch(/\"status\":\"parse_error\"/);
expect(fileCnt).toMatch(/\"errorLine\":1/);
});
it('writes error to file beautified', () => {
stdinContents = 'junks';
program.parse(['node', 'test', 'parse', '-b', '-o', 'test.txt']);
expect(mockExit).toBeCalled();
const fileCnt = fs.readFileSync('test.txt').toString();
expect(fileCnt).toMatch(/\"status\": \"parse_error\"/);
expect(fileCnt).toMatch(/\"errorLine\": 1/);
});
it('reads from correct file', () => {
program.parse(['node', 'test', 'parse', '-f', '.env']);
expect(mockExit).toBeCalled();
expect(mockStdout).toHaveBeenCalledWith("{\"status\":\"ok\",\"values\":{\"ALA\":\"makota\"}}");
});
it('parses file', () => {
program.parse(['node', 'test', 'parse', '-f', 'second.env']);
expect(mockExit).toBeCalled();
expect(mockStdout).toHaveBeenCalledWith("{\"status\":\"ok\",\"values\":{\"SERVER_PORT\":\"8080\",\"SERVER_PASSWORD\":\"secret\",\"CLIENT_HOST\":\"192.168.4.42\"}}");
});
it('normalize json keys', () => {
program.parse(['node', 'test', 'parse', '-n', '-f', 'second.env']);
expect(mockExit).toBeCalled();
expect(mockStdout).toHaveBeenCalledWith("{\"status\":\"ok\",\"values\":{\"server_port\":\"8080\",\"server_password\":\"secret\",\"client_host\":\"192.168.4.42\"}}");
});
it('beautify keys', () => {
program.parse(['node', 'test', 'parse', '-b', '-n', '-f', '.env']);
expect(mockExit).toBeCalled();
expect(mockStdout).toHaveBeenCalledWith("{\n \"status\": \"ok\",\n \"values\": {\n \"ala\": \"makota\"\n }\n}");
});
});

@ -46,64 +46,6 @@ afterEach(() => {
mock.restore();
});
describe('Standard utils', () => {
it('outputs help', () => {
expect(() => {
program.parse(['node', 'test', '--help']);
}).toThrow();
expect(mockStdout).toHaveBeenCalledWith(expect.stringContaining('Usage: dotenv-tool [options] [command] [paramsToSet...]'));
});
it('outputs version', () => {
expect(() => {
program.parse(['node', 'test', '-v']);
}).toThrow(
expect.objectContaining({
message: expect.stringMatching(/[0-9]+\.[0-9]+\.[0-9]+/),
}),
);
});
});
describe('Read command', () => {
it('reads from empty stdin', () => {
stdinContents = '';
program.parse(['node', 'test', 'get', 'TEST']);
expect(mockExit).toHaveBeenCalledWith(1);
expect(mockStderr).toBeCalledTimes(1);
expect(mockStderr).toHaveBeenNthCalledWith(1, expect.stringMatching(/Variable TEST not found in stdin/));
});
it('reads from invalid stdin', () => {
stdinContents = 'junks';
program.parse(['node', 'test', 'get', 'TEST']);
expect(mockExit).toHaveBeenCalledWith(1);
expect(mockStderr).toBeCalledTimes(2);
expect(mockStderr).toHaveBeenNthCalledWith(1, expect.stringMatching(/Parsing stdin failed/));
expect(mockStderr).toHaveBeenNthCalledWith(2, expect.stringMatching(/PARSING FAILED/));
});
it('reads from correct stdin', () => {
stdinContents = 'TEST=works';
program.parse(['node', 'test', 'get', 'TEST']);
expect(mockExit).toBeCalled();
expect(mockStdout).toHaveBeenCalledWith('works');
});
it('writes result to file', () => {
stdinContents = 'TEST=works';
program.parse(['node', 'test', 'get', '-o', 'test.txt', 'TEST']);
expect(mockExit).toBeCalled();
expect(fs.readFileSync('test.txt').toString()).toEqual('works');
});
it('reads from correct file', () => {
program.parse(['node', 'test', 'get', '-f', '.env', 'ALA']);
expect(mockExit).toBeCalled();
expect(mockStdout).toHaveBeenCalledWith('makota');
});
});
describe('Update command', () => {
it('creates empty output without params or stdin', () => {
stdinContents = '';
@ -215,12 +157,24 @@ describe('Update command', () => {
expect(mockStdout).toHaveBeenCalledWith('SERVER_PORT=8080\nSERVER_HOST=localhost\nSERVER_PASSWORD=secret\nCLIENT_HOST=192.168.4.42\nCLIENT_PORT=3000\n');
});
it('merge files without update', () => {
program.parse(['node', 'test', '-a', '-i', 'first.env', 'second.env', 'third.env']);
expect(mockExit).toBeCalled();
expect(mockStdout).toHaveBeenCalledWith('SERVER_PORT=80\nSERVER_HOST=localhost\nSERVER_PASSWORD=secret\nCLIENT_HOST=192.168.4.42\nCLIENT_PORT=3000\n');
});
it('merge files with smart append', () => {
program.parse(['node', 'test', '-p', '-i', 'first.env', 'second.env', 'third.env']);
expect(mockExit).toBeCalled();
expect(mockStdout).toHaveBeenCalledWith('CLIENT_HOST=192.168.4.42\nCLIENT_PORT=3000\nSERVER_PASSWORD=secret\nSERVER_PORT=8080\nSERVER_HOST=localhost\n');
});
it('merge files with smart append without update', () => {
program.parse(['node', 'test', '-p', '-a', '-i', 'first.env', 'second.env', 'third.env']);
expect(mockExit).toBeCalled();
expect(mockStdout).toHaveBeenCalledWith('CLIENT_HOST=192.168.4.42\nCLIENT_PORT=3000\nSERVER_PASSWORD=secret\nSERVER_PORT=80\nSERVER_HOST=localhost\n');
});
it('merge files and set from parameter', () => {
program.parse(['node', 'test', '-i', 'first.env', 'second.env', 'third.env', '--', 'SERVER_PASSWORD=updated value']);
expect(mockExit).toBeCalled();
@ -229,6 +183,14 @@ describe('Update command', () => {
);
});
it('merge files and set from parameter without update', () => {
program.parse(['node', 'test', '-a', '-i', 'first.env', 'second.env', 'third.env', '--', 'SERVER_PASSWORD=tried updated value', 'NEW_VALUE=append']);
expect(mockExit).toBeCalled();
expect(mockStdout).toHaveBeenCalledWith(
'SERVER_PORT=80\nSERVER_HOST=localhost\nSERVER_PASSWORD=secret\nCLIENT_HOST=192.168.4.42\nCLIENT_PORT=3000\nNEW_VALUE=append\n',
);
});
it('merge files and set from parameter and invalid parameter', () => {
program.parse(['node', 'test', '-i', 'first.env', 'second.env', 'third.env', '--', 'SERVER_PASSWORD=updated value', 'INVA LID', 'LAST_ARGUMENT=correct']);
expect(mockExit).toHaveBeenCalledWith(1);
@ -368,19 +330,6 @@ describe('Update command resiliency', () => {
});
describe('Silent flag', () => {
it('reads from empty stdin', () => {
stdinContents = '';
program.parse(['node', 'test', '-s', 'get', 'TEST']);
expect(mockExit).toHaveBeenCalledWith(1);
expect(mockStderr).toBeCalledTimes(0);
});
it('reads from invalid stdin', () => {
stdinContents = 'junks';
program.parse(['node', 'test', '-s', 'get', 'TEST']);
expect(mockExit).toHaveBeenCalledWith(1);
expect(mockStderr).toBeCalledTimes(0);
});
it('reads from invalid stdin', () => {
stdinContents = 'junks';

@ -6,6 +6,14 @@ import { parseMultiLine, stringifyTokens } from './parser';
import { Config, ModifyMode, TokenType, VariableToken } from './types';
import { update } from './manipulation';
type StringMapObj = { [key: string]: any };
enum ParseStatus {
OK = 'ok',
PARSE_ERROR = 'parse_error',
ERROR = 'error',
}
type ParseResult = { status: ParseStatus; values?: StringMapObj; error?: any; errorLine?: Number };
export function normalizeParamVar(name: string, value: string): string {
let fixValue = value;
if (value.match(/[\s"']/)) {
@ -88,19 +96,78 @@ function makeReadCommand() {
return getCmd;
}
function makeParseCommand() {
const getCmd = new Command('parse');
getCmd
.description('Parses and returns env file as JSON')
.option('-f, --file <filePath>', 'Input file to parse (if not given, stdio is used)')
.option('-n, --normalize', 'Normalize keys in JSON (lowercase them)')
.action((options, cmd) => {
const globOpts = cmd.optsWithGlobals();
const data = options.file ? fs.readFileSync(options.file).toString() : stdinToString();
const sourceName = options.file ? `file ${options.file}` : `stdin`;
let result: ParseResult = { status: ParseStatus.OK };
try {
const tokens = parseMultiLine(data);
let values: StringMapObj = {};
tokens
.filter((t) => t.token === TokenType.VARIABLE)
.forEach((vt) => {
let tokenName = (vt as VariableToken).name;
const tokenValue = (vt as VariableToken).value;
if (options.normalize) {
tokenName = tokenName.toLowerCase();
}
values[tokenName] = tokenValue;
});
result.values = values;
const outputStr = JSON.stringify(result, null, globOpts.beautify ? ' ' : '');
if (globOpts.outputFile) {
fs.writeFileSync(globOpts.outputFile, outputStr);
} else {
process.stdout.write(outputStr);
process.stdout.write('\n');
}
process.exit();
} catch (e: any) {
result.status = ParseStatus.ERROR;
result.error = e;
if (e.type == 'ParsimmonError') {
result.status = ParseStatus.PARSE_ERROR;
result.errorLine = e.result.index.line;
}
const outputStr = JSON.stringify(result, null, globOpts.beautify ? ' ' : '');
if (globOpts.outputFile) {
fs.writeFileSync(globOpts.outputFile, outputStr);
} else {
process.stdout.write(outputStr);
process.stdout.write('\n');
}
process.exit(1);
}
});
return getCmd;
}
export function makeProgram() {
const program = new Command();
program
.name('dotenv-tool')
.description('Tool to read and update .env files')
.description('Tool to read, parse and update .env files')
.version(version, '-v, --version')
.addCommand(makeReadCommand())
.addCommand(makeParseCommand())
.argument('[paramsToSet...]', 'space separated list of additional envs to set, in format key=value', '')
.option('-i, --files <filePaths...>', 'Input file(s)')
.option('-o, --outputFile <filePath>', 'Output file')
.option('-m, --modify', 'Modify first input file')
.option('-b, --beautify', 'Beautifies resulting env file')
.option('-u, --onlyUpdate', 'Only updates existing values, without appending new values')
.option('-a, --onlyAppend', 'Only append new values, without updating existing values')
.option('-p, --smartAppend', 'Smart appends variables depending on their names')
.option('-r, --resilient', 'Ignore files that cannot be read during update')
.option('-s, --silent', 'Mute all messages and errors')
@ -128,6 +195,7 @@ export function makeProgram() {
let sourceParsingError = '';
config.beautify = options.beautify || false;
config.noUpdate = options.onlyAppend || false;
config.modifyMode = options.onlyUpdate ? ModifyMode.NO_APPEND : options.smartAppend ? ModifyMode.SMART_APPEND : ModifyMode.APPEND;
const baseData = inputData.shift() || '';

@ -104,6 +104,75 @@ describe('Updating tokens', () => {
);
});
it('do not update in append only mode', () => {
expect(stringifyTokens(update([vtoken('ALA', 'ma kota')], [['ALA', 'ma psa']], { modifyMode: ModifyMode.APPEND, noUpdate: true }))).toEqual('ALA="ma kota"\n');
expect(
stringifyTokens(
update(
[vtoken('ALA', 'ma kota')],
[
['ALA', 'ma psa'],
['ALA', 'ma jednak kota'],
],
{ modifyMode: ModifyMode.APPEND, noUpdate: true },
),
),
).toEqual('ALA="ma kota"\n');
expect(
stringifyTokens(
update(
[vtoken('ALA', 'ma kota')],
[
['ALA', 'ma psa'],
['ALA', 'ma jednak kota'],
],
),
),
).toEqual('ALA="ma jednak kota"\n');
expect(
stringifyTokens(
update(
[vtoken('HOST', '127.0.0.1'), nltoken(), vtoken('PORT', '80'), nltoken(), vtoken('LOGIN', 'root')],
[['LOGIN', 'debian'], { name: 'PORT', value: '8080', comment: ' # debug only' }],
{ modifyMode: ModifyMode.APPEND, noUpdate: true },
),
),
).toEqual('HOST=127.0.0.1\nPORT=80\nLOGIN=root\n');
expect(
stringifyTokens(
update(
[
ctoken('###########'),
nltoken(),
ctoken('# Server'),
nltoken(),
ctoken('###########'),
nltoken('\n\n'),
vtoken('SERVER_HOST', '127.0.0.1'),
nltoken(),
vtoken('SERVER_PORT', '80'),
nltoken(),
vtoken('SERVER_LOGIN', 'root'),
nltoken('\n\n'),
ctoken('###########'),
nltoken(),
ctoken('# Client'),
nltoken(),
ctoken('###########'),
nltoken('\n\n'),
vtoken('CLIENT_LOGIN', 'john'),
],
[['SERVER_LOGIN', 'debian'], { name: 'SERVER_PORT', value: '8080', comment: ' # debug only' }, ['SERVER_PASSWORD', 'secret']],
{ modifyMode: ModifyMode.APPEND, noUpdate: true },
),
),
).toEqual(
'###########\n# Server\n###########\n\nSERVER_HOST=127.0.0.1\nSERVER_PORT=80\nSERVER_LOGIN=root\n\n###########\n# Client\n###########\n\nCLIENT_LOGIN=john\nSERVER_PASSWORD=secret\n',
);
});
it('delete value', () => {
expect(
stringifyTokens(
@ -227,6 +296,108 @@ describe('Updating tokens', () => {
'###########\n# Server\n###########\n\nSERVER_HOST=127.0.0.1\nSERVER_PASSWORD=secret\nSERVER_PORT=8080 # debug only\nSERVER_LOGIN=debian\n\n###########\n# Client\n###########\n\nCLIENT_LOGIN=john\n',
);
});
it('appends smart with no update config', () => {
expect(
stringifyTokens(
update(
[
vtoken('SERVER_HOST', '127.0.0.1'),
vtoken('SERVER_PORT', '80'),
vtoken('SERVER_LOGIN', 'root'),
ctoken('#-------------------------'),
vtoken('CLIENT_LOGIN', 'john'),
vtoken('CLIENT_X_AXIS', '12'),
ctoken('#-------------------------'),
vtoken('AUTO_RUN', 'true'),
vtoken('AUTO_CLEAN', 'false'),
],
[
['CLIENT_ACCESS', 'limited'],
['SERVER_OUTPUT', '/dev/null'],
['CLIENT_Z_AXIS', '100'],
['ZOOM', '100%'],
['AUTO_STOP', 'true'],
['QUALITY', '90%'],
['AUTO_APPEND', 'true'],
['AUTO_ZERO', '000'],
],
{ modifyMode: ModifyMode.SMART_APPEND, noUpdate: true },
),
),
).toEqual(
'SERVER_HOST=127.0.0.1\nSERVER_OUTPUT=/dev/null\nSERVER_PORT=80\nSERVER_LOGIN=root\n' +
'#-------------------------\n' +
'CLIENT_ACCESS=limited\nCLIENT_LOGIN=john\nCLIENT_X_AXIS=12\nCLIENT_Z_AXIS=100\n' +
'#-------------------------\n' +
'AUTO_APPEND=true\nAUTO_RUN=true\nAUTO_CLEAN=false\nAUTO_STOP=true\nAUTO_ZERO=000\n' +
'QUALITY=90%\nZOOM=100%\n',
);
expect(
stringifyTokens(
update(
[],
[
['SERVER_HOST', '127.0.0.1'],
['SERVER_PORT', '80'],
['SERVER_LOGIN', 'root'],
['CLIENT_LOGIN', 'john'],
['CLIENT_X_AXIS', '12'],
['AUTO_RUN', 'true'],
['AUTO_CLEAN', 'false'],
['CLIENT_ACCESS', 'limited'],
['SERVER_OUTPUT', '/dev/null'],
['CLIENT_Z_AXIS', '100'],
['ZOOM', '100%'],
['AUTO_STOP', 'true'],
['QUALITY', '90%'],
['AUTO_APPEND', 'true'],
['AUTO_ZERO', '000'],
],
{ modifyMode: ModifyMode.SMART_APPEND, noUpdate: true },
),
),
).toEqual(
'AUTO_APPEND=true\nAUTO_CLEAN=false\nAUTO_RUN=true\nAUTO_STOP=true\nAUTO_ZERO=000\n' +
'CLIENT_ACCESS=limited\nCLIENT_LOGIN=john\nCLIENT_X_AXIS=12\nCLIENT_Z_AXIS=100\n' +
'QUALITY=90%\n' +
'SERVER_HOST=127.0.0.1\nSERVER_LOGIN=root\nSERVER_OUTPUT=/dev/null\nSERVER_PORT=80\n' +
'ZOOM=100%\n',
);
expect(
stringifyTokens(
update(
[
ctoken('###########'),
nltoken(),
ctoken('# Server'),
nltoken(),
ctoken('###########'),
nltoken('\n\n'),
vtoken('SERVER_HOST', '127.0.0.1'),
nltoken(),
vtoken('SERVER_PORT', '80'),
nltoken(),
vtoken('SERVER_LOGIN', 'root'),
nltoken('\n\n'),
ctoken('###########'),
nltoken(),
ctoken('# Client'),
nltoken(),
ctoken('###########'),
nltoken('\n\n'),
vtoken('CLIENT_LOGIN', 'john'),
],
[['SERVER_LOGIN', 'debian'], { name: 'SERVER_PORT', value: '8080', comment: ' # debug only' }, ['SERVER_PASSWORD', 'secret']],
{ modifyMode: ModifyMode.SMART_APPEND, noUpdate: true },
),
),
).toEqual(
'###########\n# Server\n###########\n\nSERVER_HOST=127.0.0.1\nSERVER_PASSWORD=secret\nSERVER_PORT=80\nSERVER_LOGIN=root\n\n###########\n# Client\n###########\n\nCLIENT_LOGIN=john\n',
);
});
});
describe('Fixing token list', () => {

@ -106,6 +106,9 @@ export function update(tokens: Token[], updateWith: VariableToUpdate[], config?:
const updateVar: Variable | VariableToken = Array.isArray(u) ? { name: u[0], value: u[1] } : u;
const tokenToUpdate = tokens.findIndex((t) => t.token === TokenType.VARIABLE && (t as VariableToken).name === updateVar.name);
if (tokenToUpdate > -1) {
if (config?.noUpdate) {
return;
}
// delete
if (updateVar.value == null) {
if (tokenToUpdate < tokens.length - 1 && tokens[tokenToUpdate + 1].token === TokenType.NEWLINE) {

@ -9,7 +9,11 @@ describe('MultiLine format parser', () => {
expect(parseMultiLine('VARNAME=value \t ')).toEqual([vtoken('VARNAME', 'value', '', '', '', ' \t ')]);
expect(parseMultiLine('VARNAME=A:\\DYNA')).toEqual([vtoken('VARNAME', 'A:\\DYNA')]);
expect(parseMultiLine('VARNAME=quote\'is"ok')).toEqual([vtoken('VARNAME', 'quote\'is"ok')]);
expect(() => parseMultiLine('VARNAME=value not escaped')).toThrow('PARSING FAILED');
expect(parseMultiLine('VARNAME=value not escaped')).toEqual([vtoken('VARNAME', 'value not escaped')]);
expect(parseMultiLine('VARNAME= value not escaped')).toEqual([vtoken('VARNAME', 'value not escaped', '', '', '', '', '= ')]);
expect(parseMultiLine('VARNAME=value not escaped ')).toEqual([vtoken('VARNAME', 'value not escaped', '', '', '', ' ')]);
expect(parseMultiLine('VARNAME=this# is# value')).toEqual([vtoken('VARNAME', 'this# is# value')]);
expect(parseMultiLine('VARNAME= this# is# value # and comment')).toEqual([vtoken('VARNAME', 'this# is# value', '', ' # and comment', '', '', '= ')]);
});
it('prop have defined names', () => {

@ -13,15 +13,14 @@ const escapedStrSQ = nonEscapedValuePartSQ.or(escapedChar.map((v: string) => v.r
const nonEscapedValuePartDQ = Parsimmon.regexp(/[^"\\]+/);
const escapedStrDQ = nonEscapedValuePartDQ.or(escapedChar.map((v: string) => v.replace('\\"', '"').replace('\\\\', '\\'))).many();
// to alow spaces in simple values: [^"'\s][^\s]*([^\S\r\n]+\S+)* -- but that wont work in BASH so we do not allowed unescaped spaces!
const simpleValueWithoutSpacesAndQuotes = Parsimmon.regexp(/[^"'\s][^\s]*/);
const simpleValueNotQuoted = Parsimmon.regexp(/[^"'\s][^\s]*([^\S\r\n]+[^\s#][^\s]*)*/);
const valueWithSpacesDoubleQuoted = Parsimmon.seqObj<string, any>(['quote', doubleQuote], ['value', escapedStrDQ.map((v) => v.join(''))], doubleQuote);
const valueWithSpacesSingleQuoted = Parsimmon.seqObj<string, any>(['quote', singleQuote], ['value', escapedStrSQ.map((v) => v.join(''))], singleQuote);
const value = Parsimmon.alt(
valueWithSpacesSingleQuoted,
valueWithSpacesDoubleQuoted,
simpleValueWithoutSpacesAndQuotes.map((v) => ({ quote: '', value: v })),
simpleValueNotQuoted.map((v) => ({ quote: '', value: v })),
).desc('property value');
const commentOpt = Parsimmon.regexp(/[^\S\r\n]+#[^\r\n]*/);
const comment = Parsimmon.regexp(/[\n\r]*[^\S\r\n]*#[^\r\n]*/);

@ -35,6 +35,7 @@ export type Variable = {
export type Config = {
beautify?: boolean;
noUpdate?: boolean;
enforceNewLineEnd?: boolean;
modifyMode?: ModifyMode;
};

Loading…
Cancel
Save