File & Archive Utilities

Two recipes for file packaging and runtime configuration - bundling project files into a zip archive and loading settings from JSON + CSV config files at startup.

Back to Kookbook


Zip Bundler

Modules: zip plugin · OS · Path · Konsol

Takes a source directory and an output zip path, walks the top level, and adds every file as a zip entry. After Zip:Close writes the archive to disk, the script reopens it and lists every entry to confirm the bundle - a full create-and-verify cycle in one run.

Key patterns:

Usage

minks zip_bundler.ks <output.zip> <source-dir>

# examples
minks zip_bundler.ks release.zip ./src
minks zip_bundler.ks assets.zip ./images

Sample output

=== Zip Bundler ===
Source : ./src
Output : release.zip
  added  main.ks
  added  utils.ks
  added  config.ks
  dir    tests/

Bundled 3 file(s) - skipped 1.
Verifying: release.zip
  [1] main.ks
  [2] utils.ks
  [3] config.ks
  [4] tests/
4 entries confirmed in release.zip.
Done.

Script

// zip_bundler.ks - bundle a directory's files into a zip archive
// Modules: zip plugin, OS, Path, Konsol
// Usage:  minks zip_bundler.ks <output.zip> <source-dir>
//
// Walks the top level of <source-dir>, adds every file to <output.zip>,
// marks subdirectory names as directory entries, then reopens the archive
// to list all entries and confirm the bundle.
//
// To bundle a nested tree, call the script once per subdirectory and
// pass the same output.zip (Zip:Open on an existing archive appends).

#include "zip"

Konsol:Print("=== Zip Bundler ===");

// ── Step 1: Read arguments ────────────────────────────────────────────────────
Var:List args;
OS:Args(args);

Var:Number argc;
List:Size(args, argc);

if (argc < 2) {
    Konsol:Print("Usage: minks zip_bundler.ks <output.zip> <source-dir>");
    Konsol:Exit(1);
}

Var:String outZip;
Var:String srcDir;
List:Get(0, args, outZip);
List:Get(1, args, srcDir);

Var:Boolean dirExists;
Path:IsDirectory(srcDir, dirExists);
if (!dirExists) {
    Konsol:Print("Not a directory: ${srcDir}");
    Konsol:Exit(1);
}

Konsol:Print("Source : ${srcDir}");
Konsol:Print("Output : ${outZip}");

// ── Step 2: Open (or create) the archive ─────────────────────────────────────
// Zip:Open creates the file if it does not exist; appends if it does.
// ZipException fires when the path is not writable or the file is corrupt.
Var:Number z;
Var:Boolean ok;

try {
    Zip:Open(outZip, z);
} catch (ZipException e) {
    Konsol:Print("Cannot open archive: ${e.message}");
    Konsol:Exit(1);
}

// ── Step 3: Walk the source directory and add entries ─────────────────────────
// OS:ListDirectory fills a List with entry names (basenames only, not full paths).
Var:List entries;
OS:ListDirectory(srcDir, entries);

Var:Number entryCount;
List:Size(entries, entryCount);

Var:Number added = 0;
Var:Number skipped = 0;

for (Number i = 0; i < entryCount; i++) {
    Var:String name;
    List:Get(i, entries, name);

    // Reconstruct the full filesystem path for Path:IsFile.
    Var:String fullPath;
    Path:Join(srcDir, name, fullPath);

    Var:Boolean isFile;
    Path:IsFile(fullPath, isFile);

    if (isFile) {
        // Zip:AddFile(handle, entryName, filePath, outOk)
        // entryName is the name the file gets inside the zip.
        Zip:AddFile(z, name, fullPath, ok);
        if (ok) {
            Konsol:Print("  added  ${name}");
            added = added + 1;
        } else {
            Var:String errMsg;
            Zip:Error(z, errMsg);
            Konsol:Print("  SKIP   ${name} (${errMsg})");
            skipped = skipped + 1;
        }
    } else {
        // Record the directory entry so extraction tools recreate the folder.
        Zip:AddDir(z, name, ok);
        Konsol:Print("  dir    ${name}/");
    }
}

// ── Step 4: Write the archive to disk ────────────────────────────────────────
// Zip:Close finalises all pending file reads and flushes to disk.
// Use Zip:Discard to abort without saving changes.
Zip:Close(z);
Konsol:Print("");
Konsol:Print("Bundled ${added} file(s) - skipped ${skipped}.");

// ── Step 5: Verify by reopening and listing entries ──────────────────────────
// Zip:Name(handle, index, outName) returns the entry name at a zero-based index.
// Zip:Count(handle, outN) gives the total number of entries.
Konsol:Print("Verifying: ${outZip}");

Zip:Open(outZip, z);

Var:Number total;
Zip:Count(z, total);

for (Number j = 0; j < total; j++) {
    Var:String entryName;
    Zip:Name(z, j, entryName);
    Var:Number num = j + 1;
    Konsol:Print("  [${num}] ${entryName}");
}

Zip:Close(z);
Konsol:Print("${total} entries confirmed in ${outZip}.");
Konsol:Print("Done.");


Config Reader

Modules: JSON · CSV · Dictionary · File · String · Konsol

Loads a two-layer configuration: base settings from app.json, then per-environment overrides from profiles.csv. The merged result is stored in a Dictionary so any part of your script can query a setting with a single Dictionary:Get call - no matter whether the value came from JSON or CSV.

Key patterns:

Ships with two sample files:

File Purpose
app.json Base settings - env, host, port, debug, timeout, logLevel, maxConnections
profiles.csv Per-environment overrides - rows for dev, staging, prod

Usage

# use env from app.json ("dev" by default)
minks config_reader.ks

# override env from the command line
minks config_reader.ks prod
minks config_reader.ks staging

Sample output - prod profile

=== Config Reader ===
Active env: prod

--- Active Configuration ---
env            : prod
host           : api.example.com
port           : 443
debug          : false
timeout        : 30s
logLevel       : info
maxConnections : 500
Done.

Sample files

app.json:

{
  "env": "dev",
  "host": "localhost",
  "port": 8080,
  "debug": true,
  "timeout": 30,
  "logLevel": "info",
  "maxConnections": 100
}

profiles.csv:

env,host,port,debug,timeout,maxConnections
dev,localhost,3000,true,5,10
staging,staging.example.com,443,false,15,50
prod,api.example.com,443,false,30,500

Script

// config_reader.ks - merge JSON base config with a CSV profile override
// Modules: JSON, CSV, Dictionary, File, String, Konsol
// Usage:  minks config_reader.ks [env]
//         env defaults to the "env" field in app.json
//
// Reads app.json for base settings, looks up a matching row in profiles.csv
// to override environment-specific values, then stores the merged result in
// a Dictionary so any part of your script can query it with Dictionary:Get.
//
// Sample files shipped alongside this script:
//   app.json      - base configuration
//   profiles.csv  - per-environment overrides (dev / staging / prod)

Konsol:Print("=== Config Reader ===");

// ── Step 1: Read the JSON base config ────────────────────────────────────────
// File:Open / ReadLine / EOF gives us the raw text; JSON:Parse builds a
// document we can query with dot-notation paths.
Var:Number jfh;
File:Open("app.json", "r", jfh);

Var:String jsonContent = "";
Var:String jline;
Var:Boolean jeof;

File:EOF(jfh, jeof);
while (!jeof) {
    File:ReadLine(jfh, jline);
    jsonContent = jsonContent + jline + "\n";
    File:EOF(jfh, jeof);
}
File:Close(jfh);

JSON:Parse(jsonContent, cfg);

// Pull every base value into typed variables.
Var:String baseEnv;
Var:String baseHost;
Var:Number basePort;
Var:String baseDebug;
Var:Number baseTimeout;
Var:String baseLogLevel;
Var:Number baseMaxConn;

JSON:Get("env", cfg, baseEnv);
JSON:Get("host", cfg, baseHost);
JSON:Get("port", cfg, basePort);
JSON:Get("debug", cfg, baseDebug);
JSON:Get("timeout", cfg, baseTimeout);
JSON:Get("logLevel", cfg, baseLogLevel);
JSON:Get("maxConnections", cfg, baseMaxConn);

// ── Step 2: Determine the active environment ──────────────────────────────────
// Use the first CLI arg if provided, otherwise fall back to app.json "env".
Var:List args;
OS:Args(args);

Var:Number argc;
List:Size(args, argc);

Var:String activeEnv = baseEnv;
if (argc >= 1) {
    List:Get(0, args, activeEnv);
}

Konsol:Print("Active env: ${activeEnv}");

// ── Step 3: Read the CSV profiles ────────────────────────────────────────────
// CSV:Parse builds a document; rows/cells are accessed by zero-based indices.
// Row 0 is the header row - data rows start at index 1.
Var:Number cfh;
File:Open("profiles.csv", "r", cfh);

Var:String csvContent = "";
Var:String cline;
Var:Boolean ceof;

File:EOF(cfh, ceof);
while (!ceof) {
    File:ReadLine(cfh, cline);
    csvContent = csvContent + cline + "\n";
    File:EOF(cfh, ceof);
}
File:Close(cfh);

CSV:Parse(csvContent, profiles);

Var:Number rowCount;
CSV:Rows(profiles, rowCount);

// ── Step 4: Find and apply the matching profile row ───────────────────────────
// Column layout (from the header row):
//   0=env  1=host  2=port  3=debug  4=timeout  5=maxConnections
Var:String profHost = baseHost;
Var:String profPort = "${basePort}";
Var:String profDebug = baseDebug;
Var:String profTimeout = "${baseTimeout}";
Var:String profMaxConn = "${baseMaxConn}";
Var:Boolean profileFound = false;

for (Number i = 1; i < rowCount; i++) {
    Var:String envName;
    CSV:Get(i, 0, profiles, envName);
    String:Trim(envName, envName);

    if (envName == activeEnv) {
        CSV:Get(i, 1, profiles, profHost);
        CSV:Get(i, 2, profiles, profPort);
        CSV:Get(i, 3, profiles, profDebug);
        CSV:Get(i, 4, profiles, profTimeout);
        CSV:Get(i, 5, profiles, profMaxConn);
        profileFound = true;
    }
}

CSV:Free(profiles);

if (!profileFound) {
    Konsol:Print("No profile found for env '${activeEnv}' - using base config.");
}

// ── Step 5: Merge into a Dictionary ──────────────────────────────────────────
// Store every setting in a single Dictionary so downstream code can query any key
// with Dictionary:Get without caring whether the value came from JSON or CSV.
Dictionary:New config;
Dictionary:Set("env", activeEnv, config);
Dictionary:Set("host", profHost, config);
Dictionary:Set("port", profPort, config);
Dictionary:Set("debug", profDebug, config);
Dictionary:Set("timeout", profTimeout, config);
Dictionary:Set("logLevel", baseLogLevel, config);
Dictionary:Set("maxConnections", profMaxConn, config);

// ── Step 6: Print the active configuration ────────────────────────────────────
Konsol:Print("");
Konsol:Print("--- Active Configuration ---");

Var:String val;
Dictionary:Get("env", config, val);             Konsol:Print("env            : ${val}");
Dictionary:Get("host", config, val);            Konsol:Print("host           : ${val}");
Dictionary:Get("port", config, val);            Konsol:Print("port           : ${val}");
Dictionary:Get("debug", config, val);           Konsol:Print("debug          : ${val}");
Dictionary:Get("timeout", config, val);         Konsol:Print("timeout        : ${val}s");
Dictionary:Get("logLevel", config, val);        Konsol:Print("logLevel       : ${val}");
Dictionary:Get("maxConnections", config, val);  Konsol:Print("maxConnections : ${val}");

Konsol:Print("Done.");


Back to Kookbook