Local Debugging Techniques
Comprehensive debugging strategies for local command execution, including output inspection, error analysis, and performance profiling.
Debug Output
Enable Verbose Logging
Activate detailed logging for command execution:
import { $, configure } from '@xec-sh/core';
// Enable global debug mode
configure({
debug: true,
verbose: true
});
// Per-command debugging
const result = await $.with({
debug: true,
verbose: true
})`ls -la`;
console.log('Command:', result.command);
console.log('Exit code:', result.exitCode);
console.log('Duration:', result.duration, 'ms');
console.log('Stdout length:', result.stdout.length);
console.log('Stderr length:', result.stderr.length);
Shell Debug Modes
Use shell-specific debugging features:
// Bash trace mode - shows each command
await $.with({ shell: '/bin/bash -x' })`
VAR="hello"
echo "$VAR world"
for i in 1 2 3; do
echo "Number: $i"
done
`;
// Output shows:
// + VAR=hello
// + echo 'hello world'
// + for i in 1 2 3
// + echo 'Number: 1'
// ...
// Verbose mode - shows commands as parsed
await $.with({ shell: '/bin/bash -v' })`
if [ -f /etc/passwd ]; then
echo "File exists"
fi
`;
// Strict error mode for debugging
await $.with({ shell: '/bin/bash' })`
set -euxo pipefail
# -e: Exit on error
# -u: Error on undefined variables
# -x: Print commands
# -o pipefail: Pipe failures cause exit
command1 | command2 | command3
`;
Command Inspection
Inspect command details before and after execution:
// Pre-execution inspection
const command = {
command: 'ls',
args: ['-la', '/tmp'],
cwd: '/home/user',
env: { DEBUG: 'true' }
};
console.log('Executing:', JSON.stringify(command, null, 2));
const result = await $(command)``;
// Post-execution inspection
console.log({
command: result.command,
exitCode: result.exitCode,
signal: result.signal,
duration: result.duration,
startTime: result.startTime,
endTime: result.endTime,
failed: result.failed
});
Output Analysis
Stream Monitoring
Monitor output streams in real-time:
// Real-time stdout monitoring
const proc = $`find / -name "*.log" 2>/dev/null`;
proc.stdout.on('data', (chunk) => {
console.log('[STDOUT]', chunk.toString());
});
proc.stderr.on('data', (chunk) => {
console.error('[STDERR]', chunk.toString());
});
await proc.catch(err => {
console.error('Process failed:', err.message);
});
Output Buffering
Debug buffer-related issues:
// Test buffer limits
const testBufferLimits = async (size) => {
try {
const result = await $.with({
maxBuffer: size
})`dd if=/dev/zero bs=1024 count=${size / 1024}`;
console.log(`Buffer size ${size}: OK`);
console.log(`Output size: ${result.stdout.length}`);
} catch (error) {
console.error(`Buffer size ${size}: FAILED`);
console.error(`Error: ${error.message}`);
}
};
// Test different buffer sizes
await testBufferLimits(1024 * 1024); // 1MB
await testBufferLimits(10 * 1024 * 1024); // 10MB
await testBufferLimits(100 * 1024 * 1024); // 100MB
Output Parsing
Parse and analyze command output:
// Structured output parsing
const result = await $`ps aux`;
const processes = result.stdout
.split('\n')
.slice(1) // Skip header
.filter(line => line.trim())
.map(line => {
const parts = line.split(/\s+/);
return {
user: parts[0],
pid: parts[1],
cpu: parts[2],
mem: parts[3],
command: parts.slice(10).join(' ')
};
});
// Find high CPU processes
const highCpu = processes.filter(p => parseFloat(p.cpu) > 50);
console.log('High CPU processes:', highCpu);
// Debug JSON output
const jsonResult = await $`echo '{"key": "value"}' | jq .`;
try {
const parsed = JSON.parse(jsonResult.stdout);
console.log('Parsed JSON:', parsed);
} catch (error) {
console.error('JSON parse error:', error);
console.error('Raw output:', jsonResult.stdout);
}
Error Diagnostics
Error Classification
Identify and classify different error types:
async function diagnoseError(command) {
try {
return await $`${command}`;
} catch (error) {
console.log('Error Diagnosis:');
console.log('================');
// Command not found
if (error.code === 'ENOENT') {
console.log('Type: Command not found');
console.log('Command:', error.path || error.command);
console.log('Solution: Check if command is installed and in PATH');
}
// Permission denied
else if (error.code === 'EACCES') {
console.log('Type: Permission denied');
console.log('Path:', error.path);
console.log('Solution: Check file permissions or run with appropriate privileges');
}
// Working directory error
else if (error.code === 'ENOTDIR' || error.message.includes('cwd')) {
console.log('Type: Working directory error');
console.log('CWD:', error.cwd);
console.log('Solution: Ensure working directory exists');
}
// Timeout
else if (error.name === 'TimeoutError') {
console.log('Type: Command timeout');
console.log('Timeout:', error.timeout, 'ms');
console.log('Solution: Increase timeout or optimize command');
}
// Non-zero exit code
else if (error.exitCode) {
console.log('Type: Non-zero exit code');
console.log('Exit code:', error.exitCode);
console.log('Signal:', error.signal);
console.log('Stderr:', error.stderr);
}
console.log('\nFull error:', error);
throw error;
}
}
// Test error diagnosis
await diagnoseError('nonexistent-command');
await diagnoseError('cat /etc/shadow'); // Permission denied
await diagnoseError('cd /nonexistent && ls'); // Directory error
Exit Code Analysis
Understand command exit codes:
// Common exit codes reference
const exitCodes = {
0: 'Success',
1: 'General error',
2: 'Misuse of shell command',
126: 'Command cannot execute',
127: 'Command not found',
128: 'Invalid exit argument',
130: 'Terminated by Ctrl+C',
255: 'Exit status out of range'
};
// Analyze exit code
async function analyzeExitCode(command) {
const result = await $.with({ throwOnNonZeroExit: false })`${command}`;
console.log(`Command: ${command}`);
console.log(`Exit code: ${result.exitCode}`);
console.log(`Meaning: ${exitCodes[result.exitCode] || 'Application-specific'}`);
if (result.exitCode > 128 && result.exitCode < 255) {
const signal = result.exitCode - 128;
console.log(`Terminated by signal: ${signal}`);
}
return result;
}
// Test different exit codes
await analyzeExitCode('true'); // 0
await analyzeExitCode('false'); // 1
await analyzeExitCode('exit 42'); // 42
await analyzeExitCode('kill -9 $$'); // 137 (128 + 9)
Stack Trace Enhancement
Improve error stack traces for better debugging:
// Enhance error with context
class CommandDebugger {
constructor() {
this.history = [];
}
async execute(command, context = {}) {
const startTime = Date.now();
const entry = {
command,
context,
startTime,
stack: new Error().stack
};
this.history.push(entry);
try {
const result = await $`${command}`;
entry.endTime = Date.now();
entry.duration = entry.endTime - entry.startTime;
entry.success = true;
return result;
} catch (error) {
entry.endTime = Date.now();
entry.duration = entry.endTime - entry.startTime;
entry.success = false;
entry.error = error;
// Enhanced error
error.debugInfo = {
command,
context,
duration: entry.duration,
history: this.history.slice(-5), // Last 5 commands
originalStack: entry.stack
};
throw error;
}
}
printHistory() {
console.log('Command History:');
this.history.forEach((entry, i) => {
console.log(`${i + 1}. ${entry.command}`);
console.log(` Success: ${entry.success}`);
console.log(` Duration: ${entry.duration}ms`);
if (entry.error) {
console.log(` Error: ${entry.error.message}`);
}
});
}
}
const debugger = new CommandDebugger();
try {
await debugger.execute('ls -la', { step: 'list files' });
await debugger.execute('grep "pattern" file.txt', { step: 'search' });
} catch (error) {
console.error('Enhanced error info:', error.debugInfo);
debugger.printHistory();
}
Performance Profiling
Execution Timing
Measure and analyze execution times:
// Simple timing
const start = Date.now();
const result = await $`sleep 1 && echo "Done"`;
console.log(`Execution time: ${Date.now() - start}ms`);
console.log(`Internal duration: ${result.duration}ms`);
// Detailed profiling
class CommandProfiler {
constructor() {
this.profiles = [];
}
async profile(name, command) {
const profile = {
name,
command,
startTime: Date.now(),
memBefore: process.memoryUsage()
};
try {
const result = await $`${command}`;
profile.endTime = Date.now();
profile.duration = profile.endTime - profile.startTime;
profile.memAfter = process.memoryUsage();
profile.memDelta = {
rss: profile.memAfter.rss - profile.memBefore.rss,
heapUsed: profile.memAfter.heapUsed - profile.memBefore.heapUsed
};
profile.outputSize = result.stdout.length + result.stderr.length;
this.profiles.push(profile);
return result;
} catch (error) {
profile.error = error;
this.profiles.push(profile);
throw error;
}
}
report() {
console.table(this.profiles.map(p => ({
name: p.name,
duration: `${p.duration}ms`,
memory: `${Math.round(p.memDelta.rss / 1024)}KB`,
output: `${p.outputSize} bytes`,
status: p.error ? 'Failed' : 'Success'
})));
}
}
const profiler = new CommandProfiler();
await profiler.profile('List files', 'ls -la');
await profiler.profile('Find logs', 'find /var/log -name "*.log" | head -10');
await profiler.profile('Process check', 'ps aux | wc -l');
profiler.report();
Resource Monitoring
Monitor system resources during execution:
// Monitor CPU and memory during execution
async function monitorResources(command) {
const monitors = [];
let monitoring = true;
// Start monitoring
const monitorInterval = setInterval(() => {
if (!monitoring) return;
$.with({ throwOnNonZeroExit: false })`ps aux | grep ${process.pid} | grep -v grep`
.then(result => {
const parts = result.stdout.split(/\s+/);
monitors.push({
time: Date.now(),
cpu: parseFloat(parts[2]),
mem: parseFloat(parts[3]),
vsz: parseInt(parts[4]),
rss: parseInt(parts[5])
});
});
}, 100);
try {
const result = await $`${command}`;
monitoring = false;
clearInterval(monitorInterval);
// Analyze resource usage
const maxCpu = Math.max(...monitors.map(m => m.cpu));
const maxMem = Math.max(...monitors.map(m => m.mem));
const avgCpu = monitors.reduce((sum, m) => sum + m.cpu, 0) / monitors.length;
console.log('Resource Usage:');
console.log(` Max CPU: ${maxCpu}%`);
console.log(` Max Memory: ${maxMem}%`);
console.log(` Avg CPU: ${avgCpu.toFixed(2)}%`);
console.log(` Samples: ${monitors.length}`);
return result;
} finally {
monitoring = false;
clearInterval(monitorInterval);
}
}
await monitorResources('find / -name "*.txt" 2>/dev/null | head -100');
Interactive Debugging
REPL Integration
Create an interactive debugging environment:
import { createInterface } from 'readline';
class InteractiveDebugger {
constructor() {
this.context = {
cwd: process.cwd(),
env: { ...process.env },
lastResult: null,
history: []
};
}
async start() {
const rl = createInterface({
input: process.stdin,
output: process.stdout,
prompt: 'debug> '
});
console.log('Interactive Command Debugger');
console.log('Commands: .exit, .env, .cwd, .history, .last');
console.log('');
rl.prompt();
rl.on('line', async (line) => {
line = line.trim();
if (line === '.exit') {
rl.close();
return;
}
if (line === '.env') {
console.log('Environment:', this.context.env);
} else if (line === '.cwd') {
console.log('Working directory:', this.context.cwd);
} else if (line === '.history') {
this.context.history.forEach((cmd, i) => {
console.log(`${i + 1}: ${cmd}`);
});
} else if (line === '.last') {
console.log('Last result:', this.context.lastResult);
} else if (line.startsWith('cd ')) {
const dir = line.substring(3);
this.context.cwd = dir;
console.log(`Changed directory to: ${dir}`);
} else if (line) {
await this.executeDebug(line);
}
rl.prompt();
});
}
async executeDebug(command) {
console.log(`\n[Executing: ${command}]`);
this.context.history.push(command);
try {
const start = Date.now();
const result = await $.with({
cwd: this.context.cwd,
env: this.context.env,
throwOnNonZeroExit: false
})`${command}`;
const duration = Date.now() - start;
console.log('[Output]');
if (result.stdout) console.log(result.stdout);
if (result.stderr) console.error('[Stderr]', result.stderr);
console.log('[Metadata]');
console.log(` Exit code: ${result.exitCode}`);
console.log(` Duration: ${duration}ms`);
console.log(` Success: ${!result.failed}`);
this.context.lastResult = result;
} catch (error) {
console.error('[Error]', error.message);
console.error('[Exit Code]', error.exitCode);
if (error.stderr) console.error('[Stderr]', error.stderr);
}
console.log('');
}
}
// Start interactive debugger
const debugger = new InteractiveDebugger();
await debugger.start();
Breakpoint Simulation
Add breakpoints in command sequences:
class CommandDebuggerWithBreakpoints {
constructor() {
this.breakpoints = new Set();
this.stepMode = false;
}
setBreakpoint(lineNumber) {
this.breakpoints.add(lineNumber);
}
async executeScript(script) {
const lines = script.trim().split('\n');
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (!line || line.startsWith('#')) continue;
// Check for breakpoint
if (this.breakpoints.has(i + 1) || this.stepMode) {
console.log(`\n[Breakpoint at line ${i + 1}]: ${line}`);
const action = await this.prompt('(c)ontinue, (s)tep, (v)ars, (q)uit: ');
if (action === 'q') {
console.log('Debugging terminated');
break;
} else if (action === 's') {
this.stepMode = true;
} else if (action === 'c') {
this.stepMode = false;
} else if (action === 'v') {
console.log('Environment:', process.env);
i--; // Repeat this iteration
continue;
}
}
console.log(`[${i + 1}] Executing: ${line}`);
try {
const result = await $`${line}`;
if (result.stdout) console.log('Output:', result.stdout.trim());
} catch (error) {
console.error(`Error at line ${i + 1}: ${error.message}`);
if (this.stepMode) {
const action = await this.prompt('(c)ontinue, (q)uit: ');
if (action === 'q') break;
}
}
}
}
async prompt(message) {
const rl = createInterface({
input: process.stdin,
output: process.stdout
});
return new Promise(resolve => {
rl.question(message, answer => {
rl.close();
resolve(answer);
});
});
}
}
// Use debugger with breakpoints
const dbg = new CommandDebuggerWithBreakpoints();
dbg.setBreakpoint(3);
dbg.setBreakpoint(5);
await dbg.executeScript(`
echo "Starting script"
VAR="test"
echo "Variable set to: $VAR"
ls -la
echo "Script complete"
`);
Logging and Tracing
Custom Logger
Implement detailed logging for command execution:
class CommandLogger {
constructor(logFile = 'commands.log') {
this.logFile = logFile;
this.sessionId = Date.now();
}
async execute(command, metadata = {}) {
const logEntry = {
sessionId: this.sessionId,
timestamp: new Date().toISOString(),
command,
metadata,
environment: {
platform: process.platform,
node: process.version,
cwd: process.cwd(),
user: process.env.USER
}
};
try {
const start = Date.now();
const result = await $`${command}`;
logEntry.success = true;
logEntry.duration = Date.now() - start;
logEntry.exitCode = result.exitCode;
logEntry.outputSize = {
stdout: result.stdout.length,
stderr: result.stderr.length
};
await this.writeLog(logEntry);
return result;
} catch (error) {
logEntry.success = false;
logEntry.error = {
message: error.message,
code: error.code,
exitCode: error.exitCode
};
await this.writeLog(logEntry);
throw error;
}
}
async writeLog(entry) {
const line = JSON.stringify(entry) + '\n';
await $`echo '${line}' >> ${this.logFile}`;
}
async analyze() {
const logs = await $`cat ${this.logFile}`;
const entries = logs.stdout
.split('\n')
.filter(line => line)
.map(line => JSON.parse(line));
const stats = {
total: entries.length,
success: entries.filter(e => e.success).length,
failed: entries.filter(e => !e.success).length,
avgDuration: entries
.filter(e => e.duration)
.reduce((sum, e) => sum + e.duration, 0) / entries.length,
errors: entries
.filter(e => e.error)
.map(e => e.error.message)
};
console.log('Execution Statistics:', stats);
return stats;
}
}
const logger = new CommandLogger();
await logger.execute('ls -la', { purpose: 'list files' });
await logger.execute('grep "test" file.txt', { purpose: 'search' });
await logger.analyze();
Testing Commands
Unit Testing Commands
Test command behavior systematically:
class CommandTester {
constructor() {
this.tests = [];
}
test(description, command, expectations) {
this.tests.push({ description, command, expectations });
}
async run() {
console.log('Running Command Tests\n');
let passed = 0;
let failed = 0;
for (const test of this.tests) {
process.stdout.write(`Testing: ${test.description}... `);
try {
const result = await $.with({ throwOnNonZeroExit: false })`${test.command}`;
let success = true;
const failures = [];
// Check expectations
if ('exitCode' in test.expectations) {
if (result.exitCode !== test.expectations.exitCode) {
success = false;
failures.push(`Expected exit code ${test.expectations.exitCode}, got ${result.exitCode}`);
}
}
if ('stdout' in test.expectations) {
if (typeof test.expectations.stdout === 'string') {
if (!result.stdout.includes(test.expectations.stdout)) {
success = false;
failures.push(`Stdout doesn't contain "${test.expectations.stdout}"`);
}
} else if (test.expectations.stdout instanceof RegExp) {
if (!test.expectations.stdout.test(result.stdout)) {
success = false;
failures.push(`Stdout doesn't match pattern ${test.expectations.stdout}`);
}
}
}
if ('stderr' in test.expectations) {
if (!result.stderr.includes(test.expectations.stderr)) {
success = false;
failures.push(`Stderr doesn't contain "${test.expectations.stderr}"`);
}
}
if (success) {
console.log('✓');
passed++;
} else {
console.log('✗');
failures.forEach(f => console.log(` ${f}`));
failed++;
}
} catch (error) {
console.log('✗');
console.log(` Error: ${error.message}`);
failed++;
}
}
console.log(`\nResults: ${passed} passed, ${failed} failed`);
}
}
const tester = new CommandTester();
tester.test(
'Echo outputs text',
'echo "Hello"',
{ exitCode: 0, stdout: 'Hello' }
);
tester.test(
'False returns exit code 1',
'false',
{ exitCode: 1 }
);
tester.test(
'Grep finds pattern',
'echo "test line" | grep "test"',
{ exitCode: 0, stdout: /test/ }
);
tester.test(
'Command not found',
'nonexistent-command 2>&1',
{ exitCode: 127, stdout: /not found|command not found/ }
);
await tester.run();
Best Practices
1. Use Appropriate Debug Levels
// Development - full debugging
const dev = { debug: true, verbose: true, shell: '/bin/bash -x' };
// Testing - moderate debugging
const test = { debug: true, throwOnNonZeroExit: false };
// Production - minimal debugging
const prod = { debug: false, timeout: 30000 };
2. Preserve Debug Context
// Wrap commands with context
async function executeWithContext(command, context) {
console.log(`[${context}] Starting: ${command}`);
try {
const result = await $`${command}`;
console.log(`[${context}] Success`);
return result;
} catch (error) {
console.error(`[${context}] Failed:`, error.message);
throw error;
}
}
3. Use Structured Logging
// Structure logs for analysis
const structuredLog = {
timestamp: new Date().toISOString(),
level: 'debug',
command,
context: {
file: __filename,
function: 'processData',
line: new Error().stack.split('\n')[2]
},
result: {
success: true,
duration: 123,
output: 'truncated...'
}
};
console.log(JSON.stringify(structuredLog));
Next Steps
- Shell Configuration - Configure shell behavior
- Local Setup - Basic local environment setup
- Error Handling - Advanced error handling
- Performance Optimization - Optimize execution