bat2js
Version:
Batch to JavaScript compiler
294 lines (214 loc) • 7.22 kB
text/typescript
// -- START HEADER --
// Copyright (c) benja2998. You may redistribute under the terms of the GPL-3.0-or-later.
// bat2js is a Batch to JavaScript compiler.
// -- END HEADER --
// -- START IMPORTS --
import * as fs from 'fs';
import * as path from 'path';
const { randomUUID } = require('crypto');
// -- END IMPORTS --
export type token = {
type: string;
value: string;
argument: string;
}
export class tokenType {
public static readonly COMMENT = 'COMMENT';
public static readonly COMMAND = 'COMMAND';
public static readonly STRING = 'STRING';
public static readonly VARIABLE = 'VARIABLE';
public static readonly NUMBER = 'NUMBER';
public static readonly SETVAR = 'SETVAR';
}
// Define a function to tokenize the file contents
export function tokenize(contents: string): token[] {
// Parse the contents into an array of lines
let lines = contents.split('\n');
let tokens: token[] = [];
// Iterate over each word
for (let i = 0; i < lines.length; i++) {
let line = lines[i];
let words = line.split(' ');
if (line.startsWith('@')) {
continue;
}
if (line === '') {
continue;
}
// Check if \r comes at the end of a word or is the only word
if (words[words.length - 1].endsWith('\r')) {
// Remove the \r
words[words.length - 1] = words[words.length - 1].slice(0, -1);
}
// Do the same but for \n
if (words[words.length - 1].endsWith('\n')) {
// Remove the \n
words[words.length - 1] = words[words.length - 1].slice(0, -1);
}
// Remove empty words
words = words.filter(word => word !== '');
// Now that we've cleaned up, let's tokenize
for (let j = 0; j < words.length; j++) {
let word = words[j];
let restOfLine = line.slice(line.indexOf(word) + word.length).trim();
// Check if the word is a comment by checking if it's rem or ::
if (word.startsWith('rem') || word.startsWith('::')) {
// Get the comment by looking at the rest of the line
let comment = line.slice(line.indexOf(word) + word.length).trim();
// Add the comment to the tokens
tokens.push({
type: tokenType.COMMENT,
value: comment,
argument: '' // No argument for a comment
});
continue;
} else {
// Check if the word is a command
if (word === 'echo') {
tokens.push({
type: tokenType.COMMAND,
value: word,
argument: restOfLine
});
}
else if (word === 'set') {
// Get the rest of the line for the variable name and value
let variableNameValue = restOfLine.trim();
// Remove quotes from the variable name and value
variableNameValue = variableNameValue.replace(/['"]/g, '');
// Split the variable name and value by the first =
let variableName = variableNameValue.split('=')[0];
let variableValue = variableNameValue.split('=')[1];
// Check if the variableValue is a string
if (isNaN(Number(variableValue))) {
// It's a string, so add quotes
variableValue = `"${variableValue}"`;
}
// Add the variable to the tokens
tokens.push({
type: tokenType.SETVAR,
value: variableName,
argument: variableValue
});
}
else if (word === 'pause') {
let redirectTo = '';
// Get the next word
let nextWord = words[j + 1];
// Check if the next word contains >
if (nextWord !== undefined && nextWord.includes('>')) {
// Redirection
// Split nextWord by > and get the second part
redirectTo = nextWord.split('>')[1];
// Check if redirectTo is empty
if (redirectTo === '') {
// Set redirectTo to the rest of the line
redirectTo = line.slice(line.indexOf(nextWord) + nextWord.length).trim();
}
}
// Push the pause command to the tokens
tokens.push({
type: tokenType.COMMAND,
value: 'pause',
argument: redirectTo
});
}
continue;
}
}
}
return tokens;
}
export function tokens_to_javascript(tokens: token[], filePath: string): string {
// This is the complex part
let javascript = `#!/usr/bin/env node
// -- START HEADER --
// DO NOT MODIFY.
// This file is automatically generated by bat2js.
// To modify the contents, edit the original .bat file and run bat2js again.
// Compiled from: ${filePath}
// -- END HEADER --
const fs = require('fs');
const path = require('path');
const { spawn } = require('child_process');
const readline = require('readline');
// -- START FUNCTIONS --
// Make a waitForKeypress function
function waitForKeypress(message) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
return new Promise((resolve) => {
rl.question(message, () => {
rl.close();
resolve();
});
});
}
// -- END FUNCTIONS --
// -- START COMPILED CODE --
`;
for (let i = 0; i < tokens.length; i++) {
let token = tokens[i];
if (token.type === tokenType.COMMENT) {
javascript += `// ${token.value}\n`;
}
if (token.type === tokenType.COMMAND) {
if (token.value === 'echo') {
javascript += `console.log(\`${token.argument}\`);\n`;
}
if (token.value === 'pause') {
// Check argument
if (token.argument !== 'nul') {
// Check if argument is a file
if (token.argument !== undefined) {
// Append "Press Enter to continue..." to the end of the file
javascript += `waitForKeypress('');\n`;
javascript += `fs.appendFileSync('${token.argument}', 'Press Enter to continue...');\n`;
}
else {
javascript += `waitForKeypress('Press Enter to continue...');\n`;
}
}
else {
javascript += `waitForKeypress('');\n`;
}
}
}
if (token.type === tokenType.SETVAR) {
// Check if javascript already contains "let ${token.value} = ${token.argument};"
if (javascript.includes(`let ${token.value} = ${token.argument};`)) {
javascript += `${token.value} = ${token.argument};\n`;
} else {
javascript += `let ${token.value} = ${token.argument};\n`;
}
}
}
javascript = javascript.replace(/%([^%]+)%/g, (_match, p1) => `\${${p1}}`);
return javascript;
}
export function isPercentVariable(str: string): boolean {
return /^%.*%$/.test(str);
}
if (require.main === module) {
let filePath = process.argv[2];
if (!filePath) {
console.error('Usagee: bat2js <file>');;
process.exit(1);
}
if (!fs.existsSync(filePath)) {
console.error(`File '${filePath}' does not exist.`);
process.exit(1);
}
let fileContents = fs.readFileSync(filePath, 'utf8');
console.time('Compilation Time');
let tokens = tokenize(fileContents);
let javascript = tokens_to_javascript(tokens, filePath);
console.timeEnd('Compilation Time');
// Write the compiled JavaScript to a file
const fileName = path.parse(filePath).name;
const compiledFilePath = path.join(path.dirname(filePath), `${fileName}.js`);
fs.writeFileSync(compiledFilePath, javascript);
}