How to share code between multiple subcommands

How can I reuse code within subcommands in order to make my command line app more maintainable? This question is mostly about which language features and paradigms to apply when working with the Argument Parser.

Here's my example code:

import ArgumentParser
import Foundation

class MyLogic {
    func LoadFileAndCreateDataModel() -> String {
        // Imagine some more logic here.
        return "Dummy"
    }
    
    func WriteData(_ data: String) {
        // Imagine writing to disk etc.
    }
}

@main
struct MyCommand: ParsableCommand {
    static var configuration = CommandConfiguration(
        subcommands: [CommandA.self, CommandB.self])
     
    struct CommandA: ParsableCommand {
        func run() throws {
            let logic = MyLogic()
            var data = logic.LoadFileAndCreateDataModel()
            data += "A"
            logic.WriteData(data)
        }
    }
     
    struct CommandB: ParsableCommand {
        func run() throws {
            let logic = MyLogic()
            var data = logic.LoadFileAndCreateDataModel()
            data += "B"
            logic.WriteData(data)
        }
    }
}

Both commands run through a set of actions ABC where A and C are always the same but B is different for each command. I would like to refactor my application so that I don't repeat myself and instead reuse this code somehow. I can think of multiple design patterns to solve this but I'm having trouble applying them to Swift. E.g. the Template Method or Strategy pattern.

Maybe, in C# I would create an abstract base class which implements ParsableCommand, put the shared code there and derive the subcommands from that class:

abstract class BaseCommand : ParsableCommand {
	public void Run() {
		// Do file logic here
		RunImpl(s)
		// Do Cleanup or other things
	}

	protected abstract void RunImpl(string s)

	protected void Write(string s) { }
}

class CommandA : BaseCommand {
	protected override void RunImpl(string s) {
		s += “A”
		base.Write(s)
	}
}

But this doesn't seem to be the idiomatic way for Swift and the Argument Parser, since I cannot create an inheritance tree from structs and protocols cannot have properties or an implementation.

So what I did instead was to put the shared code into a separate class, but this is not yet perfect, because now each command still needs to hardcode its dependency to this static-like helper class.

I would rather compose my commands by supplying them with a shared helper dependency via the constructor/init or by using methods defined in the outer type "MyCommand". In other languages it's a feature that nested types like CommandA can access their outer type directly, but in Swift this doesn't seem to be the case.

What would be the idiomatic way to compose commands with more complex reusable logic with the Argument Parser?

Thank you for any pointers!

Not answering the rest of your question, but protocols can have implementations (in protocol extensions).

1 Like

Thank you for clearing that up for me! Your input prompted me to research how extensions work. Here's my improved version:

import ArgumentParser

protocol MyBaseCommand: ParsableCommand {
    func runImpl(data: inout String)
}

extension MyBaseCommand {
    public func run() throws {
        var s = "Load complex data"
        runImpl(data: &s)
        print("Write complex data: " + s)
    }
}

struct MyCommand: ParsableCommand {
    static var configuration = CommandConfiguration(
        subcommands: [CommandA.self, CommandB.self])
     
    struct CommandA: MyBaseCommand {
        func runImpl(data: inout String) {
            data += "A"
        }
    }
     
    struct CommandB: MyBaseCommand {
        func runImpl(data: inout String) {
            data += "B"
        }
    }
}

This already solves my immediate duplication issue and feels much nicer. But I'm also still interested in hearing about other ways to structure ParsableCommand objects.

Depending on what work the shared code has to do, you may also choose to just put it in a (static) function to which you would pass the non-shared code as a closure:

@main
struct MyCommand: ParsableCommand {
    static var configuration = CommandConfiguration(subcommands: [CommandA.self, CommandB.self])
	
	static func doWork(_ handleData: (inout String) -> ()) {
        let logic = MyLogic()
        var data = logic.LoadFileAndCreateDataModel()
        handleData(&data)
        logic.WriteData(data)
	}
     
    struct CommandA: ParsableCommand {
        func run() throws {
            doWork { $0 += "A" }
        }
    }
     
    struct CommandB: ParsableCommand {
        func run() throws {
            doWork { $0 += "B" }
        }
    }
}

But of course, this only works when the shared code doesn’t need to access properties of the command’s instance.

1 Like

Thanks, that's also a handy approach! So, nested types can see static functions of their outer type. In other cases I could have a static function on a different type and work in a similar way.

I also found a tip online how to access the outer instances from within a command:

struct MyCommand: ParsableCommand {
    static var configuration = CommandConfiguration(subcommands: [CommandA.self, CommandB.self])
    
    @Flag
    var someFlag = false
    
    func doWork(_ handleData: (inout String) -> ()) {
        var data = ""
        handleData(&data)
        if someFlag {
            print("With flag: " + data)
        }
        else {
            print (data)
        }
    }
     
    struct CommandA: ParsableCommand {
        @OptionGroup
        var parent: MyCommand
        
        func run() throws {
            parent.doWork { $0 += "A" }
        }
    }
     
    struct CommandB: ParsableCommand {
        @OptionGroup
        var parent: MyCommand
        
        func run() throws {
            parent.doWork { $0 += "B" }
        }
    }
}

However, I don't understand this Argument Parser magic. In plain Swift, it seems I would have to pass the instance of the outer type into the inner types via init or some method. But here, magically, the 'parent' property is resolved simply because I added the @OptionGroup wrapper and the type is the outer type. Is this simply a special feature in Argument Parser which works by detecting this special case or what else might be behind it?