Repository: sjbarag/brs
Pull request #366 (2020-01-28 18:09:32)

feat(stdlib): Add _brs_.process.argv to mirror node's process.argv
It's currently impossible to access `argv` from a BrightScript file executed with `brs`!  While it's also impossible via the reference BrightScript implementation, there's basically no concept of argv there -- instead they accept an associative array of arguments from the OS that implements RBI.

Piping `process.argv` into `brs` should enable more interactive uses of BrightScript outside of the reference implementation.  While we _could_ theoretically parse `argv` into an associative array similar to [minimist](https://github.com/substack/minimist)'s output, it's probably best to leave that to BrightScript implementations for now.


Patch:
diff --git a/src/brsTypes/components/RoSGNode.ts b/src/brsTypes/components/RoSGNode.ts
index c46a553..2415152 100644
--- a/src/brsTypes/components/RoSGNode.ts
+++ b/src/brsTypes/components/RoSGNode.ts
@@ -107,6 +107,7 @@ export class RoSGNode extends BrsComponent implements BrsValue, BrsIterable {
                 this.addfield,
                 this.addfields,
                 this.getfield,
+                this.hasfield,
                 this.observefield,
                 this.removefield,
                 this.setfield,
@@ -498,6 +499,19 @@ export class RoSGNode extends BrsComponent implements BrsValue, BrsIterable {
         },
     });
 
+    /** Returns true if the field exists */
+    private hasfield = new Callable("hasfield", {
+        signature: {
+            args: [new StdlibArgument("fieldname", ValueKind.String)],
+            returns: ValueKind.Boolean,
+        },
+        impl: (interpreter: Interpreter, fieldname: BrsString) => {
+            return this.fields.has(fieldname.value.toLowerCase())
+                ? BrsBoolean.True
+                : BrsBoolean.False;
+        },
+    });
+
     /** Registers a callback to be executed when the value of the field changes */
     private observefield = new Callable("observefield", {
         signature: {
diff --git a/src/extensions/Process.ts b/src/extensions/Process.ts
new file mode 100644
index 0000000..fb93ebd
--- /dev/null
+++ b/src/extensions/Process.ts
@@ -0,0 +1,8 @@
+import { RoAssociativeArray, BrsString, RoArray } from "../brsTypes";
+
+export const Process = new RoAssociativeArray([
+    {
+        name: new BrsString("argv"),
+        value: new RoArray(process.argv.map(arg => new BrsString(arg))),
+    },
+]);
diff --git a/src/extensions/index.ts b/src/extensions/index.ts
index 3862435..1c347ab 100644
--- a/src/extensions/index.ts
+++ b/src/extensions/index.ts
@@ -1,8 +1,10 @@
 import { RoAssociativeArray, BrsString } from "../brsTypes";
 import { mockComponent } from "./mockComponent";
 import { RunInScope } from "./RunInScope";
+import { Process } from "./Process";
 
 export const _brs_ = new RoAssociativeArray([
     { name: new BrsString("mockComponent"), value: mockComponent },
     { name: new BrsString("runInScope"), value: RunInScope },
+    { name: new BrsString("process"), value: Process },
 ]);


Test patch:
diff --git a/test/brsTypes/components/RoSGNode.test.js b/test/brsTypes/components/RoSGNode.test.js
index eada56a..ea7b418 100644
--- a/test/brsTypes/components/RoSGNode.test.js
+++ b/test/brsTypes/components/RoSGNode.test.js
@@ -441,6 +441,19 @@ describe("RoSGNode", () => {
             });
         });
 
+        describe("hasfield", () => {
+            it("returns presence of a field", () => {
+                let node = new RoSGNode([{ name: new BrsString("foo"), value: new Int32(17) }]);
+
+                let hasField = node.getMethod("hasfield");
+                let result1 = hasField.call(interpreter, new BrsString("foo"));
+                expect(result1).toEqual(new BrsBoolean(true));
+
+                let result2 = hasField.call(interpreter, new BrsString("bar"));
+                expect(result2).toEqual(new BrsBoolean(false));
+            });
+        });
+
         describe("observefield", () => {
             it("adds an observer", () => {
                 let node = new RoSGNode([
diff --git a/test/e2e/BrsComponents.test.js b/test/e2e/BrsComponents.test.js
index cf2b1f2..46e213e 100644
--- a/test/e2e/BrsComponents.test.js
+++ b/test/e2e/BrsComponents.test.js
@@ -139,6 +139,10 @@ describe("end to end brightscript functions", () => {
             "hello",
             "field3 in node now is: ",
             "false",
+            "field3 present? ",
+            "true",
+            "fieldほ present? ",
+            "false",
             "callback 1 called",
             "callback 2 called",
             "field 3 updated",
diff --git a/test/e2e/BrsNamespace.test.js b/test/e2e/BrsNamespace.test.js
new file mode 100644
index 0000000..bb021d5
--- /dev/null
+++ b/test/e2e/BrsNamespace.test.js
@@ -0,0 +1,28 @@
+const { execute } = require("../../lib");
+
+const { createMockStreams, resourceFile, allArgs } = require("./E2ETests");
+
+describe("end to end brightscript functions", () => {
+    let outputStreams;
+
+    beforeAll(() => {
+        outputStreams = createMockStreams();
+    });
+
+    afterEach(() => {
+        jest.resetAllMocks();
+    });
+
+    afterAll(() => {
+        jest.restoreAllMocks();
+    });
+
+    test("_brs_-namespace.brs", async () => {
+        await execute([resourceFile("stdlib", "_brs_-namespace.brs")], outputStreams);
+
+        expect(allArgs(outputStreams.stdout.write).filter(arg => arg !== "\n")).toEqual([
+            "_brs_.process.argv is non-empty: ",
+            "true",
+        ]);
+    });
+});
diff --git a/test/e2e/Brs.test.js b/test/e2e/MultiFile.test.js
similarity index 95%
rename from test/e2e/Brs.test.js
rename to test/e2e/MultiFile.test.js
index 20fa566..9ef88cd 100644
--- a/test/e2e/Brs.test.js
+++ b/test/e2e/MultiFile.test.js
@@ -1,4 +1,4 @@
-const { execute } = require("../../lib/");
+const { execute } = require("../../lib");
 
 const { createMockStreams, resourceFile, allArgs } = require("./E2ETests");
 
diff --git a/test/e2e/resources/components/roSGNode.brs b/test/e2e/resources/components/roSGNode.brs
index 7b99b4f..69a3848 100644
--- a/test/e2e/resources/components/roSGNode.brs
+++ b/test/e2e/resources/components/roSGNode.brs
@@ -35,6 +35,9 @@ sub main()
     print "field1 in node now is: " node1.getField("field1")      ' => hello
     print "field3 in node now is: " node1.getField("field3")      ' => false
 
+    print "field3 present? " node1.hasField("field3")
+    print "fieldほ present? " node1.hasField("fieldほ")
+
     node1.observeField("field1", "onCB1Called")
     node1.observeField("field1", "onCB2Called")
     node1.observeField("field2", "onField2Cb") ' This doesn't get called since field was removed
diff --git a/test/e2e/resources/stdlib/_brs_-namespace.brs b/test/e2e/resources/stdlib/_brs_-namespace.brs
new file mode 100644
index 0000000..42e358c
--- /dev/null
+++ b/test/e2e/resources/stdlib/_brs_-namespace.brs
@@ -0,0 +1,3 @@
+sub main()
+    print "_brs_.process.argv is non-empty: " _brs_.process.argv.count() > 0
+end sub
\ No newline at end of file
diff --git a/test/extensions/Process.test.js b/test/extensions/Process.test.js
new file mode 100644
index 0000000..d6816cf
--- /dev/null
+++ b/test/extensions/Process.test.js
@@ -0,0 +1,11 @@
+const brs = require("brs");
+const { BrsString } = brs.types;
+const { Process: brsProcess } = require("../../lib/extensions/Process");
+
+describe("_brs_.process", () => {
+    it("mirrors node's process.argv", () => {
+        let brsArgv = brsProcess.get(new BrsString("argv"));
+        expect(brsArgv).toBeDefined();
+        expect(brsArgv.getValue().map(arg => arg.value)).toEqual(process.argv);
+    });
+});