Skip to content
Merged
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
.task
.vscode
.env
.ds_store
node_modules

dotenv-*.tgz
34 changes: 25 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ A simple way to retrieve and update variables from a .env file.
npm i -g @mikegarde/dotenv-cli
```

## Usage
## CLI Usage

Get a value from a .env file:

Expand All @@ -34,32 +34,48 @@ Return a single value from a .env file as JSON:
dotenv <key> --json
```

## Multiline Values
### Multiline Values

The default behavior is to output a single line value. If you want to output a multiline value,
you can use the `--multiline` flag:

```shell
$ dotenv RSA_KEY
-----BEGIN RSA PRIVATE KEY-----\nMIIBOgIBAAJBAKj34GkxFhD90vcNLYLInFEX6Ppy1tPf...

$ dotenv RSA_KEY --multiline
-----BEGIN RSA PRIVATE KEY-----
MIIBOgIBAAJBAKj34GkxFhD90vcNLYLInFEX6Ppy1tPf9Cnzj4p4WGeKLs1Pt8Qu
KUpRKfFLfRYC9AIKjbJTWit+CqvjWYzvQwECAwEAAQJAIJLixBy2qpFoS4DSmoEm


$ dotenv RSA_KEY
-----BEGIN RSA PRIVATE KEY-----\nMIIBOgIBAAJBAKj34GkxFhD90vcNLYLInFE...
```

## Setting a Value
### Setting a Value

Set a value in a .env file:

```shell
dotenv <key> --set <value>
```

Quotes will be added if needed, but you can also force them:
Or pipe a value in:

```shell
echo <value> | dotenv <key>
```

This example will
- Generate a new RSA key pair and store it in the .env file
- Utilizing the stored private key it will generate a public key and store it in the .env file

```shell
openssl genpkey -algorithm RSA -outform PEM -pkeyopt rsa_keygen_bits:2048 2>/dev/null | dotenv RSA_KEY
dotenv RSA_KEY --multiline | openssl rsa -pubout 2>/dev/null | dotenv RSA_PUB
```

### Deleting a Value

Delete a value from a .env file:

```shell
dotenv <key> --set <value> --quote
dotenv <key> --delete
```
91 changes: 78 additions & 13 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import * as url from 'node:url';

import log, {setLogDebug} from './log.js';
import escapeAndQuote from "./escapeAndQuote.js";
import readPipe from "./readPipe.js";

async function app() {
const installDir: string = path.dirname(url.fileURLToPath(import.meta.url));
Expand All @@ -27,6 +28,7 @@ async function app() {
.option('-m, --multiline', 'Allow multiline values')
.option('-s, --set <value>', 'Update the environment variable in the .env file')
.option('-q, --quote', 'Quote the value when --set regardless of need')
.option('-D, --delete', 'Delete the environment variable from the .env file')
.option('-d, --debug', 'Output extra debugging')
.showSuggestionAfterError(true)
.parse(process.argv);
Expand All @@ -35,6 +37,10 @@ async function app() {

setLogDebug(options.debug);

const stdin: string | void = await readPipe().catch((err) => {
throw new RuleViolation(`Error reading from stdin: ${err}`);
});

const envFilePath: string = options.file || '.env';
const fullEnvPath: string = path.resolve(envFilePath);
const keys: string[] = program.args;
Expand All @@ -45,67 +51,126 @@ async function app() {
log.debug('Key count (0 or >1) defaulting to JSON');
options.json = true;
}

// Determine if we are setting a value, and if so, what's the value
let setValue: string = '';
if (stdin && set) {
// - cannot have both --set [value] and stdin
throw new RuleViolation('Cannot use --set and stdin together');
} else if (stdin) {
setValue = stdin;
} else if (set) {
setValue = set;
}

log.debug('Keys:', keys);
log.debug('Options:', options);
log.debug('File:', fullEnvPath);

const json: boolean = (options.json !== undefined);
const multiline: boolean = (options.multiline !== undefined);
const quoteSet: boolean = (options.quote !== undefined);
const deleteKey: boolean = (options.delete !== undefined);
const singleKey: boolean = (keys.length === 1);

// Qualifying Rules
// - must have a .env file
if (!fs.existsSync(envFilePath)) {
throw new RuleViolation(`.env file not found: ${fullEnvPath}`);
}
// - cannot have both --json and --set
if (json && set) {
if (json && setValue) {
throw new RuleViolation('Cannot use --json and --set together');
}
// - must have a key if using --set
if (set && (!keys.length || keys.length > 1)) {
if (setValue && !singleKey) {
throw new RuleViolation('Must specify a single key when using --set');
}
// - cannot have both --json and --multiline
if (json && multiline) {
throw new RuleViolation('Cannot use --json and --multiline together');
}
// - cannot use --delete with any other options
if (deleteKey && (setValue || json || multiline)) {
throw new RuleViolation('Cannot use --delete with any other options');
}
// - must have a key if using --delete
if (deleteKey && !singleKey) {
throw new RuleViolation('Must specify a single key when using --delete');
}

let envObject = parseEnvFile(envFilePath);

if (json && !keys.length) {
log.debug('Outputting entire .env file as JSON');
log.info(JSON.stringify(envObject));
} else if (set) {
log.info(envObject.toJsonString());
} else if (deleteKey) {
const key: string = keys[0];

log.debug(`Deleting "${key}"`);

if (envObject[key]) {
const lineStart = envObject[key].lineStart;
const lineEnd = envObject[key].lineEnd;
log.debug(`Deleting lines ${lineStart}-${lineEnd}`);

// Read the file and split it into an array of lines
let lines: string[] = fs.readFileSync(envFilePath, 'utf8').split('\n');

// Remove the lines between lineStart and lineEnd
lines.splice(lineStart, lineEnd - lineStart + 1);

// Join the lines back together and write the result back to the file
fs.writeFileSync(envFilePath, lines.join('\n'));
} else {
log.debug(`Environment variable "${key}" not found`);
process.exitCode = 1;
}
} else if (setValue) {
const key: string = keys[0];
const newValue: string = escapeAndQuote(set, quoteSet);
const line: string = `${key}=${newValue}`;
const newValue: string = escapeAndQuote(setValue, quoteSet);
const newLines: string = `${key}=${newValue}`;

log.debug(`Updating "${key}"`);

// Do we want to update or append the .env file?
if (envObject[key]) {
log.debug(`Replacing "${key}" in "${envFilePath}"`);
log.debug('Updating existing key', envObject[key]);
const lineStart = envObject[key].lineStart;
const lineEnd = envObject[key].lineEnd;
log.debug(`Replacing lines ${lineStart}-${lineEnd}`);

const regex: RegExp = new RegExp(`${key}=.+`);
const data: string = fs.readFileSync(envFilePath, 'utf8').replace(regex, line);
fs.writeFileSync(envFilePath, data);
// Split the new lines into an array
let newLinesArray: string[] = newLines.split('\n');

// Read the file and split it into an array of lines
let lines: string[] = fs.readFileSync(envFilePath, 'utf8').split('\n');

// Replace the lines between lineStart and lineEnd
lines.splice(lineStart, lineEnd - lineStart + 1, ...newLinesArray);

// Join the lines back together and write the result back to the file
fs.writeFileSync(envFilePath, lines.join('\n'));
} else {
log.debug(`Appending "${key}" to "${envFilePath}"`);

fs.writeFileSync(envFilePath, `${line}\n`, {flag: 'a'});
fs.writeFileSync(envFilePath, `${newLines}\n`, {flag: 'a'});
}
} else {
let result: string = '';

for (const key of keys) {
log.debug(`Getting "${key}"`);

let value = formatValue(envObject[key], multiline);
if (!value) {
let value = '';

if (!envObject[key]) {
log.debug(`Environment variable "${key}" not found`);
process.exitCode = 1;
} else {
value = formatValue(envObject[key].value, multiline);
}

value = json ? (value ? `"${value}"` : 'null') : value;
result += json ? `"${key}": ${value},` : `${value}\n`;
}
Expand Down
101 changes: 101 additions & 0 deletions src/envObject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/**
* Used to create an EnvObject that can be used to store environment variables
*/
class EnvValue {
value: string;
lineStart: number;
lineEnd: number;

constructor(value: string, lineStart: number = -1, lineEnd: number = -1) {
this.value = value;
this.lineStart = lineStart;
this.lineEnd = lineEnd;
}
}

/**
* Used to create an object that can be used to store environment variables
*/
class EnvObject {
[key: string]: EnvValue | any;

/**
* Constructor for the EnvObject class.
*/
constructor() {
return new Proxy(this, {
/**
* GET trap that is used to get the value of EnvObject
*/
// get(target, key: PropertyKey, receiver) {
// if (typeof key === 'string' && typeof target[key] === 'object' && target[key] !== null) {
// console.log('getting', key, target[key]);
// return Reflect.get(target[key], 'value', receiver);
// } else {
// return Reflect.get(target, key, receiver);
// }
// },
/**
* SET trap that is used to set the value of EnvObject
*/
set(target, key: PropertyKey, value, receiver) {
if (typeof key !== 'string') {
key = 'value';
}
if (value instanceof EnvValue) {
target[key] = value;
return true;
}
if (typeof value === 'object' && value !== null) {
target[key] = new EnvValue(value.value, value.lineStart, value.lineEnd);
return true;
}
if (typeof value === 'string') {
if (target[key] instanceof EnvValue) {
target[key] = value;
} else {
target[key] = new EnvValue(value);
}

}
if (typeof target[key] === 'object' && target[key] !== null) {
target[key].value = value;
} else {
// TODO: let's not allow this
target[key as string] = {
value: value,
lineStart: -1, // You might want to set these values appropriately
lineEnd: -1
};
}
return true;
}
});
}

/**
* Used to convert the EnvObject to an key/value object
*/
toObj(): { [key: string]: string } {
let obj: { [key: string]: string } = {};

for (const key in this) {
const keyName = key as string;
const value = this[keyName].value;

if (typeof value === 'string') {
obj[keyName] = value;
}
}
return obj;
}

/**
* Used to convert the EnvObject to a JSON string
*/
toJsonString(): string {
return JSON.stringify(this.toObj());
}
}

export default EnvObject;
Loading