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
5 changes: 5 additions & 0 deletions .changeset/odd-vans-glow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'dotenv-diff': minor
---

feat: --explain <key> to show a detailed breakdown of a single environment varaible
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,18 @@ API_TOKEN=

---

## Explain a variable (`--explain`)

Inspect a specific environment variable to see where it is defined, where it is used in the codebase, and its overall status:

```bash
dotenv-diff --explain DATABASE_URL
```

→ See [--explain Documentation](./docs/configuration_and_flags.md#--explain-key) for more details.

---

## Monorepo support

In monorepos with multiple apps and packages, you can include shared folders:
Expand Down
37 changes: 37 additions & 0 deletions docs/configuration_and_flags.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ CLI flags always take precedence over configuration file values.
### Display Options

- [--list-all](#--list-all)
- [--explain](#--explain-key)
- [--show-unused](#--show-unused)
- [--no-show-unused](#--no-show-unused)
- [--show-stats](#--show-stats)
Expand Down Expand Up @@ -465,6 +466,42 @@ Usage in the configuration file:

---

### `--explain <key>`

Shows a detailed breakdown of a single environment variable: where it is defined in env files, where it is used in the codebase, and its overall status.

This is useful for debugging a specific variable — for example when you want to confirm it is defined, find all usage sites, or understand why it is flagged as missing, unused, or ignored.

Example usage:

```bash
dotenv-diff --explain DATABASE_URL
```

The output shows:

- **Key** — the variable name
- **Status** — one of: `ok`, `missing`, `unused`, `ignored`, or `duplicated`
- **Defined in** — which env files the key appears in (e.g. `.env`, `.env.example`)
- **Used in** — file paths and line numbers where the key is referenced in the codebase
- **Checks** — a checklist of whether the key is defined, used, duplicated, and/or ignored

Use `--json` together with `--explain` to get the result as structured JSON:

```bash
dotenv-diff --explain DATABASE_URL --json
```

Usage in the configuration file:

```json
{
"explain": "DATABASE_URL"
}
```

---

### `--show-unused`

List variables that are defined in `.env` but not used in the codebase (enabled by default).
Expand Down
12 changes: 12 additions & 0 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,18 @@ API_TOKEN=

---

## Explain a variable (`--explain`)

Inspect a specific environment variable to see where it is defined, where it is used in the codebase, and its overall status:

```bash
dotenv-diff --explain DATABASE_URL
```

→ See [--explain Documentation](https://github.com/Chrilleweb/dotenv-diff/blob/main/docs/configuration_and_flags.md#--explain-key) for more details.

---

## Monorepo support

In monorepos with multiple apps and packages, you can include shared folders:
Expand Down
4 changes: 4 additions & 0 deletions packages/cli/src/cli/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,5 +96,9 @@ export function createProgram() {
'--list-all',
'List all unique environment variable keys found in codebase',
)
.option(
'--explain <key>',
'Show where a specific key is defined, used, and its status',
)
);
}
18 changes: 18 additions & 0 deletions packages/cli/src/cli/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type {
ExitResult,
} from '../config/types.js';
import { scanUsage } from '../commands/scanUsage.js';
import { explainKey } from '../commands/explain.js';
import { printErrorNotFound } from '../ui/compare/printErrorNotFound.js';
import { setupGlobalConfig } from '../ui/shared/setupGlobalConfig.js';
import { loadConfig } from '../config/loadConfig.js';
Expand Down Expand Up @@ -49,6 +50,23 @@ export async function run(program: Command): Promise<void> {

setupGlobalConfig(opts);

// Handle --explain flag
if (opts.explain) {
await explainKey({
key: opts.explain,
cwd: opts.cwd,
include: opts.includeFiles,
exclude: opts.excludeFiles,
ignore: opts.ignore,
ignoreRegex: opts.ignoreRegex,
files: opts.files,
secrets: opts.secrets,
ignoreUrls: opts.ignoreUrls,
json: opts.json,
});
process.exit(0);
}

// Route to appropriate command and handle exit
const exitWithError = opts.compare
? await runCompareMode(opts)
Expand Down
91 changes: 91 additions & 0 deletions packages/cli/src/commands/explain.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import fs from 'fs';
import path from 'path';
import { scanCodebase } from '../services/scanCodebase.js';
import { parseEnvFile } from '../services/parseEnvFile.js';
import { findDuplicateKeys } from '../core/duplicates.js';
import type { ScanOptions } from '../config/types.js';
import { DEFAULT_ENV_FILE, DEFAULT_EXAMPLE_FILE } from '../config/constants.js';
import { printExplain, type ExplainResult } from '../ui/scan/printExplain.js';
import { skipCommentedUsages } from '../core/helpers/skipCommentedUsages.js';

/**
* Options forwarded from the CLI for the --explain command.
*/
export interface ExplainOptions extends ScanOptions {
key: string;
}

/**
* Implements `dotenv-diff --explain <KEY>`.
*
* Reports where the key is defined in env files, where it is used in the
* codebase, and its overall status (defined / used / duplicated / ignored).
* @param opts Explain options from CLI
* @returns void
*/
export async function explainKey(opts: ExplainOptions): Promise<void> {
const { key, cwd, ignore, ignoreRegex } = opts;

// Find env files that contain the key
const envFiles = discoverEnvFilesSync(cwd);
const definedIn: string[] = [];
const isDuplicated = envFiles.some((filePath) => {
const dups = findDuplicateKeys(filePath);
return dups.some((d) => d.key === key);
});

for (const filePath of envFiles) {
const parsed = parseEnvFile(filePath);
if (Object.prototype.hasOwnProperty.call(parsed, key)) {
definedIn.push(path.relative(cwd, filePath));
}
}

// Scan codebase for usages
const scanResult = await scanCodebase(opts);

// Filter out commented usages
const filteredUsages = skipCommentedUsages(scanResult.used);
const usages = filteredUsages.filter((u) => u.variable === key);

// Check ignore status
const isIgnored =
ignore.includes(key) || ignoreRegex.some((rx) => rx.test(key));

// Print result
const result: ExplainResult = {
key,
definedIn,
usages,
isDuplicated,
isIgnored,
};

if (opts.json) {
console.log(JSON.stringify(result, null, 2));
} else {
printExplain(result);
}
}

/**
* Returns absolute paths to all .env* files in the cwd
* (both env files and example files).
* @param cwd Directory to search in
* @returns Array of absolute file paths
*/
function discoverEnvFilesSync(cwd: string): string[] {
let entries: string[] = [];
try {
entries = fs.readdirSync(cwd);
} catch {
return [];
}

return entries
.filter(
(f) =>
f.startsWith(DEFAULT_ENV_FILE) || f.startsWith(DEFAULT_EXAMPLE_FILE),
)
.map((f) => path.resolve(cwd, f));
}
3 changes: 3 additions & 0 deletions packages/cli/src/config/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ export function normalizeOptions(raw: RawOptions): Options {
const expireWarnings = raw.expireWarnings !== false;
const inconsistentNamingWarnings = raw.inconsistentNamingWarnings !== false;
const listAll = toBool(raw.listAll);
const explain =
typeof raw.explain === 'string' ? raw.explain.trim() : undefined;

const cwd = process.cwd();
const envFlag =
Expand Down Expand Up @@ -98,6 +100,7 @@ export function normalizeOptions(raw: RawOptions): Options {
expireWarnings,
inconsistentNamingWarnings,
listAll,
explain,
};
}

Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ export interface RawOptions {
expireWarnings?: boolean;
inconsistentNamingWarnings?: boolean;
listAll?: boolean;
explain?: string;
}

/**
Expand Down Expand Up @@ -137,6 +138,7 @@ export interface Options {
expireWarnings: boolean;
inconsistentNamingWarnings: boolean;
listAll: boolean;
explain: string | undefined;
}

export type EnvPatternName = 'process.env' | 'import.meta.env' | 'sveltekit';
Expand Down
Loading