Skip to content
Open
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "command-stream",
"version": "0.7.1",
"version": "0.8.0",
"description": "Modern $ shell utility library with streaming, async iteration, and EventEmitter support, optimized for Bun runtime",
"type": "module",
"main": "src/$.mjs",
Expand Down
122 changes: 121 additions & 1 deletion src/$.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2532,7 +2532,7 @@
this.finish(result);

if (globalShellSettings.errexit && result.code !== 0) {
const error = new Error(`Command failed with exit code ${result.code}`);

Check failure on line 2535 in src/$.mjs

View workflow job for this annotation

GitHub Actions / test (latest)

error: Command failed with exit code 1

at _runVirtual (/home/runner/work/command-stream/command-stream/src/$.mjs:2535:23)

Check failure on line 2535 in src/$.mjs

View workflow job for this annotation

GitHub Actions / test (latest)

error: Command failed with exit code 1

at _runVirtual (/home/runner/work/command-stream/command-stream/src/$.mjs:2535:23)
error.code = result.code;
error.stdout = result.stdout;
error.stderr = result.stderr;
Expand Down Expand Up @@ -4521,6 +4521,10 @@
import yesCommand from './commands/$.yes.mjs';
import seqCommand from './commands/$.seq.mjs';
import testCommand from './commands/$.test.mjs';
import headCommand from './commands/$.head.mjs';
import tailCommand from './commands/$.tail.mjs';
import sortCommand from './commands/$.sort.mjs';
import uniqCommand from './commands/$.uniq.mjs';

// Built-in commands that match Bun.$ functionality
function registerBuiltins() {
Expand All @@ -4547,6 +4551,10 @@
register('yes', yesCommand);
register('seq', seqCommand);
register('test', testCommand);
register('head', headCommand);
register('tail', tailCommand);
register('sort', sortCommand);
register('uniq', uniqCommand);
}


Expand Down Expand Up @@ -4615,11 +4623,122 @@
return data;
}

// ShellJS compatibility layer
function createShellJSAPI() {
const shelljs = {
// Directory operations
cd: async (path) => {
const result = await $tagged`cd ${path}`;
return { code: result.exitCode || 0, stdout: result.stdout || '', stderr: result.stderr || '' };
},
pwd: async () => {
const result = await $tagged`pwd`;
return { code: result.exitCode || 0, stdout: result.stdout || '', stderr: result.stderr || '' };
},
ls: async (...args) => {
const result = await $tagged`ls ${args.join(' ')}`;
return { code: result.exitCode || 0, stdout: result.stdout || '', stderr: result.stderr || '' };
},
mkdir: async (...args) => {
const result = await $tagged`mkdir ${args.join(' ')}`;
return { code: result.exitCode || 0, stdout: result.stdout || '', stderr: result.stderr || '' };
},

// File operations
cat: async (...args) => {
const result = await $tagged`cat ${args.join(' ')}`;
return { code: result.exitCode || 0, stdout: result.stdout || '', stderr: result.stderr || '' };
},
head: async (...args) => {
const result = await $tagged`head ${args.join(' ')}`;
return { code: result.exitCode || 0, stdout: result.stdout || '', stderr: result.stderr || '' };
},
tail: async (...args) => {
const result = await $tagged`tail ${args.join(' ')}`;
return { code: result.exitCode || 0, stdout: result.stdout || '', stderr: result.stderr || '' };
},
sort: async (...args) => {
const result = await $tagged`sort ${args.join(' ')}`;
return { code: result.exitCode || 0, stdout: result.stdout || '', stderr: result.stderr || '' };
},
uniq: async (...args) => {
const result = await $tagged`uniq ${args.join(' ')}`;
return { code: result.exitCode || 0, stdout: result.stdout || '', stderr: result.stderr || '' };
},
cp: async (...args) => {
const result = await $tagged`cp ${args.join(' ')}`;
return { code: result.exitCode || 0, stdout: result.stdout || '', stderr: result.stderr || '' };
},
mv: async (...args) => {
const result = await $tagged`mv ${args.join(' ')}`;
return { code: result.exitCode || 0, stdout: result.stdout || '', stderr: result.stderr || '' };
},
rm: async (...args) => {
const result = await $tagged`rm ${args.join(' ')}`;
return { code: result.exitCode || 0, stdout: result.stdout || '', stderr: result.stderr || '' };
},
touch: async (...args) => {
const result = await $tagged`touch ${args.join(' ')}`;
return { code: result.exitCode || 0, stdout: result.stdout || '', stderr: result.stderr || '' };
},

// Text utilities
echo: async (...args) => {
const result = await $tagged`echo ${args.join(' ')}`;
return { code: result.exitCode || 0, stdout: result.stdout || '', stderr: result.stderr || '' };
},

// Testing
test: async (...args) => {
const result = await $tagged`test ${args.join(' ')}`;
return { code: result.exitCode || 0, stdout: result.stdout || '', stderr: result.stderr || '' };
},

// Path utilities
basename: async (...args) => {
const result = await $tagged`basename ${args.join(' ')}`;
return { code: result.exitCode || 0, stdout: result.stdout || '', stderr: result.stderr || '' };
},
dirname: async (...args) => {
const result = await $tagged`dirname ${args.join(' ')}`;
return { code: result.exitCode || 0, stdout: result.stdout || '', stderr: result.stderr || '' };
},

// Process utilities
which: async (...args) => {
const result = await $tagged`which ${args.join(' ')}`;
return { code: result.exitCode || 0, stdout: result.stdout || '', stderr: result.stderr || '' };
},

// Misc utilities
env: async () => {
const result = await $tagged`env`;
return { code: result.exitCode || 0, stdout: result.stdout || '', stderr: result.stderr || '' };
},
sleep: async (seconds) => {
const result = await $tagged`sleep ${seconds}`;
return { code: result.exitCode || 0, stdout: result.stdout || '', stderr: result.stderr || '' };
},

// Configuration
config: {
silent: false,
fatal: false,
verbose: false
}
};

return shelljs;
}

// Initialize built-in commands
trace('Initialization', () => 'Registering built-in virtual commands');
registerBuiltins();
trace('Initialization', () => `Built-in commands registered: ${listCommands().join(', ')}`);

// Create ShellJS compatibility instance
const shelljs = createShellJSAPI();

export {
$tagged as $,
sh,
Expand All @@ -4642,6 +4761,7 @@
configureAnsi,
getAnsiConfig,
processOutput,
forceCleanupAll
forceCleanupAll,
shelljs
};
export default $tagged;
97 changes: 97 additions & 0 deletions src/commands/$.head.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import fs from 'fs';
import { trace, VirtualUtils } from '../$.utils.mjs';

export default async function head({ args, stdin, cwd, isCancelled, abortSignal }) {
let lines = 10; // Default number of lines
let files = [];

// Parse arguments
for (let i = 0; i < args.length; i++) {
if (args[i] === '-n' && i + 1 < args.length) {
const lineCount = parseInt(args[i + 1]);
if (isNaN(lineCount) || lineCount < 0) {
return VirtualUtils.error(`head: invalid number of lines: '${args[i + 1]}'`);
}
lines = lineCount;
i++; // Skip the next argument (line count)
} else if (args[i].startsWith('-n')) {
// Handle -n10 format
const lineCount = parseInt(args[i].substring(2));
if (isNaN(lineCount) || lineCount < 0) {
return VirtualUtils.error(`head: invalid number of lines: '${args[i].substring(2)}'`);
}
lines = lineCount;
} else if (args[i].startsWith('-') && args[i] !== '-') {
// Handle -10 format
const lineCount = parseInt(args[i].substring(1));
if (!isNaN(lineCount) && lineCount > 0) {
lines = lineCount;
} else {
return VirtualUtils.error(`head: invalid option -- '${args[i].substring(1)}'`);
}
} else {
files.push(args[i]);
}
}

// If no files specified, read from stdin
if (files.length === 0) {
if (stdin !== undefined && stdin !== '') {
const inputLines = stdin.split('\n');
const output = inputLines.slice(0, lines).join('\n');
return VirtualUtils.success(output + (inputLines.length > lines ? '\n' : ''));
}
return VirtualUtils.success();
}

try {
const outputs = [];
for (let i = 0; i < files.length; i++) {
const file = files[i];

// Check for cancellation
if (isCancelled?.() || abortSignal?.aborted) {
trace('VirtualCommand', () => `head: cancelled while processing files`);
return { code: 130, stdout: '', stderr: '' };
}

trace('VirtualCommand', () => `head: reading file | ${JSON.stringify({ file, lines }, null, 2)}`);

const resolvedPath = VirtualUtils.resolvePath(file, cwd);
try {
const content = fs.readFileSync(resolvedPath, 'utf8');
const fileLines = content.split('\n');
const headLines = fileLines.slice(0, lines);

// Add header if multiple files
let output = '';
if (files.length > 1) {
output += (i > 0 ? '\n' : '') + `==> ${file} <==\n`;
}
output += headLines.join('\n');

// Add trailing newline if original had one or if we're showing fewer lines
if (content.endsWith('\n') || fileLines.length > lines) {
output += '\n';
}

outputs.push(output);
} catch (error) {
if (error.code === 'ENOENT') {
return VirtualUtils.error(`head: cannot open '${file}' for reading: No such file or directory`);
} else if (error.code === 'EISDIR') {
return VirtualUtils.error(`head: error reading '${file}': Is a directory`);
} else {
return VirtualUtils.error(`head: ${file}: ${error.message}`);
}
}
}

const result = outputs.join('');
trace('VirtualCommand', () => `head: success | ${JSON.stringify({ files: files.length, lines }, null, 2)}`);
return VirtualUtils.success(result);
} catch (error) {
trace('VirtualCommand', () => `head: unexpected error | ${JSON.stringify({ error: error.message }, null, 2)}`);
return VirtualUtils.error(`head: ${error.message}`);
}
}
127 changes: 127 additions & 0 deletions src/commands/$.sort.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import fs from 'fs';
import { trace, VirtualUtils } from '../$.utils.mjs';

export default async function sort({ args, stdin, cwd, isCancelled, abortSignal }) {
let reverse = false;
let numeric = false;
let unique = false;
let files = [];

// Parse arguments
for (let i = 0; i < args.length; i++) {
if (args[i] === '-r' || args[i] === '--reverse') {
reverse = true;
} else if (args[i] === '-n' || args[i] === '--numeric-sort') {
numeric = true;
} else if (args[i] === '-u' || args[i] === '--unique') {
unique = true;
} else if (args[i].startsWith('-') && args[i] !== '-') {
// Handle combined flags like -rn, -nr, -ru, etc.
const flags = args[i].substring(1);
for (const flag of flags) {
if (flag === 'r') {
reverse = true;
} else if (flag === 'n') {
numeric = true;
} else if (flag === 'u') {
unique = true;
} else {
return VirtualUtils.error(`sort: invalid option -- '${flag}'`);
}
}
} else {
files.push(args[i]);
}
}

// Collect all lines to sort
let allLines = [];

try {
// If no files specified, read from stdin
if (files.length === 0) {
if (stdin !== undefined && stdin !== '') {
allLines = stdin.split('\n');
// Remove empty last line if input doesn't end with newline
if (allLines.length > 0 && allLines[allLines.length - 1] === '') {
allLines.pop();
}
}
} else {
// Read from all specified files
for (const file of files) {
// Check for cancellation
if (isCancelled?.() || abortSignal?.aborted) {
trace('VirtualCommand', () => `sort: cancelled while processing files`);
return { code: 130, stdout: '', stderr: '' };
}

trace('VirtualCommand', () => `sort: reading file | ${JSON.stringify({ file }, null, 2)}`);

const resolvedPath = VirtualUtils.resolvePath(file, cwd);
try {
const content = fs.readFileSync(resolvedPath, 'utf8');
const fileLines = content.split('\n');
// Remove empty last line if file doesn't end with newline
if (fileLines.length > 0 && fileLines[fileLines.length - 1] === '') {
fileLines.pop();
}
allLines.push(...fileLines);
} catch (error) {
if (error.code === 'ENOENT') {
return VirtualUtils.error(`sort: cannot read: ${file}: No such file or directory`);
} else if (error.code === 'EISDIR') {
return VirtualUtils.error(`sort: read failed: ${file}: Is a directory`);
} else {
return VirtualUtils.error(`sort: ${file}: ${error.message}`);
}
}
}
}

// Remove duplicates if unique flag is set
if (unique) {
allLines = [...new Set(allLines)];
}

// Sort the lines
if (numeric) {
allLines.sort((a, b) => {
const numA = parseFloat(a);
const numB = parseFloat(b);

// Handle non-numeric strings
if (isNaN(numA) && isNaN(numB)) {
return a.localeCompare(b);
} else if (isNaN(numA)) {
return 1; // Non-numeric goes to end
} else if (isNaN(numB)) {
return -1; // Non-numeric goes to end
}

return numA - numB;
});
} else {
// Lexicographic sort
allLines.sort((a, b) => a.localeCompare(b));
}

// Reverse if requested
if (reverse) {
allLines.reverse();
}

const output = allLines.join('\n') + (allLines.length > 0 ? '\n' : '');

trace('VirtualCommand', () => `sort: success | ${JSON.stringify({
files: files.length,
lines: allLines.length,
flags: { reverse, numeric, unique }
}, null, 2)}`);

return VirtualUtils.success(output);
} catch (error) {
trace('VirtualCommand', () => `sort: unexpected error | ${JSON.stringify({ error: error.message }, null, 2)}`);
return VirtualUtils.error(`sort: ${error.message}`);
}
}
Loading
Loading