WinterCG proposal for standardizing CLI APIs.
This explainer is a WIP draft, as such may drastically change in the future. This is primarily focused on what should be exposed and how. There may be a "V2" in future with more high-level APIs like parsing arguments, but for now this is only ensuring such APIs could be implemented in userland.
Process-level information such as arguments and environment variables are commonly used in many CLI applications. Currently, JS runtimes do not have any standardized method to get this information:
- Node:
process.argv
1,process.env
3 - Deno:
Deno.args
2,Deno.env
4 - Bun:
Bun.argv
1,Bun.env
3
(0: Bare arguments including binary path and script path, excluding options consumed. 2: Arguments excluding runtime args. 3: Bare object. 4: Map-like object)
Arguments should not be exposed raw, instead they should have "runtime args" removed. "Runtime args" are any arguments which are specific to the runtime itself: runtime binary path, script path, and runtime arguments. For example, Deno.args
currently excludes these while process.argv
has the runtime binary path and script path. Runtimes may wish to expose the raw arguments themselves via their own API, but that is intentionally not standardized in this proposal.
Raw arguments | Expected | process.argv (comparison) |
---|---|---|
runtime script.js |
[] |
[ '/bin/runtime', '/tmp/script.js' ] |
runtime script.js example |
[ 'example' ] |
[ '/bin/runtime', '/tmp/script.js', 'example' ] |
runtime script.js one two three |
[ 'one', 'two', 'three' ] |
[ '/bin/runtime', '/tmp/script.js', 'one', 'two', 'three' ] |
runtime --cool-runtime-argument script.js foo bar |
[ 'foo', 'bar' ] |
[ '/bin/runtime', '/tmp/script.js', 'foo', 'bar' ] |
runtime script.js --cool-runtime-argument foo bar |
[ '--cool-runtime-argument', 'foo', 'bar' ] |
[ '/bin/runtime', '/tmp/script.js', '--cool-runtime-argument', 'foo', 'bar' ] |
Environment variables should be exposed as a exotic object with getter/setter/deleter/etc as specified below. This behaves similar to process.env
, but strictly specified. It is also similar to localStorage
in some aspects (getter/setter/deleter/etc for named access of an external resource).
Important
This section is a draft of a simplified ES-like spec to detail the concept and is under discussion. This should probably be moved to a separate spec file.
The EnvironmentVariables getter gets the given environment variable in a host-defined manner. It performs the following steps when called:
- If P is not a String, return undefined.
- If, checking in a host-defined manner, the environment variable P is set, then
- Return the value of the environment variable P in a host-defined manner.
- Return undefined.
Note
Some platforms, notably Windows, have case insensitive environment variable lookups. This should be handled in the host-defined manners. For example, if FOO
was set and foo
was looked up, the value of FOO
would be used. If foo
was then set, the original case FOO
would retain (like a case-insensitive pointer lookup).
The EnvironmentVariables [[GetOwnProperty]] internal method returns a normal completion containing a Property Descriptor or undefined. It gets the given environment variable in a host-defined manner. It performs the following steps when called:
- If P is not a String, return undefined.
- If, checking in a host-defined manner, the environment variable P is set, then
- Let value be the value of the environment variable P retrieved in a host-defined manner.
- Return the PropertyDescriptor { [[Value]]: value, [[Writable]]: true, [[Enumerable]]: true, [[Configurable]]: true }.
- Return undefined.
The EnvironmentVariables setter sets the given environment variable in a host-defined manner. It performs the following steps when called:
- If P is not a String, return false.
- Let value be ? ToString(V).
- Set the environment variable P to value in a host-defined manner.
- Return true.
The EnvironmentVariables deleter unsets the given environment variable in a host-defined manner. It performs the following steps when called:
- If P is not a String, return false.
- If, checking in a host-defined manner, the environment variable P is set, then
- Unset the environment variable P in a host-defined manner.
- Return true.
Note
Set and unset environment variables both return true for deletion.
The EnvironmentVariables [[HasProperty]] internal method checks if the given environment variable is set in a host-defined manner. It performs the following steps when called:
- If P is not a String, return false.
- If, checking in a host-defined manner, the environment variable P is set, then
- Return true.
- Return false.
The EnvironmentVariables [[OwnPropertyKeys]] internal method returns a list of set environment variables retrieved in a host-defined manner. It performs the following steps when called:
- Return a list of set environment variables retrieved in a host-defined manner.
Note
For EnvironmentVariables we intentionally use [[OwnPropertyKeys]] instead of newer Iterator, entries, etc as there could be environment variables with those names (even if unlikely). Ideally Object.keys(CLI.env)
, for (const name in CLI.env)
, { ...CLI.env }
should all work from these definitions.
Note
For now, [[DefineOwnProperty]], and more are left knowingly unspecified.
Note
Should we also have get
/set
/has
/delete
methods separately as well as getter/setter/etc?
The following metadata (capabilities/preferences) about the terminal should be exposed:
- Whether the terminal is interactive or non-interactive
- Whether color should be used or avoided
There should be an exit
function, optionally allowing an exit code number defaulting to 0
(exit(code?: number)
).
// todo: exit hooks/listeners
// todo
// todo
Currently, if you want the same script which uses arguments or other CLI APIs to work across runtimes, you have to add specific code for each runtime:
let args = [];
if (typeof process !== 'undefined') args = process.argv.slice(2);
if (typeof Deno !== 'undefined') args = Deno.args.slice();
// ...
While this could be helped with a library (boilerplate :/) or by more runtimes implementing process
(not a standard :/), WinterCG looks like a good place to really standardize these APIs. The core goal of this is to make this CLI API become the default, the one used by most people and runtimes; even some runtimes which not interested in the entirety of WinterCG as they likely will want/need an API which does some of this scope anyway. This API should be good enough that people want to use it regardless of the bonus that it is specified and (in the future) standardized. (TL;DR: It is important we make this API great, not just specified.)
For the origin and selfish (@CanadaHonk) reasons, I was looking at adding an arguments API to my JS engine+runtime and didn't know how I should expose them, so started this proposal :^)