package cli import ( "flag" "fmt" "io" "os" "strings" "time" "gitea.pglikers.com/tools/slacksend/internal/slack" "gitea.pglikers.com/tools/slacksend/internal/version" ) type Options struct { ShowVersion bool WebhookURL string TimeoutSec int Title string } func Run(args []string) int { return run(args, os.Stdin, os.Stdout, os.Stderr) } func run(args []string, stdin io.Reader, stdout, stderr io.Writer) int { fs := flag.NewFlagSet("slacksend", flag.ContinueOnError) fs.SetOutput(stderr) fs.Usage = func() { Usage(stdout) } var opt Options fs.BoolVar(&opt.ShowVersion, "version", false, "show version") fs.BoolVar(&opt.ShowVersion, "V", false, "show version (short)") // other options fs.StringVar(&opt.WebhookURL, "webhook", "", "Slack Incoming Webhook URL (or env SLACK_WEBHOOK_URL)") fs.IntVar(&opt.TimeoutSec, "timeout", 10, "HTTP timeout seconds") fs.StringVar(&opt.Title, "title", "", "message title (shown as bold)") if err := fs.Parse(args[1:]); err != nil { if err == flag.ErrHelp { Usage(stdout) return 0 } fmt.Fprintln(stderr, err) Usage(stderr) return 2 } if opt.ShowVersion { fmt.Fprintln(stdout, version.String()) return 0 } if len(fs.Args()) == 0 && isTerminal(stdin) { fmt.Fprintln(stderr, "message is required. provide args or pipe via stdin") Usage(stderr) return 2 } // webhook URL の解決: flag > env webhookURL := strings.TrimSpace(opt.WebhookURL) if webhookURL == "" { webhookURL = strings.TrimSpace(os.Getenv("SLACK_WEBHOOK_URL")) } if webhookURL == "" { fmt.Fprintln(stderr, "Slack webhook URL is required. Set --webhook or SLACK_WEBHOOK_URL.") return 2 } msg, err := message(fs.Args(), stdin) if err != nil { fmt.Fprintln(stderr, "read message error:", err) return 1 } if msg == "" { Usage(stderr) return 2 } client := slack.WebhookClient{ URL: webhookURL, Timeout: time.Duration(opt.TimeoutSec) * time.Second, } if err := client.Send(opt.Title, msg); err != nil { fmt.Fprintln(stderr, "send failed:", err) return 1 } fmt.Fprintln(stdout, "ok") return 0 } func message(args []string, stdin io.Reader) (string, error) { if len(args) > 0 { return strings.Join(args, " "), nil } b, err := io.ReadAll(stdin) if err != nil { return "", err } return strings.TrimSpace(string(b)), nil } // isTerminal reports whether the given reader is a character device (e.g., TTY). func isTerminal(r io.Reader) bool { fd, ok := r.(interface { Stat() (os.FileInfo, error) }) if !ok { return false } info, err := fd.Stat() if err != nil { return false } return info.Mode()&os.ModeCharDevice != 0 }