#! /usr/bin/env node
import * as fs from "node:fs";
import { Writable as writeableStream } from "node:stream";
import { promisify } from "node:util";
import yargs from "yargs";
import { hideBin } from "yargs/helpers";
import { Converter } from "./converter.mjs";
import { Schematic } from "./schematic.mjs";
/**
* @file CLI for converting files from Keysight ADS to CircuiTikZ.
* This is the main file and kicks of the conversion or lists cell and schematic names using {@link Converter}.
* The conversion generates a {@link Schematic}, which is then serialized.
*
* If the package is properly installed, you can use `abl2tikz <args>` in any directory to start.
* Otherwise, `npm run abl2tikz <args>` or `node cli.mjs <args>` work on any system. On a UNIX system, you can shorten
* this to `cli.mjs <args>`.
*
* Functions in this file are for internal use only.
*/
global.VERBOSE = false;
global.DEBUG = false;
/**
* Opens a file for reading.
*
* @param {string} filename - the path to the file
* @throws {Error} - if the file does not exist or is not accessible
* @returns {number} the file descriptor
*/
function strToInFile(filename) {
if (filename && typeof filename === "string" && filename !== "-") {
// normal filename
try {
return fs.openSync(filename, "r");
} catch (err) {
switch (err.code) {
case "EACCES":
throw new Error('Error: Access to file "' + filename + '" denied.');
// case "EADDRINUSE":
// case "ECONNREFUSED":
// case "ECONNRESET":
// case "EEXIST":
case "EISDIR":
throw new Error('Error: Expected file path but got directory: "' + filename + '".');
case "EMFILE":
throw new Error("Error: Too many open files.");
case "ENOENT":
throw new Error('Error: File "' + filename + '" does not exist.');
// case "ENOTDIR":
// case "ENOTEMPTY":
// case "ENOTFOUND":
// case "EPERM":
case "EPIPE":
throw new Error("Error: Broken pipe");
//case "ETIMEDOUT":
default:
throw new Error("Error: Unknown error: " + err.code);
}
}
} else if (filename && (filename === "-" || typeof filename === "boolean")) {
// stdin
return process.stdin.fd; // <-- should always be 0
} else {
// empty --> error
throw new Error("Error: sourcefile must be a path to a file or - for stdin");
}
}
/**
* Opens a file for reading.
*
* @param {string} filename - the path to the file
* @param {boolean} [overwrite=false] - set to true to overwrite existing files
* @throws {Error} - if the file does not exist or is not accessible
* @returns {number} the file descriptor
*/
function strToOutFile(filename, overwrite = false) {
if (filename && typeof filename === "string" && filename !== "-") {
// normal filename
try {
return fs.openSync(filename, overwrite ? "w" : "wx");
} catch (err) {
switch (err.code) {
case "EACCES":
throw new Error('Error: Access to file "' + filename + '" denied.');
// case "EADDRINUSE":
// case "ECONNREFUSED":
// case "ECONNRESET":
case "EEXIST":
throw new Error('Error: Could not create file "' + filename + '".');
case "EISDIR":
throw new Error('Error: Expected file path but got directory: "' + filename + '".');
case "EMFILE":
throw new Error("Error: Too many open files.");
// case "ENOENT":
// case "ENOTDIR":
// case "ENOTEMPTY":
// case "ENOTFOUND":
// case "EPERM":
case "EPIPE":
throw new Error("Error: Broken pipe");
//case "ETIMEDOUT":
default:
throw new Error("Error: Unknown error: " + err.code);
}
}
} else if (filename && (filename === "-" || typeof filename === "boolean")) {
// stdin
return process.stdout.fd; // <-- should always be 1
} else {
// empty --> error
throw new Error("Error: targetfile must be a path to a file or - for stdout");
}
}
/**
* Closes an file descriptor ignoring any errors. This also works if a file is already closed.
* The stdin, stdout and stderr file descriptors won't be closed.
*
* @param {number} fd
*/
function closeFD(fd) {
if (![process.stdin.fd, process.stdout.fd, process.stderr.fd].includes(fd)) fs.close(fd);
}
/**
* Parses the opens file descriptor, closes the file and returns the array of cells.
*
* @param {number} fd - the input file descriptor
* @returns {Promise<Element[]>} the array of cells
*/
function getCellArrayForInputFD(fd) {
return Converter.parseFile(fd).then(
// close FD on success & on fail
(val) => {
closeFD(fd);
return val;
},
(err) => {
closeFD(fd);
return Promise.reject(err);
}
);
}
yargs(hideBin(process.argv))
.detectLocale(false)
.usage("$0 <command> [args]")
.option("verbose", {
alias: "v",
type: "boolean",
description: "Print more debug messages to stdout",
default: false,
global: true,
coerce: (value) => global.VERBOSE = Boolean(value)
})
.command(
"convert <source file> [target file]",
"Converts a Keysight ADS schematic from the XML/ABL (Advanced Board Link) to an CircuiTikZ schematic (.pgf).",
function convertArgumentBuilder(yargs) {
yargs
.option("cellname", {
alias: "c",
type: "string",
description: "The name of the cell to use (source file)",
default: "",
defaultDescription: "(Empty): Use first cell in file",
})
.option("schematicname", {
alias: "s",
type: "string",
description: "The name of the schematic of the cell (source file)",
default: "",
defaultDescription: "(Empty): Use schematic in cell",
})
.option("force", {
alias: "f",
boolean: true,
description: "Overwrite target file, if existing",
default: false,
})
.option("debug", {
type: "boolean",
description: "Target file will contain debug symbols (circles around specific positions etc.)",
default: false,
implies: "verbose",
coerce: (value) => {
global.DEBUG = Boolean(value);
global.VERBOSE = global.VERBOSE || global.DEBUG; // <-- implies verbose
return global.DEBUG;
}
})
.positional("sourcefile", {
describe: "The ABL/XML source file; - for stdin",
coerce: strToInFile,
})
.positional("targetfile", {
describe: "The CircuiTikZ/PGF target file",
default: "-",
defaultDescription: "Defaults to stdout",
demandOption: false,
})
.check((options) => {
// converting to file only here possible
// coerce: can't access other flags like options.force
// middleware: can't throw error and show help
options.targetfile = strToOutFile(options.targetfile, options.force);
return true;
}, false);
},
function convert(args) {
getCellArrayForInputFD(args.sourcefile)
.then((cellArray) => Converter.findCell(cellArray, args.cellname))
.then((cell) => Converter.findSchematicView(Converter.getSchematicViews(cell), args.schematicname))
.then((schematic) => Schematic.fromXML(schematic))
.then((schematic) => {
/** @type {writeableStream} */
let writeStream;
switch (args.targetfile) {
case process.stdout.fd:
writeStream = process.stdout;
break;
case process.stderr.fd:
writeStream = process.stderr;
break;
default:
writeStream = fs.createWriteStream(null, {
encoding: "utf-8",
autoClose: true,
emitClose: true,
fd: args.targetfile,
});
break;
}
return schematic
.printToStream(writeStream)
.then(() => promisify(writeStream.end).call(writeStream));
})
.catch((error) => console.error("Error: " + (error ? error.message || error : "unknown error")));
}
)
.command(
"list-cells <source file>",
"List all cells in an ABL/XML source file",
function listCellsArgumentBuilder(yargs) {
yargs.positional("sourcefile", {
describe: "The ABL/XML source file; - for stdin",
coerce: strToInFile,
});
},
function listCells(args) {
getCellArrayForInputFD(args.sourcefile)
.then((cells) => Converter.printNodeList(cells), "Cells: ")
.catch((error) => console.error("Error: " + (error ? error.message || error : "unknown error")));
}
)
.command(
"list-schematics [(-c|--cellname) <cellname>] <source file>",
"List all schematics of a selected cell in an ABL/XML source file",
function listSchematicsArgumentBuilder(yargs) {
yargs
.option("cellname", {
alias: "c",
type: "string",
description: "The name of the cell to use (source file)",
default: "",
defaultDescription: "(Empty): Use first cell in file",
})
.positional("sourcefile", {
describe: "The ABL/XML source file; - for stdin",
coerce: strToInFile,
});
},
function listSchematics(args) {
const cellArrayPromise = getCellArrayForInputFD(args.sourcefile);
let printPromise;
if (args.cellname) {
// only one cell
printPromise = cellArrayPromise
.then((cellArray) => Converter.findCell(cellArray, args.cellname))
.then((cell) => Converter.getSchematicViews(cell))
.then((cell) => Converter.printNodeList(cell, "Schematics:"));
} else {
// all cells --> promise chain
printPromise = cellArrayPromise.then((cells) =>
cells.reduce((promise, cell) => {
/** @type {string|undefined} */
let cellName;
if (cell && cell.getAttribute && (cellName = cell.getAttribute("name"))) {
// is cell with non-empty name --> append to chain
return promise.then(() => {
const schematicViews = Converter.getSchematicViews(cell);
Converter.printNodeList(schematicViews, "Schematics of " + cellName + ":");
});
} else return promise;
}, Promise.resolve())
);
}
printPromise.catch((error) =>
console.error("Error: " + (error ? error.message || error : "unknown error"))
);
}
)
.demandCommand(1, 1, "Error: No command given", "Error: Only one command is allowed")
.strict(true)
.help("help")
.alias("h", "help")
.argv;