Skip to content

Commit

Permalink
refactor: reorganize all scripts
Browse files Browse the repository at this point in the history
  • Loading branch information
EagleoutIce committed Aug 26, 2023
1 parent 979c979 commit a224964
Show file tree
Hide file tree
Showing 10 changed files with 209 additions and 140 deletions.
139 changes: 0 additions & 139 deletions src/cli/repl.ts

This file was deleted.

35 changes: 35 additions & 0 deletions src/cli/repl/commands/help.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { rawPrompt } from '../prompt'
import { bold, italic } from '../../../statistics'
import { commands, ReplCommand } from './main'



const longestKey = Array.from(Object.keys(commands), k => k.length).reduce((p, n) => Math.max(p, n), 0)
function padCmd<T>(string: T) {
return String(string).padEnd(longestKey + 2, ' ')
}

export const helpCommand: ReplCommand = {
description: 'Show help information',
script: false,
usageExample: ':help',
fn: () => {
console.log(`
You can always just enter a R expression which gets evaluated:
${rawPrompt} ${bold('1 + 1')}
${italic('[1] 2')}
Besides that, you can use the following commands. The scripts ${italic('can')} accept further arguments. There are the following basic commands:
${
Array.from(Object.entries(commands)).filter(([, {script}]) => !script).map(
([command, { description }]) => ` ${bold(padCmd(':' + command))}${description}`).join('\n')
}
Furthermore, you can directly call the following scripts which accept arguments. If you are unsure, try to add ${italic('--help')} after the command.
${
Array.from(Object.entries(commands)).filter(([, {script}]) => script).map(
([command, { description }]) => ` ${bold(padCmd(':' + command))}${description}`).join('\n')
}
`)
}
}

40 changes: 40 additions & 0 deletions src/cli/repl/commands/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { RShell, TokenMap } from '../../../r-bridge'
import { helpCommand } from './help'
import { quitCommand } from './quit'
import { scripts } from '../../scripts-info'
import { waitOnScript } from '../execute'
import { splitArguments } from '../../../util/args'

/**
* Content of a single command in the repl.
*/
export interface ReplCommand {
/** human-readable description of what the command does */
description: string
/** does the command invoke another script? this is mainly used to automatically generate two separate lists when asking for help */
script: boolean
/** example of how to use the command, for example `:slicer --help` */
usageExample: string
/** function to execute when the command is invoked */
fn: (shell: RShell, tokenMap: TokenMap, remainingLine: string) => Promise<void> | void
}


export const commands: Record<string, ReplCommand> = {
'help': helpCommand,
'quit': quitCommand
}


for(const [script, { target, description, type}] of Object.entries(scripts)) {
if(type === 'master script') {
commands.script = {
description,
script: true,
usageExample: `:${script} --help`,
fn: async(_s, _t, remainingLine) => {
await waitOnScript(`${__dirname}/${target}`, splitArguments(remainingLine))
}
}
}
}
9 changes: 9 additions & 0 deletions src/cli/repl/commands/quit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ReplCommand } from './main'
import { log } from '../../../util/log'

export const quitCommand: ReplCommand = {
description: 'End the repl',
usageExample: ':quit',
script: false,
fn: () => { log.info('bye'); process.exit(0) }
}
72 changes: 72 additions & 0 deletions src/cli/repl/core.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/**
* Basically a helper file to allow the main 'flowr' script (located in the source root) to provide its repl
*
* @module
*/
import { getStoredTokenMap, RShell, TokenMap } from '../../r-bridge'
import readline from 'readline/promises'
import { bold, italic } from '../../statistics'
import { prompt } from './prompt'
import { commands, ReplCommand } from './commands/main'




const replCompleterKeywords = Array.from(Object.keys(commands), s => `:${s}`)

/**
* Used by the repl to provide automatic completions for a given (partial) input line
*/
export function replCompleter(line: string): [string[], string] {
return [replCompleterKeywords.filter(k => k.startsWith(line)), line]
}

/**
* Provides a never-ending repl (read-evaluate-print loop) processor that can be used to interact with a {@link RShell} as well as all flowR scripts.
*
* The repl allows for two kinds of inputs:
* - Starting with a colon `:`, indicating a command (probe `:help`, and refer to {@link commands}) </li>
* - Starting with anything else, indicating default R code to be directly executed. If you kill the underlying shell, that is on you! </li>
*
* @param shell - The shell to use, if you do not pass one it will automatically create a new one with the `revive` option set to 'always'
* @param tokenMap - The pre-retrieved token map, if you pass none, it will be retrieved automatically (using the default {@link getStoredTokenMap}).
* @param rl - A potentially customized readline interface to be used for the repl to *read* from the user, we write the output with `console.log`.
* If you want to provide a custom one but use the same `completer`, refer to {@link replCompleter}.
*/
export async function repl(shell = new RShell({ revive: 'always' }), tokenMap?: TokenMap, rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
tabSize: 4,
terminal: true,
removeHistoryDuplicates: true,
completer: replCompleter
})) {

tokenMap ??= await getStoredTokenMap(shell)

// the incredible repl :D, we kill it with ':quit'
// eslint-disable-next-line no-constant-condition,@typescript-eslint/no-unnecessary-condition
while(true) {
const answer: string = await rl.question(prompt())

if(answer.startsWith(':')) {
const command = answer.slice(1).split(' ')[0].toLowerCase()
const processor = commands[command] as (ReplCommand | undefined)
if(processor) {
await processor.fn(shell, tokenMap, answer.slice(command.length + 2).trim())
} else {
console.log(`the command '${command}' is unknown, try ${bold(':help')} for more information`)
}
} else {
try {
const result = await shell.sendCommandWithOutput(answer, {
from: 'both',
automaticallyTrimOutput: true
})
console.log(`${italic(result.join('\n'))}\n`)
} catch(e) {
console.error(`Error while executing '${answer}': ${(e as Error).message}`)
}
}
}
}
17 changes: 17 additions & 0 deletions src/cli/repl/execute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { log } from '../../util/log'
import cp from 'child_process'

/**
* Run the given module with the presented arguments, and wait for it to exit.
*/
export async function waitOnScript(module: string, args: string[]): Promise<void> {
log.info(`starting script ${module} with args ${JSON.stringify(args)}`)
const child = cp.fork(module, args)
child.on('exit', (code, signal) => {
if (code) {
console.error(`Script ${module} exited with code ${JSON.stringify(code)} and signal ${JSON.stringify(signal)}`)
process.exit(code)
}
})
await new Promise<void>(resolve => child.on('exit', resolve))
}
5 changes: 5 additions & 0 deletions src/cli/repl/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from './server'
export * from './core'
export * from './prompt'
export * from './server'
export * from './execute'
5 changes: 5 additions & 0 deletions src/cli/repl/prompt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { ColorEffect, Colors, formatter } from '../../statistics'

export const rawPrompt = 'R>'
// is a function as the 'formatter' is configured only after the cli options have been read
export const prompt = () => `${formatter.format(rawPrompt, { color: Colors.cyan, effect: ColorEffect.foreground })} `
24 changes: 24 additions & 0 deletions src/cli/repl/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* Provides the capability of connecting to the repl of flowr via messages.
*
* @module
*/
import { RExpressionList } from '../../r-bridge'

interface RequestMessage {
type: 'request',
command: string,
arguments: string[]
}

interface ResponseMessage<T> {
type: 'response',
success: boolean,
message: T
}

interface NormalizedAstRequestMessage extends RequestMessage {
command: 'normalized'
}

type NormalizedAstResponseMessage = ResponseMessage<RExpressionList>
3 changes: 2 additions & 1 deletion src/flowr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ import commandLineUsage, { OptionDefinition } from 'command-line-usage'
import commandLineArgs from 'command-line-args'
import { guard } from './util/assert'
import { bold, ColorEffect, Colors, FontStyles, formatter, italic, setFormatter, voidFormatter } from './statistics'
import { repl, waitOnScript } from './cli/repl'
import { repl } from './cli/repl/core'
import { ScriptInformation, scripts } from './cli/scripts-info'
import { waitOnScript } from './cli/repl'

const scriptsText = Array.from(Object.entries(scripts).filter(([, {type}]) => type === 'master script'), ([k,]) => k).join(', ')

Expand Down

0 comments on commit a224964

Please sign in to comment.