{"id":16932967,"url":"https://github.com/arp242/zli","last_synced_at":"2025-10-12T00:41:40.911Z","repository":{"id":117655267,"uuid":"268705372","full_name":"arp242/zli","owner":"arp242","description":"Go library for writing CLI programs. Includes flag parsing, colours, testing, and various helpful utility functions","archived":false,"fork":false,"pushed_at":"2025-05-29T10:59:24.000Z","size":247,"stargazers_count":45,"open_issues_count":0,"forks_count":1,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-05-29T12:47:43.427Z","etag":null,"topics":["cli","go","golang"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/arp242.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null},"funding":{"github":"arp242"}},"created_at":"2020-06-02T05:02:30.000Z","updated_at":"2025-05-29T10:59:28.000Z","dependencies_parsed_at":"2024-06-14T19:26:55.517Z","dependency_job_id":"1d1572ea-5982-4fff-a02a-f9ddcfb8ac29","html_url":"https://github.com/arp242/zli","commit_stats":{"total_commits":90,"total_committers":1,"mean_commits":90.0,"dds":0.0,"last_synced_commit":"7a37675fadfd854fd0444c8775aa489ee78d277a"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/arp242/zli","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/arp242%2Fzli","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/arp242%2Fzli/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/arp242%2Fzli/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/arp242%2Fzli/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/arp242","download_url":"https://codeload.github.com/arp242/zli/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/arp242%2Fzli/sbom","scorecard":{"id":208202,"data":{"date":"2025-08-11","repo":{"name":"github.com/arp242/zli","commit":"08cb210424f2af609720131c6114f1ad3325cbab"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":3.6,"checks":[{"name":"Code-Review","score":0,"reason":"Found 0/30 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Token-Permissions","score":-1,"reason":"No tokens found","details":null,"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Dangerous-Workflow","score":-1,"reason":"no workflows found","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"SAST","score":0,"reason":"no SAST tool detected","details":["Warn: no pull requests merged into dev branch"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"Maintained","score":4,"reason":"5 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 4","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Pinned-Dependencies","score":-1,"reason":"no dependencies found","details":null,"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Info: FSF or OSI recognized license: MIT License: LICENSE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'main'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"Vulnerabilities","score":10,"reason":"0 existing vulnerabilities detected","details":null,"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}}]},"last_synced_at":"2025-08-17T00:12:28.800Z","repository_id":117655267,"created_at":"2025-08-17T00:12:28.800Z","updated_at":"2025-08-17T00:12:28.800Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":279009508,"owners_count":26084609,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-11T02:00:06.511Z","response_time":55,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cli","go","golang"],"created_at":"2024-10-13T20:48:09.904Z","updated_at":"2025-10-12T00:41:40.858Z","avatar_url":"https://github.com/arp242.png","language":"Go","funding_links":["https://github.com/sponsors/arp242"],"categories":[],"sub_categories":[],"readme":"zli is a Go library for writing CLI programs. It includes flag parsing, color\nescape codes, various helpful utility functions, and makes testing fairly easy.\nThere's a little example at [cmd/grep](cmd/grep), which should give a decent\noverview of how actual programs look like.\n\nImport as `zgo.at/zli`; API docs: https://godocs.io/zgo.at/zli\n\nOther packages:\n\n- [zgo.at/termtext](https://github.com/arp242/termtext) – align and wrap text.\n- [zgo.at/acidtab](https://github.com/arp242/acidtab)   – print text in tables.\n- [zgo.at/termfo](https://github.com/arp242/termfo)     – read and use terminfo.\n\n**Readme index**:\n[Utility functions](#utility-functions) ·\n[Flag parsing](#flag-parsing) ·\n[Colors](#colors) ·\n[Testing](#testing)\n\n### Utility functions\n`zli.Errorf()` and `zli.Fatalf()` work like `fmt.Printf()`, except that they\nprint to stderr, prepend the program name, and always append a newline:\n\n```go\nzli.Errorf(\"oh noes: %s\", \"u brok it\")   // \"progname: oh noes: u brok it\" to stderr\nzli.Fatalf(\"I swear it was %s\", \"Dave\")  // \"progname: I swear it was Dave\" to stderr and exit 1\n```\n\n`zli.F()` is a small wrapper/shortcut around `zli.Fatalf()` which accepts an\nerror and checks if it's `nil` first:\n\n```go\nerr := f()\nzli.F(err)\n```\n\nFor many programs it's useful to be able to read from stdin or from a file,\ndepending on what arguments the user gave. With `zli.InputOrFile()` this is\npretty easy:\n\n```go\narg := \"/a-file\"\nfp, err := zli.InputOrFile(arg, false)        // Open stdin if arg is \"-\" or \"-\", or a file otherwise.\ndefer fp.Close()                              // No-op close on stdin.\n```\n\nThe second argument controls if a `reading from stdin...` message should be\nprinted to stderr, which is a bit better UX IMHO (how often have you typed `grep\nfoo` and waited, only to realize it's waiting for stdin?) See [Better UX when\nreading from stdin][stdin].\n\n[stdin]: https://www.arp242.net/read-stdin.html\n\n`zli.Pager()` pipes the contents of a reader `$PAGER`. It will copy the contents\nto stdout if `$PAGER` isn't set or on other errors:\n\n```go\nfp, _ := os.Open(\"/file\")        // Display file in $PAGER.\nzli.Pager(fp)\n```\n\nIf you want to page output your program generates you can use\n`zli.PagerStdout()` to swap `zli.Stdout` to a buffer:\n\n```go\ndefer zli.PagerStdout()()               // Double ()()!\nfmt.Fprintln(zli.Stdout, \"page me!\")    // Displayed in the $PAGER.\n```\n\nThis does require that your program writes to `zli.Stdout` instead of\n`os.Stdout`, which is probably a good idea for testing anyway. See the\n[Testing](#testing) section.\n\nYou need to be a bit careful when calling `Exit()` explicitly, since that will\nexit immediately without running any defered functions. You have to either use a\nwrapper or call the returned function explicitly:\n\n```go\nfunc main() { zli.Exit(run()) }\n\nfunc run() int {\n    defer zli.PagerStdout()()\n    fmt.Fprintln(zli.Stdout, \"XXX\")\n    return 1\n}\n```\n\n```go\nfunc main() {\n    runPager := zli.PagerStdout()\n    fmt.Fprintln(zli.Stdout, \"XXX\")\n\n    runPager()\n    zli.Exit(1)\n}\n```\n\nzli helpfully includes the `GetSize()` and `IsTerminal()` from [x/term][term] as\nthey're so commonly used:\n\n```go\ninteractive := zli.IsTerminal(os.Stdout.Fd())  // Check if stdout is a terminal.\nw, h, err := zli.TerminalSize(os.Stdout.Fd())  // Get terminal size.\n```\n\n[term]: https://godoc.org/golang.org/x/term\n\n### Flag parsing\nzli comes with a flag parser which, IMHO, gives a better experience than Go's\n`flag` package. See [flag.md](/flag.md) for some rationale on \"why this and not\nstdlib flags?\"\n\n```go\n// Create new flags; normally you'd pass in os.Args here.\nf := zli.NewFlags([]string{\"example\", \"-vv\", \"-f=csv\", \"-a\", \"xx\", \"yy\"})\n\n// The first argument is the default and everything after that is the flag name\n// with aliases.\nvar (\n    verbose = f.IntCounter(0, \"v\", \"verbose\")        // Count the number of -v flags.\n    exclude = f.StringList(nil, \"e\", \"exclude\")      // Can appear more than once.\n    all     = f.Bool(false, \"a\", \"all\")              // Regular bool.\n    format  = f.String(\"\", \"f\", \"format\")            // Regular string.\n    asJSON  = f.String(\"\", \"-j\", \"json\")\n)\n\n// Shift the first argument (i.e. os.Args[1]). Useful to get the \"subcommand\"\n// name. This works before and after Parse().\n// Can also use \"cmd := f.ShiftCommand(\"help\", \"install\")\", in which case it\n// will use the first non-ambiguous match.\nswitch f.Shift() {\ncase \"help\":\n    // Run help\ncase \"install\":\n    // Run install\ncase \"\": // os.Args wasn't long enough.\n    // Error: need a command (or just print the usage)\ndefault:\n    // Error: Unknown command\n}\n\n// Parse the shebang!\nerr := f.Parse()\nif err != nil {\n    // Print error, usage.\n}\n\n// You can check if the flag was present on the CLI with Set(). This way you can\n// distinguish between \"was an empty value passed\" (-format '') and \"this flag\n// wasn't on the CLI\".\nif format.Set() {\n    fmt.Println(\"Format was set to\", format.String())\n}\n\n// The IntCounter adds 1 for every time the -v flag is on the CLI.\nif verbose.Int() \u003e 1 {\n    // ...Print very verbose info.\n} else if verbose.Int() \u003e 0 {\n    // ...Print less verbose info.\n}\n\n// Just a bool!\nfmt.Println(\"All:\", all.Bool())\n\n// Allow a flag to appear more than once.\nfmt.Println(\"%s exclude patterns: %v\", len(all.Strings()), all.Strings())\n\n// -json flag has an optional value.\nif asJSON.Set() {\n    printas := \"json\"\n    if asJSON.String() == \"indent\" {\n        printas = \"json-indent\"\n    }\n}\n\n// f.Args is set to everything that's not a flag or argument.\nfmt.Println(\"Remaining:\", f.Args)\n```\n\nThe flag format is as follows:\n\n- Flags can have a single `-` or two `--`, they're treated identical.\n\n- Arguments are after a space or `=`: `-f v` or `-f=v`. Arguments that start\n  with a `-` must use the `=` variant (`-f=-val`).\n\n- Booleans can be grouped; `-ab` is the same as `-a -b`; this only works with a\n  single `-` (`--ab` would be an error).\n\n- Positional arguments may appear anywhere; these are all identical:\n  `-a -b arg`, `arg -a -b`, `-a arg -b`.\n\n---\n\nThere is no automatic generation of a usage message; I find that much of the\ntime you get a much higher quality by writing one manually. It does provide\n`zli.Usage()` you can apply some generic substitutions giving a format somewhat\nreminiscent of manpages:\n\n    UsageTrim      Trim leading/trailing whitespace, and ensure it ends with \\n\n    UsageHeaders   Format headers in the form \"^Name:\" as bold and underline.\n    UsageFlags     Format flags (-v, --flag, --flag=foo) as underlined.\n\nSee the grep example.\n\n### Colors\nYou can add colors and some other text attributes to a string with\n`zli.Colorize()`, which returns a modified string with the terminal escape\ncodes, ending with reset.\n\nIt won't do anything if `zli.WantColor` is `false`; this is disabled by default\nif the output isn't a terminal or `NO_COLOR` is set, but you can override it if\nthe user sets `--color=force` or something.\n\n`zli.Colorln()` and `zli.Colorf()` are convenience wrappers for `fmt.Println()`\nand `fmt.Printf()` with colors.\n\nThere are constants for the basic terminal attributes and 16-color palette which\nmay be combined freely by adding them together:\n\n```go\nzli.Colorln(\"You're looking rather red\", zli.Red)     // Apply a color.\nzli.Colorln(\"A bold move\", zli.Bold)                  // Or an attribute.\nzli.Colorln(\"A bold move\", zli.Red | zli.Bold)        // Or both.\n```\n\nTo set a background color transform the color with the `Bg()` method:\n\n```go\nzli.Colorln(\"Tomato\", zli.Red.Bg())                   // Transform to background color.\nzli.Colorln(\"Wow, such beautiful text\",               // Can be combined.\n    zli.Bold | zli.Red | zli.Green.Bg())\n```\n\nThere are no pre-defined constants for the 256-color palette or true colors, you\nneed to use `Color256()` and `ColorHex()` to create them; you can use the `Bg()`\nto transform them to a background color as well:\n\n```go\nzli.Colorln(\"Contrast ratios is for suckers\",         // 256 color.\n    zli.Color256(56) | zli.Color256(99).Bg())\n\nzli.Colorln(\"REAL men use TRUE color!\",               // True color.\n    zli.ColorHex(\"#fff\") | zli.ColorHex(\"#00f\").Bg())\n```\n\nWith `Brighten()` you can change the brightness of a color:\n\n```go\nzli.Colorln(\"Brighter! BRIGHTER!\", zli.Color256(99).Brighten(1))\nzli.Colorln(\"Dim the lights.              // Negative values darken.\n    zli.ColorHex(\"#655199\").Brighten(-40))\n```\n\nSee [cmd/colortest/main.go](cmd/colortest/main.go) for a little program to\ndisplay and test colors.\n\n---\n\nFor some more advanced cases you can use `Color.String()` directly, but this\nwon't look at `zli.WantColor` and you'll need to manually apply the reset code:\n\n```go\nfmt.Println(zli.Red|zli.Bold, \"red!\")                 // Print escape codes.\nfmt.Println(\"and bold!\", zli.Reset)\n\nfmt.Printf(\"%sc%so%sl%so%sr%s\\n\", zli.Red, zli.Magenta, zli.Cyan, zli.Blue, zli.Yellow, zli.Reset)\n```\n\nBecause the color is stored in an `uint64` you can assign them to a constant:\n\n```go\nconst colorMatch = zli.Bold | zli.Red\n```\n\nThis won't work if you use `Color256()` or `ColorHex()`; although you can get\naround this by constructing it all yourself:\n\n```go\n// zli.Color256(99)\nconst color = zli.Bold | (zli.Color(99) \u003c\u003c zli.ColorOffsetFg) | zli.ColorMode256\n\n// zli.ColorHex(\"#ff7711\").Bg(); can also use 1144831 directly instead of the\n// bit shifts.\nconst color2 = zli.Bold | zli.Red | zli.ColorModeTrueBg |\n               (zli.Color(0xff|0x77\u003c\u003c8|0x11\u003c\u003c16) \u003c\u003c zli.ColorOffsetBg)\n```\n\nThis creates a color stored as an int, shifts it to the correct location, and\nsets the flag to signal how to interpret it.\n\nDo you really want to do this just to create a `const` instead of a `var`?\nProbably not 😅\n\n### Testing\nzli uses to `zli.Stdin`, `zli.Stdout`, `zli.Stderr`, and `zli.Exit` instead of\nthe `os.*` variants for everything. You can swap this out with test variants\nwith the `zli.Test()` function.\n\nYou can use these in your own program as well, if you want to test the output of\na program.\n\n```go\nfunc TestX(t *testing.T) {\n    exit, in, out := Test(t) // Resets everything back to os.* with t.Cleanup()\n\n    // Write something to stderr (a bytes.Buffer) and read the output.\n    Error(\"oh noes!\")\n    fmt.Println(out.String()) // zli.test: oh noes!\n\n    // Read from stdin.\n    in.WriteString(\"Hello\")\n    fp, _ := InputOrFile(\"-\", true)\n    got, _ := ioutil.ReadAll(fp)\n    fmt.Println(string(got)) // Hello\n\n    out.Reset()\n\n    et := func() {\n        fmt.Fprintln(Stdout, \"one\")\n        Exit(1)\n        fmt.Fprintln(Stdout, \"two\")\n    }\n\n    // exit panics to ensure the regular control flow of the program is aborted;\n    // to capture this run the function to be tested in a closure with\n    // exit.Recover(), which will recover() from the panic and set the exit\n    // code.\n    func() {\n        defer exit.Recover()\n        et()\n    }()\n    // Helper to check the statis code, so you don't have to derefrence and cast\n    // the value to int.\n    exit.Want(t, 1)\n\n    fmt.Println(\"Exit %d: %s\\n\", *exit, out.String()) // Exit 1: one\n```\n\nYou don't need to use the `zli.Test()` function if you won't want to, you can\njust swap out stuff yourself as well:\n\n```go\nbuf := new(bytes.Buffer)\nzli.Stderr = buf\ndefer func() { Stderr = os.Stderr }()\n\nError(\"oh noes!\")\nout := buf.String()\nfmt.Printf(\"buffer has: %q\\n\", out) // buffer has: \"zli.test: oh noes!\\n\"\n```\n\n`zli.IsTerminal()` and `zli.TerminalSize()` are variables, and can be swapped\nout as well:\n\n```go\nsave := zli.IsTerminal\nzli.IsTerminal = func(uintptr) bool { return true }\ndefer func() { IsTerminal = save }()\n```\n\n\n#### Exit\n\nA few notes on replacing `zli.Exit()` in tests: the difficulty with this is that\n`os.Exit()` will terminate the entire program, including the test, which is\nrarely what you want and difficult to test. You can replace `zli.Exit` with\nsomething like (`zli.TestExit()` takes care of all of this):\n\n```go\nvar code int\nzli.Exit = func(c int) { code = c }\nmayExit()\nfmt.Println(\"exit code\", code)\n```\n\nThis works well enough for simple cases, but there's a big caveat with this; for\nexample consider:\n\n```go\nfunc mayExit() {\n    err := f()\n    if err != nil {\n        zli.Error(err)\n        zli.Exit(4)\n    }\n\n    fmt.Println(\"X\")\n}\n```\n\nWith the above the program will continue after `zli.Exit()`; which is a\ndifferent program flow from normal execution. A simple way to fix it so to\nmodify the function to explicitly call `return`:\n\n```go\nfunc mayExit() {\n    err := f()\n    if err != nil {\n        zli.Error(err)\n        zli.Exit(4)\n        return\n    }\n\n    fmt.Println(\"X\")\n}\n```\n\nThis still isn't *quite* the same, as callers of `mayExit()` in your program\nwill still continue happily. It's also rather ugly and clunky.\n\nTo solve this you can replace `zli.Exit` with a function that panics and then\nrecover that:\n\n```go\nfunc TestFoo(t *testing.T) {\n    var code int\n    zli.Exit = func(c int) {\n        code = c\n        panic(\"zli.Exit\")\n    }\n\n    func() {\n        defer func() {\n            r := recover()\n            if r == nil {\n                return\n            }\n        }()\n\n        mayExit()\n    }()\n\n    fmt.Println(\"Exited with\", code)\n}\n```\n\nThis will abort the program flow similar to `os.Exit()`, and the call to\n`mayExit` is wrapped in a function the test function itself will continue after\nthe recover.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Farp242%2Fzli","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Farp242%2Fzli","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Farp242%2Fzli/lists"}