feat: add optional variadic args to all commands (#8)

Allow callers to pass additional systemctl flags (e.g. --no-block,
--force) via variadic string args on every exported function.

This is backward-compatible: existing callers without extra args
continue to work unchanged.

Introduces a prepareArgs helper to centralize argument construction,
replacing the duplicated args/UserMode pattern across all functions.

Closes #2
This commit is contained in:
2026-02-26 11:03:53 -05:00
committed by GitHub
parent d38136c0dc
commit 22132919e5
6 changed files with 193 additions and 195 deletions

View File

@@ -13,10 +13,10 @@ import (
func TestErrorFuncs(t *testing.T) {
errFuncs := []func(ctx context.Context, unit string, opts Options) error{
Enable,
Disable,
Restart,
Start,
func(ctx context.Context, unit string, opts Options) error { return Enable(ctx, unit, opts) },
func(ctx context.Context, unit string, opts Options) error { return Disable(ctx, unit, opts) },
func(ctx context.Context, unit string, opts Options) error { return Restart(ctx, unit, opts) },
func(ctx context.Context, unit string, opts Options) error { return Start(ctx, unit, opts) },
}
errCases := []struct {
unit string

View File

@@ -12,8 +12,10 @@ import (
// files, and recreate the entire dependency tree. While the daemon is being
// reloaded, all sockets systemd listens on behalf of user configuration will
// stay accessible.
func DaemonReload(ctx context.Context, opts Options) error {
return daemonReload(ctx, opts)
//
// Any additional arguments are passed directly to the systemctl command.
func DaemonReload(ctx context.Context, opts Options, args ...string) error {
return daemonReload(ctx, opts, args...)
}
// Reenables one or more units.
@@ -21,8 +23,10 @@ func DaemonReload(ctx context.Context, opts Options) error {
// This removes all symlinks to the unit files backing the specified units from
// the unit configuration directory, then recreates the symlink to the unit again,
// atomically. Can be used to change the symlink target.
func Reenable(ctx context.Context, unit string, opts Options) error {
return reenable(ctx, unit, opts)
//
// Any additional arguments are passed directly to the systemctl command.
func Reenable(ctx context.Context, unit string, opts Options, args ...string) error {
return reenable(ctx, unit, opts, args...)
}
// Disables one or more units.
@@ -30,8 +34,10 @@ func Reenable(ctx context.Context, unit string, opts Options) error {
// This removes all symlinks to the unit files backing the specified units from
// the unit configuration directory, and hence undoes any changes made by
// enable or link.
func Disable(ctx context.Context, unit string, opts Options) error {
return disable(ctx, unit, opts)
//
// Any additional arguments are passed directly to the systemctl command.
func Disable(ctx context.Context, unit string, opts Options, args ...string) error {
return disable(ctx, unit, opts, args...)
}
// Enable one or more units or unit instances.
@@ -40,17 +46,20 @@ func Disable(ctx context.Context, unit string, opts Options) error {
// the indicated unit files. After the symlinks have been created, the system
// manager configuration is reloaded (in a way equivalent to daemon-reload),
// in order to ensure the changes are taken into account immediately.
func Enable(ctx context.Context, unit string, opts Options) error {
return enable(ctx, unit, opts)
//
// Any additional arguments are passed directly to the systemctl command.
func Enable(ctx context.Context, unit string, opts Options, args ...string) error {
return enable(ctx, unit, opts, args...)
}
// Check whether any of the specified units are active (i.e. running).
//
// Returns true if the unit is active, false if inactive or failed.
// Also returns false in an error case.
func IsActive(ctx context.Context, unit string, opts Options) (bool, error) {
result, err := isActive(ctx, unit, opts)
return result, err
//
// Any additional arguments are passed directly to the systemctl command.
func IsActive(ctx context.Context, unit string, opts Options, args ...string) (bool, error) {
return isActive(ctx, unit, opts, args...)
}
// Checks whether any of the specified unit files are enabled (as with enable).
@@ -62,15 +71,17 @@ func IsActive(ctx context.Context, unit string, opts Options) (bool, error) {
//
// See https://www.freedesktop.org/software/systemd/man/systemctl.html#is-enabled%20UNIT%E2%80%A6
// for more information
func IsEnabled(ctx context.Context, unit string, opts Options) (bool, error) {
result, err := isEnabled(ctx, unit, opts)
return result, err
//
// Any additional arguments are passed directly to the systemctl command.
func IsEnabled(ctx context.Context, unit string, opts Options, args ...string) (bool, error) {
return isEnabled(ctx, unit, opts, args...)
}
// Check whether any of the specified units are in a "failed" state.
func IsFailed(ctx context.Context, unit string, opts Options) (bool, error) {
result, err := isFailed(ctx, unit, opts)
return result, err
//
// Any additional arguments are passed directly to the systemctl command.
func IsFailed(ctx context.Context, unit string, opts Options, args ...string) (bool, error) {
return isFailed(ctx, unit, opts, args...)
}
// Mask one or more units, as specified on the command line. This will link
@@ -79,26 +90,33 @@ func IsFailed(ctx context.Context, unit string, opts Options) (bool, error) {
// Notably, Mask may return ErrDoesNotExist if a unit doesn't exist, but it will
// continue masking anyway. Calling Mask on a non-existing masked unit does not
// return an error. Similarly, see Unmask.
func Mask(ctx context.Context, unit string, opts Options) error {
return mask(ctx, unit, opts)
//
// Any additional arguments are passed directly to the systemctl command.
func Mask(ctx context.Context, unit string, opts Options, args ...string) error {
return mask(ctx, unit, opts, args...)
}
// Stop and then start one or more units specified on the command line.
// If the units are not running yet, they will be started.
func Restart(ctx context.Context, unit string, opts Options) error {
return restart(ctx, unit, opts)
//
// Any additional arguments are passed directly to the systemctl command.
func Restart(ctx context.Context, unit string, opts Options, args ...string) error {
return restart(ctx, unit, opts, args...)
}
// Show a selected property of a unit. Accepted properties are predefined in the
// properties subpackage to guarantee properties are valid and assist code-completion.
func Show(ctx context.Context, unit string, property properties.Property, opts Options) (string, error) {
str, err := show(ctx, unit, property, opts)
return str, err
//
// Any additional arguments are passed directly to the systemctl command.
func Show(ctx context.Context, unit string, property properties.Property, opts Options, args ...string) (string, error) {
return show(ctx, unit, property, opts, args...)
}
// Start (activate) a given unit
func Start(ctx context.Context, unit string, opts Options) error {
return start(ctx, unit, opts)
//
// Any additional arguments are passed directly to the systemctl command.
func Start(ctx context.Context, unit string, opts Options, args ...string) error {
return start(ctx, unit, opts, args...)
}
// Get back the status string which would be returned by running
@@ -106,14 +124,17 @@ func Start(ctx context.Context, unit string, opts Options) error {
//
// Generally, it makes more sense to programmatically retrieve the properties
// using Show, but this command is provided for the sake of completeness
func Status(ctx context.Context, unit string, opts Options) (string, error) {
stat, err := status(ctx, unit, opts)
return stat, err
//
// Any additional arguments are passed directly to the systemctl command.
func Status(ctx context.Context, unit string, opts Options, args ...string) (string, error) {
return status(ctx, unit, opts, args...)
}
// Stop (deactivate) a given unit
func Stop(ctx context.Context, unit string, opts Options) error {
return stop(ctx, unit, opts)
//
// Any additional arguments are passed directly to the systemctl command.
func Stop(ctx context.Context, unit string, opts Options, args ...string) error {
return stop(ctx, unit, opts, args...)
}
// Unmask one or more unit files, as specified on the command line.
@@ -123,6 +144,8 @@ func Stop(ctx context.Context, unit string, opts Options) error {
// doesn't exist, but only if it's not already masked.
// If the unit doesn't exist but it's masked anyway, no error will be
// returned. Gross, I know. Take it up with Poettering.
func Unmask(ctx context.Context, unit string, opts Options) error {
return unmask(ctx, unit, opts)
//
// Any additional arguments are passed directly to the systemctl command.
func Unmask(ctx context.Context, unit string, opts Options, args ...string) error {
return unmask(ctx, unit, opts, args...)
}

View File

@@ -8,58 +8,58 @@ import (
"github.com/taigrr/systemctl/properties"
)
func daemonReload(ctx context.Context, opts Options) error {
func daemonReload(_ context.Context, _ Options, _ ...string) error {
return nil
}
func reenable(ctx context.Context, unit string, opts Options) error {
func reenable(_ context.Context, _ string, _ Options, _ ...string) error {
return nil
}
func disable(ctx context.Context, unit string, opts Options) error {
func disable(_ context.Context, _ string, _ Options, _ ...string) error {
return nil
}
func enable(ctx context.Context, unit string, opts Options) error {
func enable(_ context.Context, _ string, _ Options, _ ...string) error {
return nil
}
func isActive(ctx context.Context, unit string, opts Options) (bool, error) {
func isActive(_ context.Context, _ string, _ Options, _ ...string) (bool, error) {
return false, nil
}
func isEnabled(ctx context.Context, unit string, opts Options) (bool, error) {
func isEnabled(_ context.Context, _ string, _ Options, _ ...string) (bool, error) {
return false, nil
}
func isFailed(ctx context.Context, unit string, opts Options) (bool, error) {
func isFailed(_ context.Context, _ string, _ Options, _ ...string) (bool, error) {
return false, nil
}
func mask(ctx context.Context, unit string, opts Options) error {
func mask(_ context.Context, _ string, _ Options, _ ...string) error {
return nil
}
func restart(ctx context.Context, unit string, opts Options) error {
func restart(_ context.Context, _ string, _ Options, _ ...string) error {
return nil
}
func show(ctx context.Context, unit string, property properties.Property, opts Options) (string, error) {
func show(_ context.Context, _ string, _ properties.Property, _ Options, _ ...string) (string, error) {
return "", nil
}
func start(ctx context.Context, unit string, opts Options) error {
func start(_ context.Context, _ string, _ Options, _ ...string) error {
return nil
}
func status(ctx context.Context, unit string, opts Options) (string, error) {
func status(_ context.Context, _ string, _ Options, _ ...string) (string, error) {
return "", nil
}
func stop(ctx context.Context, unit string, opts Options) error {
func stop(_ context.Context, _ string, _ Options, _ ...string) error {
return nil
}
func unmask(ctx context.Context, unit string, opts Options) error {
func unmask(_ context.Context, _ string, _ Options, _ ...string) error {
return nil
}

View File

@@ -9,74 +9,33 @@ import (
"github.com/taigrr/systemctl/properties"
)
// Reload systemd manager configuration.
//
// This will rerun all generators (see systemd. generator(7)), reload all unit
// files, and recreate the entire dependency tree. While the daemon is being
// reloaded, all sockets systemd listens on behalf of user configuration will
// stay accessible.
func daemonReload(ctx context.Context, opts Options) error {
args := []string{"daemon-reload", "--system"}
if opts.UserMode {
args[1] = "--user"
}
_, _, _, err := execute(ctx, args)
func daemonReload(ctx context.Context, opts Options, args ...string) error {
a := prepareArgs("daemon-reload", opts, args...)
_, _, _, err := execute(ctx, a)
return err
}
// Reenables one or more units.
//
// This removes all symlinks to the unit files backing the specified units from
// the unit configuration directory, then recreates the symlink to the unit again,
// atomically. Can be used to change the symlink target.
func reenable(ctx context.Context, unit string, opts Options) error {
args := []string{"reenable", "--system", unit}
if opts.UserMode {
args[1] = "--user"
}
_, _, _, err := execute(ctx, args)
func reenable(ctx context.Context, unit string, opts Options, args ...string) error {
a := prepareArgs("reenable", opts, append([]string{unit}, args...)...)
_, _, _, err := execute(ctx, a)
return err
}
// Disables one or more units.
//
// This removes all symlinks to the unit files backing the specified units from
// the unit configuration directory, and hence undoes any changes made by
// enable or link.
func disable(ctx context.Context, unit string, opts Options) error {
args := []string{"disable", "--system", unit}
if opts.UserMode {
args[1] = "--user"
}
_, _, _, err := execute(ctx, args)
func disable(ctx context.Context, unit string, opts Options, args ...string) error {
a := prepareArgs("disable", opts, append([]string{unit}, args...)...)
_, _, _, err := execute(ctx, a)
return err
}
// Enable one or more units or unit instances.
//
// This will create a set of symlinks, as encoded in the [Install] sections of
// the indicated unit files. After the symlinks have been created, the system
// manager configuration is reloaded (in a way equivalent to daemon-reload),
// in order to ensure the changes are taken into account immediately.
func enable(ctx context.Context, unit string, opts Options) error {
args := []string{"enable", "--system", unit}
if opts.UserMode {
args[1] = "--user"
}
_, _, _, err := execute(ctx, args)
func enable(ctx context.Context, unit string, opts Options, args ...string) error {
a := prepareArgs("enable", opts, append([]string{unit}, args...)...)
_, _, _, err := execute(ctx, a)
return err
}
// Check whether any of the specified units are active (i.e. running).
//
// Returns true if the unit is active, false if inactive or failed.
// Also returns false in an error case.
func isActive(ctx context.Context, unit string, opts Options) (bool, error) {
args := []string{"is-active", "--system", unit}
if opts.UserMode {
args[1] = "--user"
}
stdout, _, _, err := execute(ctx, args)
func isActive(ctx context.Context, unit string, opts Options, args ...string) (bool, error) {
a := prepareArgs("is-active", opts, append([]string{unit}, args...)...)
stdout, _, _, err := execute(ctx, a)
stdout = strings.TrimSuffix(stdout, "\n")
switch stdout {
case "inactive":
@@ -92,21 +51,9 @@ func isActive(ctx context.Context, unit string, opts Options) (bool, error) {
}
}
// Checks whether any of the specified unit files are enabled (as with enable).
//
// Returns true if the unit is enabled, aliased, static, indirect, generated
// or transient.
//
// Returns false if disabled. Also returns an error if linked, masked, or bad.
//
// See https://www.freedesktop.org/software/systemd/man/systemctl.html#is-enabled%20UNIT%E2%80%A6
// for more information
func isEnabled(ctx context.Context, unit string, opts Options) (bool, error) {
args := []string{"is-enabled", "--system", unit}
if opts.UserMode {
args[1] = "--user"
}
stdout, _, _, err := execute(ctx, args)
func isEnabled(ctx context.Context, unit string, opts Options, args ...string) (bool, error) {
a := prepareArgs("is-enabled", opts, append([]string{unit}, args...)...)
stdout, _, _, err := execute(ctx, a)
stdout = strings.TrimSuffix(stdout, "\n")
switch stdout {
case "enabled":
@@ -140,13 +87,9 @@ func isEnabled(ctx context.Context, unit string, opts Options) (bool, error) {
return false, ErrUnspecified
}
// Check whether any of the specified units are in a "failed" state.
func isFailed(ctx context.Context, unit string, opts Options) (bool, error) {
args := []string{"is-failed", "--system", unit}
if opts.UserMode {
args[1] = "--user"
}
stdout, _, _, err := execute(ctx, args)
func isFailed(ctx context.Context, unit string, opts Options, args ...string) (bool, error) {
a := prepareArgs("is-failed", opts, append([]string{unit}, args...)...)
stdout, _, _, err := execute(ctx, a)
stdout = strings.TrimSuffix(stdout, "\n")
switch stdout {
case "inactive":
@@ -160,91 +103,47 @@ func isFailed(ctx context.Context, unit string, opts Options) (bool, error) {
}
}
// Mask one or more units, as specified on the command line. This will link
// these unit files to /dev/null, making it impossible to start them.
//
// Notably, Mask may return ErrDoesNotExist if a unit doesn't exist, but it will
// continue masking anyway. Calling Mask on a non-existing masked unit does not
// return an error. Similarly, see Unmask.
func mask(ctx context.Context, unit string, opts Options) error {
args := []string{"mask", "--system", unit}
if opts.UserMode {
args[1] = "--user"
}
_, _, _, err := execute(ctx, args)
func mask(ctx context.Context, unit string, opts Options, args ...string) error {
a := prepareArgs("mask", opts, append([]string{unit}, args...)...)
_, _, _, err := execute(ctx, a)
return err
}
// Stop and then start one or more units specified on the command line.
// If the units are not running yet, they will be started.
func restart(ctx context.Context, unit string, opts Options) error {
args := []string{"restart", "--system", unit}
if opts.UserMode {
args[1] = "--user"
}
_, _, _, err := execute(ctx, args)
func restart(ctx context.Context, unit string, opts Options, args ...string) error {
a := prepareArgs("restart", opts, append([]string{unit}, args...)...)
_, _, _, err := execute(ctx, a)
return err
}
// Show a selected property of a unit. Accepted properties are predefined in the
// properties subpackage to guarantee properties are valid and assist code-completion.
func show(ctx context.Context, unit string, property properties.Property, opts Options) (string, error) {
args := []string{"show", "--system", unit, "--property", string(property)}
if opts.UserMode {
args[1] = "--user"
}
stdout, _, _, err := execute(ctx, args)
func show(ctx context.Context, unit string, property properties.Property, opts Options, args ...string) (string, error) {
extra := append([]string{unit, "--property", string(property)}, args...)
a := prepareArgs("show", opts, extra...)
stdout, _, _, err := execute(ctx, a)
stdout = strings.TrimPrefix(stdout, string(property)+"=")
stdout = strings.TrimSuffix(stdout, "\n")
return stdout, err
}
// Start (activate) a given unit
func start(ctx context.Context, unit string, opts Options) error {
args := []string{"start", "--system", unit}
if opts.UserMode {
args[1] = "--user"
}
_, _, _, err := execute(ctx, args)
func start(ctx context.Context, unit string, opts Options, args ...string) error {
a := prepareArgs("start", opts, append([]string{unit}, args...)...)
_, _, _, err := execute(ctx, a)
return err
}
// Get back the status string which would be returned by running
// `systemctl status [unit]`.
//
// Generally, it makes more sense to programmatically retrieve the properties
// using Show, but this command is provided for the sake of completeness
func status(ctx context.Context, unit string, opts Options) (string, error) {
args := []string{"status", "--system", unit}
if opts.UserMode {
args[1] = "--user"
}
stdout, _, _, err := execute(ctx, args)
func status(ctx context.Context, unit string, opts Options, args ...string) (string, error) {
a := prepareArgs("status", opts, append([]string{unit}, args...)...)
stdout, _, _, err := execute(ctx, a)
return stdout, err
}
// Stop (deactivate) a given unit
func stop(ctx context.Context, unit string, opts Options) error {
args := []string{"stop", "--system", unit}
if opts.UserMode {
args[1] = "--user"
}
_, _, _, err := execute(ctx, args)
func stop(ctx context.Context, unit string, opts Options, args ...string) error {
a := prepareArgs("stop", opts, append([]string{unit}, args...)...)
_, _, _, err := execute(ctx, a)
return err
}
// Unmask one or more unit files, as specified on the command line.
// This will undo the effect of Mask.
//
// In line with systemd, Unmask will return ErrDoesNotExist if the unit
// doesn't exist, but only if it's not already masked.
// If the unit doesn't exist but it's masked anyway, no error will be
// returned. Gross, I know. Take it up with Poettering.
func unmask(ctx context.Context, unit string, opts Options) error {
args := []string{"unmask", "--system", unit}
if opts.UserMode {
args[1] = "--user"
}
_, _, _, err := execute(ctx, args)
func unmask(ctx context.Context, unit string, opts Options, args ...string) error {
a := prepareArgs("unmask", opts, append([]string{unit}, args...)...)
_, _, _, err := execute(ctx, a)
return err
}

14
util.go
View File

@@ -55,6 +55,20 @@ func execute(ctx context.Context, args []string) (string, string, int, error) {
return output, warnings, code, err
}
// prepareArgs builds the systemctl command arguments from a base command,
// options, and any additional arguments the caller wants to pass through.
func prepareArgs(base string, opts Options, extra ...string) []string {
args := make([]string, 0, 2+len(extra))
args = append(args, base)
if opts.UserMode {
args = append(args, "--user")
} else {
args = append(args, "--system")
}
args = append(args, extra...)
return args
}
func filterErr(stderr string) error {
switch {
case strings.Contains(stderr, `does not exist`):

62
util_test.go Normal file
View File

@@ -0,0 +1,62 @@
package systemctl
import (
"reflect"
"testing"
)
func TestPrepareArgs(t *testing.T) {
tests := []struct {
name string
base string
opts Options
extra []string
expected []string
}{
{
name: "system mode no extra",
base: "start",
opts: Options{},
extra: nil,
expected: []string{"start", "--system"},
},
{
name: "user mode no extra",
base: "start",
opts: Options{UserMode: true},
extra: nil,
expected: []string{"start", "--user"},
},
{
name: "system mode with unit",
base: "start",
opts: Options{},
extra: []string{"nginx.service"},
expected: []string{"start", "--system", "nginx.service"},
},
{
name: "user mode with unit and extra args",
base: "restart",
opts: Options{UserMode: true},
extra: []string{"foo.service", "--no-block"},
expected: []string{"restart", "--user", "foo.service", "--no-block"},
},
{
name: "daemon-reload no extra",
base: "daemon-reload",
opts: Options{},
extra: nil,
expected: []string{"daemon-reload", "--system"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := prepareArgs(tt.base, tt.opts, tt.extra...)
if !reflect.DeepEqual(got, tt.expected) {
t.Errorf("prepareArgs(%q, %+v, %v) = %v, want %v",
tt.base, tt.opts, tt.extra, got, tt.expected)
}
})
}
}