diff --git a/.gitignore b/.gitignore index e8920c7..38562b5 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,4 @@ bootstrap compile_commands.json docs/ -test/runner +test/test diff --git a/Makefile b/Makefile index 9a9f4cb..0a69287 100644 --- a/Makefile +++ b/Makefile @@ -8,10 +8,10 @@ build: include/*.h src/*.c bear: include/*.h src/*.c @bear -- ${BUILD} -test: test/runner +test: test/test $^ -test/runner: include/*.h src/*.c test/*.h test/*.c - clang -I include src/*.c test/*.c -o test/runner -lm +test/test: include/*.h src/*.c test/*.h test/*.c + clang -I include src/*.c test/*.c -o test/test -lm .PHONY: test diff --git a/README.md b/README.md index 4a8ee23..5d0343e 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,9 @@ CLI utility for defining custom project initialization scripts. TODO: - [ ] Add evaluator tests. - [ ] Color output to console. +- [ ] string -> line, case insensitive +- [ ] Ensure keys are alphanumeric, underscore +- [ ] sophie ## 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 your new projects in a consistent way. +--- + 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 { "filename": { - "type": "STRING", + "type": "text", "prompt": "What file should I create for you? " } } ``` -and its associated *builder*: +and its associated *runner*: ```bash #!/usr/bin/env bash @@ -42,40 +47,131 @@ touch "$OUT/$FILENAME" Running `bootstrap` with these two files configured will invoke the following interactive script: ```bash -> bootstrap example -I: What file should I create for you? hello-world.txt -O: Creating hello-world.txt - -> +$> bootstrap touch +What file should I create for you? 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 -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 +You should now see a new `hello-world.txt` file in your current working directory. ## Usage -TODO - ### Installation 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 `, `` 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 -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 -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 - -TODO +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 +this. ## Development diff --git a/include/error.h b/include/error.h index a8772d4..3108cda 100644 --- a/include/error.h +++ b/include/error.h @@ -45,10 +45,10 @@ enum ErrorCode { /// The `prompt` of a `spec.json` field is not a string. ERROR_VALIDATOR_FIELD_PROMPT_INVALID, - /// The `run.sh` file could not be found. - ERROR_EVALUATOR_RUN_SH_NOT_FOUND, - /// The `run.sh` file is not executable. - ERROR_EVALUATOR_RUN_SH_NOT_EXEC, + /// The `runner` file could not be found. + ERROR_EVALUATOR_RUNNER_NOT_FOUND, + /// The `runner` file is not executable. + ERROR_EVALUATOR_RUNNER_NOT_EXEC, /// A user response to a prompt is not valid. ERROR_EVALUATOR_RESPONSE_INVALID, }; diff --git a/include/evaluator.h b/include/evaluator.h index 0df096c..c0e7170 100644 --- a/include/evaluator.h +++ b/include/evaluator.h @@ -1,6 +1,6 @@ /** @file -@brief `spec.json` and `run.sh` evaluator. +@brief `spec.json` and `runner` evaluator. */ #ifndef _BOOTSTRAP_EVALUATOR_H #define _BOOTSTRAP_EVALUATOR_H @@ -10,21 +10,21 @@ #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 A reference to the parameters describing the desired spec. @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. @param error The out parameter containing a possible error. Set to a null pointer if no error occurs. Otherwise set to an @ref Error instance. @return 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 DynArray *const fields, struct Error **error diff --git a/include/validator.h b/include/validator.h index 754866f..0f6d061 100644 --- a/include/validator.h +++ b/include/validator.h @@ -13,7 +13,7 @@ @brief The types of fields `bootstrap` can handle. */ enum FieldType { - FT_STRING = 1, + FT_TEXT = 1, }; /** @@ -25,12 +25,12 @@ file. For instance, the fields of: ```json { "abc": { - "type": "STRING", - "prompt": "ABC" + "type": "text", + "prompt": "ABC> " }, "def": { - "type": "STRING", - "prompt": "DEF" + "type": "text", + "prompt": "DEF> " }, } ``` diff --git a/main.c b/main.c index 49c0f28..bcbc83d 100644 --- a/main.c +++ b/main.c @@ -38,7 +38,7 @@ static int run(const char *root_dir, const char *target) { goto cleanup_parsed; } - if ((retval = evaluate_run_sh(config, prompts, &error))) { + if ((retval = evaluate_runner(config, prompts, &error))) { if (error) { fprintf(stderr, "%s", error->message); } diff --git a/specs/test/run.sh b/specs/test/run.sh deleted file mode 100755 index c914ca6..0000000 --- a/specs/test/run.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env bash - -echo "hello world" diff --git a/specs/test/spec.json b/specs/test/spec.json deleted file mode 100644 index a0e72c2..0000000 --- a/specs/test/spec.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "display": { - "type": "STRING", - "prompt": "This is a prompt" - } -} diff --git a/specs/touch/runner b/specs/touch/runner new file mode 100755 index 0000000..abee60d --- /dev/null +++ b/specs/touch/runner @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +echo "Creating $FILENAME" +touch "$OUT/$FILENAME" diff --git a/specs/touch/spec.json b/specs/touch/spec.json new file mode 100644 index 0000000..37cb2e7 --- /dev/null +++ b/specs/touch/spec.json @@ -0,0 +1,6 @@ +{ + "filename": { + "type": "text", + "prompt": "What file should I create for you? " + } +} diff --git a/src/evaluator.c b/src/evaluator.c index 45b7c54..228a99c 100644 --- a/src/evaluator.c +++ b/src/evaluator.c @@ -11,11 +11,11 @@ #include "string_buf.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); struct stat sb; - const char *segments[] = {config->root_dir, config->target, "run.sh"}; + const char *segments[] = {config->root_dir, config->target, "runner"}; char *filepath = join_path_segments(sizeof(segments) / sizeof(char *), segments); 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) { return ERROR_NEW( - ERROR_EVALUATOR_RUN_SH_NOT_FOUND, + ERROR_EVALUATOR_RUNNER_NOT_FOUND, "Could not find ", config->target, - "/run.sh" + "/runner" ); } if (!(sb.st_mode & S_IXUSR)) { return ERROR_NEW( - ERROR_EVALUATOR_RUN_SH_NOT_EXEC, + ERROR_EVALUATOR_RUNNER_NOT_EXEC, 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); switch (field->type) { - case FT_STRING: + case FT_TEXT: printf("%s", field->prompt); // TODO: Probably want this buffer size to be a bit more dynamic. char *input = calloc(1, 1024); @@ -74,12 +74,12 @@ static void push_env( string_buf_sappend(env, "' "); } -int evaluate_run_sh( +int evaluate_runner( const struct Config *const config, const struct DynArray *const fields, struct Error **error ) { - *error = find_run_sh(config); + *error = find_run_exec(config); if (*error) { 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 = join_path_segments(sizeof(segments) / sizeof(char *), segments); const char *env = string_buf_convert(env_buf); diff --git a/src/validator.c b/src/validator.c index f899500..1ddd491 100644 --- a/src/validator.c +++ b/src/validator.c @@ -28,7 +28,7 @@ static struct Error *read_field(const cJSON *const field, struct Field **out) { } if (strcmp(type->valuestring, "STRING") == 0) { - (*out)->type = FT_STRING; + (*out)->type = FT_TEXT; } else { error = ERROR_NEW( ERROR_VALIDATOR_FIELD_TYPE_UNKNOWN, diff --git a/test/runner.c b/test/test.c similarity index 100% rename from test/runner.c rename to test/test.c