Compare commits

..

No commits in common. 'master' and 'v1.0.1' have entirely different histories.

@ -1,11 +1,9 @@
The command line tool and library to read, manipulate and update .env files. The command line tool to read, manipulate and update .env files.
## Features ## Features
* by default, preserves white spaces, line breaks and comments * by default, preserves white spaces, line breaks and comments
* supports reading and updating .env files * supports reading and updating .end 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 * 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 * support multiple input files for updates as well as list of arguments at end of command
* supports single and double quoted values * supports single and double quoted values
@ -36,8 +34,6 @@ This can be changed by options:
* `--onlyUpdate` - prevents new keys to be appended, only existing keys are updated * `--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. * `--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`. To operate on files, specify input file(s) with `-i` and output file with `-o`. To modify first input file use `-m`.
### Modify files ### Modify files
@ -49,7 +45,7 @@ $ dotenv-tool -h
``` ```
Usage: dotenv-tool [options] [command] [paramsToSet...] Usage: dotenv-tool [options] [command] [paramsToSet...]
Tool to read, parse and update .env files Tool to read and update .env files
Arguments: Arguments:
paramsToSet space separated list of additional envs to set, in format key=value (default: "") paramsToSet space separated list of additional envs to set, in format key=value (default: "")
@ -61,7 +57,6 @@ Options:
-m, --modify Modify first input file -m, --modify Modify first input file
-b, --beautify Beautifies resulting env file -b, --beautify Beautifies resulting env file
-u, --onlyUpdate Only updates existing values, without appending new values -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 -p, --smartAppend Smart appends variables depending on their names
-r, --resilient Ignore files that cannot be read during update -r, --resilient Ignore files that cannot be read during update
-s, --silent Mute all messages and errors -s, --silent Mute all messages and errors
@ -69,7 +64,6 @@ Options:
Commands: Commands:
get [options] <key> Returns given variable from env file (if specified) get [options] <key> Returns given variable from env file (if specified)
parse [options] Parses and returns env file as JSON
``` ```
### Read prop from file ### Read prop from file
@ -91,27 +85,6 @@ Options:
-h, --help display help for command -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 ## API
Internally, library parses into and manipulates on array of tokens `Token[]`: Internally, library parses into and manipulates on array of tokens `Token[]`:
@ -171,7 +144,6 @@ Both `update` as well as `stringifyTokens` take `config` parameter:
type Config = { type Config = {
beautify?: boolean; // if true, output will be beautified/normalized. Default: false 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 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 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", "name": "dotenv-tool",
"version": "1.3.0", "version": "1.0.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "dotenv-tool", "name": "dotenv-tool",
"version": "1.3.0", "version": "1.0.1",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"commander": "^11.0.0", "commander": "^11.0.0",

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

@ -46,6 +46,64 @@ afterEach(() => {
mock.restore(); 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', () => { describe('Update command', () => {
it('creates empty output without params or stdin', () => { it('creates empty output without params or stdin', () => {
stdinContents = ''; stdinContents = '';
@ -157,24 +215,12 @@ 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'); 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', () => { it('merge files with smart append', () => {
program.parse(['node', 'test', '-p', '-i', 'first.env', 'second.env', 'third.env']); program.parse(['node', 'test', '-p', '-i', 'first.env', 'second.env', 'third.env']);
expect(mockExit).toBeCalled(); 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'); 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', () => { it('merge files and set from parameter', () => {
program.parse(['node', 'test', '-i', 'first.env', 'second.env', 'third.env', '--', 'SERVER_PASSWORD=updated value']); program.parse(['node', 'test', '-i', 'first.env', 'second.env', 'third.env', '--', 'SERVER_PASSWORD=updated value']);
expect(mockExit).toBeCalled(); expect(mockExit).toBeCalled();
@ -183,14 +229,6 @@ 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', () => { 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']); program.parse(['node', 'test', '-i', 'first.env', 'second.env', 'third.env', '--', 'SERVER_PASSWORD=updated value', 'INVA LID', 'LAST_ARGUMENT=correct']);
expect(mockExit).toHaveBeenCalledWith(1); expect(mockExit).toHaveBeenCalledWith(1);
@ -330,6 +368,19 @@ describe('Update command resiliency', () => {
}); });
describe('Silent flag', () => { 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', () => { it('reads from invalid stdin', () => {
stdinContents = 'junks'; stdinContents = 'junks';

@ -1,121 +0,0 @@
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);
});
});

@ -1,121 +0,0 @@
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}");
});
});

@ -6,14 +6,6 @@ import { parseMultiLine, stringifyTokens } from './parser';
import { Config, ModifyMode, TokenType, VariableToken } from './types'; import { Config, ModifyMode, TokenType, VariableToken } from './types';
import { update } from './manipulation'; 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 { export function normalizeParamVar(name: string, value: string): string {
let fixValue = value; let fixValue = value;
if (value.match(/[\s"']/)) { if (value.match(/[\s"']/)) {
@ -96,78 +88,19 @@ function makeReadCommand() {
return getCmd; 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() { export function makeProgram() {
const program = new Command(); const program = new Command();
program program
.name('dotenv-tool') .name('dotenv-tool')
.description('Tool to read, parse and update .env files') .description('Tool to read and update .env files')
.version(version, '-v, --version') .version(version, '-v, --version')
.addCommand(makeReadCommand()) .addCommand(makeReadCommand())
.addCommand(makeParseCommand())
.argument('[paramsToSet...]', 'space separated list of additional envs to set, in format key=value', '') .argument('[paramsToSet...]', 'space separated list of additional envs to set, in format key=value', '')
.option('-i, --files <filePaths...>', 'Input file(s)') .option('-i, --files <filePaths...>', 'Input file(s)')
.option('-o, --outputFile <filePath>', 'Output file') .option('-o, --outputFile <filePath>', 'Output file')
.option('-m, --modify', 'Modify first input file') .option('-m, --modify', 'Modify first input file')
.option('-b, --beautify', 'Beautifies resulting env file') .option('-b, --beautify', 'Beautifies resulting env file')
.option('-u, --onlyUpdate', 'Only updates existing values, without appending new values') .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('-p, --smartAppend', 'Smart appends variables depending on their names')
.option('-r, --resilient', 'Ignore files that cannot be read during update') .option('-r, --resilient', 'Ignore files that cannot be read during update')
.option('-s, --silent', 'Mute all messages and errors') .option('-s, --silent', 'Mute all messages and errors')
@ -195,7 +128,6 @@ export function makeProgram() {
let sourceParsingError = ''; let sourceParsingError = '';
config.beautify = options.beautify || false; config.beautify = options.beautify || false;
config.noUpdate = options.onlyAppend || false;
config.modifyMode = options.onlyUpdate ? ModifyMode.NO_APPEND : options.smartAppend ? ModifyMode.SMART_APPEND : ModifyMode.APPEND; config.modifyMode = options.onlyUpdate ? ModifyMode.NO_APPEND : options.smartAppend ? ModifyMode.SMART_APPEND : ModifyMode.APPEND;
const baseData = inputData.shift() || ''; const baseData = inputData.shift() || '';

@ -104,75 +104,6 @@ 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', () => { it('delete value', () => {
expect( expect(
stringifyTokens( stringifyTokens(
@ -296,108 +227,6 @@ 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', '###########\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', () => { describe('Fixing token list', () => {

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

@ -9,11 +9,7 @@ describe('MultiLine format parser', () => {
expect(parseMultiLine('VARNAME=value \t ')).toEqual([vtoken('VARNAME', 'value', '', '', '', ' \t ')]); expect(parseMultiLine('VARNAME=value \t ')).toEqual([vtoken('VARNAME', 'value', '', '', '', ' \t ')]);
expect(parseMultiLine('VARNAME=A:\\DYNA')).toEqual([vtoken('VARNAME', 'A:\\DYNA')]); 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=quote\'is"ok')).toEqual([vtoken('VARNAME', 'quote\'is"ok')]);
expect(parseMultiLine('VARNAME=value not escaped')).toEqual([vtoken('VARNAME', 'value not escaped')]); 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=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', () => { it('prop have defined names', () => {

@ -13,14 +13,15 @@ const escapedStrSQ = nonEscapedValuePartSQ.or(escapedChar.map((v: string) => v.r
const nonEscapedValuePartDQ = Parsimmon.regexp(/[^"\\]+/); const nonEscapedValuePartDQ = Parsimmon.regexp(/[^"\\]+/);
const escapedStrDQ = nonEscapedValuePartDQ.or(escapedChar.map((v: string) => v.replace('\\"', '"').replace('\\\\', '\\'))).many(); const escapedStrDQ = nonEscapedValuePartDQ.or(escapedChar.map((v: string) => v.replace('\\"', '"').replace('\\\\', '\\'))).many();
const simpleValueNotQuoted = Parsimmon.regexp(/[^"'\s][^\s]*([^\S\r\n]+[^\s#][^\s]*)*/); // 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 valueWithSpacesDoubleQuoted = Parsimmon.seqObj<string, any>(['quote', doubleQuote], ['value', escapedStrDQ.map((v) => v.join(''))], doubleQuote); 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 valueWithSpacesSingleQuoted = Parsimmon.seqObj<string, any>(['quote', singleQuote], ['value', escapedStrSQ.map((v) => v.join(''))], singleQuote);
const value = Parsimmon.alt( const value = Parsimmon.alt(
valueWithSpacesSingleQuoted, valueWithSpacesSingleQuoted,
valueWithSpacesDoubleQuoted, valueWithSpacesDoubleQuoted,
simpleValueNotQuoted.map((v) => ({ quote: '', value: v })), simpleValueWithoutSpacesAndQuotes.map((v) => ({ quote: '', value: v })),
).desc('property value'); ).desc('property value');
const commentOpt = Parsimmon.regexp(/[^\S\r\n]+#[^\r\n]*/); const commentOpt = Parsimmon.regexp(/[^\S\r\n]+#[^\r\n]*/);
const comment = Parsimmon.regexp(/[\n\r]*[^\S\r\n]*#[^\r\n]*/); const comment = Parsimmon.regexp(/[\n\r]*[^\S\r\n]*#[^\r\n]*/);

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

Loading…
Cancel
Save