Skip to content
Draft
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
13 changes: 13 additions & 0 deletions src/commands.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { countHandler } from './commands/count.js'
import { csvToJsonHandler } from './commands/csvToJson.js'
import { jsonToCsvHandler } from './commands/jsonToCsv.js'
import { upHandler, cdHandler, lsHandler } from './navigation.js'

export const COMMAND_HANDLERS_MAP = {
up: upHandler,
cd: cdHandler,
ls: lsHandler,
'csv-to-json': csvToJsonHandler,
'json-to-csv': jsonToCsvHandler,
count: countHandler,
}
56 changes: 56 additions & 0 deletions src/commands/count.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@

import { extname } from 'node:path';
import { createReadStream } from 'node:fs';
import { argParser } from '../utils/argParser.js';
import { pathResolver } from '../utils/pathResolver.js';
import { InvalidInputError } from '../utils/errors.js';

const LINE_SEPARATOR = /\r?\n/;
const WORD_SEPARATOR = /\s+/;

const getWordsFromString = (str) => {
return str.split(WORD_SEPARATOR).filter(w => w.length > 0);
}

export const countHandler = async (args) => {
const inputArg = argParser(args, 'input', true);
const inputPath = pathResolver(inputArg);

const inputFileExt = extname(inputPath).toLowerCase();

if (inputFileExt !== '.txt') {
throw new InvalidInputError('Invalid file extention');
}

const readableStream = createReadStream(inputPath, { encoding: 'utf-8' });

let buffer = '';
let linesCount = 0;
let wordsCount = 0;
let charsCount = 0;

for await (const chunk of readableStream) {
buffer += chunk;
charsCount += chunk.length;

const lines = buffer.split(LINE_SEPARATOR);
buffer = lines.pop();

linesCount += lines.length;

const words = getWordsFromString(lines.join(' '));
wordsCount += words.length;
}

if (buffer.length) {
charsCount += buffer.length;
linesCount++;

const words = getWordsFromString(buffer);
wordsCount += words.length;
}

console.log(`Lines: ${linesCount}`);
console.log(`Words: ${wordsCount}`);
console.log(`Characters: ${charsCount}`);
}
79 changes: 79 additions & 0 deletions src/commands/csvToJson.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { Transform } from 'node:stream';
import { fileConfersionHandler } from '../utils/fileConversionHandler.js';

const EXTRA_HEADER_NAME = 'Extra';
const INDENT = 2;
const LINE_SEPARATOR = /\r?\n/;

const getCsvToJsonTransformSteam = () => {
let transformBuffer = '';
let headers = [];
let isFirstDataRow = true;

const getDataPrefix = () => {
let prefix = `,\n${' '.repeat(INDENT)}`;
if (isFirstDataRow) {
prefix = ' '.repeat(INDENT);
isFirstDataRow = false;
}
return prefix;
}

const transformDataToJsonString = (data) => {
const obj = {};
let unknownHeaderCounter = 1;
data.forEach((d, i) => {
let header;
if (headers[i]) {
header = headers[i];
} else {
header = `${EXTRA_HEADER_NAME}${unknownHeaderCounter}`;
unknownHeaderCounter++;
}
obj[header] = d;
});
return JSON.stringify(obj);
}

return new Transform({
transform(chunk, _, callback) {
transformBuffer += chunk;

const lines = transformBuffer.split(LINE_SEPARATOR);
transformBuffer = lines.pop();

lines.forEach((line) => {
if (!line.trim()) {
return;
}

const parsedLineData = line.split(',');

if (!headers.length) {
headers = parsedLineData.map(h => h.trim());
this.push('[\n');
} else {
this.push(`${getDataPrefix()}${transformDataToJsonString(parsedLineData)}`);
}
});

callback();
},
flush(callback) {
if (!transformBuffer.trim()) {
return '\n]';
}

const parsedLineData = transformBuffer.split(',');
this.push(`${getDataPrefix()}${transformDataToJsonString(parsedLineData)}\n]\n`);

transformBuffer = '';
callback();
}
});
}

export const csvToJsonHandler = async (args) => {
const transformStream = getCsvToJsonTransformSteam();
return fileConfersionHandler(args, '.csv', transformStream);
}
38 changes: 38 additions & 0 deletions src/commands/jsonToCsv.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Transform } from 'node:stream';
import { fileConfersionHandler } from '../utils/fileConversionHandler.js';

const getJsonToCsvTransformSteam = () => {
let jsonStringBuffer = '';

return new Transform({
transform(chunk, _, callback) {
jsonStringBuffer += chunk;
callback();
},
flush(callback) {
const jsonData = JSON.parse(jsonStringBuffer);
jsonStringBuffer = '';
if (!Array.isArray(jsonData)) {
throw new Error('Invalid json type');
}

const headerSet = new Set();
jsonData.map((row) => Object.keys(row).forEach(h => headerSet.add(h)));
const headers = Array.from(headerSet);

this.push(`${headers.join(',')}\n`)

jsonData.forEach(row => {
const valuesInHeadersOrder = headers.map((header) => row[header] || '');
this.push(`${valuesInHeadersOrder.join(',')}\n`);
})

callback();
}
});
}

export const jsonToCsvHandler = async (args) => {
const transformStream = getJsonToCsvTransformSteam();
return fileConfersionHandler(args, '.json', transformStream);
}
17 changes: 17 additions & 0 deletions src/cwdState.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import os from 'node:os';
import { InvalidInputError } from './utils/errors.js';

export const initialCwd = os.homedir();
let cwd = initialCwd;

export const getCwd = () => {
return cwd;
}

// dir - absolute path
export const chCwd = (dir) => {
if (!dir.startsWith(initialCwd)) {
throw new InvalidInputError('Cannot go higher than initial directory');
}
cwd = dir;
}
7 changes: 7 additions & 0 deletions src/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { initRepl } from './repl.js';

const init = () => {
initRepl();
}

init();
56 changes: 56 additions & 0 deletions src/navigation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import fs from 'node:fs/promises';
import { getCwd, chCwd, initialCwd } from './cwdState.js';
import { InvalidInputError } from './utils/errors.js';
import { pathResolver } from './utils/pathResolver.js';


export const upHandler = () => {
if (getCwd() !== initialCwd) {
const newDirectory = pathResolver(`${getCwd}/..`);
chDir(newDirectory);
}
}

export const cdHandler = async (args) => {
const pathToDirectory = args[0];
if (!pathToDirectory) {
throw new InvalidInputError('No path/to/directory');
}
const newDirPath = pathResolver(pathToDirectory);

const stats = await fs.stat(newDirPath);

if (stats.isDirectory()) {
chCwd(newDirPath)
} else {
throw new InvalidInputError('Path is not a directory');
}
}

export const lsHandler = async () => {
const dirEntities = await fs.readdir(getCwd(), { withFileTypes: true });

const folders = [];
const files = [];

let maxEntitName = 0;

dirEntities.forEach((dirEnt) => {
maxEntitName = Math.max(maxEntitName, dirEnt.name.length);

if (dirEnt.isDirectory()) {
folders.push(dirEnt.name);
} else if (dirEnt.isFile()) {
files.push(dirEnt.name);
}
});

const formatEntityName = (entitiName) => `${entitiName}${' '.repeat(maxEntitName - entitiName.length)}`

folders.sort((a, b) => a.localeCompare(b)).forEach((ent) => {
console.log(`${formatEntityName(ent)} [folder]`)
});
files.sort((a, b) => a.localeCompare(b)).forEach((ent) => {
console.log(`${formatEntityName(ent)} [file]`)
});
}
81 changes: 81 additions & 0 deletions src/repl.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@


import readline from 'node:readline';
import process from 'node:process';
import { getCwd } from './cwdState.js';
import { InvalidInputError, INVALID_INPUT_ERROR_CODE } from './utils/errors.js';
import { COMMAND_HANDLERS_MAP } from './commands.js';

const WELCOME_TEXT = 'Welcome to Data Processing CLI!';
const EXIT_TEXT = 'Thank you for using Data Processing CLI!';
const INVALID_COMMAND_TEXT = 'Invalid input';
const OPERATION_FAILED_TEXT = 'Operation failed';
const CURRENT_DIRECTORY_PREFIX = 'You are currently in';

const ANSI_COLORS = {
red: '\x1b[31m',
green: '\x1b[32m',
};
const ANSI_COLOR_RESET = '\x1b[0m';

const onSuccess = () => {
console.log(`${ANSI_COLORS.green}${CURRENT_DIRECTORY_PREFIX} ${getCwd()}${ANSI_COLOR_RESET}`);
}

const onError = (err) => {
// For testing only, TODO: delete before review next line
console.log(err);
if (err.code === INVALID_INPUT_ERROR_CODE) {
console.log(`${ANSI_COLORS.red}${INVALID_COMMAND_TEXT}${ANSI_COLOR_RESET}`);
} else {
console.log(`${ANSI_COLORS.red}${OPERATION_FAILED_TEXT}${ANSI_COLOR_RESET}`);
}
}

const handleCommand = async(command, commandArgs) => {
if (command in COMMAND_HANDLERS_MAP) {
try {
await COMMAND_HANDLERS_MAP[command](commandArgs);
onSuccess()
} catch (err) {
onError(err)
}

} else {
onError(new InvalidInputError('Invalid command'));
}
}

export const initRepl = () => {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
prompt: '> '
});

rl.on('line', async (line) => {
const lineTrimmed = line.trim();
const [command, ...commandArgs] = lineTrimmed.split(' ');

if (command === '.exit' && commandArgs.length === 0) {
rl.close()
} else {
await handleCommand(command, commandArgs);
rl.prompt();
}
});

rl.on('SIGINT', () => {
rl.close();
});

rl.on('close', () => {
console.log(EXIT_TEXT)
process.exit(0);
});

console.log(WELCOME_TEXT);
// Initial display of directory
onSuccess();
rl.prompt();
}
32 changes: 32 additions & 0 deletions src/utils/argParser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { InvalidInputError } from './errors.js';

export const argParser = (args, argName, required = false) => {
const argIndex = args.findIndex(arg => arg.startsWith(`--${argName}`));

if (argIndex === -1) {
if (required) {
throw new InvalidInputError('Argument is required');
} else {
return;
}
}

const arg = args[argIndex];
let argValue;

if (arg.includes('=')) {
argValue = arg.split('=')[1];
} else {
const nextArgvValue = args[argIndex + 1];

if (nextArgvValue && !nextArgvValue.startsWith('--')) {
argValue = nextArgvValue;
}
}

if (!argValue && required) {
throw new InvalidInputError('Argument is required');
}

return argValue;
}
Loading