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