From 08caf5bd6e6ea8a2a3518d12f00e08c94bffc4ef Mon Sep 17 00:00:00 2001 From: Panos Vagenas Date: Wed, 24 Jun 2026 14:59:33 +0200 Subject: [PATCH] add archive packaging - Add `pack()` Python API and `doclang pack` CLI for assembling `.dclx` archives from markup, optional pages, and assets - Generate OPC metadata automatically; drop `utils/pack-archive.sh` in favor of the toolkit implementation - Rebrand docs and package metadata from "validator" to "reference toolkit"; update spec to point to the DocLang Project org - Add packaging test suite covering default output, pages/assets modes, validation, and error cases Signed-off-by: Panos Vagenas --- CONTRIBUTING.md | 4 +- README.md | 14 ++- doclang/README.md | 44 ++++++++-- doclang/__init__.py | 5 +- doclang/_packaging.py | 184 ++++++++++++++++++++++++++++++++++++++++ doclang/cli.py | 127 +++++++++++++++++++++++++-- doclang/packaging.py | 49 +++++++++++ pyproject.toml | 2 +- spec.md | 2 +- tests/test_packaging.py | 142 +++++++++++++++++++++++++++++++ utils/pack-archive.sh | 65 -------------- 11 files changed, 552 insertions(+), 86 deletions(-) create mode 100644 doclang/_packaging.py create mode 100644 doclang/packaging.py create mode 100644 tests/test_packaging.py delete mode 100755 utils/pack-archive.sh diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a1badba..32ed39f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributing -Thanks for helping improve the DocLang standard and reference validator. +Thanks for helping improve the DocLang standard and reference toolkit. ## Prerequisites @@ -20,7 +20,7 @@ CI installs only the `ci` group (`uv sync --frozen --no-default-groups --group c ## Repository layout - **`spec.md`** — normative specification -- **`doclang/`** — reference validator (XSD, Schematron, CLI); see [doclang/README.md](./doclang/README.md) for package usage +- **`doclang/`** — reference toolkit (Python package, CLI); see [doclang/README.md](./doclang/README.md) for usage - **`reference/`** — source data for Appendix A (Excel, examples) - **`exports/`** — generated Word exports from `spec.md` - **`utils/`** — maintenance scripts (version sync, reference generation, DOCX export, release preparation) diff --git a/README.md b/README.md index ac8c772..92d3cda 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ **[DocLang](https://www.doclang.ai/) is the AI-native markup format for unstructured content** — including documents, images, and more. It maps cleanly to LLM tokens while preserving structure, semantics, layout, and geometry in a single, unambiguous representation. -This repository is the home of the normative specification and the reference validator for DocLang. If you build with LLMs and VLMs on real-world content, this is where the standard lives. +This repository is the home of the normative specification and the reference toolkit for DocLang. If you build with LLMs and VLMs on real-world content, this is where the standard lives. ## Specification @@ -24,20 +24,26 @@ The source of the specification is available in [spec.md](https://github.com/doc and exports to different formats can be found in the [exports/](https://github.com/doclang-project/doclang/tree/main/exports) directory. -## Reference Validator +## Reference Toolkit -You can install the validator from PyPI: +You can install the toolkit from PyPI: ```bash pip install doclang ``` -You can then validate a DocLang document as follows: +### Validation ```bash doclang validate -n my_document.dclg ``` +### Packaging + +```bash +doclang pack my_document.dclg +``` + For more details, see the [doclang/README.md](https://github.com/doclang-project/doclang/blob/main/doclang/README.md). ## Citation diff --git a/doclang/README.md b/doclang/README.md index baa318c..7ba6530 100644 --- a/doclang/README.md +++ b/doclang/README.md @@ -1,6 +1,6 @@ -# DocLang Validation +# DocLang Toolkit -Validate DocLang XML documents against XSD schema and Schematron rules. +Official Python toolkit for working with DocLang — CLI commands and library APIs. ## Installation @@ -8,15 +8,15 @@ Validate DocLang XML documents against XSD schema and Schematron rules. pip install doclang ``` -## Usage +## CLI -### Basic CLI Usage +### Validation ```bash doclang validate my_document.dclg ``` -### More CLI Usage Scenarios +#### More validation scenarios ```bash ## Inject DocLang namespace if document doesn't declare it: @@ -38,7 +38,26 @@ doclang validate my_document.dclg --quiet doclang --help ``` -### Python API +### Packaging + +```bash +doclang pack markup.dclg +``` + +#### More packaging scenarios + +```bash +doclang pack markup.dclg -o report.dclx +doclang pack markup.dclg --pages screenshots/ +doclang pack markup.dclg --page a.png --page b.png +doclang pack markup.dclg --asset chart.svg=exports/diagram.svg +doclang pack markup.dclg --assets payload/ +doclang pack markup.dclg --validate +``` + +## Python API + +### Validation ```python from doclang import validate, ValidationError @@ -52,6 +71,19 @@ except ValidationError as exc: print(f"{exc.schematron_errors=}") ``` +### Packaging + +```python +from doclang import pack, PackagingError + +path = pack( + "markup.dclg", + pages="screenshots/", + assets={"chart.svg": "exports/diagram.svg"}, +) +print(f"Created {path}") +``` + ## Validation Rules ### XSD Validation (doclang.xsd) diff --git a/doclang/__init__.py b/doclang/__init__.py index cbb74da..eff3a59 100644 --- a/doclang/__init__.py +++ b/doclang/__init__.py @@ -1,5 +1,6 @@ -"""DocLang reference validator.""" +"""DocLang reference toolkit.""" +from doclang.packaging import PackagingError, pack from doclang.validation import ValidationError, validate -__all__ = ["ValidationError", "validate"] +__all__ = ["PackagingError", "ValidationError", "pack", "validate"] diff --git a/doclang/_packaging.py b/doclang/_packaging.py new file mode 100644 index 0000000..2ce23a9 --- /dev/null +++ b/doclang/_packaging.py @@ -0,0 +1,184 @@ +"""Internal implementation for DocLang archive packaging.""" + +from __future__ import annotations + +import shutil +import tempfile +import zipfile +from collections.abc import Mapping, Sequence +from pathlib import Path +from typing import Union + +_CONTENT_TYPES_XML = """\ + + + + + + + + + +""" + +_RELS_XML = """\ + + + + +""" + +PagesInput = Union[ + str, + Path, + Sequence[Union[str, Path]], + Mapping[int, Union[str, Path]], +] + +AssetsInput = Union[ + str, + Path, + Mapping[str, Union[str, Path]], +] + + +class PackagingError(Exception): + """Raised when DocLang archive packaging fails.""" + + +def _require_file(path: Path, *, label: str) -> None: + if not path.is_file(): + raise PackagingError(f"{label} not found or not a file: {path}") + + +def _require_directory(path: Path, *, label: str) -> None: + if not path.is_dir(): + raise PackagingError(f"{label} not found or not a directory: {path}") + + +def _validate_archive_relative_path(path: str, *, label: str) -> None: + if not path or path.startswith("/") or "\\" in path: + raise PackagingError(f"Invalid {label} path: {path!r}") + parts = Path(path).parts + if ".." in parts or path in {".", ".."}: + raise PackagingError(f"Invalid {label} path: {path!r}") + + +def _copy_tree_into(source: Path, destination: Path) -> None: + destination.mkdir(parents=True, exist_ok=True) + for item in source.iterdir(): + target = destination / item.name + if item.is_dir(): + shutil.copytree(item, target, dirs_exist_ok=True) + else: + shutil.copy2(item, target) + + +def _place_document(stage: Path, document: Path) -> None: + _require_file(document, label="Document") + shutil.copy2(document, stage / "document.xml") + + +def _place_pages(stage: Path, pages: PagesInput) -> None: + pages_dir = stage / "pages" + if isinstance(pages, Mapping): + for page_number, source in pages.items(): + if not isinstance(page_number, int) or page_number < 1: + raise PackagingError(f"Page numbers must be positive integers, got {page_number!r}") + source_path = Path(source) + _require_file(source_path, label="Page file") + destination = pages_dir / f"{page_number}{source_path.suffix}" + destination.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(source_path, destination) + return + + if isinstance(pages, str | Path): + source_dir = Path(pages) + _require_directory(source_dir, label="Pages directory") + _copy_tree_into(source_dir, pages_dir) + return + + for index, source in enumerate(pages, start=1): + source_path = Path(source) + _require_file(source_path, label="Page file") + destination = pages_dir / f"{index}{source_path.suffix}" + destination.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(source_path, destination) + + +def _place_assets(stage: Path, assets: AssetsInput) -> None: + assets_dir = stage / "assets" + if isinstance(assets, Mapping): + for archive_path, source in assets.items(): + _validate_archive_relative_path(archive_path, label="asset") + source_path = Path(source) + _require_file(source_path, label="Asset file") + destination = assets_dir / archive_path + destination.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(source_path, destination) + return + + source_dir = Path(assets) + _require_directory(source_dir, label="Assets directory") + _copy_tree_into(source_dir, assets_dir) + + +def _write_opc_metadata(stage: Path) -> None: + (stage / "[Content_Types].xml").write_text(_CONTENT_TYPES_XML, encoding="utf-8") + rels_dir = stage / "_rels" + rels_dir.mkdir(parents=True, exist_ok=True) + (rels_dir / ".rels").write_text(_RELS_XML, encoding="utf-8") + + +def _should_exclude_zip_member(arcname: str) -> bool: + parts = arcname.split("/") + if "__MACOSX" in parts: + return True + name = parts[-1] + return name == ".DS_Store" or name.startswith("._") + + +def _create_zip(stage: Path, output: Path) -> None: + output.parent.mkdir(parents=True, exist_ok=True) + if output.exists(): + output.unlink() + with zipfile.ZipFile(output, "w", compression=zipfile.ZIP_DEFLATED) as archive: + for path in sorted(stage.rglob("*")): + if not path.is_file(): + continue + arcname = path.relative_to(stage).as_posix() + if _should_exclude_zip_member(arcname): + continue + archive.write(path, arcname) + + +def _pack( + document: Union[str, Path], + *, + output: Union[str, Path, None] = None, + pages: PagesInput | None = None, + assets: AssetsInput | None = None, + validate: bool = False, +) -> Path: + document_path = Path(document) + output_path = Path(output) if output is not None else document_path.with_suffix(".dclx") + + with tempfile.TemporaryDirectory() as temp_dir: + stage = Path(temp_dir) + _place_document(stage, document_path) + if pages is not None: + _place_pages(stage, pages) + if assets is not None: + _place_assets(stage, assets) + _write_opc_metadata(stage) + + if validate: + from doclang.validation import validate as validate_document + + validate_document(stage / "document.xml") + + _create_zip(stage, output_path) + + return output_path.resolve() diff --git a/doclang/cli.py b/doclang/cli.py index 0391131..a8ad1bc 100644 --- a/doclang/cli.py +++ b/doclang/cli.py @@ -1,8 +1,7 @@ """ -DocLang CLI - Command-line interface for XML validation. +DocLang CLI - Command-line interface for the DocLang toolkit. -Provides a user-friendly CLI using Typer for validating DocLang XML documents -against XSD schemas and Schematron rules. +Provides a user-friendly CLI using Typer for working with DocLang documents. """ import json @@ -13,13 +12,15 @@ import typer from doclang._schemas import _bundled_schema_paths +from doclang.packaging import PackagingError +from doclang.packaging import pack as pack_document from doclang.utils import _VERSION from doclang.validation import ValidationError from doclang.validation import validate as validate_document app = typer.Typer( name="doclang", - help="DocLang XML validation tool with XSD and Schematron support", + help="DocLang toolkit", add_completion=False, no_args_is_help=True, ) @@ -145,6 +146,122 @@ def validate( typer.echo("VALIDATION SUCCESSFUL") +def _parse_asset_mapping(value: str) -> tuple[str, Path]: + if "=" not in value: + raise typer.BadParameter(f"Expected ARCHIVE_PATH=SOURCE, got {value!r}") + archive_path, source = value.split("=", 1) + if not archive_path or not source: + raise typer.BadParameter(f"Expected ARCHIVE_PATH=SOURCE, got {value!r}") + return archive_path, Path(source) + + +@app.command( + context_settings={"help_option_names": ["-h", "--help"]}, + no_args_is_help=True, +) +def pack( + document: Path = typer.Argument(..., help="DocLang markup file (.dclg, .xml, …)", exists=True), + output: Path | None = typer.Option( + None, + "-o", + "--output", + help="Output .dclx file (default: same stem as document, .dclx extension)", + ), + pages_dir: Path | None = typer.Option( + None, + "--pages", + help="Directory of page images (1.png, 2.png, …)", + exists=True, + file_okay=False, + dir_okay=True, + ), + page_files: list[Path] | None = typer.Option( + None, + "--page", + help="Page image; repeat to add pages in order (renumbered as 1.ext, 2.ext, …)", + exists=True, + file_okay=True, + dir_okay=False, + ), + asset_mappings: list[str] | None = typer.Option( + None, + "--asset", + help="Asset mapping ARCHIVE_PATH=SOURCE; repeat for multiple", + ), + assets_dir: Path | None = typer.Option( + None, + "--assets", + help="Directory tree copied into assets/", + exists=True, + file_okay=False, + dir_okay=True, + ), + validate_before_pack: bool = typer.Option(False, "--validate", help="Validate document before packing"), + quiet: bool = typer.Option(False, "--quiet", "-q", help="Quiet mode (exit code only)"), +): + """ + Pack a DocLang markup file and optional media into a .dclx archive. + + DOCUMENT is copied to document.xml. Optional page images (--pages, --page) + are placed under pages/. Optional payload files (--assets, --asset) are + placed under assets/ for URIs referenced in the markup. OPC metadata + ([Content_Types].xml, _rels/.rels) is generated automatically. + + By default, writes .dclx next to the input file. + + Examples: + + doclang pack markup.dclg + doclang pack markup.dclg -o report.dclx --pages screenshots/ + doclang pack markup.dclg --page a.png --page b.png + doclang pack markup.dclg --asset chart.svg=exports/diagram.svg + doclang pack markup.dclg --assets payload/ --validate + """ + if pages_dir is not None and page_files: + typer.echo("Error: --pages and --page are mutually exclusive", err=True) + raise typer.Exit(1) + if assets_dir is not None and asset_mappings: + typer.echo("Error: --assets and --asset are mutually exclusive", err=True) + raise typer.Exit(1) + + output_path = output or document.with_suffix(".dclx") + + pages: Path | list[Path] | None + if pages_dir is not None: + pages = pages_dir + elif page_files: + pages = page_files + else: + pages = None + + assets: Path | dict[str, Path] | None + if assets_dir is not None: + assets = assets_dir + elif asset_mappings: + assets = dict(_parse_asset_mapping(value) for value in asset_mappings) + else: + assets = None + + try: + created = pack_document( + document, + output=output_path, + pages=pages, + assets=assets, + validate=validate_before_pack, + ) + except ValidationError as exc: + if not quiet: + typer.echo(str(exc), err=True) + raise typer.Exit(1) + except PackagingError as exc: + typer.echo(f"Error: {exc}", err=True) + raise typer.Exit(1) + + if not quiet: + typer.echo(f"Created {created}") + + def _version_callback(value: bool): """Show version and exit.""" if value: @@ -163,7 +280,7 @@ def main( help="Show version and exit", ), ): - """DocLang XML validation tool.""" + """DocLang toolkit.""" if __name__ == "__main__": diff --git a/doclang/packaging.py b/doclang/packaging.py new file mode 100644 index 0000000..4aebb8d --- /dev/null +++ b/doclang/packaging.py @@ -0,0 +1,49 @@ +"""Public packaging API for DocLang archives.""" + +from __future__ import annotations + +from collections.abc import Mapping, Sequence +from pathlib import Path +from typing import Union + +from doclang._packaging import PackagingError, _pack + +__all__ = ["PackagingError", "pack"] + + +def pack( + document: Union[str, Path], + *, + output: Union[str, Path, None] = None, + pages: (Union[str, Path] | Sequence[Union[str, Path]] | Mapping[int, Union[str, Path]] | None) = None, + assets: (Union[str, Path] | Mapping[str, Union[str, Path]] | None) = None, + validate: bool = False, +) -> Path: + """Pack a DocLang markup file and optional media into a ``.dclx`` OPC archive. + + ``document`` is copied to ``document.xml`` inside the archive. OPC metadata + (``[Content_Types].xml``, ``_rels/.rels``) is generated automatically. + + By default, writes ``.dclx`` next to the input file. Pass ``output`` + to choose a different path. + + ``pages`` may be a directory (copied into ``pages/``), a sequence of image + paths (renumbered as ``1.ext``, ``2.ext``, …), or a mapping of page number + to image path. + + ``assets`` may be a directory (copied into ``assets/``) or a mapping of + archive-relative asset path to source file. + + Returns the resolved path to the created archive. + + Raises :class:`PackagingError` on packaging failure. + Raises :class:`~doclang.ValidationError` when ``validate=True`` and the + document fails validation. + """ + return _pack( + document, + output=output, + pages=pages, + assets=assets, + validate=validate, + ) diff --git a/pyproject.toml b/pyproject.toml index aba8a91..e7bd9a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta" [project] name = "doclang" version = "0.6.0" # DO NOT EDIT MANUALLY, updated automatically -description = "DocLang reference validator" +description = "DocLang reference toolkit" readme = "README.md" requires-python = ">=3.10" license = "Apache-2.0" diff --git a/spec.md b/spec.md index d03e1cc..109d7e5 100644 --- a/spec.md +++ b/spec.md @@ -258,7 +258,7 @@ Non-normative recommendation guidelines are covered in [Recommendations](#recomm Planned extensions are discussed in [Future Extensions](#future-extensions). -Machine-checkable conformance is defined by the [DocLang reference validator](https://github.com/doclang-project). +A reference toolkit for DocLang is provided by the [DocLang Project](https://github.com/doclang-project). ## Usage Examples diff --git a/tests/test_packaging.py b/tests/test_packaging.py new file mode 100644 index 0000000..c5e107b --- /dev/null +++ b/tests/test_packaging.py @@ -0,0 +1,142 @@ +"""Tests for DocLang archive packaging.""" + +from __future__ import annotations + +import zipfile +from pathlib import Path + +import pytest + +from doclang import PackagingError, ValidationError, pack + +REPO_ROOT = Path(__file__).resolve().parents[1] +ARCHIVE_DEMO = REPO_ROOT / "examples" / "archive-demo" +VALID_DIR = Path(__file__).parent / "data" / "valid" + + +def _zip_members(archive_path: Path) -> set[str]: + with zipfile.ZipFile(archive_path) as archive: + return set(archive.namelist()) + + +def test_pack_markup_only_default_output(tmp_path: Path) -> None: + document = tmp_path / "markup.dclg" + document.write_text((ARCHIVE_DEMO / "document.xml").read_text(encoding="utf-8"), encoding="utf-8") + + created = pack(document) + + assert created == document.with_suffix(".dclx").resolve() + members = _zip_members(created) + assert "document.xml" in members + assert "[Content_Types].xml" in members + assert "_rels/.rels" in members + assert not any(member.startswith("pages/") for member in members) + + +def test_pack_markup_only_explicit_output(tmp_path: Path) -> None: + document = ARCHIVE_DEMO / "document.xml" + output = tmp_path / "report.dclx" + + created = pack(document, output=output) + + assert created == output.resolve() + members = _zip_members(created) + assert "document.xml" in members + + +def test_pack_with_pages_directory(tmp_path: Path) -> None: + document = ARCHIVE_DEMO / "document.xml" + output = tmp_path / "demo.dclx" + + pack(document, output=output, pages=ARCHIVE_DEMO / "pages") + + members = _zip_members(output) + assert "pages/1.png" in members + assert "pages/3.png" in members + + +def test_pack_with_pages_sequence(tmp_path: Path) -> None: + document = VALID_DIR / "ok_description_element_head.dclg" + page_one = ARCHIVE_DEMO / "pages" / "1.png" + page_two = ARCHIVE_DEMO / "pages" / "3.png" + output = tmp_path / "ordered.dclx" + + pack(document, output=output, pages=[page_one, page_two]) + + members = _zip_members(output) + assert "pages/1.png" in members + assert "pages/2.png" in members + + +def test_pack_with_pages_mapping(tmp_path: Path) -> None: + document = VALID_DIR / "ok_description_element_head.dclg" + page_one = ARCHIVE_DEMO / "pages" / "1.png" + page_three = ARCHIVE_DEMO / "pages" / "3.png" + output = tmp_path / "mapped.dclx" + + pack(document, output=output, pages={1: page_one, 3: page_three}) + + members = _zip_members(output) + assert "pages/1.png" in members + assert "pages/3.png" in members + assert "pages/2.png" not in members + + +def test_pack_with_asset_mapping(tmp_path: Path) -> None: + document = VALID_DIR / "ok_description_element_head.dclg" + asset_source = ARCHIVE_DEMO / "pages" / "1.png" + output = tmp_path / "assets.dclx" + + pack( + document, + output=output, + assets={"chart.svg": asset_source, "img/sample.png": asset_source}, + ) + + members = _zip_members(output) + assert "assets/chart.svg" in members + assert "assets/img/sample.png" in members + + +def test_pack_with_assets_directory(tmp_path: Path) -> None: + document = VALID_DIR / "ok_description_element_head.dclg" + assets_dir = tmp_path / "payload" + nested = assets_dir / "img" + nested.mkdir(parents=True) + source = ARCHIVE_DEMO / "pages" / "1.png" + (assets_dir / "chart.svg").write_bytes(source.read_bytes()) + (nested / "sample.png").write_bytes(source.read_bytes()) + output = tmp_path / "assets-dir.dclx" + + pack(document, output=output, assets=assets_dir) + + members = _zip_members(output) + assert "assets/chart.svg" in members + assert "assets/img/sample.png" in members + + +def test_pack_validate_invalid_document(tmp_path: Path) -> None: + document = Path(__file__).parent / "data" / "invalid" / "nok_summary_in_doclang.dclg" + output = tmp_path / "invalid.dclx" + + with pytest.raises(ValidationError): + pack(document, output=output, validate=True) + + assert not output.exists() + + +def test_pack_missing_document(tmp_path: Path) -> None: + with pytest.raises(PackagingError, match="Document not found"): + pack(tmp_path / "missing.dclg") + + +def test_pack_invalid_asset_path(tmp_path: Path) -> None: + document = VALID_DIR / "ok_description_element_head.dclg" + asset_source = ARCHIVE_DEMO / "pages" / "1.png" + + with pytest.raises(PackagingError, match="Invalid asset path"): + pack( + document, + output=tmp_path / "bad-asset.dclx", + assets={"../escape.svg": asset_source}, + ) diff --git a/utils/pack-archive.sh b/utils/pack-archive.sh deleted file mode 100755 index 07e45f8..0000000 --- a/utils/pack-archive.sh +++ /dev/null @@ -1,65 +0,0 @@ -#!/usr/bin/env bash -# Pack a DocLang archive directory into a .dclx OPC ZIP file. -# Usage: ./utils/pack-archive.sh [output-file] - -set -euo pipefail - -if [[ $# -lt 1 ]]; then - echo "Usage: $0 [output-file]" >&2 - exit 1 -fi - -SRC=$(cd "$1" && pwd) -NAME=$(basename "$SRC") -OUT=${2:-"${NAME}.dclx"} - -if [[ ! -f "$SRC/document.xml" ]]; then - echo "Error: $SRC/document.xml not found" >&2 - exit 1 -fi - -if [[ "$OUT" != /* ]]; then - OUT="$(pwd)/$OUT" -fi - -STAGE=$(mktemp -d) -trap 'rm -rf "$STAGE"' EXIT - -cp -R "$SRC"/. "$STAGE"/ - -if [[ ! -f "$STAGE/[Content_Types].xml" ]]; then - cat > "$STAGE/[Content_Types].xml" <<'EOF' - - - - - - - - - -EOF -fi - -if [[ ! -f "$STAGE/_rels/.rels" ]]; then - mkdir -p "$STAGE/_rels" - cat > "$STAGE/_rels/.rels" <<'EOF' - - - - -EOF -fi - -rm -f "$OUT" -( - cd "$STAGE" - zip -r "$OUT" . \ - -x "*.DS_Store" \ - -x "__MACOSX/*" \ - -x "*/._*" \ - -x "._*" -) -echo "Created $OUT"