bare-script 2.3.2 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,8 +3,22 @@
3
3
  [![npm](https://img.shields.io/npm/v/bare-script)](https://www.npmjs.com/package/bare-script)
4
4
  [![GitHub](https://img.shields.io/github/license/craigahobbs/bare-script)](https://github.com/craigahobbs/bare-script/blob/main/LICENSE)
5
5
 
6
- The bare-script package is the JavaScript implementation of the
7
- [BareScript language](https://craigahobbs.github.io/bare-script/language/).
6
+ [BareScript](https://craigahobbs.github.io/bare-script/language/)
7
+ is a simple, lightweight, and portable programming language. Its Pythonic syntax is influenced by
8
+ JavaScript, C, and the Unix Shell. BareScript also has a
9
+ [library of built-in functions](#the-barescript-library)
10
+ for common programming operations. BareScript can be
11
+ [embedded within applications](#markdownup-a-markdown-viewer-with-barescript)
12
+ or used as a
13
+ stand-alone programming language using the
14
+ [command-line interface](#the-barescript-command-line-interface-cli).
15
+
16
+ There are two implementations of BareScript:
17
+ [BareScript for JavaScript](https://github.com/craigahobbs/bare-script#readme)
18
+ (this package) and
19
+ [BareScript for Python](https://github.com/craigahobbs/bare-script-py#readme).
20
+ Both implementations have 100% unit test coverage with identical unit test suites, so you can be
21
+ confident that BareScript will execute the same regardless of the underlying runtime environment.
8
22
 
9
23
 
10
24
  ## Links
@@ -70,7 +84,7 @@ import {parseScript} from 'bare-script/lib/parser.js';
70
84
  // Parse the script
71
85
  const script = parseScript(`\
72
86
  # Fetch the BareScript library documentation JSON
73
- docs = systemFetch('https://craigahobbs.github.io/bare-script/library/library.json')
87
+ docs = jsonParse(systemFetch('https://craigahobbs.github.io/bare-script/library/library.json'))
74
88
 
75
89
  # Return the number of library functions
76
90
  return 'The BareScript Library has ' + arrayLength(objectGet(docs, 'functions')) + ' functions'
@@ -83,7 +97,7 @@ console.log(await executeScriptAsync(script, {'fetchFn': fetch}));
83
97
  This outputs:
84
98
 
85
99
  ~~~
86
- The BareScript Library has 100 functions
100
+ The BareScript Library has 105 functions
87
101
  ~~~
88
102
 
89
103
 
@@ -136,6 +150,10 @@ bare script.bare
136
150
  **Note:** In the BareScript CLI, import statements and the
137
151
  [systemFetch](https://craigahobbs.github.io/bare-script/library/#var.vGroup='System'&systemfetch)
138
152
  function read non-URL paths from the local file system.
153
+ [systemFetch](https://craigahobbs.github.io/bare-script/library/#var.vGroup='System'&systemfetch)
154
+ calls with a non-URL path and a
155
+ [request body](https://craigahobbs.github.io/bare-script-py/library/model.html#var.vName='SystemFetchRequest')
156
+ write the body to the path.
139
157
 
140
158
 
141
159
  ## MarkdownUp, a Markdown Viewer with BareScript
package/bin/bare.js CHANGED
@@ -2,22 +2,9 @@
2
2
  // Licensed under the MIT License
3
3
  // https://github.com/craigahobbs/bare-script/blob/main/LICENSE
4
4
 
5
- import {argv, exit, stdout} from 'node:process';
5
+ import {argv, exit} from 'node:process';
6
+ import {fetchReadWrite, logStdout} from '../lib/optionsNode.js';
6
7
  import {main} from '../lib/bare.js';
7
- import {readFile} from 'node:fs/promises';
8
8
 
9
9
 
10
- const rURL = /^[a-z]+:/;
11
- exit(await main({
12
- argv,
13
- 'fetchFn': (fetchURL, fetchOptions) => {
14
- if (rURL.test(fetchURL)) {
15
- return fetch(fetchURL, fetchOptions);
16
- }
17
- return {
18
- 'ok': true,
19
- 'text': () => readFile(fetchURL, 'utf-8')
20
- };
21
- },
22
- 'logFn': (message) => stdout.write(`${message}\n`)
23
- }));
10
+ exit(await main({argv, 'fetchFn': fetchReadWrite, 'logFn': logStdout}));
package/bin/baredoc.js CHANGED
@@ -2,22 +2,9 @@
2
2
  // Licensed under the MIT License
3
3
  // https://github.com/craigahobbs/bare-script/blob/main/LICENSE
4
4
 
5
- import {argv, exit, stdout} from 'node:process';
5
+ import {argv, exit} from 'node:process';
6
+ import {fetchReadWrite, logStdout} from '../lib/optionsNode.js';
6
7
  import {main} from '../lib/baredoc.js';
7
- import {readFile} from 'node:fs/promises';
8
8
 
9
9
 
10
- const rURL = /^[a-z]+:/;
11
- exit(await main({
12
- argv,
13
- 'fetchFn': (fetchURL, fetchOptions) => {
14
- if (rURL.test(fetchURL)) {
15
- return fetch(fetchURL, fetchOptions);
16
- }
17
- return {
18
- 'ok': true,
19
- 'text': () => readFile(fetchURL, 'utf-8')
20
- };
21
- },
22
- 'logFn': (message) => stdout.write(`${message}\n`)
23
- }));
10
+ exit(await main({argv, 'fetchFn': fetchReadWrite, 'logFn': logStdout}));
package/lib/bare.js CHANGED
@@ -5,23 +5,8 @@ import {parseExpression, parseScript} from './parser.js';
5
5
  import {evaluateExpression} from './runtime.js';
6
6
  import {executeScriptAsync} from './runtimeAsync.js';
7
7
  import {lintScript} from './model.js';
8
-
9
-
10
- // The command-line interface (CLI) help text
11
- export const helpText = `\
12
- usage: bare [-h] [-c CODE] [-d] [-v VAR EXPR] [filename ...]
13
-
14
- The BareScript command-line interface
15
-
16
- positional arguments:
17
- filename
18
-
19
- options:
20
- -h, --help show this help message and exit
21
- -c, --code CODE execute the BareScript code
22
- -d, --debug enable debug mode
23
- -s, --static perform static analysis
24
- -v, --var VAR EXPR set a global variable to an expression value`;
8
+ import {urlFileRelative} from './options.js';
9
+ import {valueBoolean} from './value.js';
25
10
 
26
11
 
27
12
  /**
@@ -29,8 +14,8 @@ options:
29
14
  *
30
15
  * @typedef {Object} BareOptions
31
16
  * @property {string[]} argv - The process command-line arguments
32
- * @property {function} fetchFn - The [fetch function]{@link module:lib/runtime~FetchFn}
33
- * @property {function} logFn - The [log function]{@link module:lib/runtime~LogFn}
17
+ * @property {function} fetchFn - The [fetch function]{@link module:lib/options~FetchFn}
18
+ * @property {function} logFn - The [log function]{@link module:lib/options~LogFn}
34
19
  * @ignore
35
20
  */
36
21
 
@@ -43,53 +28,62 @@ options:
43
28
  * @ignore
44
29
  */
45
30
  export async function main(options) {
31
+ // Command line arguments
32
+ let args;
33
+ try {
34
+ args = parseArgs(options.argv);
35
+ } catch ({message}) {
36
+ options.logFn(`error: ${message}`);
37
+ return 1;
38
+ }
39
+ if (args.help || args.scripts.length === 0) {
40
+ options.logFn(helpText);
41
+ return 1;
42
+ }
43
+
46
44
  let statusCode = 0;
47
- let currentFile = null;
45
+ let inlineCount = 0;
46
+ let errorName = null;
48
47
  try {
49
- const args = parseArgs(options.argv);
48
+ // Evaluate the global variable expression arguments
49
+ const globals = {};
50
+ for (const [varName, varExpr] of Object.entries(args.var)) {
51
+ globals[varName] = evaluateExpression(parseExpression(varExpr));
52
+ }
50
53
 
51
- // Read the source files
52
- const responses = await Promise.all(args.files.map(async ([url, source]) => {
53
- if (source !== null) {
54
- return {'ok': true, 'text': () => source};
55
- }
56
- try {
57
- return await options.fetchFn(url);
58
- } catch {
59
- throw Error(`Failed to load "${url}"`);
60
- }
61
- }));
62
- const files = await Promise.all(responses.map(async (response, ixResponse) => {
63
- const [url] = args.files[ixResponse];
64
- let source = null;
65
- if (response.ok) {
54
+ // Parse and execute all source files in order
55
+ for (const [scriptType, scriptValue] of args.scripts) {
56
+ // Get the script source
57
+ let scriptName;
58
+ let scriptSource;
59
+ if (scriptType === 'file') {
60
+ scriptName = scriptValue;
61
+ scriptSource = null;
66
62
  try {
67
- source = await response.text();
63
+ const scriptResponse = await options.fetchFn(scriptValue);
64
+ if (scriptResponse.ok) {
65
+ scriptSource = await scriptResponse.text();
66
+ }
68
67
  } catch {
69
- // Do nothing
68
+ // Do nothing...
70
69
  }
70
+ if (scriptSource === null) {
71
+ throw Error(`Failed to load "${scriptValue}"`);
72
+ }
73
+ } else {
74
+ inlineCount += 1;
75
+ scriptName = `-c ${inlineCount}`;
76
+ scriptSource = scriptValue;
71
77
  }
72
- if (source === null) {
73
- throw Error(`Failed to load "${url}"`);
74
- }
75
- return [url, source];
76
- }));
77
-
78
- // Parse the source files
79
- const scripts = [];
80
- for (const [file, source] of files) {
81
- currentFile = file;
82
- scripts.push([file, parseScript(source)]);
83
- }
84
78
 
85
- // Lint and execute the source scripts
86
- for (const [file, script] of scripts) {
87
- currentFile = file;
79
+ // Parse the script source
80
+ errorName = scriptName;
81
+ const script = parseScript(scriptSource);
88
82
 
89
83
  // Run the bare-script linter?
90
84
  if (args.static || args.debug) {
91
85
  const warnings = lintScript(script);
92
- const warningPrefix = `BareScript: Static analysis "${file}" ...`;
86
+ const warningPrefix = `BareScript: Static analysis "${scriptName}" ...`;
93
87
  if (warnings.length === 0) {
94
88
  options.logFn(`${warningPrefix} OK`);
95
89
  } else {
@@ -98,29 +92,31 @@ export async function main(options) {
98
92
  options.logFn(`BareScript: ${warning}`);
99
93
  }
100
94
  if (args.static) {
101
- throw Error('Static analysis failed');
95
+ statusCode = 1;
96
+ break;
102
97
  }
103
98
  }
104
-
105
- // Skip code execution if linter requested
106
- if (args.static) {
107
- continue;
108
- }
99
+ }
100
+ if (args.static) {
101
+ continue;
109
102
  }
110
103
 
111
104
  // Execute the script
112
105
  const timeBegin = performance.now();
113
- // eslint-disable-next-line no-await-in-loop
114
106
  const result = await executeScriptAsync(script, {
115
107
  'debug': args.debug ?? false,
116
108
  'fetchFn': options.fetchFn,
117
- 'globals': args.variables,
118
- 'logFn': (message) => options.logFn(message),
109
+ 'globals': globals,
110
+ 'logFn': options.logFn,
119
111
  'systemPrefix': 'https://craigahobbs.github.io/markdown-up/include/',
120
- 'urlFn': (url) => (rURL.test(url) || url.startsWith('/') ? url : `${file.slice(0, file.lastIndexOf('/') + 1)}${url}`)
112
+ 'urlFn': scriptType === 'file' ? (url) => urlFileRelative(scriptName, url) : null
121
113
 
122
114
  });
123
- statusCode = (Number.isInteger(result) && result >= 0 && result <= 255 ? result : (result ? 1 : 0));
115
+ if (Number.isInteger(result) && result >= 0 && result <= 255) {
116
+ statusCode = result;
117
+ } else {
118
+ statusCode = valueBoolean(result) ? 1 : 0;
119
+ }
124
120
 
125
121
  // Log script execution end with timing
126
122
  if (args.debug) {
@@ -134,8 +130,8 @@ export async function main(options) {
134
130
  }
135
131
  }
136
132
  } catch ({message}) {
137
- if (currentFile !== null) {
138
- options.logFn(`${currentFile}:`);
133
+ if (errorName !== null) {
134
+ options.logFn(`${errorName}:`);
139
135
  }
140
136
  options.logFn(message);
141
137
  statusCode = 1;
@@ -145,10 +141,6 @@ export async function main(options) {
145
141
  }
146
142
 
147
143
 
148
- // Regex to match a URL
149
- const rURL = /^[a-z]+:/;
150
-
151
-
152
144
  /**
153
145
  * Parse the BareScript command-line interface (CLI) arguments
154
146
  *
@@ -158,15 +150,14 @@ const rURL = /^[a-z]+:/;
158
150
  * @ignore
159
151
  */
160
152
  export function parseArgs(argv) {
161
- const args = {'files': [], 'variables': {}};
162
- let codeCount = 0;
153
+ const args = {'scripts': [], 'var': {}};
163
154
  for (let iArg = 2; iArg < argv.length; iArg++) {
164
155
  const arg = argv[iArg];
165
156
  if (arg === '-c' || arg === '--code') {
166
157
  if (iArg + 1 >= argv.length) {
167
- throw new Error(`Missing value for ${arg}`);
158
+ throw new Error('argument -c/--code: expected one argument');
168
159
  }
169
- args.files.push([`-c ${++codeCount}`, argv[iArg + 1]]);
160
+ args.scripts.push(['code', argv[iArg + 1]]);
170
161
  iArg++;
171
162
  } else if (arg === '-d' || arg === '--debug') {
172
163
  args.debug = true;
@@ -176,21 +167,33 @@ export function parseArgs(argv) {
176
167
  args.static = true;
177
168
  } else if (arg === '-v' || arg === '--var') {
178
169
  if (iArg + 2 >= argv.length) {
179
- throw new Error(`Missing values for ${arg}`);
170
+ throw new Error('argument -v/--var: expected 2 arguments');
180
171
  }
181
- args.variables[argv[iArg + 1]] = evaluateExpression(parseExpression(argv[iArg + 2]));
172
+ args.var[argv[iArg + 1]] = argv[iArg + 2];
182
173
  iArg += 2;
183
174
  } else if (arg.startsWith('-')) {
184
- throw new Error(`Unknown option ${arg}`);
175
+ throw new Error(`unrecognized arguments: ${arg}`);
185
176
  } else {
186
- args.files.push([arg, null]);
177
+ args.scripts.push(['file', arg]);
187
178
  }
188
179
  }
189
180
 
190
- // Show help, if necessary
191
- if (args.help || args.files.length === 0) {
192
- throw new Error(helpText);
193
- }
194
-
195
181
  return args;
196
182
  }
183
+
184
+
185
+ // The command-line interface (CLI) help text
186
+ export const helpText = `\
187
+ usage: bare [-h] [-c CODE] [-d] [-s] [-v VAR EXPR] [file ...]
188
+
189
+ The BareScript command-line interface
190
+
191
+ positional arguments:
192
+ file files to process
193
+
194
+ options:
195
+ -h, --help show this help message and exit
196
+ -c, --code CODE execute the BareScript code
197
+ -d, --debug enable debug mode
198
+ -s, --static perform static analysis
199
+ -v, --var VAR EXPR set a global variable to an expression value`;
package/lib/baredoc.js CHANGED
@@ -1,74 +1,62 @@
1
1
  // Licensed under the MIT License
2
2
  // https://github.com/craigahobbs/bare-script/blob/main/LICENSE
3
3
 
4
+ import {valueJSON} from './value.js';
5
+
4
6
 
5
7
  /**
6
- * BareScript library documentation CLI options
8
+ * BareScript documentation tool options
7
9
  *
8
10
  * @typedef {Object} BaredocOptions
9
11
  * @property {string[]} argv - The process command-line arguments
10
- * @property {function} fetchFn - The [fetch function]{@link module:lib/runtime~FetchFn}
11
- * @property {function} logFn - The [log function]{@link module:lib/runtime~LogFn}
12
+ * @property {function} fetchFn - The [fetch function]{@link module:lib/options~FetchFn}
13
+ * @property {function} logFn - The [log function]{@link module:lib/options~LogFn}
12
14
  * @ignore
13
15
  */
14
16
 
15
17
 
16
18
  /**
17
- * BareScript library documentation command-line interface (CLI) main entry point
19
+ * BareScript documentation tool main entry point
18
20
  *
19
- * @param {Object} options - The CLI [options]{@link module:lib/bare~BaredocOptions}
21
+ * @param {Object} options - The [baredoc options]{@link module:lib/bare~BaredocOptions}
20
22
  * @returns {Number} The exit code
21
23
  * @ignore
22
24
  */
23
25
  export async function main(options) {
26
+ // Command line arguments
27
+ let args;
24
28
  try {
25
- const urls = options.argv.slice(2);
26
- const responses = await Promise.all(urls.map(async (url) => {
27
- try {
28
- return await options.fetchFn(url);
29
- } catch {
30
- throw Error(`Failed to load "${url}"`);
31
- }
32
- }));
33
- const files = await Promise.all(responses.map(async (response, ixResponse) => {
34
- const url = urls[ixResponse];
35
- let text = null;
36
- if (response.ok) {
37
- try {
38
- text = await response.text();
39
- } catch {
40
- // Do nothing
41
- }
42
- }
43
- if (text === null) {
44
- throw Error(`Failed to load "${url}"`);
45
- }
46
- return [url, text];
47
- }));
48
- options.logFn(JSON.stringify(parseBaredoc(files)));
29
+ args = parseArgs(options.argv);
49
30
  } catch ({message}) {
50
- options.logFn(message);
31
+ options.logFn(`error: ${message}`);
32
+ return 1;
33
+ }
34
+ if (args.help || args.files.length === 0) {
35
+ options.logFn(helpText);
51
36
  return 1;
52
37
  }
53
38
 
54
- return 0;
55
- }
56
-
57
-
58
- /**
59
- * Parse the library source for documentation tags
60
- *
61
- * @param {string[][]} files - The list of file name and source tuples
62
- * @returns {Object} The [library documentation model]{@link https://craigahobbs.github.io/bare-script/library/#var.vDoc=1}
63
- * @throws {Error}
64
- * @ignore
65
- */
66
- export function parseBaredoc(files) {
67
39
  // Parse each source file line-by-line
68
40
  const errors = [];
69
41
  const funcs = {};
70
42
  let func = null;
71
- for (const [file, source] of files) {
43
+ for (const file of args.files) {
44
+ // Read the source file
45
+ let source = null;
46
+ try {
47
+ const response = await options.fetchFn(file);
48
+ if (response.ok) {
49
+ source = await response.text();
50
+ }
51
+ } catch {
52
+ // Do nothing...
53
+ }
54
+ if (source === null) {
55
+ errors.push(`Failed to load "${file}"`);
56
+ continue;
57
+ }
58
+
59
+ // Split the source lines and process documentation comments
72
60
  const lines = source.split(rSplit);
73
61
  for (const [ixLine, line] of lines.entries()) {
74
62
  // function/group/doc/return documentation keywords?
@@ -137,21 +125,32 @@ export function parseBaredoc(files) {
137
125
  continue;
138
126
  }
139
127
 
140
- // Add the function arg documentation line - don't add leading blank lines
141
- let args = func.args ?? null;
142
- let arg = (args !== null ? args.find((argFind) => argFind.name === name) : null) ?? null;
143
- if (arg !== null || textTrim !== '') {
144
- if (args === null) {
145
- args = [];
146
- func.args = args;
147
- }
148
- if (arg === null) {
149
- arg = {'name': name, 'doc': []};
150
- args.push(arg);
151
- }
152
- arg.doc.push(text);
128
+ // Get the function argument model, if it exists
129
+ let funcArgs = func.args ?? null;
130
+ let funcArg = null;
131
+ if (funcArgs !== null) {
132
+ funcArg = funcArgs.find((findArg) => findArg.name === name) ?? null;
133
+ }
134
+
135
+ // Ignore leading argument documentation blank lines
136
+ if (funcArg === null && textTrim === '') {
137
+ continue;
153
138
  }
154
139
 
140
+ // Add the fuction model arguments member, if necessary
141
+ if (funcArgs === null) {
142
+ funcArgs = [];
143
+ func.args = funcArgs;
144
+ }
145
+
146
+ // Add the function argument model, if necessary
147
+ if (funcArg === null) {
148
+ funcArg = {'name': name, 'doc': []};
149
+ funcArgs.push(funcArg);
150
+ }
151
+
152
+ // Add the function argument documentation line
153
+ funcArg.doc.push(text);
155
154
  continue;
156
155
  }
157
156
 
@@ -188,10 +187,35 @@ export function parseBaredoc(files) {
188
187
 
189
188
  // Errors?
190
189
  if (errors.length !== 0) {
191
- throw new Error(errors.join('\n'));
190
+ options.logFn(errors.join('\n'));
191
+ return 1;
192
192
  }
193
193
 
194
- return library;
194
+ // JSON-serialize the library documentation model
195
+ const libraryJSON = valueJSON(library);
196
+
197
+ // Output to stdout?
198
+ if (args.output === '-') {
199
+ options.logFn(libraryJSON);
200
+ } else {
201
+ // Output to file
202
+ let success = false;
203
+ try {
204
+ const response = await options.fetchFn(args.output, {'body': libraryJSON});
205
+ if (response.ok) {
206
+ await response.text();
207
+ success = true;
208
+ }
209
+ } catch {
210
+ // Do nothing
211
+ }
212
+ if (!success) {
213
+ options.logFn(`error: Failed to write "${args.output}"`);
214
+ return 1;
215
+ }
216
+ }
217
+
218
+ return 0;
195
219
  }
196
220
 
197
221
 
@@ -200,3 +224,53 @@ const rKey = /^\s*(?:\/\/|#)\s*\$(?<key>function|group|doc|return):\s?(?<text>.*
200
224
  const rArg = /^\s*(?:\/\/|#)\s*\$arg\s+(?<name>[A-Za-z_][A-Za-z0-9_]*(?:\.\.\.)?):\s?(?<text>.*)$/;
201
225
  const rUnknown = /^\s*(?:\/\/|#)\s*\$(?<unknown>[^:]+):/;
202
226
  const rSplit = /\r?\n/;
227
+
228
+
229
+ /**
230
+ * Parse the baredoc command-line arguments
231
+ *
232
+ * @param {string} argv - The command-line arguments
233
+ * @returns {Object} The arguments object
234
+ * @throws {Error}
235
+ * @ignore
236
+ */
237
+ export function parseArgs(argv) {
238
+ const args = {'files': [], 'output': '-'};
239
+ for (let iArg = 2; iArg < argv.length; iArg++) {
240
+ const arg = argv[iArg];
241
+ if (arg === '-o') {
242
+ if (iArg + 1 >= argv.length) {
243
+ throw new Error('argument -o: expected one argument');
244
+ }
245
+ args.output = argv[iArg + 1];
246
+ iArg++;
247
+ } else if (arg === '-h' || arg === '--help') {
248
+ args.help = true;
249
+ } else if (arg.startsWith('-')) {
250
+ throw new Error(`unrecognized arguments: ${arg}`);
251
+ } else {
252
+ args.files.push(arg);
253
+ }
254
+ }
255
+
256
+ // Files is required
257
+ if (!args.help && args.files.length === 0) {
258
+ throw new Error('the following arguments are required: file');
259
+ }
260
+
261
+ return args;
262
+ }
263
+
264
+
265
+ // The baredoc command-line help text
266
+ export const helpText = `\
267
+ usage: baredoc [-h] [-o file] file [file ...]
268
+
269
+ The BareScript documentation tool
270
+
271
+ positional arguments:
272
+ file files to process
273
+
274
+ options:
275
+ -h, --help show this help message and exit
276
+ -o file write output to file (default is "-")`;