diff --git a/cmd/envc/main.go b/cmd/envc/main.go index 9d0a112..7b2009f 100644 --- a/cmd/envc/main.go +++ b/cmd/envc/main.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "io" "os" ) @@ -13,15 +14,36 @@ var ( date = "unknown" ) +// printUsage writes the root help text to w. +func printUsage(w io.Writer) { + _, _ = io.WriteString(w, `envc — convert, query, and merge configuration across env, YAML, JSON, and INI. + +Usage: + envc [arguments] + +Commands: + convert Read one source, normalize keys, write another format (--from, --to) + get Read one source and print the value at a dot path (--from, --path) + merge Deep-merge multiple sources in order, then write one output (--from, --to) + version Print version and build metadata (also -v, --version) + +Each command accepts its own flags; run: + envc -h +`) +} + func main() { stdin, stdout, stderr := os.Stdin, os.Stdout, os.Stderr args := os.Args[1:] if len(args) < 1 { - _, _ = fmt.Fprintln(stderr, "usage: envc [flags]") + printUsage(stderr) os.Exit(2) } var code int switch args[0] { + case "help", "-h", "--help": + printUsage(stdout) + code = 0 case "convert": code = RunConvert(args[1:], stdin, stdout, stderr) case "get": @@ -31,7 +53,8 @@ func main() { case "version", "--version", "-v": _, _ = fmt.Fprintf(stdout, "envc %s (commit %s, built %s)\n", version, commit, date) default: - _, _ = fmt.Fprintf(stderr, "unknown command %q\n", args[0]) + _, _ = fmt.Fprintf(stderr, "unknown command %q\n\n", args[0]) + printUsage(stderr) code = 2 } os.Exit(code) diff --git a/cmd/envc/test_hang.sh b/cmd/envc/test_hang.sh new file mode 100755 index 0000000..3712fc9 --- /dev/null +++ b/cmd/envc/test_hang.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# Reproduce and explain envc "hangs": default input is stdin (-); io.ReadAll +# blocks until EOF. A TTY waits for Ctrl-D; /dev/zero never EOF (infinite read). +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +cd "$ROOT" + +ENVC="${TMPDIR:-/tmp}/envc-hang-test.$$" +cleanup() { rm -f "$ENVC"; } +trap cleanup EXIT + +echo "== build envc -> $ENVC" +go build -o "$ENVC" ./cmd/envc + +echo +echo "== 1) expected to finish quickly (YAML on stdin, explicit -)" +printf 'k: 1\n' | "$ENVC" convert --from yaml --to json --input - --output - | head -c 200 +echo +echo " (ok)" + +echo +echo "== 2) empty stdin should finish quickly (EOF immediately)" +if ! out="$( "$ENVC" convert --from yaml --to json --input - --output - &1 )"; then + echo " exit non-zero (often empty YAML parse); stderr/out:" + echo "$out" | sed 's/^/ | /' + echo " (ok — no hang, process terminated)" +else + echo "$out" | sed 's/^/ /' + echo " (ok — no hang)" +fi + +echo +echo "== 3) infinite stdin: should hit wall-clock limit (GNU timeout -> 124)" +if ! command -v timeout >/dev/null; then + echo " skip: no 'timeout' in PATH" +else + set +e + timeout -v 2s "$ENVC" convert --from yaml --to json --input - --output - /dev/null 2>&1 + ec=$? + set -e + if [[ "$ec" -eq 124 ]]; then + echo " timeout fired (exit 124) — binary was blocked in read (no EOF on stdin)." + elif [[ "$ec" -eq 137 ]] || [[ "$ec" -eq 143 ]]; then + echo " killed by signal (exit $ec) — still indicates blocked read until SIGKILL/SIGTERM." + else + echo " unexpected exit $ec (expected 124 from GNU timeout on hang)" + exit 1 + fi +fi + +echo +echo "== why it happens" +cat <<'EOF' + convert/get default to reading the input source once with io.ReadAll. + For --input - (convert) or positional "-" (get), that is os.Stdin until EOF. + + - Interactive shell, no pipe: stdin is a TTY — blocked until you press Ctrl-D + or pipe data in. + - /dev/zero (or any endless reader): never EOF — blocked until killed or OOM. + + merge uses the same pattern for non-"-" inputs via bytesutil; URL fetches use + a 30s HTTP client timeout (see internal/source.URL). + + Fix for users: pass a file or URL (--input path), or pipe finite data into stdin. +EOF + +echo +echo "== done"