Created
January 29, 2025 03:52
-
-
Save diegovgsilva95/e80672585abc842d00226d5cf71073ba to your computer and use it in GitHub Desktop.
Node.js - Parsing and sorting Pacman (Arch, btw) packages by decreasing size, together with their install reason (e.g., explicitly installed, installed as a dependency)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Intended usage: | |
| // $ pacman -Qi | node index.mjs | |
| // I know there are (plenty of) existing solutions to this. I just made my own. | |
| import { stdin, stdout } from "process" | |
| import { Console, log } from "console" | |
| import { createWriteStream, writeFileSync } from "fs" | |
| import { writeFile } from "fs/promises" | |
| const sleep = ms => new Promise(r => setTimeout(r, ms)) | |
| // Historical debugging purposes... Keeping as a comment so I can query my own code in the future. | |
| // let auxConsole = new Console({ | |
| // colorMode: true, | |
| // stdout: createWriteStream(null, {fd: 3}) | |
| // }) | |
| // I chose to keep comments from my TDD approach. | |
| let packages = {} | |
| let reminiscent = "" | |
| let endIsNigh = false | |
| let timer = null | |
| const parsePackage = function(packageStr){ | |
| let name, size, reason | |
| let props = [...packageStr.trim().matchAll(/^(?<key>[^\:]+):(?<val>.+)$/gm)] | |
| for(let {groups: {key, val}} of props){ | |
| key = key.trim() | |
| val = val.trim() | |
| if(key == "Name") | |
| name = val | |
| else if(key.includes("Size")) | |
| size = val | |
| else if(key.includes("Reason")) | |
| reason = val | |
| } | |
| if(typeof name !== "string" || typeof size !== "string" || typeof reason !== "string"){ | |
| throw new TypeError(`Detected rogue string!`) | |
| } | |
| packages[name] = {name, size, reason} | |
| // auxConsole.log(name) | |
| return name | |
| } | |
| const sizeScales = ["B", "KiB", "MiB", "GiB", "TiB", "PiB"] | |
| const parseSize = function(size){ | |
| let cmavs = size?.match(/(?<value>[^ ]+) (?<scale>[A-Z]+)/i) | |
| if(!cmavs) | |
| throw new TypeError("Not a size!") | |
| let {value, scale} = cmavs.groups | |
| let scaleExp = sizeScales.indexOf(scale) | |
| value = parseFloat(value) | |
| return {value, scaleExp} | |
| } | |
| const compareSizes = function(sizeA, sizeB){ | |
| let parsedSizeA = parseSize(sizeA) | |
| let parsedSizeB = parseSize(sizeB) | |
| return parsedSizeA.scaleExp == parsedSizeB.scaleExp ? Math.sign(parsedSizeA.value-parsedSizeB.value) : Math.sign(parsedSizeA.scaleExp - parsedSizeB.scaleExp) | |
| } | |
| const onFinished = async function(){ | |
| if(reminiscent.length > 0){ | |
| log("Parsing remaining block of package info...") | |
| parsePackage(reminiscent) | |
| } | |
| log("Reached the finish with " + Object.keys(packages).length + " packages computed") | |
| log("Largest packages:") | |
| let sortedPackages = Object.entries(packages).sort((a,b)=>compareSizes(b[1].size, a[1].size)).map(([_,info])=>`${info.name} (${info.size}): ${info.reason}`) | |
| log(sortedPackages.map(line=>` - ${line}`).slice(0,10).join("\n")) | |
| await writeFile("sorted-package-list.lst", sortedPackages.join("\n")) | |
| } | |
| stdin.on("data", async function(data){ | |
| // stdin.pause() | |
| let dataStr = data.toString("utf-8") | |
| // log("Received", dataStr.length) | |
| if(dataStr.length > 0 && dataStr.includes("\n\n")){ | |
| let doubleMatches = [...dataStr.matchAll(/\n\n/g)] | |
| log(`Parsing ${doubleMatches.length} blocks of package info...`) | |
| // log("What about the reminiscent?", reminiscent.length, reminiscent.length>0?`<"${reminiscent.slice(0, 5)}...${reminiscent.slice(-5)}">`:"") | |
| let lastOffset = 0 | |
| let i = 0 | |
| for(let match of doubleMatches){ | |
| // if((iter == 0 && (++i == doubleMatches.length)) || (iter == 1 && ++i == 1)){ | |
| // log(`Parsing from ${lastOffset} to ${match.index}...`) | |
| parsePackage(reminiscent + dataStr.slice(lastOffset, match.index)) | |
| if(reminiscent.length>0) reminiscent = "" | |
| // log(`[${}]`) | |
| // } | |
| lastOffset = match.index+match[0].length | |
| // await sleep(10) | |
| } | |
| if(dataStr.length > lastOffset){ | |
| // log(`last offset = ${lastOffset}, len = ${dataStr.length}, setting reminiscent`) | |
| reminiscent = dataStr.slice(lastOffset, dataStr.length) | |
| } | |
| } else { | |
| log("WARN: Got reminiscent without another reminiscent, trying to parse them...") | |
| // Should I try to detect and break double lines here? | |
| // Bc the reminiscent block isn't expected to be so large so to contain info from multiple packages. | |
| log({reminiscent, dataStr}) | |
| parsePackage(reminiscent + dataStr) | |
| } | |
| // await sleep(50) | |
| if(endIsNigh){ | |
| timer && clearTimeout(timer) | |
| onFinished() | |
| } | |
| // else | |
| // stdin.resume() | |
| }) | |
| stdin.on("end", function(){ | |
| endIsNigh = true // Beware the end, for the end is nigh! | |
| if(stdin.readableFlowing) | |
| timer = setTimeout(onFinished, 100) | |
| }) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment