← Runnable Projects

阅读中文版本

Env File Diff CLI

A zero-dependency CLI that parses two .env files and reports keys only on the left, only on the right, and keys whose values changed.

beginnerjavascriptunverified

Project

node-env-diff-cli

Entry point: envdiff.mjs

Install

node --version

Run

node envdiff.mjs base.env feature.env

Test

node --test envdiff.test.mjs

Expected output

For a base and feature env file the CLI prints only-in-left, only-in-right, and changed sections with one key per line.

Dependencies

  • node >=20

Security note

Prints key names only, never values, so the output is safe to paste into issues and pull request reviews.

What it does

Compares two .env files and classifies every key as only-in-left, only-in-right, or changed. Values never print, only key names, so the output is safe to share.

import { readFileSync } from 'node:fs';

export function parseEnv(text) {
  const out = new Map();
  for (const rawLine of text.split('\n')) {
    const line = rawLine.trim();
    if (line === '' || line.startsWith('#')) continue;
    const stripped = line.startsWith('export ') ? line.slice(7) : line;
    const eq = stripped.indexOf('=');
    if (eq <= 0) continue;
    const key = stripped.slice(0, eq).trim();
    let value = stripped.slice(eq + 1).trim();
    if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
      value = value.slice(1, -1);
    }
    out.set(key, value);
  }
  return out;
}

export function diffEnv(a, b) {
  const onlyLeft = [];
  const onlyRight = [];
  const changed = [];
  for (const [key, value] of a) {
    if (!b.has(key)) onlyLeft.push(key);
    else if (b.get(key) !== value) changed.push({ key, from: value, to: b.get(key) });
  }
  for (const key of b.keys()) {
    if (!a.has(key)) onlyRight.push(key);
  }
  return { onlyLeft, onlyRight, changed };
}

const [, , leftPath, rightPath] = process.argv;
if (leftPath && rightPath) {
  const result = diffEnv(parseEnv(readFileSync(leftPath, 'utf8')), parseEnv(readFileSync(rightPath, 'utf8')));
  const lines = [];
  for (const key of result.onlyLeft) lines.push('only-in-left: ' + key);
  for (const key of result.onlyRight) lines.push('only-in-right: ' + key);
  for (const c of result.changed) lines.push('changed: ' + c.key);
  console.log(lines.length ? lines.join('\n') : 'no differences');
}
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { parseEnv, diffEnv } from './envdiff.mjs';

test('parses quotes, comments, and export prefixes', () => {
  const env = parseEnv('export HOST=localhost\n# comment\nEMPTY=""\nPORT=8080');
  assert.equal(env.get('HOST'), 'localhost');
  assert.equal(env.get('PORT'), '8080');
  assert.equal(env.get('EMPTY'), '');
  assert.equal(env.size, 3);
});

test('diff classifies keys into three buckets', () => {
  const a = parseEnv('SHARED=same\nCHANGED=old\nONLY_A=1');
  const b = parseEnv('SHARED=same\nCHANGED=new\nONLY_B=2');
  const d = diffEnv(a, b);
  assert.deepEqual(d.onlyLeft, ['ONLY_A']);
  assert.deepEqual(d.onlyRight, ['ONLY_B']);
  assert.equal(d.changed.length, 1);
  assert.equal(d.changed[0].key, 'CHANGED');
});

Run and verify

node --test envdiff.test.mjs
node envdiff.mjs base.env feature.env

Run the test first, then diff two env files. The demo output lists the three buckets with one key per line.

Expected Output

The test run should print two passing tests for parsing and bucketing. The CLI’s console.log output should print the classified keys grouped by bucket, one key per line, with values never shown. Diffing a base.env that defines DEBUG and PORT=8080 against a feature.env that defines VERBOSE and PORT=3000, the Output: is:

only-in-left: DEBUG
only-in-right: VERBOSE
changed: PORT

Two files with the same keys and values print the single line no differences, and the parser tolerates export prefixes, comments, and quoted values before comparing.

Limitations

The parser does not interpolate variables inside values, and duplicated keys resolve last-wins without a warning. It is a diff tool, not a validator.