diff --git a/docs/index.md b/docs/index.md deleted file mode 100644 index 1cb622a..0000000 --- a/docs/index.md +++ /dev/null @@ -1,32 +0,0 @@ -# smartshell - -shell actions designed as promises - -## Availabililty - -[![npm](https://pushrocks.gitlab.io/assets/repo-button-npm.svg)](https://www.npmjs.com/package/smartshell) -[![git](https://pushrocks.gitlab.io/assets/repo-button-git.svg)](https://GitLab.com/pushrocks/smartshell) -[![git](https://pushrocks.gitlab.io/assets/repo-button-mirror.svg)](https://github.com/pushrocks/smartshell) -[![docs](https://pushrocks.gitlab.io/assets/repo-button-docs.svg)](https://pushrocks.gitlab.io/smartshell/) - -## Status for master - -[![build status](https://GitLab.com/pushrocks/smartshell/badges/master/build.svg)](https://GitLab.com/pushrocks/smartshell/commits/master) -[![coverage report](https://GitLab.com/pushrocks/smartshell/badges/master/coverage.svg)](https://GitLab.com/pushrocks/smartshell/commits/master) -[![npm downloads per month](https://img.shields.io/npm/dm/smartshell.svg)](https://www.npmjs.com/package/smartshell) -[![Dependency Status](https://david-dm.org/pushrocks/smartshell.svg)](https://david-dm.org/pushrocks/smartshell) -[![bitHound Dependencies](https://www.bithound.io/github/pushrocks/smartshell/badges/dependencies.svg)](https://www.bithound.io/github/pushrocks/smartshell/master/dependencies/npm) -[![bitHound Code](https://www.bithound.io/github/pushrocks/smartshell/badges/code.svg)](https://www.bithound.io/github/pushrocks/smartshell) -[![TypeScript](https://img.shields.io/badge/TypeScript-2.x-blue.svg)](https://nodejs.org/dist/latest-v6.x/docs/api/) -[![node](https://img.shields.io/badge/node->=%206.x.x-blue.svg)](https://nodejs.org/dist/latest-v6.x/docs/api/) -[![JavaScript Style Guide](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](http://standardjs.com/) - -## Usage - -Use TypeScript for best in class instellisense. - -For further information read the linked docs at the top of this README. - -> MIT licensed | **©** [Lossless GmbH](https://lossless.gmbh) - -[![repo-footer](https://pushrocks.gitlab.io/assets/repo-footer.svg)](https://push.rocks) diff --git a/test/test.ts b/test/test.ts index 69d9334..fe22fce 100644 --- a/test/test.ts +++ b/test/test.ts @@ -1,23 +1,31 @@ import { expect, tap } from '@pushrocks/tapbundle'; -import * as smartshell from '../ts/index'; +import * as smartshell from '../ts'; import * as smartpromise from '@pushrocks/smartpromise'; let testSmartshell: smartshell.Smartshell; +tap.test('smartshell should create a Smartshell instance', async () => { + testSmartshell = new smartshell.Smartshell({ + executor: 'bash', + sourceFilePaths: [] + }); + expect(testSmartshell).to.be.instanceof(smartshell.Smartshell); +}); + tap.test('smartshell should run async', async () => { - let execResult = await smartshell.exec('npm -v'); + let execResult = await testSmartshell.exec('npm -v'); expect(execResult.stdout).to.match(/[0-9\.]*/); }); tap.test('smartshell should run async and silent', async () => { - let execResult = await smartshell.execSilent('npm -v'); + let execResult = await testSmartshell.execSilent('npm -v'); expect(execResult.stdout).to.match(/[0-9\.]*/); }); tap.test('smartshell should stream a shell execution', async () => { let done = smartpromise.defer(); - let execStreamingResponse = smartshell.execStreaming('npm -v'); + let execStreamingResponse = await testSmartshell.execStreaming('npm -v'); execStreamingResponse.childProcess.stdout.on('data', data => { done.resolve(data); }); @@ -27,17 +35,7 @@ tap.test('smartshell should stream a shell execution', async () => { }); tap.test('it should execute and wait for a line in the output', async () => { - await smartshell.execAndWaitForLine('echo "5.0.4"', /5.0.4/); -}); - -// Smartshell class - -tap.test('smartshell should create a Smartshell instance', async () => { - testSmartshell = new smartshell.Smartshell({ - executor: 'bash', - sourceFilePaths: [] - }); - expect(testSmartshell).to.be.instanceof(smartshell.Smartshell); + await testSmartshell.execAndWaitForLine('echo "5.0.4"', /5.0.4/); }); tap.test('smartshell should run async', async () => { diff --git a/ts/index.ts b/ts/index.ts index 760ab72..66f1de1 100644 --- a/ts/index.ts +++ b/ts/index.ts @@ -1,2 +1 @@ -export * from './smartshell.wrap'; export * from './smartshell.classes.smartshell'; diff --git a/ts/smartshell.classes.shellenv.ts b/ts/smartshell.classes.shellenv.ts new file mode 100644 index 0000000..8da84fc --- /dev/null +++ b/ts/smartshell.classes.shellenv.ts @@ -0,0 +1,50 @@ +export type TExecutor = 'sh' | 'bash'; + +export interface IShellEnvContructorOptions { + executor: TExecutor; + sourceFilePaths: string[]; +} + +export class ShellEnv { + executor: TExecutor; + sourceFileArray: string[] = []; + + /** + * constructor for the shellenv + */ + constructor(optionsArg: IShellEnvContructorOptions) { + this.executor = optionsArg.executor; + for (let sourceFilePath of optionsArg.sourceFilePaths) { + this.sourceFileArray.push(sourceFilePath); + } + }; + + /** + * add files that are going to be sourced when running a command + * @param sourceFilePathsArray + */ + addSourceFiles(sourceFilePathsArray: string[]) { + for (let sourceFilePath of sourceFilePathsArray) { + this.sourceFileArray.push(sourceFilePath); + } + } + + /** + * cleans the source files array + */ + cleanSourceFiles() { + this.sourceFileArray = []; + } + + createEnvExecString(commandArg): string { + if (this.executor === 'bash') { + let sourceString = ''; + for (let sourceFilePath of this.sourceFileArray) { + sourceString = sourceString + `source ${sourceFilePath} && `; + } + return `bash -c '${sourceString} ${commandArg}'`; + } else { + return commandArg; + } + } +} \ No newline at end of file diff --git a/ts/smartshell.classes.shelllog.ts b/ts/smartshell.classes.shelllog.ts new file mode 100644 index 0000000..12f0b26 --- /dev/null +++ b/ts/smartshell.classes.shelllog.ts @@ -0,0 +1,44 @@ +import * as plugins from './smartshell.plugins'; + +/** + * a log handler for spawned logs + * making sure the process doesn't run out of memory + */ +export class ShellLog { + logStore = Buffer.from(''); + + /** + * log data to console + * @param dataArg + */ + logToConsole(dataArg: string | Buffer): void { + // make sure we have the data as string + const dataString: string = (() => { + if (Buffer.isBuffer(dataArg)) { + return dataArg.toString(); + } + return dataArg; + })(); + console.log(dataString); + } + + /** + * add data to Buffer for later consumption + * @param dataArg + */ + addToBuffer(dataArg: string | Buffer): void { + // make sure we have the data as Buffer + const dataBuffer: Buffer = (() => { + if (!Buffer.isBuffer(dataArg)) { + return Buffer.from(dataArg); + } + return dataArg; + })(); + this.logStore = Buffer.concat([this.logStore,dataBuffer]); + } + + logAndAdd(dataArg: string | Buffer): void { + this.logToConsole(dataArg); + this.addToBuffer(dataArg); + } +} \ No newline at end of file diff --git a/ts/smartshell.classes.smartshell.ts b/ts/smartshell.classes.smartshell.ts index 2ee3582..7b74f4e 100644 --- a/ts/smartshell.classes.smartshell.ts +++ b/ts/smartshell.classes.smartshell.ts @@ -1,64 +1,177 @@ +// -- imports -- import * as plugins from './smartshell.plugins'; import * as smartshellWrap from './smartshell.wrap'; +import { ShellEnv, IShellEnvContructorOptions, TExecutor, } from './smartshell.classes.shellenv'; +import { ShellLog } from './smartshell.classes.shelllog'; -export type TExecutor = 'sh' | 'bash'; +import * as cp from "child_process"; +import { Deferred } from "@pushrocks/smartpromise"; -export interface ISmartshellContructorOptions { - executor: TExecutor; - sourceFilePaths: string[]; +// -- interfaces -- +/** + * interface for ExecResult + */ +export interface IExecResult { + exitCode: number; + stdout: string; } +/** + * interface for streaming ExecResult + */ +export interface IExecResultStreaming { + childProcess: cp.ChildProcess; + finalPromise: Promise; +} + +// -- SmartShell -- export class Smartshell { - executor: TExecutor; - sourceFileArray: string[] = []; - constructor(optionsArg: ISmartshellContructorOptions) { - this.executor = optionsArg.executor; - for (let sourceFilePath of optionsArg.sourceFilePaths) { - this.sourceFileArray.push(sourceFilePath); - } - } + shellEnv: ShellEnv; - addSourceFiles(sourceFilePathsArray: string[]) { - for (let sourceFilePath of sourceFilePathsArray) { - this.sourceFileArray.push(sourceFilePath); - } - } - - cleanSourceFiles() { - this.sourceFileArray = []; + constructor(optionsArg: IShellEnvContructorOptions) { + this.shellEnv = new ShellEnv(optionsArg); } /** - * executes silently and returns IExecResult - * @param commandArg + * imports path into the shell from env if available and returns it with */ - async execSilent(commandArg: string) { - let execCommand = this.createExecString(commandArg); - return await smartshellWrap.execSilent(execCommand); - } - - /** - * executes and returns IExecResult - * @param commandArg - */ - async exec(commandArg: string) { - let execCommand = this.createExecString(commandArg); - return await smartshellWrap.exec(execCommand); - } - - /** - * creates the final sourcing string - * @param commandArg - */ - private createExecString(commandArg): string { - if (this.executor === 'bash') { - let sourceString = ''; - for (let sourceFilePath of this.sourceFileArray) { - sourceString = sourceString + `source ${sourceFilePath} && `; - } - return `bash -c '${sourceString} ${commandArg}'`; + private _importEnvVarPath (stringArg): string { + if (process.env.SMARTSHELL_PATH) { + let commandResult = `PATH=${process.env.SMARTSHELL_PATH} && ${stringArg}`; + // console.log(commandResult) + return commandResult; } else { - return commandArg; + return stringArg; } - } + }; + + /** + * executes a given command async + * @param commandStringArg + */ + private async _exec ( + commandStringArg: string, + silentArg: boolean = false, + strictArg = false, + streamingArg = false + ): Promise { + // flow control promises + const done = plugins.smartpromise.defer(); + const childProcessEnded = plugins.smartpromise.defer(); + // build commandToExecute + let commandToExecute = commandStringArg + commandToExecute = this.shellEnv.createEnvExecString(commandStringArg); + commandToExecute = this._importEnvVarPath(commandToExecute); + const spawnlogInstance = new ShellLog(); + const execChildProcess = cp.spawn(commandToExecute, [], { + shell: true, + env: process.env + }); + + execChildProcess.stdout.on("data", data => { + if (!silentArg) { + spawnlogInstance.logToConsole(data); + } + spawnlogInstance.addToBuffer(data); + }); + execChildProcess.stderr.on("data", data => { + if (!silentArg) { + spawnlogInstance.logToConsole(data); + } + spawnlogInstance.addToBuffer(data); + }); + + if(streamingArg) { + done.resolve({ + childProcess: execChildProcess, + finalPromise: childProcessEnded.promise + }); + } + + execChildProcess.on("exit", (code, signal) => { + if(strictArg && code === 1) { + done.reject(); + } + + const execResult = { + exitCode: code, + stdout: spawnlogInstance.logStore.toString() + } + + if(!streamingArg) { + done.resolve(execResult); + } + childProcessEnded.resolve(execResult); + }); + + const result = await done.promise; + return result; + }; + + async exec ( + commandStringArg: string + ): Promise { + return (await this._exec(commandStringArg, false)) as IExecResult; + }; + + /** + * executes a given command async and silent + * @param commandStringArg + */ + async execSilent ( + commandStringArg: string + ): Promise { + return (await this._exec(commandStringArg, true)) as IExecResult; + }; + + /** + * executes a command async and strict, meaning it rejects the promise if something happens + */ + async execStrict ( + commandStringArg: string + ): Promise { + return (await this._exec(commandStringArg, true, true)) as IExecResult; + }; + + /** + * executes a command and allows you to stream output + */ + async execStreaming ( + commandStringArg: string, + silentArg: boolean = false + ): Promise { + return (await this._exec(commandStringArg, silentArg, false, true)) as IExecResultStreaming; + }; + + async execStreamingSilent (commandStringArg: string) { + return (await this.execStreaming(commandStringArg, true)) as IExecResultStreaming; + }; + + /** + * executes a command and returns promise that will be fullfilled once an putput line matches RegexArg + * @param commandStringArg + * @param regexArg + */ + async execAndWaitForLine ( + commandStringArg: string, + regexArg: RegExp, + silentArg: boolean = false + ) { + let done = plugins.smartpromise.defer(); + let execStreamingResult = await this.execStreaming(commandStringArg, silentArg); + execStreamingResult.childProcess.stdout.on("data", (stdOutChunk: string) => { + if (regexArg.test(stdOutChunk)) { + done.resolve(); + } + }); + return done.promise; + }; + + async execAndWaitForLineSilent ( + commandStringArg: string, + regexArg: RegExp + ) { + this.execAndWaitForLine(commandStringArg, regexArg, true); + }; + } diff --git a/ts/smartshell.wrap.ts b/ts/smartshell.wrap.ts index 0909b12..1f5c819 100644 --- a/ts/smartshell.wrap.ts +++ b/ts/smartshell.wrap.ts @@ -1,4 +1,5 @@ import * as plugins from "./smartshell.plugins"; +import { ShellLog } from "./smartshell.classes.shelllog"; // interfaces import * as cp from "child_process"; @@ -20,174 +21,7 @@ export interface IExecResultStreaming { finalPromise: Promise; } -/** - * imports path into the shell from env if available and returns it with - */ -let importEnvVarPath = (stringArg): string => { - if (process.env.SMARTSHELL_PATH) { - let commandResult = `PATH=${process.env.SMARTSHELL_PATH} && ${stringArg}`; - // console.log(commandResult) - return commandResult; - } else { - return stringArg; - } -}; -/** - * executes a given command async - * @param commandStringArg - */ -export let exec = ( - commandStringArg: string, - silentArg: boolean = false, - strictArg = false -): Promise => { - let done = plugins.smartpromise.defer(); - const commandToExecute = importEnvVarPath(commandStringArg); - try { - const execChildProcess = cp.exec(commandToExecute, { - timeout: null, - maxBuffer: 1000000, - env: process.env - }); - - let logStore = ""; - - execChildProcess.stdout.on("data", (data: string) => { - if (!silentArg) { - console.log(data); - } - logStore += data; - }); - execChildProcess.stderr.on("data", data => { - if (!silentArg) { - console.log(data); - } - logStore += data; - }); - execChildProcess.on("exit", (code, signal) => { - done.resolve({ - exitCode: code, - stdout: logStore - }); - }); - } catch (e) { - const error = e; - } - - /*plugins.shelljs.exec(importEnvVarPath(commandStringArg), { async: true, silent: silentArg }, (code, stdout, stderr) => { - if ( - stderr - && (stderr !== '') - && (!silentArg || strictArg) - && (process.env.DEBUG === 'true') - ) { - console.log('StdErr found.') - console.log(stderr) - } - if (strictArg) { - done.reject(new Error(stderr)) - return - } - done.resolve({ - exitCode: code, - stdout: stdout - }) - })*/ - return done.promise; -}; - -/** - * executes a given command async and silent - * @param commandStringArg - */ -export let execSilent = async ( - commandStringArg: string -): Promise => { - return await exec(commandStringArg, true); -}; - -/** - * executes strict, meaning it rejects the promise if something happens - */ -export let execStrict = async ( - commandStringArg: string -): Promise => { - return await exec(commandStringArg, true, true); -}; - -/** - * executes a command and allws you to stream output - */ -export let execStreaming = ( - commandStringArg: string, - silentArg: boolean = false -) => { - let childProcessEnded = plugins.smartpromise.defer(); - const commandToExecute = importEnvVarPath(commandStringArg); - let execChildProcess = cp.exec(commandToExecute, { - timeout: null, - maxBuffer: 1000000, - env: process.env - }); - - let logStore = ""; - - execChildProcess.stdout.on("data", (data: string) => { - if (!silentArg) { - console.log(data); - } - logStore += data; - }); - execChildProcess.stderr.on("data", data => { - if (!silentArg) { - console.log(data); - } - logStore += data; - }); - execChildProcess.on("exit", (code, signal) => { - childProcessEnded.resolve({ - exitCode: code, - stdout: logStore - }); - }); - - return { - childProcess: execChildProcess, - finalPromise: childProcessEnded.promise - }; -}; - -export let execStreamingSilent = (commandStringArg: string) => { - return execStreaming(commandStringArg, true); -}; - -/** - * executes a command and returns promise that will be fullfilled once an putput line matches RegexArg - * @param commandStringArg - * @param regexArg - */ -export let execAndWaitForLine = ( - commandStringArg: string, - regexArg: RegExp, - silentArg: boolean = false -) => { - let done = plugins.smartpromise.defer(); - let execStreamingResult = execStreaming(commandStringArg, silentArg); - execStreamingResult.childProcess.stdout.on("data", (stdOutChunk: string) => { - if (regexArg.test(stdOutChunk)) { - done.resolve(); - } - }); - return done.promise; -}; - -export let execAndWaitForLineSilent = ( - commandStringArg: string, - regexArg: RegExp -) => { - execAndWaitForLine(commandStringArg, regexArg, true); -}; /** * get a path @@ -202,3 +36,24 @@ export let which = (cmd: string): Promise => { }); return done.promise; }; + + +/* ///////////////////////////////////////////////////////// + +/** + * executes silently and returns IExecResult + * @param commandArg + + async execSilent(commandArg: string) { + let execCommand = this..createExecString(commandArg); + return await smartshellWrap.execSilent(execCommand); + } + + /** + * executes and returns IExecResult + * @param commandArg + + async exec(commandArg: string) { + let execCommand = this.createExecString(commandArg); + return await smartshellWrap.exec(execCommand); + } */ \ No newline at end of file