working demo and discussion
This commit is contained in:
commit
6f2001618b
|
|
@ -0,0 +1 @@
|
||||||
|
node_modules
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
# Demonstration of shell handler accepting arguments
|
||||||
|
|
||||||
|
The current shell handler in Web Origami allows passing an argument to a `.sh` file which will be passed to the child process' stdin. Here, we demonstrate how the shell handler could also take arguments and pass those to the child process.
|
||||||
|
|
||||||
|
## Rationale
|
||||||
|
|
||||||
|
|
||||||
|
## Sample and walkthrough
|
||||||
|
In this example, we're going to filter filenames based on a pattern, using both the stdin mechanism and the argument-passing mechanism.
|
||||||
|
|
||||||
|
### Modified `sh_handler.js`
|
||||||
|
To make this possible, I use a local modified version of the [distribution `sh_handler.js`](https://github.com/WebOrigami/origami/blob/main/language/src/handlers/sh_handler.js) in this directory. It accepts optional further arguments after the first inputText argument. If these are present, they will be passed as additional arguments in the shell invocation. Hence, you can use both stdin and arguments, and if you do _not_ want to use stdin, you need to pass `null` as the first argument.
|
||||||
|
|
||||||
|
### Data
|
||||||
|
two markdown files, `a_pubfile.md` and `anotherfile.md`.
|
||||||
|
|
||||||
|
### Handlers: stdin approach
|
||||||
|
* `allfiles.sh` is a one-liner: `fd -e md`. So it returns all Markdown filenames in this tree. As such, it has no need to use `stdin`. (If you don't have `fd`, you can replace the contents of the script with `find -type f -name "*.md"`.)
|
||||||
|
* `pubfiles.sh` is another one-liner: `rg '_pub' || true`. It selects only filenames matching the pattern `_pub` from its `stdin`. (If you don't have `ripgrep`, you can replace `rg` with `grep`.)
|
||||||
|
|
||||||
|
The purpose of this pair is to demonstrate shell script files as pipe filters: you pass the output of allfiles.sh to the input of pubfiles.sh and get a filtered list. In the shell, that works like this: `sh allfiles.sh | sh pubfiles.sh`. In Ori: `ori "pubfiles.sh allfiles.sh()"`. In `site.ori`:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
allfiles: allfiles.sh()
|
||||||
|
pubfiles: pubfiles.sh(allfiles)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Handlers: Argument approach
|
||||||
|
The remaining shell file `select.sh` takes an argument and passes it through to `fd` as a match pattern. In the shell: `sh select.sh '_pub'`. Ori cli: `ori "select.sh(null, '_pub')"`. In `site.ori`:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
selectedfiles: pubfiles.sh(null, '_pub')
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Discussion
|
||||||
|
Whether or not passing arguments to a shell script makes sense in Origami land is actually a subtle question. An Origami project is an interesting type of program in that a lot of config is usually hard-coded. Origami projects are usually things like websites, where there is a single, specific intended output. You don't reuse the project source to generate lots of different outputs; you add data over time (e.g. blog posts) and then regenerate the same site with the latest data set. Hence, it makes sense if database connection strings, or remote http requests, or other 'config' values are hard-coded in the source.
|
||||||
|
|
||||||
|
I want to generate a public and a private version of a static website. I could keep two completely separate `site.ori` files, but that would lead to maintenance problems. So the ability to pass a parameter to the part that selects input files allows me to use one `site.ori`.
|
||||||
|
|
||||||
|
Still, this isn't technically necessary, because you could do this parameterization in Ori-land and call two different shell scripts: since they are one-liners, it's not a problem to have one script for the public source and one script for the private source. Where this would be more useful is if a shell script was wrapping some more complex, opaque data source or calculation engine. In such a case, it could be conceivable that a shell script handler would be worth distributing and reusing, in which case it might be worth making it configurable.
|
||||||
|
|
||||||
|
Making data sources parameterizable might open Web Origami up to other types of projects, like data pipelines. I don't really have a compelling case, but having this option would allow more experimentation.
|
||||||
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
#Public File
|
||||||
|
|
||||||
|
Hello, world
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
find -e md
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
#Nobody knows
|
||||||
|
|
||||||
|
About this
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
sh_handler = ./sh_handler.js
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"name": "ori-sh-handler-args",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "ori-sh-handler-args",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@weborigami/async-tree": "^0.6.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@weborigami/async-tree": {
|
||||||
|
"version": "0.6.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/@weborigami/async-tree/-/async-tree-0.6.11.tgz",
|
||||||
|
"integrity": "sha512-iRocQlSpp6lmr2o9uOdt8UxdLqsSoM/AL8KYbomykaIdBFK7Td/1Ed89E8fWS2eTvR3pjMEdJj7wVV1F1EdEPw=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"name": "ori-sh-handler-args",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "sh_handler.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"type": "module",
|
||||||
|
"dependencies": {
|
||||||
|
"@weborigami/async-tree": "^0.6.11"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
rg '_pub' || true
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
import { toString } from "@weborigami/async-tree";
|
||||||
|
import { spawn } from "node:child_process";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shell script file extension handler
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
mediaType: "text/plain",
|
||||||
|
|
||||||
|
/** @type {import("@weborigami/async-tree").UnpackFunction} */
|
||||||
|
async unpack(packed) {
|
||||||
|
const scriptText = toString(packed);
|
||||||
|
|
||||||
|
if (scriptText === null) {
|
||||||
|
throw new Error(".sh handler: input isn't text");
|
||||||
|
}
|
||||||
|
|
||||||
|
//HANS: take remaining arguments
|
||||||
|
return async (input, ...rest) => {
|
||||||
|
return runShellScript(scriptText, input, rest);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run arbitrary shell script text in /bin/sh and feed it stdin.
|
||||||
|
* Supports multiple commands, pipelines, redirects, etc.
|
||||||
|
*
|
||||||
|
* @param {string} scriptText - Shell code (may contain newlines/side effects)
|
||||||
|
* @param {import("@weborigami/async-tree").Stringlike} inputText - Text to pipe to the script's stdin
|
||||||
|
* @returns {Promise<string>}
|
||||||
|
*/
|
||||||
|
function runShellScript(scriptText, inputText, rest) {
|
||||||
|
if (inputText instanceof Function) {
|
||||||
|
throw new Error(
|
||||||
|
"A .sh file expects text input but got a function instead. Did you mean to invoke the function?",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// Use sh -c "<scriptText>" so stdin is free for inputText
|
||||||
|
// HANS: pass remaining arguments to child process.
|
||||||
|
// 'command' is inserted here because sh -c expects command_name at that position.
|
||||||
|
const child = spawn("sh", ["-c", scriptText, "command", ...rest], {
|
||||||
|
env: { ...process.env },
|
||||||
|
stdio: ["pipe", "pipe", "pipe"],
|
||||||
|
});
|
||||||
|
|
||||||
|
let stdout = "";
|
||||||
|
let stderr = "";
|
||||||
|
|
||||||
|
child.stdout.on("data", (c) => (stdout += c));
|
||||||
|
child.stderr.on("data", (c) => (stderr += c));
|
||||||
|
|
||||||
|
child.on("error", reject);
|
||||||
|
|
||||||
|
child.on("close", (code) => {
|
||||||
|
if (code !== 0) {
|
||||||
|
/** @type {any} */
|
||||||
|
const err = new Error(
|
||||||
|
`Shell exited with code ${code}${stderr ? `: ${stderr}` : ""}`,
|
||||||
|
);
|
||||||
|
err.code = code;
|
||||||
|
err.stdout = stdout;
|
||||||
|
err.stderr = stderr;
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
resolve(stdout);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Feed the input to the script's stdin and close it
|
||||||
|
child.stdin.end(inputText);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
Reference in New Issue