icns

Easily create .icns files (Mac Icons) with this Go library or the included CLI.
Log | Files | Refs | LICENSE

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:
Mcmd/icnsify/main.go | 75+++++++++++++++++++++++++++++++++++++++++++++++----------------------------
Mcmd/icnsify/pipe.go | 17+++++++----------
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 }