Introducing CALPM

This might be of interest to the few of us that are interested in cmd-arg-lib "CAL". It has been tedious to experiment with CAL without package initialization and temporary installation support.

cmd-arg-lib-package-manager "CALPM" provides a command line utility, calpm, that has two subcommands

  • init - initialize a new package base on a template
  • install - install executables and associated completion scripts

Package Initialization

Setting up a new package for an executable product is not as simple as people might say, especially when libraries are imported. If you want to use Swift Argument Parser you have SPM's swift package init --type tool. But, naturally, SPM does not help if you want to try out CAL.

CALPM fills that void. It provides six templates that can be used to initialize a package that uses cmd-arg-lib:

  • opaque - A product without a help screen.
  • basic - A product with a help screen.
  • testing - A product with unit testing.
  • manpage - A product with a manual page.
  • simple-tree - A product with commands and subcommands.
  • stateful-tree - A product with stateful commands and subcommands.

You choose your template and set options to fill it. The result is a package that is set up and ready to be customized for your purposes. The generated packages have a number of example parameters to clarify how things interact.

Product installation

CALPM is not trying to replace Homebrew, of course. Rather calpm install is meant to speed up the edit, build, test cycle. For utilities written for personal use, however, t does in fact serve as a valid installer.

Example

Initialize, build, install and run a sample product with unit testing, a help screen and completion scripts.

> mkdir Demo && cd Demo
Demo> calpm init --with-completion --template testing
Demo> swift test
Demo> swift build -c release
Demo> calpm install --shell fish --shell zsh
Demo> demo -h
Demo> demo -uc2 wolf

Here is the initialized folder. It shows how to set up a main target and a support target for which you can set up tests.

[I] Demo> tree -L3
.
├── Package.swift
├── Sources
│   ├── Demo
│   │   └── Main.swift
│   └── Support
│       └── Support.swift
└── Tests
    └── DemoTests
        └── Tests.swift

Here is the filled out template, with completion, that you can edit and use. calm init has options if you want a less populated filled template.

// Copyright (c) <YEAR> <AUTHOR>

import CmdArgLib
import CmdArgLibMacros
import Support

@main
struct Main {

    @MainFunction(shadowGroups: ["upper lower"])
    private static func demo( 
        __generateFishCompletionScript fish: MetaFlag = MetaFlag(completionScriptFor: .fish, name: "demo", showElements: helpElements),
        __generateZshCompletionScript zsh: MetaFlag = MetaFlag(completionScriptFor: .zsh, name: "demo", showElements: helpElements),
        i__index index: Flag = false,
        u__upper upper: Flag = false,
        l__lower lower: Flag = false,
        c__count count: Count = 1,
        _ animals: [Animal],
        v__version version: MetaFlag = MetaFlag(string: "version 0.1.0"),
        h__help help: MetaFlag = MetaFlag(helpElements: helpElements),
    ) throws {
        try print(Support.showAnimals(animals, count: count, index: index, upper: upper, lower: lower))
    }

    private static let helpElements: [ShowElement] = [
        .text("DESCRIPTION\n", "Print lines containing the names of animals."),
        .synopsis("\nUSAGE\n"),
        .text("\nARGUMENT"),
        .parameter("animals", "The name of an animal (can be repeated)", .list(Animal.cases)),
        .text("\nOPTIONS"),
        .parameter("count", "The number time to repeat the line"),
        .parameter("index", "Prefix each line with an index"),
        .parameter("upper", "Print text in upper case"),
        .parameter("lower", "Print text in lower case"),
        .parameter("help", "Show this help message"),
        .parameter("version", "Show version information"),
        .text("\nNOTES:\n", note1),
        .text("\n", note2),
    ]

    private static let note1 = """
        The available animals are \(Animal.casesJoinedWith("and")).
        """

    private static let note2 = """
        The $S{upper} and $S{lower} options shadow each other. The last one encountered, if
        any, determines the case of the printed text.
        """
}

Here is the test file. It includes a test for an error to be thrown.

// Copyright (c) <YEAR> <AUTHOR>

import CmdArgLib
import Testing

@testable import Support

struct TeatShowAnimals {

    @Test func showAnimalsTest() throws {
        var result = try Support.showAnimals([.bear, .fox], count: 1, index: false)
        #expect(result == "bear and fox")

        result = try Support.showAnimals([.bear, .fox], count: 1, index: false, upper: true)
        #expect(result == "BEAR AND FOX")

        result = try Support.showAnimals([.bear, .fox], count: 1, index: false, lower: true)
        #expect(result == "bear and fox")

        result = try Support.showAnimals([.bear, .fox], count: 2, index: true)
        #expect(result == "1: bear and fox\n2: bear and fox")

        result = try Support.showAnimals([], count: 1, index: false)
        #expect(result == "")

        #expect(throws: Exception.errors( ["$T{count} must be between 1 and 3."])) {
            result = try Support.showAnimals([.bear, .fox], count: 5, index: false)
        }
    }
}

The build time was 6.2 seconds, and the binary size was 415,336 bytes.

If you don't like extensive examples - use Vim to edit them out "at the speed of thought". Seriously, there is an option to generate sparsely filled versions of some of the simpler templates. For the complex templates, like 'stateful-tree, extensive examples are essential.

Sidenote - SPM and SAP

I love SPM. And it just keeps getting better. When I started using macros to implement an argument parser, the build times on my Mac Pro were up to 180 seconds on a fresh build. Now they are down to around 8 seconds or less. :folded_hands:

That said, it would be nice if swift package init --type tool --enable-swift-testing produced a package with better examples. All I got was basically pointless:

SAP-Demo> swift package init --type tool --enable-swift-testing
... blah, blah

[I] SAP-Demo> tree -L3
.
├── Package.swift
└── Sources
    └── SAP-Demo
        └── SAP_Demo.swift

SAP_Demo.swift did not even use the import.

import ArgumentParser

@main
struct SAP_Demo: ParsableCommand {
    mutating func run() throws {
        print("Hello, world!")
    }
}

I know, it got Package.swift set up. But, IMHO, a little more substance would be helpful.

Besides that, build time was 10.9 seconds and the binary size was 1,562,856. That's a lot for a "tool" that prints "Hello, world!".

1 Like