Add additional content around README.
parent
27c097eb72
commit
eb598fe639
|
@ -3,4 +3,4 @@
|
||||||
bootstrap
|
bootstrap
|
||||||
compile_commands.json
|
compile_commands.json
|
||||||
docs/
|
docs/
|
||||||
test/runner
|
test/test
|
||||||
|
|
6
Makefile
6
Makefile
|
@ -8,10 +8,10 @@ build: include/*.h src/*.c
|
||||||
bear: include/*.h src/*.c
|
bear: include/*.h src/*.c
|
||||||
@bear -- ${BUILD}
|
@bear -- ${BUILD}
|
||||||
|
|
||||||
test: test/runner
|
test: test/test
|
||||||
$^
|
$^
|
||||||
|
|
||||||
test/runner: include/*.h src/*.c test/*.h test/*.c
|
test/test: include/*.h src/*.c test/*.h test/*.c
|
||||||
clang -I include src/*.c test/*.c -o test/runner -lm
|
clang -I include src/*.c test/*.c -o test/test -lm
|
||||||
|
|
||||||
.PHONY: test
|
.PHONY: test
|
||||||
|
|
136
README.md
136
README.md
|
@ -5,6 +5,9 @@ CLI utility for defining custom project initialization scripts.
|
||||||
TODO:
|
TODO:
|
||||||
- [ ] Add evaluator tests.
|
- [ ] Add evaluator tests.
|
||||||
- [ ] Color output to console.
|
- [ ] Color output to console.
|
||||||
|
- [ ] string -> line, case insensitive
|
||||||
|
- [ ] Ensure keys are alphanumeric, underscore
|
||||||
|
- [ ] sophie
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
|
@ -21,17 +24,19 @@ this project will feel at home. Ultimately the goal is to create (optionally)
|
||||||
interactive scripts like those mentioned in the above list to quickly scaffold
|
interactive scripts like those mentioned in the above list to quickly scaffold
|
||||||
your new projects in a consistent way.
|
your new projects in a consistent way.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
We start with an example. Consider the following *spec*, which we'll name
|
We start with an example. Consider the following *spec*, which we'll name
|
||||||
`example`:
|
`touch` (this example exists as a [pre-packaged spec](./specs/touch)):
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"filename": {
|
"filename": {
|
||||||
"type": "STRING",
|
"type": "text",
|
||||||
"prompt": "What file should I create for you? "
|
"prompt": "What file should I create for you? "
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
and its associated *builder*:
|
and its associated *runner*:
|
||||||
```bash
|
```bash
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
@ -42,40 +47,131 @@ touch "$OUT/$FILENAME"
|
||||||
Running `bootstrap` with these two files configured will invoke the following
|
Running `bootstrap` with these two files configured will invoke the following
|
||||||
interactive script:
|
interactive script:
|
||||||
```bash
|
```bash
|
||||||
> bootstrap example
|
$> bootstrap touch
|
||||||
I: What file should I create for you? hello-world.txt
|
What file should I create for you? hello-world.txt
|
||||||
O: Creating hello-world.txt
|
Creating hello-world.txt
|
||||||
|
$> ls
|
||||||
>
|
... hello-world.txt ...
|
||||||
```
|
```
|
||||||
Here the line prefixed with `I:` indicates a prompt that must be answered by
|
You should now see a new `hello-world.txt` file in your current working
|
||||||
the user. The line prefixed with `O:` indicates the output of the builder
|
|
||||||
script. You should now see a new `hello-world.txt` file in your current working
|
|
||||||
directory.
|
directory.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
TODO
|
|
||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
TODO
|
TODO
|
||||||
|
|
||||||
### Specs and Builders
|
### Runners
|
||||||
|
|
||||||
TODO
|
A spec refers to any directory containing a file named `runner`. The only
|
||||||
|
requirement enforced by `bootstrap` is for this file to be an executable (e.g.
|
||||||
|
`chmod +x`), but typically the `runner` is a shell script:
|
||||||
|
```bash
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
The `runner` is invoked with its current working directory set to that of the
|
||||||
|
directory containing it. For instance, if we have a `runner` script living in
|
||||||
|
directory `~/Documents/specs/example` with contents:
|
||||||
|
```bash
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
echo "$PWD"
|
||||||
|
```
|
||||||
|
the output of `bootstrap example` will *always* be e.g.
|
||||||
|
```bash
|
||||||
|
> bootstrap example
|
||||||
|
/home/jrpotter/Documents/specs/example
|
||||||
|
```
|
||||||
|
regardless of where we call the `bootstrap` command.
|
||||||
|
|
||||||
|
#### Exit Code
|
||||||
|
|
||||||
|
`bootstrap` always invokes the `runner` using the system `sh` command:
|
||||||
|
```bash
|
||||||
|
> /bin/sh sh -c ./runner
|
||||||
|
```
|
||||||
|
The exit code emitted by `bootstrap` will mirror that returned by the `runner`
|
||||||
|
executable.
|
||||||
|
|
||||||
|
### Specs
|
||||||
|
|
||||||
|
If interested in making the `runner` more flexible, you can provide different
|
||||||
|
environment variables in the form of a `spec.json` file. This file must live
|
||||||
|
in the same spec as the `runner`. When invoking `bootstrap`, the file is used to
|
||||||
|
determine what prompts should be displayed to the user before executing the
|
||||||
|
`runner` file. The user's responses are then included as environment variables
|
||||||
|
to the `runner` process.
|
||||||
|
|
||||||
|
The file contents should consist of a top-level JSON object and any number of
|
||||||
|
child objects called *fields*. A typical `spec.json` file looks like:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"fieldname": {
|
||||||
|
"type": "text",
|
||||||
|
"prompt": "Prompt for field> "
|
||||||
|
},
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
In this example, the first field is called `"fieldname"`. `bootstrap` sees this
|
||||||
|
field and writes the prompt `"Prompt for field> "` to `stdout`. Since
|
||||||
|
`"fieldname"` has type `"text"`, `bootstrap` will wait for the user to input
|
||||||
|
a string (submitted with a newline).
|
||||||
|
|
||||||
|
If the user were to enter, say `fieldvalue`, in response to the prompt,
|
||||||
|
the `runner` script would then have access to an environment variable
|
||||||
|
`FIELDNAME` set to `fieldvalue` on launch.
|
||||||
|
|
||||||
|
#### Types
|
||||||
|
|
||||||
|
The value of `type` determines how a field is prompted for. Note the value of
|
||||||
|
`type` is case insenstive. The currently supported list of types are:
|
||||||
|
|
||||||
|
* `text`
|
||||||
|
* The simplest prompt type. Takes in a free-form response submitted after a
|
||||||
|
newline (`\n`) is encountered.
|
||||||
|
|
||||||
|
#### Root Directory
|
||||||
|
|
||||||
|
All specs should exist in the same *root directory*. As an example of what this
|
||||||
|
directory might look like, refer to `specs` at the top-level of this project.
|
||||||
|
When invoking `bootstrap <name>`, `<name>` is expected to correspond to some
|
||||||
|
spec found within the root directory.
|
||||||
|
|
||||||
|
To tell `bootstrap` where your specs are located, you can provide the path to
|
||||||
|
the root directory using the `-d` option like so:
|
||||||
|
```bash
|
||||||
|
> bootstrap -d ~/Documents/specs example
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
If no option is set, `bootstrap` will fallback to using the value of the
|
||||||
|
`BOOTSTRAP_ROOT_DIR` environment variable. If this also isn't set, `bootstrap`
|
||||||
|
will abort with an appropriate error message.
|
||||||
|
|
||||||
### Other Environment Variables
|
### Other Environment Variables
|
||||||
|
|
||||||
TODO
|
By default, the `runner` command will have the following environment variables
|
||||||
|
defined. Defining these fields in a `spec.json` file will override the default
|
||||||
|
values:
|
||||||
|
|
||||||
|
* `OUT`
|
||||||
|
* The directory `bootstrap` was invoked from. Named since this is usually
|
||||||
|
where you want to initialize new files of your project in.
|
||||||
|
|
||||||
### Supplied Specs
|
### Supplied Specs
|
||||||
|
|
||||||
TODO
|
A number of specs are provided out of the box. If you installed `bootstrap`
|
||||||
|
using `nix`, the `BOOTSTRAP_ROOT_DIR` will automatically be set to the location
|
||||||
|
of these [specs](./specs). Keep in mind this list is very opinionated - they
|
||||||
|
reflect my personal needs for projects. Feel free to specify a different specs
|
||||||
|
root directory if these do not fit your needs.
|
||||||
|
|
||||||
### Using With Nix
|
As a suggestion, use `nix` from within your `runner` scripts for maximum
|
||||||
|
reproducibility. Refer to the provided specs for inspiration on how you can do
|
||||||
TODO
|
this.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
|
|
@ -45,10 +45,10 @@ enum ErrorCode {
|
||||||
/// The `prompt` of a `spec.json` field is not a string.
|
/// The `prompt` of a `spec.json` field is not a string.
|
||||||
ERROR_VALIDATOR_FIELD_PROMPT_INVALID,
|
ERROR_VALIDATOR_FIELD_PROMPT_INVALID,
|
||||||
|
|
||||||
/// The `run.sh` file could not be found.
|
/// The `runner` file could not be found.
|
||||||
ERROR_EVALUATOR_RUN_SH_NOT_FOUND,
|
ERROR_EVALUATOR_RUNNER_NOT_FOUND,
|
||||||
/// The `run.sh` file is not executable.
|
/// The `runner` file is not executable.
|
||||||
ERROR_EVALUATOR_RUN_SH_NOT_EXEC,
|
ERROR_EVALUATOR_RUNNER_NOT_EXEC,
|
||||||
/// A user response to a prompt is not valid.
|
/// A user response to a prompt is not valid.
|
||||||
ERROR_EVALUATOR_RESPONSE_INVALID,
|
ERROR_EVALUATOR_RESPONSE_INVALID,
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/**
|
/**
|
||||||
@file
|
@file
|
||||||
@brief `spec.json` and `run.sh` evaluator.
|
@brief `spec.json` and `runner` evaluator.
|
||||||
*/
|
*/
|
||||||
#ifndef _BOOTSTRAP_EVALUATOR_H
|
#ifndef _BOOTSTRAP_EVALUATOR_H
|
||||||
#define _BOOTSTRAP_EVALUATOR_H
|
#define _BOOTSTRAP_EVALUATOR_H
|
||||||
|
@ -10,21 +10,21 @@
|
||||||
#include "validator.h"
|
#include "validator.h"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@brief Run the `run.sh` script found in the configured spec.
|
@brief Run the `runner` file found in the configured spec.
|
||||||
|
|
||||||
@param config
|
@param config
|
||||||
A reference to the parameters describing the desired spec.
|
A reference to the parameters describing the desired spec.
|
||||||
@param fields
|
@param fields
|
||||||
The list of prompts to have answered by the user prior to executing `run.sh`.
|
The list of prompts to have answered by the user prior to executing `runner`.
|
||||||
Responses are included as environment variables in the invoked subshell.
|
Responses are included as environment variables in the invoked subshell.
|
||||||
@param error
|
@param error
|
||||||
The out parameter containing a possible error. Set to a null pointer if no
|
The out parameter containing a possible error. Set to a null pointer if no
|
||||||
error occurs. Otherwise set to an @ref Error instance.
|
error occurs. Otherwise set to an @ref Error instance.
|
||||||
@return
|
@return
|
||||||
If @p error is set, returns `EXIT_FAILURE`. Otherwise returns the exit code
|
If @p error is set, returns `EXIT_FAILURE`. Otherwise returns the exit code
|
||||||
returned by `run.sh`.
|
returned by the `runner`.
|
||||||
*/
|
*/
|
||||||
int evaluate_run_sh(
|
int evaluate_runner(
|
||||||
const struct Config *const config,
|
const struct Config *const config,
|
||||||
const struct DynArray *const fields,
|
const struct DynArray *const fields,
|
||||||
struct Error **error
|
struct Error **error
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
@brief The types of fields `bootstrap` can handle.
|
@brief The types of fields `bootstrap` can handle.
|
||||||
*/
|
*/
|
||||||
enum FieldType {
|
enum FieldType {
|
||||||
FT_STRING = 1,
|
FT_TEXT = 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -25,12 +25,12 @@ file. For instance, the fields of:
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"abc": {
|
"abc": {
|
||||||
"type": "STRING",
|
"type": "text",
|
||||||
"prompt": "ABC"
|
"prompt": "ABC> "
|
||||||
},
|
},
|
||||||
"def": {
|
"def": {
|
||||||
"type": "STRING",
|
"type": "text",
|
||||||
"prompt": "DEF"
|
"prompt": "DEF> "
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
2
main.c
2
main.c
|
@ -38,7 +38,7 @@ static int run(const char *root_dir, const char *target) {
|
||||||
goto cleanup_parsed;
|
goto cleanup_parsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((retval = evaluate_run_sh(config, prompts, &error))) {
|
if ((retval = evaluate_runner(config, prompts, &error))) {
|
||||||
if (error) {
|
if (error) {
|
||||||
fprintf(stderr, "%s", error->message);
|
fprintf(stderr, "%s", error->message);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
echo "hello world"
|
|
|
@ -1,6 +0,0 @@
|
||||||
{
|
|
||||||
"display": {
|
|
||||||
"type": "STRING",
|
|
||||||
"prompt": "This is a prompt"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
echo "Creating $FILENAME"
|
||||||
|
touch "$OUT/$FILENAME"
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"filename": {
|
||||||
|
"type": "text",
|
||||||
|
"prompt": "What file should I create for you? "
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,11 +11,11 @@
|
||||||
#include "string_buf.h"
|
#include "string_buf.h"
|
||||||
#include "validator.h"
|
#include "validator.h"
|
||||||
|
|
||||||
static struct Error *find_run_sh(const struct Config *const config) {
|
static struct Error *find_run_exec(const struct Config *const config) {
|
||||||
assert(config);
|
assert(config);
|
||||||
|
|
||||||
struct stat sb;
|
struct stat sb;
|
||||||
const char *segments[] = {config->root_dir, config->target, "run.sh"};
|
const char *segments[] = {config->root_dir, config->target, "runner"};
|
||||||
char *filepath =
|
char *filepath =
|
||||||
join_path_segments(sizeof(segments) / sizeof(char *), segments);
|
join_path_segments(sizeof(segments) / sizeof(char *), segments);
|
||||||
int stat_res = stat(filepath, &sb);
|
int stat_res = stat(filepath, &sb);
|
||||||
|
@ -23,18 +23,18 @@ static struct Error *find_run_sh(const struct Config *const config) {
|
||||||
|
|
||||||
if (stat_res == -1 && errno == ENOENT) {
|
if (stat_res == -1 && errno == ENOENT) {
|
||||||
return ERROR_NEW(
|
return ERROR_NEW(
|
||||||
ERROR_EVALUATOR_RUN_SH_NOT_FOUND,
|
ERROR_EVALUATOR_RUNNER_NOT_FOUND,
|
||||||
"Could not find ",
|
"Could not find ",
|
||||||
config->target,
|
config->target,
|
||||||
"/run.sh"
|
"/runner"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(sb.st_mode & S_IXUSR)) {
|
if (!(sb.st_mode & S_IXUSR)) {
|
||||||
return ERROR_NEW(
|
return ERROR_NEW(
|
||||||
ERROR_EVALUATOR_RUN_SH_NOT_EXEC,
|
ERROR_EVALUATOR_RUNNER_NOT_EXEC,
|
||||||
config->target,
|
config->target,
|
||||||
"/run.sh is not executable."
|
"/runner is not executable."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,7 +45,7 @@ static const char *prompt_field(struct Field *field) {
|
||||||
assert(field);
|
assert(field);
|
||||||
|
|
||||||
switch (field->type) {
|
switch (field->type) {
|
||||||
case FT_STRING:
|
case FT_TEXT:
|
||||||
printf("%s", field->prompt);
|
printf("%s", field->prompt);
|
||||||
// TODO: Probably want this buffer size to be a bit more dynamic.
|
// TODO: Probably want this buffer size to be a bit more dynamic.
|
||||||
char *input = calloc(1, 1024);
|
char *input = calloc(1, 1024);
|
||||||
|
@ -74,12 +74,12 @@ static void push_env(
|
||||||
string_buf_sappend(env, "' ");
|
string_buf_sappend(env, "' ");
|
||||||
}
|
}
|
||||||
|
|
||||||
int evaluate_run_sh(
|
int evaluate_runner(
|
||||||
const struct Config *const config,
|
const struct Config *const config,
|
||||||
const struct DynArray *const fields,
|
const struct DynArray *const fields,
|
||||||
struct Error **error
|
struct Error **error
|
||||||
) {
|
) {
|
||||||
*error = find_run_sh(config);
|
*error = find_run_exec(config);
|
||||||
if (*error) {
|
if (*error) {
|
||||||
return EXIT_FAILURE;
|
return EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
|
@ -102,7 +102,7 @@ int evaluate_run_sh(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const char *segments[] = {config->root_dir, config->target, "run.sh"};
|
const char *segments[] = {config->root_dir, config->target, "runner"};
|
||||||
const char *filepath =
|
const char *filepath =
|
||||||
join_path_segments(sizeof(segments) / sizeof(char *), segments);
|
join_path_segments(sizeof(segments) / sizeof(char *), segments);
|
||||||
const char *env = string_buf_convert(env_buf);
|
const char *env = string_buf_convert(env_buf);
|
||||||
|
|
|
@ -28,7 +28,7 @@ static struct Error *read_field(const cJSON *const field, struct Field **out) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (strcmp(type->valuestring, "STRING") == 0) {
|
if (strcmp(type->valuestring, "STRING") == 0) {
|
||||||
(*out)->type = FT_STRING;
|
(*out)->type = FT_TEXT;
|
||||||
} else {
|
} else {
|
||||||
error = ERROR_NEW(
|
error = ERROR_NEW(
|
||||||
ERROR_VALIDATOR_FIELD_TYPE_UNKNOWN,
|
ERROR_VALIDATOR_FIELD_TYPE_UNKNOWN,
|
||||||
|
|
Loading…
Reference in New Issue