viteshell
Version:
A minimalistic shell implementation written in TypeScript.
8 lines (7 loc) • 10.7 kB
JavaScript
/**
* viteshell - v0.8.1
* @author Henry Hale
* @license MIT
* @url https://github.com/henryhale/viteshell#readme
*/
function t(t){return"function"==typeof t}function e(){return Math.floor(1024*Math.random()*50)}class i{buffer;extractor;constructor(){this.buffer=[],this.extractor=void 0}get isBusy(){return void 0!==this.extractor}insert(e=""){this.buffer.push(e),t(this.extractor)&&this.extractor.call(void 0)}get extract(){return this.buffer.shift()?.trim()||""}readline(){return new Promise((t=>{this.buffer.length?t(this.extract):this.extractor=()=>{this.extractor=void 0,t(this.extract)}}))}reset(){this.buffer.splice(0),this.extractor=void 0}}class r{isActive;bufferOutput;buffer;onoutput;onerror;onclear;constructor(){this.isActive=!0,this.bufferOutput=!1,this.buffer=[]}disable(){this.isActive=!1}enable(){this.isActive=!0}clear(){this.onclear?.call(void 0)}write(t,e="data"){this.isActive&&(t=t?.toString(),this.bufferOutput?this.buffer.push({type:e,data:t}):("data"==e?this.onoutput:this.onerror)?.call(void 0,t))}error(t){this.write(t,"error")}get extract(){return this.buffer.splice(0).map((t=>t.data+""))}flush(){this.bufferOutput=!1,this.buffer.length&&this.buffer.splice(0).forEach((t=>{this.write(t.data,t.type)}))}reset(){this.flush(),this.enable()}}const s="v0.8.1",n="vsh",o="?",a=n+": process aborted!",c=n+": process timed out!",u=n+": process terminated!",h="RANDOM",l=n+": inactive, use shell.reset() to activate",p={SHELL:n,USERNAME:"user",HOSTNAME:"web",CWD:"/",PS1:"$USERNAME@$HOSTNAME: $CWD $ ",PS2:"> ","?":"0",RANDOM:""+e()};function d(t,e=""){let i;return(""+e).replace(/(?:\$([a-z_][a-z0-9_]+|\?))/gi,(e=>(i=t[e.slice(1)],void 0===i?"":i.toString())))}function f(t){return t.match(/^([a-zA-Z0-9_]+)=(.*)$/)}function m(t,e){if(t)return e&&t.AND?t.AND:!e&&t.OR?t.OR:m(t.AND,e)||m(t.OR,e)}let v;const g=[";","|","&&","||"];function w(t){const e={cmd:"",args:"",argv:[],PIPE:void 0,AND:void 0,OR:void 0};e.cmd=t.shift()||"";const i=t.findIndex((t=>g.slice(1).includes(t)));return-1===i?(e.argv=t.splice(0),e.args=(e.cmd+" "+e.argv.join(" ")).trim(),e):(e.argv=t.splice(0,i),e.args=(e.cmd+" "+e.argv.join(" ")).trim(),t[0]===g[1]&&(e.PIPE=w(t.splice(1))),t[0]===g[2]&&(e.AND=w(t.splice(1))),t[0]===g[3]&&(e.OR=w(t.splice(1))),e)}function y(t){return function(t){const e=[];if(!t||"string"!=typeof t)return e;const i=[];let r,s=!1,n=!1,o=!1,a=!1,c="";if(g.some((e=>t.startsWith(r=e)||t.endsWith(e))))throw new SyntaxError("unexpected token '"+r+"'");if(t.split("").forEach(((t,r,u)=>{if(s&&"'"===t)return s=!1,void(a=!0);if(!s&&!n&&!o){if("'"===t)return void(s=!0);if('"'===t)return void(n=!0);if("\\"===t)return void(o=!0);if(["\b","\f","\n","\r","\t"," ",";"].includes(t))return(c.length>0||a)&&(i.push(c),a=!1),";"===t&&i.length&&e.push(i.splice(0)),void(c="")}if(!s&&n&&!o&&'"'===t)return n=!1,void(a=!0);!s&&n&&!o&&"\\"===t&&(o=!0,['"',"`","$","\\"].includes(u[r+1]))||(o&&(o=!1),c+=t)})),(c.length>0||a)&&(i.push(c),a=!1),i.length&&e.push(i),n)throw new SyntaxError("unexpected end of string while looking for matching double quote");if(s)throw new SyntaxError("unexpected end of string while looking for matching single quote");if(o)throw new SyntaxError("unexpected end of string right after slash");return e}(t).map((t=>w(t)))}function b(t,e,i,r){let n=!1;const{signal:a}=r;a.addEventListener("abort",(()=>n=!0));const c={readline:()=>n?Promise.resolve(""):e.readline()},h={clear:()=>!n&&i.clear(),write:e=>{n||i.write(d(t.env,e))},writeln:t=>h.write(t+"\n")},l={write:e=>{n||i.error(d(t.env,e))},writeln:t=>l.write(t+"\n")};return{cmd:"",args:"",argv:[],get env(){return t.env},get stderr(){return l},get stdin(){return c},get stdout(){return h},get history(){return t.history},get version(){return s},get exitCode(){return function(t,e=0){const i=parseInt(""+t);return isNaN(i)?e:i}(t.env[o])},exit:t=>{r.abort(t||u)},onExit:t=>{a.addEventListener("abort",(()=>{t.call(void 0,a.reason.toString())}))}}}function x(){return{env:Object.assign(Object.create(null),p),alias:{},history:[]}}function E(t){return JSON.parse("string"==typeof t?t:JSON.stringify(t))}class S{#t;#e;#i;#r;#s;#n;#o;constructor(){var t,e;this.#t=new r,this.#e=new i,this.#i=x(),this.#r=new Map,this.#s=!1,this.#n=void 0,t=this.#r,e=this.#i,t.set("exit",{synopsis:"exit",description:"Terminate the current process",action:({exit:t})=>{v?.call(void 0),t()}}),t.set("clear",{synopsis:"clear",description:"Clear the entire standard output stream.",action:({stdout:t})=>t.clear()}),e.alias.cls="clear",t.set("pwd",{synopsis:"pwd",description:"Print current working directory.",action:({stdout:t})=>t.writeln("$CWD")}),t.set("echo",{synopsis:"echo [...args]",description:"Write arguments to the standard output followed by a new line character.",action:({argv:t,stdout:e})=>e.writeln(t.join(" "))}),e.alias.print="echo",t.set("alias",{synopsis:"alias [-p] [name=[value] ... ]",description:"Defines aliases for commands",action:({argv:t,stdout:i})=>{!t.length||t.includes("-p")?(i.write("Aliases:"),Object.entries(e.alias).forEach((([t,e])=>{i.write("\n\talias "+t+"='"+e+"'")})),i.write("\n")):t.forEach((t=>{const i=f(t);if(i){const[,t,r]=i;e.alias[t.trim()]=r.trim()}}))}}),t.set("unalias",{synopsis:"unalias [name ... ]",description:"Removes aliases for commands",action:({argv:t})=>{t.length&&t.forEach((t=>{delete e.alias[t]}))}}),t.set("export",{synopsis:"export [-p] [name=[value] ... ]",description:"Set shell variables by name and value",action:({argv:t,env:e,stdout:i})=>{!t.length||t.includes("-p")?Object.entries(e).forEach((([t,e])=>{i.write("var "+t+'="'+(e?.toString().includes("$")?e.toString().split("").join("\\"):e)+'"\n')})):t.forEach((t=>{const i=f(t);if(i){const[,t,r]=i;e[t.trim()]=r.trim()}}))}}),t.set("history",{synopsis:"history [-c] [-n]",description:"Retrieve previous input entries",action:({argv:t,history:e,stdout:i})=>{t.includes("-c")?e.splice(0):t.includes("-n")?i.writeln(`History: ${e.length}`):e.forEach(((t,e)=>{i.writeln(" "+e+"\t"+t)}))}}),t.set("help",{synopsis:"help [command]",description:"Displays information on available commands.",action:({argv:e,stdout:i})=>{if(e[0]){const r=e[0],s=t.get(r);if(!s)throw"help: no information matching '"+r+"'";const{synopsis:n,description:o}=s;i.writeln(r+": "+n+"\n\t"+o)}else i.write("ViteShell, "+s+" Help\n\nA list of all available commands\n\n"),Array.from(t.values()).map((t=>t.synopsis)).sort().forEach((t=>i.writeln(t)))}}),e.alias.info="help",e.alias.man="help",t.set("read",{synopsis:"read [prompt] [variable]",description:"Capture input and save it in the env object.",action:async({argv:t,env:e,stdin:i,stdout:r})=>{if(!t[0]||!t[1])throw"invalid arguments: specify the prompt and variable name";r.write(t[0]),e[t[1]]=await i.readline()}}),t.set("sleep",{synopsis:"sleep [seconds]",description:"Delay for a specified amount of time (in seconds).",action:async({argv:t,onExit:e})=>{const i=parseInt(t[0],10);if(isNaN(i)||i<=0)throw"invalid time specified (minimum is 1)";await new Promise((t=>{let r;e((()=>{clearTimeout(r),t()})),r=setTimeout((()=>t()),1e3*i)}))}}),t.set("grep",{synopsis:"grep [keyword] [context ...]",description:"Searches for matching phrases in the text",action:async({argv:t,stdout:e})=>{if(t.length<2)throw"invalid arguments";const i=new RegExp(t[0],"g");t.slice(1).forEach((r=>{i.test(r)&&e.writeln(r.replaceAll(t[0],(t=>"**"+t+"**")))}))}}),t.set("date",{synopsis:"date",description:"Displays the current time and date",action:({stdout:t})=>t.writeln((new Date).toString())})}get alias(){return this.#i.alias}get env(){return this.#i.env}get history(){return this.#i.history}set onoutput(e){if(!t(e))throw new TypeError(`${n}: onoutput handler must be a function.`);this.#t.onoutput=e}set onerror(e){if(!t(e))throw new TypeError(`${n}: onerror handler must be a function.`);this.#t.onerror=e}set onclear(e){if(!t(e))throw new TypeError(`${n}: onclear handler must be a function.`);this.#t.onclear=e}set onexit(e){if(!t(e))throw new TypeError(`${n}: onexit handler must be a function.`);v=e}addCommand(e,i){if(this.#r.has(e))throw new Error(`${n}: '${e}' command already exists. If you are providing a custom command implementation, remove the command first.`);if(!function(e){return"object"==typeof(i=e)&&null!==i&&t(e.action)&&!!e.description&&!!e.synopsis;var i}(i))throw new Error(`${n}: invalid command configuration.`);this.#r.set(e,i)}removeCommand(t){this.#r.delete(t)}listCommands(){return Array.from(this.#r.keys())}exportState(){return JSON.stringify(this.#i)}loadState(t){this.#i=Object.assign(x(),E(t))}#a(){this.env[h]=""+e(),this.#t.reset(),this.#t.write(d(this.env,this.env.PS1))}reset(t=""){this.#s=!0,this.#e.reset(),this.#t.clear(),t&&(t=d(this.env,t+"\n"),this.#t.write(t)),this.#a()}setExecutionTimeout(t){if(!("number"==typeof t&&t>0))throw new TypeError(`${n}: invalid value for timeout.`);this.#o=1e3*t}async#c(t,i){let r="";const s=async()=>{if(t.OR||t.AND){const e=m(t,!r.length);e&&(r.length&&(i.stderr.writeln(r),r=""),await this.#c(e,i))}if(r.length)throw r},n=this.alias[t.cmd];if(n){const e=n.split(" ");t.cmd=e.shift()||"",e.length&&t.argv.unshift(...e),t.args=(t.cmd+" "+t.argv.join(" ")).trim()}const o=this.#r.get(t.cmd);if(!o)return r=t.cmd+": command not found",await s();i.cmd=t.cmd,i.argv=t.argv,i.env[h]=""+e(),this.#t.bufferOutput=void 0!==t.PIPE;try{"exit"===t.cmd&&(this.#s=!1),await o.action.call(void 0,i)}catch(e){r=t.cmd+": "+e}t.PIPE&&(t.PIPE.argv.push(...this.#t.extract),await this.#c(t.PIPE,i)),await s()}async execute(t=""){if(!this.#s)return Promise.reject(l);if(this.#e.isBusy)return this.#e.insert(t),Promise.resolve();if(this.#e.reset(),this.#t.reset(),"string"!=typeof t||!t.trim())return this.#a(),Promise.resolve();(t=t.trim())!=this.history.at(-1)&&this.history.push(t);const e=E(this.#i),i=new AbortController,r=i.signal;this.#n=i,r.addEventListener("abort",(()=>{this.#e.reset()}));const s=b(e,this.#e,this.#t,i),n=function(t,e,i){const{signal:r}=t;let s;return i&&i>0&&(s=setTimeout((()=>{t.abort(c)}),i)),new Promise(((t,i)=>{r.addEventListener("abort",(()=>{clearTimeout(s),i(r.reason.toString()||a)})),e(r,i).then(t).catch(i).finally((()=>clearTimeout(s)))}))}(i,(async(i,r)=>{try{const r=y(t);for(const t of r){if(i.aborted)throw a;try{await this.#c(t,s),e.env[o]="0",n=this.#i,c=e,Object.assign(n.alias,c.alias),Object.assign(n.env,c.env),n.history.splice(0),n.history.push(...c.history)}catch(t){if(!(r.length>1))throw t;s.stderr.write(t+"\n"),e.env[o]="1"}}}catch(t){r(t.toString())}var n,c}),this.#o);return n.catch((t=>{this.#t.error(t+"\n"),this.#i.env[o]="1"})).finally((()=>{this.#a()}))}abort(t){this.#n?.abort(t||a)}static get version(){return s}}export{S as default};