While working on a CLI project recently, I encountered the need for a robust configuration system. I wanted the ability to store a base configuration in the user’s home directory and allow overrides in the current working directory and any parent directories. Additionally, I needed the flexibility to support both JSON and YAML configuration files without relying on file extensions. This blog post details the solution I implemented, which you can easily adapt for your own CLI projects.
Thank me by sharing on Twitter 🙏
Versatile Config for Your CLI Overview
To achieve this, we’ll cover the following steps:
- Setting Up the Environment
- Loading and Parsing Configuration Files
- Recursively Merging Configurations
- Handling Multiple Configuration Formats
- Bringing It All Together
Let’s dive in!
1. Setting Up the Environment
Before diving into the code, ensure you have the necessary packages installed. You’ll need commander
for CLI command parsing, fs
for file system operations, path
for handling file paths, os
for accessing the user’s home directory, lodash-es
for merging objects, and js-yaml
for parsing YAML files. Here are the commands to install these packages:
npm install commander fs path os lodash-es js-yaml @types/lodash-es @types/js-yaml
With the dependencies in place, let’s move on to loading and parsing configuration files.
Unprepared Healer: A Fantasy LitRPG Isekai Adventure (Earthen Contenders Book 2)
$4.99 (as of January 11, 2025 10:31 GMT +00:00 - More infoProduct prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on [relevant Amazon Site(s), as applicable] at the time of purchase will apply to the purchase of this product.)Legend of Zelda: Tears of the Kingdom 2025 Wall Calendar
$11.00 (as of January 11, 2025 10:31 GMT +00:00 - More infoProduct prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on [relevant Amazon Site(s), as applicable] at the time of purchase will apply to the purchase of this product.)The Nvidia Way: Jensen Huang and the Making of a Tech Giant
$17.50 (as of January 11, 2025 10:31 GMT +00:00 - More infoProduct prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on [relevant Amazon Site(s), as applicable] at the time of purchase will apply to the purchase of this product.)2. Loading and Parsing Configuration Files
The first step is to load configuration files and detect their format. We’ll create a function called loadConfig
that reads a file and tries to parse it as JSON first, and if that fails, it attempts to parse it as YAML. Here’s how I implemented this function:
import * as fs from 'fs';
import * as yaml from 'js-yaml';
const loadConfig = (filePath: string): any => {
if (fs.existsSync(filePath)) {
const fileContent = fs.readFileSync(filePath, 'utf-8');
try {
return JSON.parse(fileContent);
} catch (jsonError: unknown) {
if (jsonError instanceof SyntaxError) {
try {
return yaml.load(fileContent);
} catch (yamlError: unknown) {
if (yamlError instanceof Error) {
console.error(`Failed to parse config file: ${filePath}`);
console.error(`YAML error: ${yamlError.message}`);
}
}
} else if (jsonError instanceof Error) {
console.error(`Failed to parse config file: ${filePath}`);
console.error(`JSON error: ${jsonError.message}`);
}
}
}
return {};
};
This function checks if the file exists, reads its content, and then tries to parse it as JSON. If JSON parsing fails due to a SyntaxError
, it attempts to parse the content as YAML. If both attempts fail, it logs the errors and returns an empty object.
3. Recursively Merging Configurations
Next, we need to recursively load configuration files from the current working directory up to the root directory. We’ll use a function called loadRecursiveConfig
for this purpose. This function starts from a given directory, looks for a .myclirc
file, and merges configurations as it traverses up the directory tree.
import * as path from 'path';
import { merge } from 'lodash-es';
const loadRecursiveConfig = (startPath: string): any => {
let currentDir = startPath;
let config = {};
while (currentDir !== path.dirname(currentDir)) {
const configPath = path.join(currentDir, '.myclirc');
const currentConfig = loadConfig(configPath);
config = merge(currentConfig, config);
currentDir = path.dirname(currentDir);
}
return config;
};
The loadRecursiveConfig
function initializes an empty configuration object and iteratively looks for .myclirc
files in the current directory and each parent directory. The merge
function from lodash-es
ensures that configurations are combined correctly.
4. Handling Multiple Configuration Formats
To ensure the versatility of our configuration loader, we need to handle multiple formats seamlessly. The loadConfig
function already detects and parses JSON and YAML files. Now, we need to incorporate this function into our CLI application, starting with loading the base configuration from the user’s home directory.
Here’s how I did it:
import * as os from 'os';
const homeConfigPath = path.join(os.homedir(), '.myclirc');
let finalConfig = loadConfig(homeConfigPath);
Next, we merge the configuration from the home directory with the recursively loaded configurations from the current working directory and its parents.
const cwd = process.cwd();
const recursiveConfig = loadRecursiveConfig(cwd);
finalConfig = merge(finalConfig, recursiveConfig);
console.log('Final Config:', finalConfig);
This ensures that configurations are merged in the correct order, with the current directory and parent configurations overriding the base configuration from the home directory.
5. Bringing It All Together
Now, let’s integrate everything into a single CLI script using the commander
package to handle command parsing.
import { Command } from 'commander';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { merge } from 'lodash-es';
import * as yaml from 'js-yaml';
const program = new Command();
// Define the command and options
program
.version('1.0.0')
.description('CLI with recursive config loading');
// Parse the arguments
program.parse(process.argv);
// Function to detect and load config from a given file content
const loadConfig = (filePath: string): any => {
if (fs.existsSync(filePath)) {
const fileContent = fs.readFileSync(filePath, 'utf-8');
try {
return JSON.parse(fileContent);
} catch (jsonError: unknown) {
if (jsonError instanceof SyntaxError) {
try {
return yaml.load(fileContent);
} catch (yamlError: unknown) {
if (yamlError instanceof Error) {
console.error(`Failed to parse config file: ${filePath}`);
console.error(`YAML error: ${yamlError.message}`);
}
}
} else if (jsonError instanceof Error) {
console.error(`Failed to parse config file: ${filePath}`);
console.error(`JSON error: ${jsonError.message}`);
}
}
}
return {};
};
// Function to recursively load config from current directory up to root
const loadRecursiveConfig = (startPath: string): any => {
let currentDir = startPath;
let config = {};
while (currentDir !== path.dirname(currentDir)) {
const configPath = path.join(currentDir, '.myclirc');
const currentConfig = loadConfig(configPath);
config = merge(currentConfig, config);
currentDir = path.dirname(currentDir);
}
return config;
};
// Load the config from the user's home directory
const homeConfigPath = path.join(os.homedir(), '.myclirc');
let finalConfig = loadConfig(homeConfigPath);
// Load and merge the config from the current working directory and upwards
const cwd = process.cwd();
const recursiveConfig = loadRecursiveConfig(cwd);
finalConfig = merge(finalConfig, recursiveConfig);
console.log('Final Config:', finalConfig);
With this setup, the CLI application can now detect and parse configuration files in both JSON and YAML formats, merge configurations from the home directory and recursively from the current working directory and its parents.
Conclusion
Creating a versatile configuration loader for your CLI in TypeScript involves detecting multiple file formats and recursively merging configurations from various directories. By using fs
, path
, os
, lodash-es
, and js-yaml
, we can build a robust solution that handles configuration files efficiently.
This approach ensures that your CLI applications are flexible and can adapt to different user needs, providing a seamless configuration experience. I hope this guide helps you implement a similar solution in your own projects. Happy coding!