// Package sysx — обёртки над системными командами и файлами Linux. package sysx import ( "bufio" "bytes" "context" "fmt" "io" "os" "os/exec" "strings" ) // Output выполняет команду и возвращает stdout без хвостовых пробелов. func Output(ctx context.Context, name string, args ...string) (string, error) { var stdout, stderr bytes.Buffer cmd := exec.CommandContext(ctx, name, args...) cmd.Stdout = &stdout cmd.Stderr = &stderr if err := cmd.Run(); err != nil { msg := strings.TrimSpace(stderr.String()) if msg == "" { msg = strings.TrimSpace(stdout.String()) } return "", fmt.Errorf("%s %s: %w: %s", name, strings.Join(args, " "), err, msg) } return strings.TrimSpace(stdout.String()), nil } // ToFile выполняет команду, направляя stdout в файл path. func ToFile(ctx context.Context, path string, name string, args ...string) error { f, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o600) if err != nil { return err } var stderr bytes.Buffer cmd := exec.CommandContext(ctx, name, args...) cmd.Stdout = f cmd.Stderr = &stderr runErr := cmd.Run() closeErr := f.Close() if runErr != nil { return fmt.Errorf("%s: %w: %s", name, runErr, strings.TrimSpace(stderr.String())) } return closeErr } // FromFile выполняет команду, подавая файл path на stdin. func FromFile(ctx context.Context, path string, name string, args ...string) error { f, err := os.Open(path) if err != nil { return err } defer f.Close() return Stream(ctx, f, name, args...) } // Stream выполняет команду с заданным stdin, вывод — в консоль. func Stream(ctx context.Context, stdin io.Reader, name string, args ...string) error { cmd := exec.CommandContext(ctx, name, args...) cmd.Stdin = stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { return fmt.Errorf("%s %s: %w", name, strings.Join(args, " "), err) } return nil } // Run выполняет команду с выводом в консоль. func Run(ctx context.Context, name string, args ...string) error { return Stream(ctx, nil, name, args...) } // Shell выполняет строку через bash -c с выводом в консоль. func Shell(ctx context.Context, script string, env ...string) error { cmd := exec.CommandContext(ctx, "bash", "-c", script) cmd.Env = append(os.Environ(), env...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { return fmt.Errorf("bash -c %q: %w", script, err) } return nil } func Exists(p string) bool { _, err := os.Lstat(p) return err == nil } func HasCommand(name string) bool { _, err := exec.LookPath(name) return err == nil } // OSRelease читает /etc/os-release. func OSRelease() map[string]string { out := map[string]string{} f, err := os.Open("/etc/os-release") if err != nil { return out } defer f.Close() sc := bufio.NewScanner(f) for sc.Scan() { k, v, ok := strings.Cut(sc.Text(), "=") if !ok { continue } out[k] = strings.Trim(v, `"'`) } return out } // DpkgVersion возвращает установленную версию пакета или "". func DpkgVersion(ctx context.Context, pkg string) string { out, err := Output(ctx, "dpkg-query", "-W", "-f=${db:Status-Status} ${Version}", pkg) if err != nil { return "" } status, ver, _ := strings.Cut(out, " ") if status != "installed" { return "" } return ver } func DpkgArch(ctx context.Context) string { out, err := Output(ctx, "dpkg", "--print-architecture") if err != nil { return "" } return out } // UnitState возвращает (enabled, active) для systemd-юнита. func UnitState(ctx context.Context, unit string) (bool, bool) { en, _ := Output(ctx, "systemctl", "is-enabled", unit) ac, _ := Output(ctx, "systemctl", "is-active", unit) return en == "enabled", ac == "active" } // UnitLoaded сообщает, известен ли systemd такой юнит. func UnitLoaded(ctx context.Context, unit string) bool { out, err := Output(ctx, "systemctl", "show", "-p", "LoadState", "--value", unit) return err == nil && out == "loaded" } // GroupMembers возвращает членов группы из /etc/group. func GroupMembers(group string) []string { data, err := os.ReadFile("/etc/group") if err != nil { return nil } for _, line := range strings.Split(string(data), "\n") { f := strings.Split(line, ":") if len(f) == 4 && f[0] == group && f[3] != "" { return strings.Split(f[3], ",") } } return nil }