UNPKG

csv-sort-cli

Version:

Command line app to sort double-entry CSVs coming from internet banking statements

276 lines (247 loc) 8.44 kB
#!/usr/bin/env node /* eslint array-callback-return:0, consistent-return:0, no-loop-func:0 */ // VARS // ----------------------------------------------------------------------------- import fs from "fs"; import meow from "meow"; import path from "path"; import chalk from "chalk"; import { sort } from "csv-sort"; import { select, confirm } from "@inquirer/prompts"; import { globbySync } from "globby"; import { createRequire } from "module"; import updateNotifier from "update-notifier"; import { pullAll } from "codsen-utils"; const { log } = console; const require1 = createRequire(import.meta.url); const pkg = require1("./package.json"); const state = { toDoList: [], overwrite: false, }; const cli = meow( ` Usage $ csvsort YOURFILE.csv or, just type "csvsort" and it will let you pick a file. Options -o, --overwrite Will overwrite the target file instead -h, --help Shows this help -v, --version Shows the version of your ${pkg.name} Example Just call it in the root, where your csv file is located `, { importMeta: import.meta, flags: { overwrite: { type: "boolean", shortFlag: "o", default: false, }, }, }, ); updateNotifier({ pkg }).notify(); // FUNCTIONS // ----------------------------------------------------------------------------- function hasOwnProperty(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); } // consumes a plain object: { // toDoList - array, // overwrite - boolean // } async function offerAListOfCSVsToPickFrom(stateObj) { let allCSVsHere = globbySync("./*.csv", "!**/node_modules/**"); if (!allCSVsHere.length) { return Promise.reject( new Error( "\ncsv-sort-cli: Alas, program couldn't find any CSV files in this folder!", ), ); } let chosenCSVFile = await select({ message: "Which CSV would you like to check?", choices: allCSVsHere, }); let overwrite = false; if ( stateObj === undefined || !hasOwnProperty(stateObj, "overwrite") || (hasOwnProperty(stateObj, "overwrite") && stateObj.overwrite === false) || typeof stateObj.overwrite !== "boolean" ) { overwrite = await confirm({ message: "Do you want to overwrite this file with a sorted result?", }); } return { toDoList: [path.basename(chosenCSVFile)], overwrite, }; } // Step #0. take care of -v and -h flags that are left out in meow. // ----------------------------------------------------------------------------- if (cli.flags.v) { log(pkg.version); process.exit(0); } else if (cli.flags.h) { log(cli.help); process.exit(0); } // Step #1. gather the to-do list of files. // ----------------------------------------------------------------------------- if (cli.input.length > 0) { state.toDoList = cli.input; } // if --overwrite/-o flag is used, the following argument will be put as flag's // value, not in "cli.input[]": // we anticipate the can be multiple, potentially-false flags mixed with valid file names if (Object.keys(cli.flags).length !== 0) { state.toDoList = [...new Set(cli.input)]; } if (cli.flags.o) { // variables that can be misinterpreted as falsy, yet the flag still be in // for example, in "csvsort -o false simples.csv simples2.csv", // the cli.flags.overwrite === false (WTF?) state.overwrite = true; // we normalise the flag since its value in CLI can precede } // Step #2. create a promise variable and assign it to one of the promises, // depending on was the acceptable file passed via args or queries afterwards. // ----------------------------------------------------------------------------- let thePromise; if ( state.toDoList.length === 0 && // no input args given (Object.keys(cli.flags).length === 0 || (Object.keys(cli.flags).length === 1 && cli.flags.overwrite !== undefined)) ) { // --------------------------------- 1 ------------------------------------- // if no arguments were given, offer a list: thePromise = offerAListOfCSVsToPickFrom(state); } else if ( state.toDoList.map((onePath) => path.resolve(onePath)).filter(fs.existsSync) .length > 0 ) { // --------------------------------- 2 ------------------------------------- // basically achieving: (!fs.existsSync) let erroneous = pullAll( state.toDoList.map((onePath) => path.resolve(onePath)), state.toDoList .map((onePath) => path.resolve(onePath)) .filter(fs.existsSync), ).map((singlePath) => path.basename(singlePath)); // then filtering file names-only // write the list of unrecognised file names into the console: if (erroneous.length > 0) { log( chalk.red( `\ncsv-sort-cli: Alas, the following file${ erroneous.length > 1 ? "s don't" : " doesn't" } exist: "${erroneous.join('", "')}"`, ), ); } // remove non-existing paths from toDoList: state.toDoList = state.toDoList .map((onePath) => path.resolve(onePath)) .filter(fs.existsSync); // create the final promise variable we're going to use later: thePromise = Promise.resolve(state); } else { // --------------------------------- 3 ------------------------------------- let butStateWasRecognisedMsg = ""; if (state.overwrite) { butStateWasRecognisedMsg = 'But it recognised your "-o" flag.'; } log( chalk.yellow( `\ncsv-sort-cli: Program didn't recognise any CSV files in your input!\n${butStateWasRecognisedMsg}`, ), ); // if there were no valid path in the arguments, query the files from the // existing CSV's in the current folder: thePromise = offerAListOfCSVsToPickFrom(state); } // Step #3. // ----------------------------------------------------------------------------- thePromise .then((receivedState) => { receivedState.toDoList.map((requestedCSVsPath) => { // read the source fs.readFile(requestedCSVsPath, "utf8", (csvError, csvData) => { if (csvData) { try { let cleaned = sort(csvData); if (receivedState.overwrite) { // overwrite fs.writeFile( path.basename(requestedCSVsPath), cleaned.res.join("\n"), "utf8", (err) => { if (err) { throw err; } log( chalk.green( `csv-sort-cli: Yay! The ${path.basename( requestedCSVsPath, )} has been fixed and overwritten! Check it out.`, ), ); process.exit(0); }, ); } else { // create a new file with appended hyphen+integer before extension let proposedNewFileName; for (let i = 1; i < 1001; i++) { proposedNewFileName = `${path.basename( requestedCSVsPath, path.extname(requestedCSVsPath), )}-${i}${path.extname(requestedCSVsPath)}`; if (!fs.existsSync(path.resolve(proposedNewFileName))) { fs.writeFile( proposedNewFileName, cleaned.res.join("\n"), "utf8", (err) => { if (err) { throw err; } log( chalk.green( `csv-sort-cli: Yay! A new file, ${proposedNewFileName} has been created! Check it out.`, ), ); process.exit(0); }, ); break; } } path.basename(requestedCSVsPath, path.extname(requestedCSVsPath)); } } catch (e) { return Promise.reject( new Error(`\ncsv-sort-cli: Alas, we encountered an error:\n${e}`), ); } } if (csvError) { return Promise.reject( new Error( `\ncsv-sort-cli: Alas, we couldn't fetch the file "${path.basename( requestedCSVsPath, )}" you requested!`, ), ); } }); }); }) .catch((err) => { log(chalk.red(err)); process.exit(1); });