🍶 Swift-based utility for managing command execution with dependencies and conditions, inspired by Make.
MIT License
A Swift-based utility for managing command execution with dependencies and conditions, inspired by Make.
struct Commands: SakeApp {
public static var hello: Command {
Command(
run: { context in
print("Hello, world!")
}
)
}
}
❯ sake hello
Hello, world!
⚠️ Note: Sake is still under active development and is not yet production-ready. Use with caution in production environments.
Installation • Getting Started • Commands • Features In Detail • Configuration • Advanced Usage
Sake is designed for Swift developers who prefer to stay within the Swift ecosystem for managing command execution, rather than switching to shell scripting or using tools like Make. By leveraging Swift, Sake ensures type safety, readability, and maintainability in defining commands, making it an intuitive solution for automating project tasks.
Currently only macOS is supported and requires Swift 5.10 (Xcode 15.3) or higher.
brew install kattouf/sake/sake
mint install kattouf/Sake
mise use -g spm:kattouf/Sake
Download the binary for your platform from the releases page, and place it in your executable path.
To start using Sake, follow these steps:
Initialize a new SakeApp:
Run the following command to generate a new SakeApp template in the current directory:
sake init
This will create a new project structure in the SakeApp
directory with a basic Sakefile.swift
containing a simple command.
Inspect the generated Sakefile.swift
:
Navigate to the SakeApp
directory and open Sakefile.swift
to see the pre-defined hello
command:
import Foundation
import Sake
@main
@CommandGroup
struct Commands: SakeApp {
public static var hello: Command {
Command(
run: { _ in
print("Hello, world!")
}
)
}
}
This command prints "Hello, world!" to the console.
Run your first command:
To execute the hello
command, run:
sake hello
This will print "Hello, world!" in your terminal.
When defining commands in Sake, there are a few important rules to follow:
Visibility: Only public
commands are visible for execution. This allows you to define internal commands that are not meant to be run directly. By marking some commands as non-public, you can control which commands are exposed for execution.
Static declaration: Only static
commands are supported in Sake. All commands must be declared as static
within the command group or main structure.
Sake allows you to organize commands into groups for better structure and maintainability. This is especially useful when you have multiple related commands and want to keep them organized in logical groupings.
You can define command groups by using the @CommandGroup
attribute for each structure. All command groups should be listed in the main structure that conforms to the SakeApp
protocol. For example:
import Foundation
import Sake
@main
@CommandGroup
struct Commands: SakeApp {
static let configuration = SakeAppConfiguration(
commandGroups: [BuildCommands.self, TestCommands.self] // List all command groups here
)
}
@CommandGroup
struct BuildCommands {
public static var build: Command {
Command(description: "Build the project") { _ in
print("Building the project...")
}
}
}
@CommandGroup
struct TestCommands {
public static var test: Command {
Command(description: "Run tests") { _ in
print("Running tests...")
}
}
}
In this example, the commands are organized into BuildCommands
and TestCommands
groups. Each group is annotated with @CommandGroup
, and all groups are listed in the main Commands
structure that conforms to SakeApp
.
When running Sake, you can execute commands from these groups as usual:
sake build
sake test
skipIf
and dependencies
Sake provides two powerful features for managing command execution: skipIf
and dependencies
.
skipIf
The skipIf
feature allows you to define conditions under which a command will be skipped. This is useful when you want to avoid running a command in certain scenarios. The skipIf
closure receives the command context (arguments and environment) and returns true
if the command should be skipped.
For example:
Command(
description: "Deploy the project",
skipIf: { context in
// Skip the command if deployment is not needed
return !shouldDeploy()
}
) { _ in
print("Deploying the project...")
}
In this example, the deploy
command will only run if the shouldDeploy()
function returns true
.
dependencies
The dependencies
feature allows you to define commands that must be run before the main command. Dependencies ensure that any prerequisite commands are executed in the correct order.
For example:
Command(
description: "Build the project",
dependencies: [clean]
) { _ in
print("Building the project...")
}
In this example, the clean
command will always be executed before the build
command. This ensures that the project is cleaned before building.
skipIf
and dependencies
You can combine both features in a single command to control when the command should run and ensure all dependencies are executed:
Command(
description: "Run tests",
dependencies: [build],
skipIf: { context in
return !shouldRunTests()
}
) { _ in
print("Running tests...")
}
In this example, the tests
command will only run if shouldRunTests()
returns true
, and it will ensure that the build
command is executed beforehand.
Sake supports automatic conversion of command names to different case styles. You can specify the conversion strategy via configuration or environment variables.
Available conversion strategies:
snake_case
.kebab-case
.You can configure this in the .sake.yml
file:
case_converting_strategy: toSnakeCase
The command name conversion is applied when execute or listing commands.
Sake provides flexible options for configuration, which can be set via:
.sake.yml
): Defines default settings for your project.You can define configuration options in the .sake.yml
file, located in the root of your project. For example:
case_converting_strategy: toSnakeCase
sake_app_path: some/path
Sake also supports configuration via environment variables. The following environment variables are available:
SAKE_CONFIG_PATH
: Path to the .sake.yml
file.SAKE_APP_PATH
: Path to the SakeApp package.For example, you can set these variables in your environment:
export SAKE_CONFIG_PATH="/path/to/.sake.yml"
export SAKE_APP_PATH="/path/to/SakeApp"
Configuration is resolved in the following order:
.sake.yml
file.In Sake, you can use additional Swift libraries to enhance your commands, such as parsing arguments or executing CLI commands. Below is an example of how you can integrate ArgumentParser
for argument parsing and SwiftShell
for running CLI commands.
import ArgumentParser
import Foundation
import Sake
import SwiftShell
@main
@CommandGroup
struct Commands: SakeApp {
static let configuration = SakeAppConfiguration(
commandGroups: [BrewCommands.self]
)
public static var lint: Command {
Command(
description: "Run SwiftLint",
dependencies: [BrewCommands.ensureSwiftLintInstalled]
) { context in
struct Arguments: ParsableArguments {
@Flag(name: .shortAndLong, help: "Quite mode")
var quite: Bool = false
}
let arguments: Arguments = try Arguments.parse(context.arguments)
if arguments.quite {
try run("swiftlint")
} else {
try runAndPrint("swiftlint")
}
}
}
}
@CommandGroup
struct BrewCommands {
static var ensureSwiftLintInstalled: Command {
Command(
skipIf: { _ in
run("which", "swiftlint").succeeded
},
run: { _ in
print("Installing SwiftLint...")
try runAndPrint("brew", "install", "swiftlint")
}
)
}
}
Argument Parsing:
In the lint
command, we use ArgumentParser
to define a flag (--quite
or -q
). This flag can be passed by the user when running the command (sake lint --quite
). We parse the arguments using Arguments.parse(context.arguments)
.
CLI Command Execution:
We use SwiftShell
to execute the swiftlint
command. Depending on whether the --quite
flag is set, the command is either run silently (run
) or with output to the console (runAndPrint
).
You can easily integrate other tools or libraries into your SakeApp
by adding them to the Package.swift
file. In this example, the ArgumentParser
and SwiftShell
libraries are used, but you're free to choose the tools you prefer.
For example, to include ArgumentParser
and SwiftShell
in your SakeApp
, add the following dependencies to your SakeApp/Package.swift
:
dependencies: [
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.2.0"),
.package(url: "https://github.com/kareman/SwiftShell", from: "5.1.0")
]
This setup allows you to flexibly use different libraries and tools in your project, just like in any other Swift package.
I’m open to suggestions and would be happy to receive any reports, questions, or pull requests. Feel free to contribute by opening issues or submitting pull requests to help improve Sake!
Sake is released under the MIT License. See the LICENSE file for more details.