• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

RauliL / juokse / 6085297530

05 Sep 2023 01:29PM UTC coverage: 66.667% (-0.9%) from 67.596%
6085297530

push

github

web-flow
Merge pull request #27 from RauliL/return-command

Add builtin return command

174 of 265 branches covered (0.0%)

Branch coverage included in aggregate %.

23 of 23 new or added lines in 3 files covered. (100.0%)

534 of 797 relevant lines covered (67.0%)

128.93 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

69.91
/src/context.ts
1
import { spawn } from "child_process";
9✔
2
import { EventEmitter } from "events";
9✔
3
import fs from "fs";
9✔
4
import { isArray, isFunction } from "lodash";
9✔
5
import isExe from "isexe";
9✔
6
import os from "os";
9✔
7
import path from "path";
9✔
8
import { PassThrough } from "stream";
9✔
9

10
import { Statement } from "./ast";
11
import { BuiltinCommandCallback, builtinCommandMapping } from "./builtins";
9✔
12
import { JuokseError, ReturnError } from "./error";
9✔
13
import { ExitStatus } from "./status";
9✔
14
import { executeScript } from "./execute";
9✔
15

16
export type ExecutionResult = {
17
  pid?: number;
18
  signal?: NodeJS.Signals | null;
19
  status: number;
20
};
21

22
/**
23
 * Class which represents context of an script execution.
24
 */
25
export class Context extends EventEmitter {
9✔
26
  public stdout: PassThrough;
27
  public readonly stderr: PassThrough;
28
  public readonly stdin: PassThrough;
29
  public readonly environment: Record<string, string>;
30
  public readonly variables: Record<string, string>;
31
  public readonly functions: Record<string, Statement>;
32

33
  public constructor() {
34
    super();
120✔
35

36
    this.stdout = new PassThrough();
120✔
37
    this.stderr = new PassThrough();
120✔
38
    this.stdin = new PassThrough();
120✔
39

40
    this.environment = {};
120✔
41
    this.variables = {};
120✔
42
    this.functions = {};
120✔
43
  }
44

45
  /**
46
   * Returns the current working directory of this context. Current working
47
   * directory path is stored into an environment variable called `PWD`, but if
48
   * that is empty, current working directory of the Node.js process is used
49
   * instead.
50
   *
51
   * @return Path of the current working directory of this context.
52
   */
53
  public get cwd(): string {
54
    return this.environment.PWD || process.cwd();
42✔
55
  }
56

57
  /**
58
   * Sets the current working directory of this context to path given as
59
   * argument.
60
   *
61
   * If the given path does not point to a valid directory in the underlying
62
   * file system, an exception is thrown.
63
   *
64
   * @param path Path to the new current working directory of this context.
65
   */
66
  public set cwd(path: string) {
67
    if (path === "-") {
24✔
68
      path = this.environment.OLDPWD;
6✔
69
      if (!path) {
6✔
70
        return;
3✔
71
      }
72
    }
73
    if (!fs.lstatSync(path).isDirectory()) {
21!
74
      throw new JuokseError(`'${path}' is not a valid directory.`);
×
75
    }
76
    this.environment.OLDPWD = this.cwd;
18✔
77
    this.environment.PWD = path;
18✔
78
  }
79

80
  /**
81
   * Returns the home directory of user running this context. Home directory
82
   * path is stored into an environment variable called `HOME`, but if that is
83
   * empty, home directory path is requested from Node.js instead.
84
   *
85
   * @return Path to the home directory of the user running this context.
86
   */
87
  public get home(): string {
88
    return this.environment.HOME || os.homedir();
6✔
89
  }
90

91
  /**
92
   * Sets the home directory of this context to path given as argument.
93
   *
94
   * If the given path does not point to a valid directory in the underlying
95
   * file system, an exception is thrown.
96
   *
97
   * @param path Path to the new home directory in this context.
98
   */
99
  public set home(path: string) {
100
    if (!fs.lstatSync(path).isDirectory()) {
6!
101
      throw new JuokseError(`'${path}' is not a valid directory.`);
×
102
    }
103
    this.environment.HOME = path;
3✔
104
  }
105

106
  /**
107
   * Returns directories from `PATH` environment variable in an array.
108
   *
109
   * @return Array of path components read from `PATH` environment variable or
110
   *         empty array if it's empty.
111
   */
112
  public get path(): string[] {
113
    const value = this.environment.PATH;
12✔
114

115
    if (!value) {
12✔
116
      return [];
9✔
117
    }
118

119
    return value.split(path.delimiter);
3✔
120
  }
121

122
  /**
123
   * Sets the `PATH` environment variable of this context to given value, which
124
   * can be either array of strings or a string.
125
   *
126
   * @param value New value for `PATH` environment variable of this context.
127
   */
128
  public set path(value: string | string[]) {
129
    if (isArray(value)) {
21✔
130
      this.environment.PATH = value.join(path.delimiter);
9✔
131
    } else {
132
      this.environment.PATH = `${value}`;
12✔
133
    }
134
  }
135

136
  /**
137
   * Determines location of an executable by searching for it from builtin
138
   * commands and directories included in this context's `PATH` environment
139
   * variable. Returns the full absolute path of the first suitable executable
140
   * found, or null if it cannot be found.
141
   *
142
   * This method also searches for builtin commands, such as 'cd'. If a
143
   * builtin command's name matches with given executable name, a function
144
   * callback is returned instead of a path. This function callback takes in
145
   * three arguments: context instance, name of the executable and variadic
146
   * amount of command line arguments given for the command. The function
147
   * callback returns exit status as integer, which is 0 if the builtin command
148
   * executed successfully and non-zero if an error occurred.
149
   *
150
   * @param executable Name of the executable to look for. If absolute path is
151
   *                   given, no search through `PATH` nor builtin commands is
152
   *                   being performed.
153
   * @return Either a function or full path to the first executable found from
154
   *         the file system that matches with given name of an executable, or
155
   *         null if no suitable match was found.
156
   */
157
  public resolveExecutable(
158
    executable: string
159
  ): BuiltinCommandCallback | Statement | string | null {
160
    if (path.isAbsolute(executable)) {
78✔
161
      return isExe.sync(executable) ? executable : null;
3!
162
    }
163

164
    if (Object.prototype.hasOwnProperty.call(this.functions, executable)) {
75✔
165
      return this.functions[executable];
3✔
166
    }
167

168
    if (
72✔
169
      Object.prototype.hasOwnProperty.call(builtinCommandMapping, executable)
170
    ) {
171
      const builtin = builtinCommandMapping[executable];
63✔
172

173
      if (isFunction(builtin)) {
63✔
174
        return builtin;
63✔
175
      }
176
    }
177

178
    for (const pathComponent of this.path) {
9✔
179
      const candidate = path.join(pathComponent, executable);
3✔
180

181
      if (fs.existsSync(candidate) && isExe.sync(candidate)) {
3✔
182
        return candidate;
3✔
183
      }
184
    }
185

186
    return null;
6✔
187
  }
188

189
  /**
190
   * Executes given executable or builtin command with given command line
191
   * arguments. The method will launch the builtin command or executable found
192
   * from the file system and return it's exit status in a promise.
193
   *
194
   * @param executable Name of the executable or builtin command to execute
195
   *                   with given command line arguments.
196
   * @param args Optional command line arguments given for the executable or
197
   *             builtin command.
198
   * @return A promise that will contain an object containing the exit status
199
   *         of the executed executable, along with the process identifier
200
   *         signal that was used to kill the process, if the process was
201
   *         killed.
202
   */
203
  public execute(
204
    executable: string,
205
    ...args: string[]
206
  ): Promise<ExecutionResult> {
207
    const resolvedExecutable = this.resolveExecutable(executable);
63✔
208

209
    if (!resolvedExecutable) {
63✔
210
      return Promise.reject(
3✔
211
        new JuokseError(`No such file or directory: ${executable}`)
212
      );
213
    }
214

215
    this.emit("process start", { executable, args: args });
60✔
216

217
    if (isFunction(resolvedExecutable)) {
60✔
218
      return resolvedExecutable(this, executable, ...args).then((status) => {
60✔
219
        this.variables["?"] = `${status}`;
42✔
220
        this.emit("process finish", { executable, args, status });
42✔
221

222
        return { status };
42✔
223
      });
224
    }
225

226
    if (typeof resolvedExecutable !== "string") {
×
227
      // TODO: Implement variable scopes to avoid variable pollution.
228
      this.variables["#"] = `${args.length}`;
×
229
      this.variables["*"] = args.join(" ");
×
230
      for (let i = 0; i < 9; ++i) {
×
231
        this.variables[`${i + 1}`] = args[i];
×
232
      }
233

234
      return executeScript(this, [resolvedExecutable])
×
235
        .then(() => {
236
          this.variables["?"] = `${ExitStatus.OK}`;
×
237
          this.emit("process finish", {
×
238
            executable,
239
            args,
240
            status: ExitStatus.OK,
241
          });
242

243
          return { status: ExitStatus.OK };
×
244
        })
245
        .catch((err) => {
246
          if (err instanceof ReturnError) {
×
247
            this.variables["?"] = `${err.status}`;
×
248
            this.emit("process finish", {
×
249
              executable,
250
              args,
251
              status: err.status,
252
            });
253

254
            return Promise.resolve({ status: err.status });
×
255
          }
256

257
          return Promise.reject(err);
×
258
        });
259
    }
260

261
    return new Promise<ExecutionResult>((resolve, reject) => {
×
262
      const childProcess = spawn(resolvedExecutable, args, {
×
263
        cwd: this.cwd,
264
        env: this.environment,
265
        argv0: executable,
266
        stdio: "pipe",
267
      });
268

269
      childProcess.on("error", reject);
×
270

271
      childProcess.on("close", (status, signal) => {
×
272
        this.variables["?"] = `${status}`;
×
273
        this.emit("process finish", { executable, args, status, signal });
×
274
        resolve({
×
275
          pid: childProcess.pid,
276
          status: status ?? ExitStatus.OK,
×
277
          signal,
278
        });
279
      });
280

281
      // TODO: Add support for pipes. Also pipe stdin from context by default.
282
      childProcess.stdout.on("data", (data) => this.stdout.emit("data", data));
×
283
      childProcess.stderr.on("data", (data) => this.stderr.emit("data", data));
×
284
    });
285
  }
286
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc