commit 715ac90bc6d971cd067eebf509b8f700abae0f7a
parent 3a017757b6e880fb4b863f992151b745a9d157b0
Author: Jack Mordaunt <jackmordaunt.dev@gmail.com>
Date: Wed, 16 Sep 2026 21:30:10 -0400
cmd/icnsify: exit non-zero on failure and detect pipes reliably
Every error path returned from main with status 0, so scripts could not tell
a failed conversion from a success. Pipe detection keyed on stdin's current
size, which is zero before the producer writes; checking for a terminal is
stable. An explicit --input now wins over a redirected stdin.
Diffstat:
2 files changed, 54 insertions(+), 38 deletions(-)
diff --git a/cmd/icnsify/main.go b/cmd/icnsify/main.go
@@ -2,6 +2,8 @@ package main
import (
"bytes"
+ "errors"
+ "fmt"
"image"
"image/jpeg"
"image/png"
@@ -17,14 +19,21 @@ import (
"github.com/spf13/pflag"
)
-var (
- fs = afero.NewOsFs()
- piping bool
- input io.Reader
- output io.Writer
-)
+var fs = afero.NewOsFs()
+
+// errUsage signals that no work was requested; usage has been printed.
+var errUsage = errors.New("usage")
func main() {
+ if err := run(); err != nil {
+ if !errors.Is(err, errUsage) {
+ slog.Error("icnsify failed", "err", err)
+ }
+ os.Exit(1)
+ }
+}
+
+func run() error {
var (
inputPath = pflag.StringP(
"input",
@@ -46,41 +55,52 @@ func main() {
)
)
pflag.Parse()
+
+ var (
+ input io.Reader
+ output io.Writer
+ )
+ // An explicit --input wins; otherwise a non-terminal stdin means we are
+ // part of a pipeline and both paths are ignored.
+ piping := false
+ if *inputPath == "" {
+ var err error
+ if piping, err = stdinIsPipe(); err != nil {
+ return err
+ }
+ }
in, out, algorithm := sanitiseInputs(*inputPath, *outputPath, *resize)
- if !piping {
+ if piping {
+ input, output = os.Stdin, os.Stdout
+ } else {
if in == "" {
usage()
- os.Exit(0)
+ return errUsage
}
sourcef, err := fs.Open(in)
if err != nil {
- slog.Error("opening source image", "err", err)
- return
+ return fmt.Errorf("opening source image: %w", err)
}
defer sourcef.Close()
input = sourcef
if err := fs.MkdirAll(filepath.Dir(out), 0o755); err != nil {
- slog.Error("preparing output directory: %v", "err", err)
- return
+ return fmt.Errorf("preparing output directory: %w", err)
}
outputf, err := fs.Create(out)
if err != nil {
- slog.Error("creating icns file", "err", err)
- return
+ return fmt.Errorf("creating output file: %w", err)
}
defer outputf.Close()
output = outputf
}
- if filepath.Ext(*inputPath) == ".icns" {
+ if filepath.Ext(in) == ".icns" {
by, err := io.ReadAll(input)
if err != nil {
- slog.Error("probing file: reading file", "err", err)
- return
+ return fmt.Errorf("probing file: reading file: %w", err)
}
icons, err := icns.Probe(bytes.NewReader(by))
if err != nil {
- slog.Error("probing file", "err", err)
- return
+ return fmt.Errorf("probing file: %w", err)
}
for _, icon := range icons {
slog.Info("found", "icon", icon)
@@ -89,8 +109,7 @@ func main() {
}
img, format, err := image.Decode(input)
if err != nil {
- slog.Error("decoding input", "err", err)
- return
+ return fmt.Errorf("decoding input: %w", err)
}
if format == "icns" {
imageType := strings.ToLower(filepath.Ext(out))
@@ -98,15 +117,15 @@ func main() {
imageType = ".png"
}
if err := encoders[imageType](output, img); err != nil {
- slog.Error("encoding", "err", err, "type", imageType)
- }
- } else {
- enc := icns.NewEncoder(output).
- WithAlgorithm(algorithm)
- if err := enc.Encode(img); err != nil {
- slog.Error("encoding icns", "err", err)
+ return fmt.Errorf("encoding %s: %w", imageType, err)
}
+ return nil
+ }
+ enc := icns.NewEncoder(output).WithAlgorithm(algorithm)
+ if err := enc.Encode(img); err != nil {
+ return fmt.Errorf("encoding icns: %w", err)
}
+ return nil
}
func sanitiseInputs(
diff --git a/cmd/icnsify/pipe.go b/cmd/icnsify/pipe.go
@@ -5,17 +5,14 @@ import (
"os"
)
-func init() {
+// stdinIsPipe reports whether stdin is a pipe or redirected file rather than
+// an interactive terminal. It deliberately ignores the stream's current size:
+// a producer such as `cat icon.png | icnsify` may not have written anything
+// by the time we look, and a zero size would wrongly read as "no input".
+func stdinIsPipe() (bool, error) {
info, err := os.Stdin.Stat()
if err != nil {
- panic(fmt.Sprintf("getting info on stdin file descriptor: %v", err))
- }
- if (info.Mode() & os.ModeCharDevice) == os.ModeCharDevice {
- return
- }
- if info.Size() > 0 {
- piping = true
- input = os.Stdin
- output = os.Stdout
+ return false, fmt.Errorf("getting info on stdin file descriptor: %w", err)
}
+ return info.Mode()&os.ModeCharDevice == 0, nil
}