A simple command/argument manager with a simple API that makes it easy to build dynamic commands and dynamic arguments with computed values.
The Builder's builds and manages commands. When a new Builder
instance is created, it provides an simple API to create and execute commands, append and prepend dynamic arguments.
When creating commands through the builder, it goes through the list of file patterns recursively, reads each file, parses each line in the file, resolves dynamic variables to their values, and generates an executable command string. When a command is executed, it returns an object which contains both a promise which is resolved or rejected based on how the process exits (0 / 1), along with a Readable streams object for listening to stdout and stderr streams.
So instead of manually creating the following command in our CI pipeline, with dynamic arguments and dynamic values that need to be computed at runtime:
docker build . -t myimage:1.0.0 --label org.label-schema.build-date=2019-07-14 --label org.label-schema.name=argster-120-example --label org.label-schema.vendor=Vendor --label org.label-schema.version=1.0.0 --label org.label-schema.schema-version=1.0.0-rc.1
You could create a simple JavaScript script and create complex commands with dynamic arguments (evaluated JavaScript functions) and version control it all with Git.
Pay special attention to the org.label-schema.build-date
label with the dynamic date.
1. If we created the following builder and command
const options = {
dynamicVariables: {
BUILD_DATE: () => new Date().toISOString().slice(0, 10),
NAME: () => pkg.name,
VERSION: () => pkg.version,
DESCRIPTION: () => pkg.description,
VENDOR: () => "Vendor",
SCHEMA_VERSION: () => "1.0.0-rc.1"
},
skipUnresolvedVariables: true
};
const builder = new Builder(options);
const cmd = builder.createCommand('docker build .', [
{
patterns: ['**/*.lbl'],
prefix: '--label'
}
]);
cmd.prependArgument({
argument: `myimage:${pkg.version}`,
prefix: '-t'
});
command.exec();
2. And had this pattern file
./some/deep/path/file.lbl
org.label-schema.build-date=${BUILD_DATE}
org.label-schema.name=${NAME}
org.label-schema.description=${DESCRIPTION}
org.label-schema.vendor=${VENDOR}
org.label-schema.version=${VERSION}
org.label-schema.schema-version=${SCHEMA_VERSION}
3. This will be the executed command:
docker build . -t myimage:1.0.0 --label org.label-schema.build-date=2019-07-14 --label org.label-schema.name=argster-120-example --label org.label-schema.vendor=Vendor --label org.label-schema.version=1.0.0 --label org.label-schema.schema-version=1.0.0-rc.1
I always felt the lack of a tool/library to easily create dynamic commands with dynamic arguments that were derived by functions but had the possibility to version control, without creating complex shell scripts. It first started when I was experimenting with DevOps and started to work with Docker trying to create sane images. I started reading about best practices around labeling and tagging Docker images and stumbled up on Label Schema.
I sat down and thought about the following technologies:
- Label Schema and label semantics for e.g. Docker images
- Dockerfile and
ENV
/ARG
- Dockerfile and
.env
file - Power of JavaScript
- Power of variables
- Power of Git
With the great tech above I wanted to utilise it all and create a Node library that was able to build up complex dynamic command-line commands and arguments. Then the idea of argster was born.
Check out this CodeSandbox for examples.
Checkout the roadmap for what to come.
You can install the npm package with yarn or npm:
yarn add argster
npm install argster
A builder is a core component that builds and manages commands. The builder accepts options to adjust how command arguments are parsed, etc.
A command is a string that is executed as a process. It contains a base command and arguments that can be assembled and manipulated. When a command is created, it accepts file patterns as arguments, that are then processed, evaluated and converted to a finalized command string, which is then executed.
File patterns are structures that contain a prefix and a list of file patterns, where each file matching the pattern is read and each line assembled into a command string, which is then executed.
Example for docker build
:
{
prefix: '--build-arg',
patterns: ['**/*.arg']
},
{
prefix: '--label',
patterns: ['**/*.lbl']
}
All files matching the glob pattern **/*.arg
and **/*.lbl
would be read and a command generated from those arguments.
When the command arguments are evaluated, a finalized command string is generated from those file contents.
Root directory to resolve all paths from.
process.cwd()
Key-Value map containing the name of the variable and its value.
[key: string]: string | (() => string);
{
BUILD_DATE: () => new Date().toISOString().slice(0, 10),
VERSION: '1.0.0'
}
Handling of unresolved variables. These are the possible options:
skipUnresolvedVariables
(Default:false
)- Skip command arguments where variables have no value
warnUnresolvedVariables
(Default:true
)- Warn on command arguments where variables have no value
throwUnresolvedVariables
(Default:false
)- Throw exception on command arguments where variables have no value
RegExp pattern for matching variables.
For example: ${SOME_VARIABLE}
/\$\{(.+)\}/
RegExp pattern for ignoring lines in files when generating arguments.
For example: # Some comment
or // Some comment
/^(\#|\/{2,})/
Convert variables based on format. An example would be to convert Windows environment variables to Linux or vice versa.
For example: %FOO% -> $FOO
false
| [
true,
{
from: RegExp;
to: string;
}
];
[true, { from: /\%([A-Z]+)\%/, to: '$$$1' }]
This would convert all Windows variables %FOO%
to Linux variables $FOO
Shell to use for executing commands
/bin/bash
Transformer is an object which consists of two parts: predicate
and replacer
.
predicate
is a function that returns a boolean.replacer
is a function that returns a new value.
Here's a simple transformer that surrounds sentences in quotes.
sentencesInQuotes: {
predicate: (val: string): boolean => {
if (!val) return false;
return val.split(' ').length > 1;
},
replacer: (val: string): string => `"${val}"`
}
options: IBuilderOptions;
/**
* Create a new command and register it in the builder
* @param command Main command, e.g. 'docker build'
* @param filePatterns Command argument prefix and glob file patterns
* @returns A new command object
*/
createCommand(
command: string,
filePatterns?: IArgumentFilePatterns[]
): ICommand;
/**
* Get all commands that are registered in the builder
* @returns Array of command objects
*/
getAllCommands(): ICommand[];
/**
* Executes a command
* @param stdout Callback on STDOUT
* @param stderr Callback on STDERR
*/
exec(
stdout?: (chunk: string) => string,
stderr?: (chunk: string) => string
): ICommandProcess;
/**
* Prepend an argument
* @param argument Argument
*/
prependArgument(argument: TCommandArgumentInput): ICommand;
/**
* Append an argument
* @param argument Argument
*/
appendArgument(argument: TCommandArgumentInput): ICommand;
/**
* Get the command string
*/
toString(): string;
/**
* Get the command string as an array
*/
toArray(): ReadonlyArray<string>;
- Set up automated TypeScript documentation for APIs
- Follow Conventional Commits
- Set up CI to run automated tests
- Plugin / Middleware architecture
- Ability to hook into events
- Ability to add custom behavior with middlewares
- Ability to create custom transformers
- CLI
- Optimizations + asynchronous operations
- Batch command handling
- Interactive process attaching, e.g. for
docker run -it
All contributions are very well appreciated. Just fork this repo and submit a PR!
Just remember to run the following beforehand:
yarn test
yarn typecheck
yarn format
yarn lint