Metadata-Version: 2.1
Name: shellscriptor
Version: 0.1.0
Summary: Programmatic shell script generator with argparse, logging, validation, and structured output
Author: RepoDynamics
License: MIT
Project-URL: Homepage, https://github.com/RepoDynamics/ShellScriptor
Project-URL: Repository, https://github.com/RepoDynamics/ShellScriptor
Keywords: shell,bash,script,codegen,argparse
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Topic :: Software Development :: Code Generators
Classifier: Topic :: System :: Shells
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: pydantic>=2.0
Provides-Extra: yaml
Requires-Dist: pyyaml>=6.0; extra == "yaml"
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"

# ShellScriptor

Programmatic shell script generator with argument parsing, logging, validation,
and structured output — driven by Python dicts or YAML/JSON definition files.

---

## Contents

- [ShellScriptor](#shellscriptor)
  - [Contents](#contents)
  - [Overview](#overview)
  - [Installation](#installation)
  - [Quick start](#quick-start)
    - [Python](#python)
    - [CLI](#cli)
  - [Definition format](#definition-format)
    - [Script definition (`ScriptDef`)](#script-definition-scriptdef)
    - [Function definition (`FunctionDef`)](#function-definition-functiondef)
    - [Parameter definition (`ParameterDef`)](#parameter-definition-parameterdef)
    - [Validation rules (`ValidationDef`)](#validation-rules-validationdef)
    - [Return value definition (`ReturnDef`)](#return-value-definition-returndef)
  - [Script types](#script-types)
    - [`shell_src` — sourced library](#shell_src--sourced-library)
    - [`shell_exec` — executable script](#shell_exec--executable-script)
  - [Parameter types](#parameter-types)
  - [Argument parsing](#argument-parsing)
    - [Required vs optional parameters](#required-vs-optional-parameters)
    - [Short flag aliases](#short-flag-aliases)
    - [Help / usage output](#help--usage-output)
    - [Environment variable fallback (`shell_exec`)](#environment-variable-fallback-shell_exec)
  - [Validation](#validation)
    - [Enum (allowed values)](#enum-allowed-values)
    - [Path existence](#path-existence)
    - [Integer range](#integer-range)
    - [Regex pattern](#regex-pattern)
    - [Custom shell lines](#custom-shell-lines)
  - [Return values / structured output](#return-values--structured-output)
  - [Embedded functions](#embedded-functions)
  - [Global function pool](#global-function-pool)
  - [Logging](#logging)
  - [Body sections](#body-sections)
  - [CLI usage](#cli-usage)
    - [Definition file formats](#definition-file-formats)
  - [Python API](#python-api)
    - [`create_script(name, data, script_type, global_functions=None) -> str`](#create_scriptname-data-script_type-global_functionsnone---str)
    - [`create_function(name, data, script_type) -> list[str]`](#create_functionname-data-script_type---liststr)
    - [Low-level generators](#low-level-generators)
  - [Examples](#examples)
    - [Required string parameter with short flag](#required-string-parameter-with-short-flag)
    - [All four parameter types](#all-four-parameter-types)
    - [Enum + path validation](#enum--path-validation)
    - [Integer range](#integer-range-1)
    - [Regex pattern](#regex-pattern-1)
    - [Return values](#return-values)
    - [Genuinely optional parameter (no default, no required check)](#genuinely-optional-parameter-no-default-no-required-check)

---

## Overview

`shellscriptor` generates complete, production-quality shell scripts from a
structured description.  You provide a dict (or a YAML/JSON file) that declares
parameters, validation rules, embedded functions, and body code; the library
assembles the boilerplate so you don't have to:

| Feature | `shell_src` | `shell_exec` |
|---|---|---|
| `while/case` argparse loop | ✓ | ✓ |
| `--help` / `__usage__` | ✓ (when descriptions present) | ✓ |
| Validation checks | ✓ | ✓ |
| Entry / exit log markers | — | ✓ |
| Argument-read logs | — | ✓ |
| `tee` log capture to `$LOGFILE` | — | ✓ |
| `trap __cleanup__ EXIT` | — | ✓ |
| `set -x` when `$DEBUG=true` | — | ✓ |
| Env-var fallback (called with no args) | — | ✓ |

---

## Installation

```bash
pip install shellscriptor
```

YAML definition files require the optional `pyyaml` dependency:

```bash
pip install "shellscriptor[yaml]"
```

---

## Quick start

### Python

```python
from shellscriptor import create_script

script = create_script(
    name="greet",
    script_type="shell_src",
    data={
        "interpreter": "usr/bin/env bash",
        "parameter": {
            "name": {"type": "string", "description": "Name to greet"},
        },
        "body": 'echo "Hello, $NAME!"',
    },
)
print(script)
```

### CLI

```bash
# Write to stdout
shellscriptor greet.yaml

# Write to a file
shellscriptor greet.yaml --output dist/greet.sh
```

`greet.yaml`:

```yaml
name: greet
type: shell_src
script:
  interpreter: usr/bin/env bash
  parameter:
    name:
      type: string
      description: Name to greet
  body: 'echo "Hello, $NAME!"'
```

---

## Definition format

### Script definition (`ScriptDef`)

Top-level keys accepted by `create_script()` and by the CLI wrapper format:

| Key | Type | Description |
|---|---|---|
| `interpreter` | `str \| null` | Shebang path without the leading `#!`. `"usr/bin/env bash"` → `#!/usr/bin/env bash`. Omit for no shebang. |
| `flags` | `str \| null` | Arguments passed to `set` after the shebang, e.g. `"-euo pipefail"`. |
| `import` | `list[str]` | Names of functions to pull in from the `global_functions` pool passed to `create_script()`. |
| `function` | `dict[str, FunctionDef]` | Locally defined functions embedded in the script. |
| `parameter` | `dict[str, ParameterDef]` | Script-level parameters accepted on the command line (or from env vars in `shell_exec` mode). |
| `body` | body form | Main script body executed after argument parsing and validation. |
| `return` | `list[ReturnDef]` | Values written to stdout on exit (`shell_exec` only). |

### Function definition (`FunctionDef`)

Accepted inside `function` and by `create_function()`:

| Key | Type | Description |
|---|---|---|
| `parameter` | `dict[str, ParameterDef]` | Parameters accepted by the function. |
| `body` | body form | Function body. |
| `return` | `list[ReturnDef]` | Values written to stdout when the function returns. |

### Parameter definition (`ParameterDef`)

| Key | Type | Default | Description |
|---|---|---|---|
| `type` | `"string" \| "boolean" \| "integer" \| "array"` | **required** | Shell type. |
| `default` | `str \| list[str] \| null` | `null` | Default value. `null` with no explicit `required` field implies the parameter is required. |
| `required` | `bool \| null` | `null` | Explicit required flag. `null` → inferred from `default`. |
| `description` | `str` | `""` | Human-readable description for `--help` output. Triggers `--help`/`__usage__` generation when non-empty on any parameter. |
| `short` | `str` | `""` | Single-character short flag alias, e.g. `"o"` creates `-o` alongside `--<name>`. |
| `array_delimiter` | `str` | `" "` | Delimiter used when reading an array from a single environment variable (env-var fallback, `shell_exec` only). |
| `validation` | `ValidationDef \| null` | `null` | Optional validation rules. |

**Required vs optional — rules:**

| `required` | `default` | Behaviour |
|---|---|---|
| `null` (omitted) | `null` | Required — exits if missing. |
| `null` (omitted) | `"value"` | Optional — assigns default if missing. |
| `true` | any | Required — exits if missing; default ignored. |
| `false` | `"value"` | Optional — assigns default if missing. |
| `false` | `null` | Genuinely optional — no check emitted; variable may remain empty. |

### Validation rules (`ValidationDef`)

Nested inside `ParameterDef.validation`:

| Key | Type | Description |
|---|---|---|
| `enum` | `list[str]` | Exhaustive list of accepted values.  A value not in the list causes `exit 1`. |
| `path_existence` | `PathExistenceDef` | Assert the value is a path satisfying an existence condition. |
| `integer_range` | `IntegerRangeDef` | Assert the value is an integer within an inclusive numeric range. |
| `regex` | `str` | ERE pattern the full value must match.  Non-matching values cause `exit 1`. |
| `custom` | `list[str]` | Verbatim shell lines appended after all other checks. |

**`PathExistenceDef`**:

| Key | Type | Description |
|---|---|---|
| `must_exist` | `bool` | `true` → path must exist; `false` → path must not exist. |
| `type` | `"dir" \| "exec" \| "file" \| "symlink" \| null` | Kind of filesystem entry to test.  `null` accepts any entry. |

**`IntegerRangeDef`**:

| Key | Type | Description |
|---|---|---|
| `min` | `int \| null` | Inclusive lower bound.  `null` for no lower bound. |
| `max` | `int \| null` | Inclusive upper bound.  `null` for no upper bound. |

### Return value definition (`ReturnDef`)

| Key | Type | Description |
|---|---|---|
| `name` | `str` | Human-readable label used in log messages. |
| `variable` | `str` | Shell variable name whose value is written to stdout. |
| `type` | `"string" \| "boolean" \| "integer" \| "array"` | Type of the return value; drives encoding. |

---

## Script types

### `shell_src` — sourced library

Generates a lean script intended to be sourced (`. script.sh`).

- Functions and top-level `while/case` argument parsing are emitted.
- No logging boilerplate, no `trap`, no `tee`.
- Good for utility libraries and helper functions.

### `shell_exec` — executable script

Generates a self-contained, directly executable script with:

- **Entry/exit log markers** for the script and every function.
- **Tee log capture**: all output is mirrored to a temp file; when `$LOGFILE`
  is set, the capture is flushed to it on exit via the `__cleanup__` trap.
- **`$DEBUG` flag**: `set -x` is enabled when `DEBUG=true` is passed.
- **Env-var fallback**: when the script is called with no arguments, each
  parameter is read from the corresponding environment variable instead.

---

## Parameter types

| Type | Shell representation | Argparse | Env-var fallback |
|---|---|---|---|
| `string` | `VAR=""` | `--name) shift; VAR="$1"; shift;;` | `${VAR+defined}` one-liner |
| `integer` | `VAR=""` | Same as `string` (validation handles numeric check) | Same as `string` |
| `boolean` | `VAR=""` | `--flag) shift; VAR=true;;` (no value consumed) | `${VAR+defined}` one-liner |
| `array` | `VAR=()` | Reads all following non-`--` words into `VAR` | `IFS="<delim>" read -ra VAR` |

Variable names are derived from parameter names: hyphens become underscores, and
for script-level parameters (global scope) names are uppercased.
Function-local parameters stay lowercase.

---

## Argument parsing

Every script or function with parameters gets a `while [[ $# -gt 0 ]]; do … done`
loop.  Unknown flags emit `⛔ Unknown option: '<flag>'` to stderr and exit 1.
Positional (non-flag) arguments emit `⛔ Unexpected argument: '<arg>'` and exit 1.

### Required vs optional parameters

See the [parameter definition table](#parameter-definition-parameterdef) for the
full matrix.  The generated check for a required parameter:

```bash
[ -z "${OUTPUT_DIR-}" ] && { echo "⛔ Missing required argument 'OUTPUT_DIR'." >&2; exit 1; }
```

For an optional parameter with a default value:

```bash
[ -z "${COUNT-}" ] && { echo "ℹ️ Argument 'COUNT' set to default value '1'." >&2; COUNT="1"; }
```

### Short flag aliases

Set `short: "o"` to generate a `-o` arm alongside `--output-dir`:

```bash
--output-dir|-o) shift; OUTPUT_DIR="$1"; ...;;
```

### Help / usage output

When **any** parameter in a parameter set carries a non-empty `description`,
shellscriptor:

1. Generates a `__usage__()` function that prints a summary table to stderr
   and exits with code 0.
2. Adds a `--help|-h)` arm to the argparse loop that calls `__usage__`.

For script-level parameters `__usage__` is defined at the top of the script
(before all other functions).  For function parameters it is defined at the
start of the function body, before the argparse loop.

Example output:

```
Usage:
  -o, --output-dir (string): Directory to write results into
```

### Environment variable fallback (`shell_exec`)

When a `shell_exec` script is invoked with no arguments it reads parameters
from environment variables.  Variable names mirror the script-level variable
name (uppercase, hyphens → underscores).  The env-var block only imports a
variable when it is already set in the environment (`${VAR+defined}` test), so
unset variables are not silently turned into empty strings.

For array parameters the env var is split on `array_delimiter` using
`IFS` + `read -ra`.

---

## Validation

Validation checks are applied after argument parsing, in this order:

1. Missing-arg / default assignment
2. Enum membership
3. Path existence
4. Integer range
5. Regex pattern
6. Custom lines

For `array` parameters, checks 2–5 are run per element inside a
`for elem in "${VAR[@]}"; do … done` loop.

### Enum (allowed values)

```yaml
validation:
  enum: [read, write, delete]
```

Generated shell:

```bash
case "${MODE}" in
  "read"|"write"|"delete");;
  *) echo "⛔ Invalid value for argument '--MODE': '${MODE}'" >&2; exit 1;;
esac
```

### Path existence

```yaml
validation:
  path_existence:
    must_exist: true
    type: file
```

Generated shell (`must_exist: true`, `type: file`):

```bash
[ ! -f "${INPUT_FILE}" ] && { echo "⛔ File argument to parameter 'INPUT_FILE' not found: '${INPUT_FILE}'" >&2; exit 1; }
```

Available `type` values: `"file"` (`-f`), `"dir"` (`-d`), `"symlink"` (`-L`),
`"exec"` (`-x`), `null` (uses `-e`, accepts any entry).

`must_exist: false` inverts the check — the path must **not** exist.

### Integer range

```yaml
validation:
  integer_range:
    min: 1
    max: 100
```

Generates checks for both bounds independently (omit either for an open-ended
range).  `min` and `max` must satisfy `min <= max` when both are given.

### Regex pattern

```yaml
validation:
  regex: "^[a-z][a-z0-9_-]{2,31}$"
```

Uses `[[ "$VAR" =~ <pattern> ]]`:

```bash
[[ ! "${SLUG}" =~ ^[a-z][a-z0-9_-]{2,31}$ ]] && { echo "⛔ ..." >&2; exit 1; }
```

### Custom shell lines

```yaml
validation:
  custom:
    - '[ -w "${OUTPUT_DIR}" ] || { echo "⛔ Directory is not writable." >&2; exit 1; }'
```

Custom lines are appended verbatim after all other validation checks.

---

## Return values / structured output

`return` (or `return_` in Python) accepts a list of `ReturnDef` entries.
shellscriptor encodes return values so that callers can unpack them
unambiguously:

| Situation | Encoding | Caller idiom |
|---|---|---|
| Single scalar | `echo "$VAR"` | `val=$(fn)` |
| Single array | `printf '%s\0' "${VAR[@]}"` | `mapfile -d '' -t arr < <(fn)` |
| Multiple values (scalars and/or arrays) | Each value followed by `\0`; array elements delimited by `\x1F` internally | `IFS= read -r -d $'\0'` per value |

In `shell_exec` mode each return value also emits a write-log line.

---

## Embedded functions

Functions defined under `function` are emitted before the script body.
Each function is generated by `create_function()` and follows the same rules
as the top-level script: parameters get an argparse loop, validation checks
are emitted, and in `shell_exec` mode entry/exit log markers are added.

```yaml
function:
  greet:
    parameter:
      person:
        type: string
        required: true
        description: Name to greet
    body: 'echo "Hello, ${person}!"'
```

Generated:

```bash
greet() {
  echo "↪️ Function entry: greet" >&2
  __usage__() {
    echo "Usage:" >&2
    echo "  --person (string): Name to greet" >&2
    exit 0
  }
  local person=""
  while [[ $# -gt 0 ]]; do
    case $1 in
      --person) shift; person="$1"; ...; shift;;
      --help|-h) __usage__;;
      ...
    esac
  done
  [ -z "${person-}" ] && { echo "⛔ Missing required argument 'person'." >&2; exit 1; }
  echo "Hello, ${person}!"
  echo "↩️ Function exit: greet" >&2
}
```

---

## Global function pool

`create_script()` accepts an optional `global_functions` argument — a dict of
`FunctionDef`-compatible dicts keyed by function name.  Only functions listed
in `import` (or `import_`) are pulled in:

```python
GLOBALS = {
    "check_root": {"body": 'if [ "$(id -u)" -ne 0 ]; then echo "must be root" >&2; exit 1; fi'},
}

script = create_script(
    name="setup",
    script_type="shell_exec",
    data={"import": ["check_root"], "body": "check_root"},
    global_functions=GLOBALS,
)
```

---

## Logging

The `log()` function and related helpers generate consistent `echo … >&2`
statements.

| Function | Output |
|---|---|
| `log(msg)` | `echo "<msg>" >&2` |
| `log(msg, "info")` | `echo "ℹ️ <msg>" >&2` |
| `log(msg, "warn")` | `echo "⚠️ <msg>" >&2` |
| `log(msg, "error")` | `echo "❌ <msg>" >&2; return 1` |
| `log(msg, "critical")` | `echo "⛔ <msg>" >&2; exit 1` |
| `log_endpoint("fn", "function", "entry")` | `echo "↪️ Function entry: fn" >&2` |
| `log_arg_read("param", "VAR")` | `echo "📩 Read argument 'param': '${VAR}'" >&2` |
| `log_arg_write("result", "RESULT")` | `echo "📤 Write output 'result': '${RESULT}'" >&2` |

---

## Body sections

The `body` field is flexible.  All three forms are equivalent:

```python
# Plain string
body = 'echo hello\necho world'

# List of strings
body = ["echo hello", "echo world"]

# List of section dicts (YAML-friendly; only "content" is used)
body = [
    {"summary": "Greet", "content": "echo hello"},
    {"content": "echo world"},
]
```

Comments (`#`) and blank lines are stripped by default by `sanitize_code()`.

---

## CLI usage

```
shellscriptor DEFINITION_FILE [--output PATH] [--type {shell_src,shell_exec}] [--name NAME]
```

| Option | Description |
|---|---|
| `DEFINITION_FILE` | Path to a YAML or JSON definition file. |
| `--output`, `-o` | Write the generated script to this path (default: stdout). |
| `--type`, `-t` | Override the script type (`shell_src` or `shell_exec`). Required when the file does not contain a top-level `type` key. |
| `--name`, `-n` | Script name used in log messages.  Defaults to the file stem. |

### Definition file formats

**Wrapper format** (recommended for YAML):

```yaml
name: deploy
type: shell_exec
script:
  interpreter: usr/bin/env bash
  flags: "-euo pipefail"
  parameter:
    env:
      type: string
      required: true
      description: Target environment
  body: echo "Deploying to $ENV"
```

**Bare format** (must pass `--type` and optionally `--name` on the CLI):

```json
{
  "interpreter": "usr/bin/env bash",
  "parameter": {
    "env": {"type": "string", "required": true}
  },
  "body": "echo \"Deploying to $ENV\""
}
```

---

## Python API

### `create_script(name, data, script_type, global_functions=None) -> str`

Generate a complete shell script string.

```python
from shellscriptor import create_script

code = create_script(
    name="deploy",
    data={...},           # dict or ScriptDef
    script_type="shell_exec",
    global_functions={},  # optional pool
)
```

### `create_function(name, data, script_type) -> list[str]`

Generate lines for a single shell function definition.

```python
from shellscriptor import create_function

lines = create_function("greet", {"body": ['echo "hi"']}, script_type="shell_src")
```

### Low-level generators

These are exported from `shellscriptor` and can be combined freely to build
custom script snippets:

| Function | Returns |
|---|---|
| `create_validation_block(parameters, *, local, script_type)` | Validation lines for all parameters |
| `validate_variable(var_name, var_type, default, required, validations, script_type)` | Validation lines for one variable |
| `validate_missing_arg(var_name, var_type, default, script_type)` | One-liner: required-or-default check |
| `validate_enum(var_name, enum)` | `case` block for allowed values |
| `validate_path_existence(var_name, must_exist, path_type)` | `[ -f/-d/… ]` check |
| `validate_integer_range(var_name, min_val, max_val)` | Numeric bound checks |
| `validate_regex(var_name, pattern)` | `[[ =~ ]]` check |
| `create_output(returns, script_type)` | Output/return encoding lines |
| `log(msg, level, code, indent_level)` | `echo … >&2` command string |
| `log_arg_read(param_name, var_name)` | Read-log string |
| `log_arg_write(param_name, var_name)` | Write-log string |
| `log_endpoint(name, typ, stage)` | Entry/exit-log string |
| `indent(code, level)` | Indent a string or list of lines |
| `sanitize_code(code, ...)` | Strip comments, blank lines, trailing whitespace |

---

## Examples

### Required string parameter with short flag

```python
create_script(
    name="build",
    script_type="shell_exec",
    data={
        "interpreter": "usr/bin/env bash",
        "flags": "-euo pipefail",
        "parameter": {
            "output-dir": {
                "type": "string",
                "required": True,
                "description": "Directory to write build artefacts into",
                "short": "o",
            },
        },
        "body": 'mkdir -p "$OUTPUT_DIR"',
    },
)
```

### All four parameter types

```yaml
parameter:
  name:
    type: string
    required: true
    description: Name to greet
  count:
    type: integer
    default: "1"
    description: How many times to greet
  verbose:
    type: boolean
    description: Enable verbose output
  tags:
    type: array
    description: Additional tags
    array_delimiter: ","
```

### Enum + path validation

```yaml
parameter:
  action:
    type: string
    required: true
    validation:
      enum: [copy, move, delete]
  source:
    type: string
    required: true
    validation:
      path_existence:
        must_exist: true
        type: file
```

### Integer range

```yaml
parameter:
  workers:
    type: integer
    default: "4"
    validation:
      integer_range:
        min: 1
        max: 32
```

### Regex pattern

```yaml
parameter:
  slug:
    type: string
    required: true
    validation:
      regex: "^[a-z][a-z0-9_-]{2,31}$"
```

### Return values

```python
data = {
    "body": 'result="hello"',
    "return": [{"name": "result", "variable": "result", "type": "string"}],
}
```

Caller:

```bash
output=$(my_function --arg value)
```

### Genuinely optional parameter (no default, no required check)

```yaml
parameter:
  comment:
    type: string
    required: false   # variable may remain empty; no exit-1 check emitted
```
