BLOG POSTS
Node.js Command Line Arguments and Node Scripts

Node.js Command Line Arguments and Node Scripts

Node.js command line arguments and scripting capabilities are fundamental tools that every server-side JavaScript developer should master. These features allow you to create dynamic, configurable applications that can adapt their behavior based on runtime parameters, making your code more flexible and reusable. This post will walk you through the essential techniques for handling command line arguments, creating robust Node.js scripts, and implementing best practices that’ll save you countless hours of debugging and manual configuration.

How Command Line Arguments Work in Node.js

Node.js automatically captures command line arguments through the global process.argv array. When you execute a Node.js script, the runtime populates this array with the following structure:

// process.argv structure:
// [0] - Path to Node.js executable
// [1] - Path to the script file
// [2+] - Actual command line arguments

console.log(process.argv);

// Running: node script.js --port 3000 --env production
// Output: [
//   '/usr/local/bin/node',
//   '/path/to/script.js',
//   '--port',
//   '3000',
//   '--env',
//   'production'
// ]

The raw process.argv gives you everything, but you’ll typically want to work with just the user-provided arguments:

const args = process.argv.slice(2);
console.log('User arguments:', args);

Step-by-Step Implementation Guide

Let’s build a practical example that demonstrates different argument parsing techniques, starting with manual parsing and progressing to more sophisticated approaches.

Basic Manual Parsing

#!/usr/bin/env node

const args = process.argv.slice(2);
const config = {};

// Simple flag detection
if (args.includes('--verbose')) {
    config.verbose = true;
}

if (args.includes('--help')) {
    console.log('Usage: node script.js [options]');
    console.log('Options:');
    console.log('  --verbose    Enable verbose logging');
    console.log('  --port PORT  Set server port');
    console.log('  --help       Show this help message');
    process.exit(0);
}

// Key-value pair parsing
for (let i = 0; i < args.length; i++) {
    if (args[i] === '--port' && args[i + 1]) {
        config.port = parseInt(args[i + 1]);
        i++; // Skip next argument since we consumed it
    }
    if (args[i] === '--env' && args[i + 1]) {
        config.environment = args[i + 1];
        i++;
    }
}

console.log('Configuration:', config);

Using Built-in parseArgs (Node.js 18.3+)

Node.js introduced a built-in argument parser that eliminates the need for external libraries in many cases:

import { parseArgs } from 'node:util';

const options = {
    'port': {
        type: 'string',
        short: 'p',
        default: '3000'
    },
    'verbose': {
        type: 'boolean',
        short: 'v',
        default: false
    },
    'env': {
        type: 'string',
        short: 'e',
        default: 'development'
    },
    'help': {
        type: 'boolean',
        short: 'h',
        default: false
    }
};

try {
    const { values, positionals } = parseArgs({ options });
    
    if (values.help) {
        console.log('Usage: node script.js [options]');
        console.log('Options:');
        Object.entries(options).forEach(([key, opt]) => {
            console.log(`  --${key}, -${opt.short}  ${opt.type} (default: ${opt.default})`);
        });
        process.exit(0);
    }
    
    console.log('Parsed options:', values);
    console.log('Positional arguments:', positionals);
    
} catch (error) {
    console.error('Argument parsing error:', error.message);
    process.exit(1);
}

Advanced Parsing with External Libraries

For complex applications, consider using yargs or commander.js. Here's a commander.js example:

import { Command } from 'commander';
const program = new Command();

program
    .name('my-app')
    .description('A sample Node.js CLI application')
    .version('1.0.0')
    .option('-p, --port ', 'server port', '3000')
    .option('-v, --verbose', 'enable verbose logging')
    .option('-e, --env ', 'application environment', 'development')
    .option('-c, --config ', 'configuration file path');

program
    .command('start')
    .description('Start the server')
    .action(() => {
        const options = program.opts();
        console.log('Starting server with options:', options);
        // Server startup logic here
    });

program
    .command('migrate')
    .description('Run database migrations')
    .option('--rollback', 'rollback last migration')
    .action((cmdOptions) => {
        console.log('Running migrations:', cmdOptions);
        // Migration logic here
    });

program.parse();

Real-World Examples and Use Cases

Database Connection Script

#!/usr/bin/env node

import { parseArgs } from 'node:util';
import mysql from 'mysql2/promise';

const options = {
    'host': { type: 'string', default: 'localhost' },
    'port': { type: 'string', default: '3306' },
    'user': { type: 'string', default: 'root' },
    'password': { type: 'string', default: '' },
    'database': { type: 'string' },
    'query': { type: 'string' },
    'dry-run': { type: 'boolean', default: false }
};

const { values } = parseArgs({ options });

if (!values.database || !values.query) {
    console.error('Error: --database and --query are required');
    process.exit(1);
}

async function executeQuery() {
    if (values['dry-run']) {
        console.log('DRY RUN - Would execute query:', values.query);
        console.log('Against database:', values.database);
        return;
    }
    
    try {
        const connection = await mysql.createConnection({
            host: values.host,
            port: parseInt(values.port),
            user: values.user,
            password: values.password,
            database: values.database
        });
        
        const [results] = await connection.execute(values.query);
        console.log('Query results:', results);
        
        await connection.end();
    } catch (error) {
        console.error('Database error:', error.message);
        process.exit(1);
    }
}

executeQuery();

File Processing Utility

#!/usr/bin/env node

import fs from 'fs/promises';
import path from 'path';
import { parseArgs } from 'node:util';

const options = {
    'input': { type: 'string', short: 'i' },
    'output': { type: 'string', short: 'o' },
    'format': { type: 'string', default: 'json' },
    'recursive': { type: 'boolean', default: false },
    'verbose': { type: 'boolean', short: 'v', default: false }
};

const { values } = parseArgs({ options });

async function processFiles() {
    if (!values.input) {
        console.error('Error: Input path is required (--input or -i)');
        process.exit(1);
    }
    
    try {
        const inputPath = path.resolve(values.input);
        const stats = await fs.stat(inputPath);
        
        if (stats.isDirectory()) {
            const files = await fs.readdir(inputPath, { 
                recursive: values.recursive 
            });
            
            if (values.verbose) {
                console.log(`Found ${files.length} files in ${inputPath}`);
            }
            
            // Process each file
            for (const file of files) {
                const filePath = path.join(inputPath, file);
                await processFile(filePath);
            }
        } else {
            await processFile(inputPath);
        }
        
    } catch (error) {
        console.error('Processing error:', error.message);
        process.exit(1);
    }
}

async function processFile(filePath) {
    if (values.verbose) {
        console.log(`Processing: ${filePath}`);
    }
    
    const content = await fs.readFile(filePath, 'utf8');
    
    // Transform content based on format
    let result;
    switch (values.format) {
        case 'json':
            result = JSON.stringify({ 
                file: filePath, 
                size: content.length, 
                lines: content.split('\n').length 
            }, null, 2);
            break;
        case 'csv':
            result = `"${filePath}","${content.length}","${content.split('\n').length}"`;
            break;
        default:
            result = content;
    }
    
    if (values.output) {
        const outputPath = path.resolve(values.output);
        await fs.appendFile(outputPath, result + '\n');
    } else {
        console.log(result);
    }
}

processFiles();

Comparison of Argument Parsing Methods

Method Complexity Bundle Size Features Best For
Manual parsing High 0 KB Basic flags and values Simple scripts, minimal dependencies
parseArgs (built-in) Medium 0 KB Type validation, short flags, defaults Modern Node.js apps (18.3+)
yargs Low ~200 KB Commands, validation, auto-help, completion Feature-rich CLI applications
commander.js Low ~25 KB Commands, subcommands, type coercion Clean API, moderate complexity
minimist Medium ~5 KB Lightweight parsing Size-conscious applications

Best Practices and Common Pitfalls

Environment Variable Integration

Combine command line arguments with environment variables for maximum flexibility:

import { parseArgs } from 'node:util';

const options = {
    'port': { type: 'string' },
    'host': { type: 'string' },
    'database-url': { type: 'string' }
};

const { values } = parseArgs({ options });

// Priority: CLI args > Environment variables > Defaults
const config = {
    port: values.port || process.env.PORT || '3000',
    host: values.host || process.env.HOST || 'localhost',
    databaseUrl: values['database-url'] || process.env.DATABASE_URL || 'sqlite://./app.db'
};

console.log('Final configuration:', config);

Input Validation and Error Handling

Always validate your inputs and provide meaningful error messages:

function validateConfig(config) {
    const errors = [];
    
    // Port validation
    const port = parseInt(config.port);
    if (isNaN(port) || port < 1 || port > 65535) {
        errors.push(`Invalid port: ${config.port}. Must be between 1-65535`);
    }
    
    // URL validation
    if (config.databaseUrl) {
        try {
            new URL(config.databaseUrl);
        } catch {
            errors.push(`Invalid database URL: ${config.databaseUrl}`);
        }
    }
    
    // Required fields
    if (!config.apiKey && process.env.NODE_ENV === 'production') {
        errors.push('API key is required in production mode');
    }
    
    if (errors.length > 0) {
        console.error('Configuration errors:');
        errors.forEach(error => console.error(`  - ${error}`));
        process.exit(1);
    }
    
    return { ...config, port }; // Return with parsed port
}

Script Performance Considerations

For scripts that might process large amounts of data or run frequently:

#!/usr/bin/env node

import { performance } from 'perf_hooks';

const startTime = performance.now();

// Your script logic here
process.on('exit', () => {
    const duration = performance.now() - startTime;
    if (process.env.TIMING || process.argv.includes('--timing')) {
        console.error(`Script execution time: ${duration.toFixed(2)}ms`);
    }
});

// Memory usage monitoring
process.on('SIGINT', () => {
    const usage = process.memoryUsage();
    console.error('\nMemory usage:');
    console.error(`  RSS: ${Math.round(usage.rss / 1024 / 1024)}MB`);
    console.error(`  Heap Used: ${Math.round(usage.heapUsed / 1024 / 1024)}MB`);
    process.exit(0);
});

Security Considerations

When handling command line arguments, especially in production environments:

  • Never log sensitive information like passwords or API keys
  • Validate file paths to prevent directory traversal attacks
  • Sanitize inputs before using them in shell commands
  • Use environment variables for secrets instead of command line arguments
  • Implement rate limiting for scripts that make external API calls
import path from 'path';

function sanitizeFilePath(inputPath) {
    // Resolve to absolute path and check if it's within allowed directory
    const resolvedPath = path.resolve(inputPath);
    const allowedDir = path.resolve('./data');
    
    if (!resolvedPath.startsWith(allowedDir)) {
        throw new Error(`Access denied: ${inputPath} is outside allowed directory`);
    }
    
    return resolvedPath;
}

// Usage
try {
    const safePath = sanitizeFilePath(values.input);
    // Proceed with file operations
} catch (error) {
    console.error('Security error:', error.message);
    process.exit(1);
}

Making Scripts Executable

To make your Node.js scripts executable without explicitly calling node:

#!/usr/bin/env node

// Add this shebang line at the top of your script
// Then make it executable:
// chmod +x your-script.js
//
// Now you can run it directly:
// ./your-script.js --port 8080

For deployment on servers, consider using process managers like PM2 or systemd services. When setting up on a VPS or dedicated server, you can create systemd service files that automatically handle your Node.js scripts with proper argument passing.

Advanced Integration Patterns

Configuration File Support

Combine command line arguments with configuration files for complex applications:

import fs from 'fs/promises';
import path from 'path';

async function loadConfig(configPath) {
    try {
        const configFile = await fs.readFile(configPath, 'utf8');
        return JSON.parse(configFile);
    } catch (error) {
        if (error.code === 'ENOENT') {
            return {}; // Config file doesn't exist, use defaults
        }
        throw new Error(`Invalid config file: ${error.message}`);
    }
}

// Merge configurations with proper precedence
async function buildConfig() {
    const { values } = parseArgs({ options });
    
    // Load base configuration
    const fileConfig = values.config ? 
        await loadConfig(values.config) : 
        await loadConfig('./config.json');
    
    // CLI arguments override file config
    return {
        ...fileConfig,
        ...Object.fromEntries(
            Object.entries(values).filter(([_, value]) => value !== undefined)
        )
    };
}

Command line arguments and Node.js scripting form the backbone of robust server-side applications and automation tools. By mastering these techniques, you'll build more maintainable, configurable, and professional Node.js applications. The built-in parseArgs function provides an excellent starting point for modern applications, while libraries like commander.js offer additional features for complex CLI tools.

For comprehensive documentation and additional examples, check out the official Node.js Process documentation and the parseArgs API reference.



This article incorporates information and material from various online sources. We acknowledge and appreciate the work of all original authors, publishers, and websites. While every effort has been made to appropriately credit the source material, any unintentional oversight or omission does not constitute a copyright infringement. All trademarks, logos, and images mentioned are the property of their respective owners. If you believe that any content used in this article infringes upon your copyright, please contact us immediately for review and prompt action.

This article is intended for informational and educational purposes only and does not infringe on the rights of the copyright owners. If any copyrighted material has been used without proper credit or in violation of copyright laws, it is unintentional and we will rectify it promptly upon notification. Please note that the republishing, redistribution, or reproduction of part or all of the contents in any form is prohibited without express written permission from the author and website owner. For permissions or further inquiries, please contact us.

Leave a reply

Your email address will not be published. Required fields are marked