CLI Tools & Automation

Ready-to-run starter scripts for the three most common CLI tasks. Copy any script, run it immediately, then extend it for your own use case.

Back to Kookbook


File Organizer

Sort every file in a directory into a subfolder named after its extension. Files with no extension land in no_ext/.

Modules: OS Path String List

Usage:

minks file_organizer.ks <directory>

Sample output:

Scanning 4 entries in './downloads'...
  report.pdf  →  pdf/
  photo.jpg   →  jpg/
  notes.txt   →  txt/
  resume.pdf  →  pdf/
Done. 4 file(s) organised.

Script:

// file_organizer.ks - Sort files in a directory into extension-based subfolders
// Usage: minks file_organizer.ks <directory>
//
// Example:
//   minks file_organizer.ks ./downloads
//
// Before:
//   downloads/report.pdf
//   downloads/photo.jpg
//   downloads/notes.txt
//   downloads/resume.pdf
//
// After:
//   downloads/pdf/report.pdf
//   downloads/pdf/resume.pdf
//   downloads/jpg/photo.jpg
//   downloads/txt/notes.txt
//
// Modules used: OS, Path, File, String, List

// ── 1. Read the target directory from the command line ────────────────────────
// OS:Args fills a pre-declared List:String with the positional script arguments.

List:New args:String;
OS:Args(args);

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

if (argc < 1) {
    Konsol:Print("Usage: minks file_organizer.ks <directory>");
    Konsol:Exit(1);
}

Var:String dir;
List:Get(0, args, dir);

// ── 2. Validate that the path is a directory ──────────────────────────────────

Var:Boolean isDir;
Path:IsDirectory(dir, isDir);

if (isDir == false) {
    Konsol:Print("Error: '${dir}' is not a directory.");
    Konsol:Exit(1);
}

// ── 3. List all entries in the directory ──────────────────────────────────────
// OS:ListDirectory fills the list with filenames (sorted); it does not include paths.

List:New entries:String;
OS:ListDirectory(dir, entries);

Var:Number total;
List:Size(entries, total);
Konsol:Print("Scanning ${total} entries in '${dir}'...");

// ── 4. Move each file into its extension subfolder ────────────────────────────
// Declare all working variables up front (KonsolScript scoping convention).

Var:Number i     = 0;
Var:Number moved = 0;
Var:String name;     // filename from the listing
Var:String src;      // full source path
Var:String ext;      // raw extension, e.g. ".pdf"
Var:String folder;   // clean folder name, e.g. "pdf"
Var:String subdir;   // full path to the target subfolder
Var:String dest;     // full destination path
Var:Boolean isFile;
Var:Boolean ok;

while (i < total) {
    // Build the full source path from the directory and filename.
    List:Get(i, entries, name);
    Path:Join(dir, name, src);

    // Skip subdirectories - only move regular files.
    Path:IsFile(src, isFile);

    if (isFile) {
        // Path:Extension returns the extension with its leading dot, e.g. ".txt".
        Path:Extension(name, ext);

        if (ext == "") {
            // Files with no extension go into a catch-all folder.
            folder = "no_ext";
        } else {
            // Strip the dot so ".pdf" becomes the folder name "pdf".
            String:Replace(ext, ".", "", folder);
        }

        // Create the subfolder only if it does not exist yet.
        Path:Join(dir, folder, subdir);
        Path:IsDirectory(subdir, isDir);
        if (isDir == false) {
            OS:MakeDirectory(subdir, ok);
        }

        // Move the file by renaming its path.
        Path:Join(subdir, name, dest);
        OS:Rename(src, dest);

        Konsol:Print("  ${name}  →  ${folder}/");
        moved = moved + 1;
    }

    i = i + 1;
}

Konsol:Print("Done. ${moved} file(s) organised.");


Build Runner

Read a task file line by line and execute echo, check, and run tasks. Exits with code 1 if any task fails - suitable for CI scripts.

Modules: File OS Path Regex List String

Usage:

minks build_runner.ks <tasks.txt>

Task file format (tasks.txt):

# Lines starting with # are comments and are skipped.
echo: <message>          - print a section header (no pass/fail)
check: <path>            - verify a file or directory exists
run: <shell command>     - run a command; non-zero exit code = failure

Sample tasks.txt:

# tasks.txt - sample task file for build_runner.ks
# Run with: minks build_runner.ks tasks.txt
#           (from inside the cli-tools-and-automation/ folder)
#
# Format:
#   echo: <message>        - section header (no pass/fail)
#   check: <path>          - verify a file or directory exists
#   run: <shell command>   - run a command; non-zero exit = failure

echo: Checking sample scripts are present
check: file_organizer.ks
check: build_runner.ks
check: log_analyzer.ks
check: sample.log

echo: Running a quick shell sanity check
run: echo hello from build_runner

echo: Done

Sample output:


── Checking sample scripts are present
  [OK]   check: file_organizer.ks
  [OK]   check: build_runner.ks
  [OK]   check: log_analyzer.ks
  [OK]   check: sample.log

── Running a quick shell sanity check
  [OK]   run: echo hello from build_runner

── Done

4/4 tasks passed.

Script:

// build_runner.ks - Lightweight task runner that reads a task file
// Usage: minks build_runner.ks <tasks.txt>
//
// Task file format (see tasks.txt for a ready-to-run example):
//   # Lines starting with # are comments and are skipped.
//   echo: <message>          - print a section header (no pass/fail)
//   check: <path>            - verify a file or directory exists
//   run: <shell command>     - run a command; non-zero exit code = failure
//
// The runner prints [OK] or [FAIL] for every check/run task and exits
// with code 1 if any task failed.
//
// Modules used: File, OS, Path, Regex, List, String

// ── 1. Read the task file path from the command line ─────────────────────────

List:New args:String;
OS:Args(args);

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

if (argc < 1) {
    Konsol:Print("Usage: minks build_runner.ks <tasks.txt>");
    Konsol:Exit(1);
}

Var:String taskFile;
List:Get(0, args, taskFile);

// ── 2. Verify the task file exists ───────────────────────────────────────────

Var:Boolean fileExists;
File:Exists(taskFile, fileExists);

if (fileExists == false) {
    Konsol:Print("Task file not found: ${taskFile}");
    Konsol:Exit(1);
}

// ── 3. Open the file and process each line ───────────────────────────────────
// Declare all working variables before the loop.

Var:Number fh;
File:Open(taskFile, "r", fh);

Var:Number passed   = 0;
Var:Number failed   = 0;
Var:String line;
Var:String trimmed;
Var:Boolean eof;
Var:Boolean isComment;
Var:Boolean pathOk;
Var:Number exitCode;
Var:Number gc;
Var:String verb;
Var:String value;

// Regex:Groups writes its captures into a pre-declared List:String.
// We call List:Clear before each use so stale results never bleed across lines.
List:New groups:String;

File:EOF(fh, eof);
while (eof == false) {
    File:ReadLine(fh, line);

    // Trim whitespace; skip blank lines entirely.
    String:Trim(line, trimmed);
    if (trimmed != "") {

        // Skip comment lines (any line whose first non-space character is #).
        Regex:Test("^#", trimmed, isComment);

        if (isComment == false) {
            // Parse "verb: value" with one regex.
            //   Group 1 - the verb: echo, check, or run
            //   Group 2 - everything after the colon and optional whitespace
            List:Clear(groups);
            Regex:Groups("^(echo|check|run):\\s*(.+)$", trimmed, groups);
            List:Size(groups, gc);

            // gc == 3 means: [full-match, group1, group2]
            if (gc >= 3) {
                List:Get(1, groups, verb);
                List:Get(2, groups, value);
                String:Lower(verb, verb);

                if (verb == "echo") {
                    // Section header - print it but do not score it.
                    Konsol:Print("");
                    Konsol:Print("── ${value}");

                } else {
                    if (verb == "check") {
                        // Path:Exists returns true for both files and directories.
                        Path:Exists(value, pathOk);
                        if (pathOk) {
                            Konsol:Print("  [OK]   check: ${value}");
                            passed = passed + 1;
                        } else {
                            Konsol:Print("  [FAIL] check: ${value}  (not found)");
                            failed = failed + 1;
                        }

                    } else {
                        // verb == "run" - OS:System runs the command and writes
                        // the exit code into the last argument.
                        OS:System(value, exitCode);
                        if (exitCode == 0) {
                            Konsol:Print("  [OK]   run: ${value}");
                            passed = passed + 1;
                        } else {
                            Konsol:Print("  [FAIL] run: ${value}  (exit ${exitCode})");
                            failed = failed + 1;
                        }
                    }
                }
            }
        }
    }

    File:EOF(fh, eof);
}
File:Close(fh);

// ── 4. Summary ───────────────────────────────────────────────────────────────

Var:Number total2;
total2 = passed + failed;
Konsol:Print("");
Konsol:Print("${passed}/${total2} tasks passed.");

if (failed > 0) {
    Konsol:Exit(1);
}


Log Analyzer

Parse a structured log file, count lines by severity level, and print the last five error messages with their timestamps.

Modules: File Regex String List Konsol

Usage:

minks log_analyzer.ks <logfile>

Expected log format:

2025-01-15 10:23:45 [INFO]  server started on port 8080
2025-01-15 10:24:12 [WARN]  memory usage above 80%
2025-01-15 10:24:45 [ERROR] database connection refused

Sample sample.log:

2025-01-15 10:23:40 [INFO]  application starting
2025-01-15 10:23:41 [INFO]  loading configuration from config.json
2025-01-15 10:23:42 [INFO]  connected to database
2025-01-15 10:23:45 [INFO]  server started on port 8080
2025-01-15 10:24:12 [WARN]  memory usage above 80%
2025-01-15 10:24:30 [INFO]  request GET /api/users 200
2025-01-15 10:24:45 [ERROR] database connection refused: timeout after 30s
2025-01-15 10:24:46 [INFO]  retrying connection (attempt 1)
2025-01-15 10:24:50 [INFO]  retrying connection (attempt 2)
2025-01-15 10:24:55 [ERROR] database connection refused: max retries exceeded
2025-01-15 10:25:00 [WARN]  falling back to read-only mode
2025-01-15 10:25:05 [INFO]  request GET /api/status 200
2025-01-15 10:25:30 [ERROR] failed to write session data: disk quota exceeded
2025-01-15 10:25:45 [INFO]  request GET /api/health 200
2025-01-15 10:26:00 [WARN]  disk usage above 90%
2025-01-15 10:26:10 [ERROR] unable to create temp file: /tmp/app_cache_001
2025-01-15 10:26:15 [INFO]  cleanup task started
2025-01-15 10:26:20 [INFO]  cleanup task finished - freed 2.1 GB
2025-01-15 10:26:25 [ERROR] audit log write failed: permission denied
2025-01-15 10:26:30 [INFO]  graceful shutdown initiated

Sample output:

Log summary: sample.log
  Total lines : 20
  INFO        : 12
  WARN        : 3
  ERROR       : 5

Recent errors:
  2025-01-15 10:24:55  database connection refused: max retries exceeded
  2025-01-15 10:25:30  failed to write session data: disk quota exceeded
  2025-01-15 10:26:10  unable to create temp file: /tmp/app_cache_001
  2025-01-15 10:26:25  audit log write failed: permission denied

Script:

// log_analyzer.ks - Parse a structured log file; report level counts + recent errors
// Usage: minks log_analyzer.ks <logfile>
//
// Expected log format (one entry per line):
//   2025-01-15 10:23:45 [INFO]  server started on port 8080
//   2025-01-15 10:24:12 [WARN]  memory usage above 80%
//   2025-01-15 10:24:45 [ERROR] database connection refused
//
// Try it with the bundled sample:
//   minks log_analyzer.ks sample.log
//
// Output:
//   - Total lines parsed
//   - Count per level (INFO / WARN / ERROR)
//   - Last 5 error messages with their timestamps
//
// Modules used: File, Regex, String, List, Konsol

// ── 1. Read the log file path from the command line ───────────────────────────

List:New args:String;
OS:Args(args);

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

if (argc < 1) {
    Konsol:Print("Usage: minks log_analyzer.ks <logfile>");
    Konsol:Exit(1);
}

Var:String logFile;
List:Get(0, args, logFile);

// ── 2. Verify the file exists ─────────────────────────────────────────────────

Var:Boolean fileExists;
File:Exists(logFile, fileExists);

if (fileExists == false) {
    Konsol:Print("File not found: ${logFile}");
    Konsol:Exit(1);
}

// ── 3. Open and parse the log line by line ────────────────────────────────────
// Pre-declare all working variables before the loop (KonsolScript convention).

Var:Number fh;
File:Open(logFile, "r", fh);

Var:Number totalLines = 0;
Var:Number infoCount  = 0;
Var:Number warnCount  = 0;
Var:Number errorCount = 0;

Var:String line;
Var:String trimmed;
Var:Boolean eof;
Var:Number gc;
Var:String ts;
Var:String level;
Var:String msg;
Var:String entry;

// Collect error messages so we can show the most recent ones at the end.
List:New recentErrors:String;

// Regex:Groups output list - always clear before calling Regex:Groups
// so results from the previous line do not bleed into the current one.
List:New groups:String;

File:EOF(fh, eof);
while (eof == false) {
    File:ReadLine(fh, line);
    String:Trim(line, trimmed);

    if (trimmed != "") {
        totalLines = totalLines + 1;

        // Match the log line and extract its three fields with a single regex.
        //
        // Pattern:
        //   ^                                          - start of line
        //   (\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})   - timestamp  (group 1)
        //   \s\[(INFO|WARN|ERROR)\]\s*                - level in brackets (group 2)
        //   (.+)$                                     - message    (group 3)
        //
        // Regex:Groups returns: index 0 = full match, 1+ = capture groups.
        // We check gc >= 4 (full match + 3 groups) before reading the groups.
        List:Clear(groups);
        Regex:Groups("^(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}) \\[(INFO|WARN|ERROR)\\]\\s*(.+)$", trimmed, groups);
        List:Size(groups, gc);

        if (gc >= 4) {
            List:Get(1, groups, ts);
            List:Get(2, groups, level);
            List:Get(3, groups, msg);

            if (level == "INFO") {
                infoCount = infoCount + 1;
            } else {
                if (level == "WARN") {
                    warnCount = warnCount + 1;
                } else {
                    // ERROR - count it and save the message for the summary.
                    errorCount = errorCount + 1;
                    List:Push("${ts}  ${msg}", recentErrors);
                }
            }
        }
    }

    File:EOF(fh, eof);
}
File:Close(fh);

// ── 4. Print the summary ──────────────────────────────────────────────────────

Konsol:Print("Log summary: ${logFile}");
Konsol:Print("  Total lines : ${totalLines}");
Konsol:Print("  INFO        : ${infoCount}");
Konsol:Print("  WARN        : ${warnCount}");
Konsol:Print("  ERROR       : ${errorCount}");

// Print the last 5 errors (or fewer if there aren't that many).
Var:Number errTotal;
List:Size(recentErrors, errTotal);

if (errTotal > 0) {
    Konsol:Print("");
    Konsol:Print("Recent errors:");

    // Clamp showFrom so we never read a negative index.
    Var:Number showFrom;
    showFrom = errTotal - 5;
    if (showFrom < 0) { showFrom = 0; }

    Var:Number j = showFrom;
    while (j < errTotal) {
        List:Get(j, recentErrors, entry);
        Konsol:Print("  ${entry}");
        j = j + 1;
    }
}


Back to Kookbook