{"id":16847470,"url":"https://github.com/indygreg/clitest-rs","last_synced_at":"2026-03-05T23:04:50.874Z","repository":{"id":189373141,"uuid":"680561618","full_name":"indygreg/clitest-rs","owner":"indygreg","description":"Proposal for a command line testing tool implemented in Rust","archived":false,"fork":false,"pushed_at":"2023-08-20T00:39:15.000Z","size":32,"stargazers_count":7,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-03-25T04:28:10.055Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/indygreg.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE-APACHE","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}},"created_at":"2023-08-19T16:39:29.000Z","updated_at":"2025-01-08T16:26:17.000Z","dependencies_parsed_at":"2025-02-18T22:32:13.066Z","dependency_job_id":"faa99431-1f2d-4da4-876e-c0f672b9e216","html_url":"https://github.com/indygreg/clitest-rs","commit_stats":null,"previous_names":["indygreg/clitest-rs"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/indygreg%2Fclitest-rs","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/indygreg%2Fclitest-rs/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/indygreg%2Fclitest-rs/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/indygreg%2Fclitest-rs/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/indygreg","download_url":"https://codeload.github.com/indygreg/clitest-rs/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248357748,"owners_count":21090400,"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","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":[],"created_at":"2024-10-13T13:08:04.397Z","updated_at":"2026-03-05T23:04:45.822Z","avatar_url":"https://github.com/indygreg.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# clitest-rs\n\nclitest-rs aims to be the following:\n\n* An expressive / literate way to define tests of CLI applications.\n* A standalone executable that can evaluate CLI tests. Just point it at an\n  executable you want to test and some test files and it does the rest.\n* A testing library that can be embedded into Rust code bases. Rust projects\n  can execute integration tests via `cargo test`. Behavior of the test runner\n  can be customized using the crate's Rust API to tailor to the specific use\n  cases of individual projects.\n\n**This project is still in its design phase. We're working on the test\nfile format definition and execution semantics before writing a lot of\ncode because as we learned from deficiencies in prior art mistakes here\nare difficult to impossible to correct. So we want to *get it right* from\nthe beginning.**\n\n## Example Test File\n\nHere's a high-level example showing the proposed syntax and some high-level features.\n\n~~~\nThis is a test file. This line is ignored by the test runner.\n\nThe HTML processing instruction says this file and its tests should only\nrun on Windows.\n\u003c?clitest require=windows ?\u003e\n\nThe code fence below defines commands to execute.\n\n```\n$ myapp help\nMyApp Version 1.0\n\nUsage: myapp \u003caction\u003e\n```\n\nDemonstrate that we can redirect output to a file and use common\ncoreutils programs `cut` and `hexdump` to effectively create a snapshot\ntest of content.\n\n```\n$ myapp hello \u003e hello\n$ cut -z -b 1-32 hello | hexdump -C\n00000000  23 20 63 6c 69 74 65 73  74 2d 72 73 0a 0a 63 6c  |# clitest-rs..cl|\n00000010  69 74 65 73 74 2d 72 73  20 61 69 6d 73 20 74 6f  |itest-rs aims to|\n00000020  00                                                |.|\n00000021\n\n```\n\n```\n$ myapp process\n\u003c input to stderr of spawned process\n\u003e1 expected stdout output\n\u003e2 expected stderr output\n```\n\n```ignore\nThis code fence is ignored by the test runner. It does not define any commands.\n```\n\n~~~\n\n## `.clitest` Syntax\n\nA `.clitest` is an expressive / literate file format to define tests that\nrun executables and verify their behavior.\n\nThe syntax of `.clitest` file leverages Markdown syntax conventions and\nmany `.clitest` files can also be parsed / rendered as Markdown. But\nwell-formed Markdown is not a hard requirement. (Leveraging Markdown as the\nfile format has the added benefit that a project's Markdown based documentation\n*could* potentially also be executed as tests to ensure the documentation\nis accurate.)\n\nThe most important part of the file format are *test cases*. These describe\ncommands to execute and their expected behavior.\n\nTest cases live between [code fences](https://spec.commonmark.org/0.30/#code-fence),\nwhich are pairs of 3 backticks or tildes. e.g.\n\n~~~\n```\nCode fence\n```\n~~~\n\n```\n~~~\nAnother code fence\n~~~\n```\n\nUnlike the CommonMark specification, we do not (yet) support optionally\nprefixing the code fence with up to 3 spaces.\n\nAll lines outside of *code fences* are ignored with the exception of lines\nstarting with a [processing instruction](https://spec.commonmark.org/0.30/#processing-instruction)\nwith the `clitest` tag. e.g.\n\n~~~\n```\n\u003c?clitest this metadata is recognized ?\u003e\n```\n\n```\n\u003c?xml this is ignored since the tag is xml not clitest ?\u003e\n```\n\n```\n  \u003c?clitest ignored since it isn't at the start of a line ?\u003e\n```\n~~~\n\n### Test Case Code Fences\n\nEach code fence is parsed into a single *test case*. Each case can\ndefine 0 to N command invocations to run. The execution environment\nfor each test case is shared among its invocations, allowing invoked\ncommands to interact with each other.\n\nThe opening line of the *code fence* can contain an optional *info block*\nafter the backticks or tildes, per the CommonMark specification. This\ninfo block content is made available to the test runner so it can influence\nbehavior.\n\n~~~\n```info block content\n...\n```\n~~~\n\nWithin each code fence we define a custom syntax for declaring commands\nto invoke and their expected output.\n\nThe initial content of each line can denote special meaning:\n\n* `$` denotes a command string to run.\n* `\u003c` define stdin to feed to the spawned command.\n* `[N]` defines the expected exit code of the command. MUST be the final\n  line for a command definition.\n* `\u003e1` and `\u003e2` define expected output for stdout and stderr, respectively.\n  If not used, stdout and stderr are merged. This syntax allows verification\n  that output is written to a specific stdio file descriptor.\n* All other lines are treated as expected output from the command.\n\n### Command Strings\n\nLines beginning with `$` declare a command string. A command string is a\n[POSIX shell](https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html)\ninspired grammar that defines how to invoke processes.\n\nOften, a command string is just the name of the program under test and its\narguments. e.g.\n\n```\n$ myapp arg0 arg1\n```\n\n#### Redirection\n\nProcess file descriptors can be controlled using the redirection grammar\n`[n]operation word`.\n\n`[n]` is an optional file descriptor. 0 is stdin, 1 is stdout, 2 is stderr. \n\n`word` describes where to read/write from.\n\nThe `\u003c` operation reads content from the file `word` and sends it to stdin.\n\nThe `\u003e` operation redirects content from a file descriptor (default 1 / stdout)\nand writes it to the file `word`. The file is truncated after opening.\n\nThe `\u003e\u003e` operation behaves like `\u003e` but appends to the destination file instead\nof truncating.\n\n`\u003c\u0026` duplicates an input file descriptor. Default of 0 / stderr. `word` of `-`\ncloses the file descriptor.\n\n`\u003e\u0026` duplicates an output file descriptor. Default of 1 / stdout. `word` of `-`\ncloses the file descriptor.\n\nThe test runner proxies the following virtual files or emulates them if they\ndon't exist:\n\n* `/dev/null` - A write only file that does nothing with data written to it.\n  Typically used for redirecting output so it isn't display. e.g. `\u003e/dev/null`\n  to redirect stdout to nothing.\n\nExamples:\n\n~~~\nExecute `myapp` and write its stdout to the file `stdout`.\n```\n$ myapp \u003e stdout\n```\n\nExecute `myapp` and write its stderr to the file `stderr`.\n```\n$ myapp 2\u003e stderr\n```\n\nExecute `myapp` and merge its stdout and stderr to the file `combined`.\n```\n$ myapp 1\u003e\u00262 \u003e combined\n```\n\nExecute `myapp` with stdout closed.\n```\n$ myapp 1\u003e\u0026-\n```\n\n~~~\n\n#### Pipelines\n\nPipelines are sequences of 2 or more commands separated by the operator `|`.\n\nThe grammar is `command1 | command2`.\n\nThe stdout of `command1` is connected to the stdin of `command2` via a pipe.\n\nThe processes are spawned in the order they are defined by the pipeline.\n\nThe exit status of the pipeline is the exit status of the last command.\n\nExamples:\n\n~~~\nSend stdout to stdin of another:\n\n```\n$ myapp | grep expected-output\n```\n\n~~~\n\n### Test Case Info Block Directives\n\nOptional *code fence* *info blocks* may pass directives to the test runner\nto influence execution of that code fence. Directives are intended to be\nsingle words. But the library API allows custom parsing to be performed.\n\nThe following single word directives are recognized by default:\n\n* `ignore` says to ignore this *code fence*. It will not be parsed as a\n  test case.\n\n### Processing Instructions\n\nHTML processing instructions (`\u003c?clitest ... ?\u003e`) are used to convey\ninstructions to the test runner outside the context of a single test\ncase.\n\nThe default test runner exposes various functionality via processing\ninstructions. But behavior can be customized. Library customers may\nimplement their own processing instructions.\n\n## Test Case Execution Semantics\n\nBy default each *test case* is executed in a temporary directory in order\nto try to ensure a reproducible test environment. The temporary directory\nis populated with the executable under test along with copies of additional\nfiles that have been registered.\n\nEnvironment variables exposed to spawned processes are normalized by default\nto try to ensure a reproducible test environment. The following environment\nvariables are set by default:\n\n* `PWD` Current directory the test case is executing in.\n* `USER` The value `clitest`, which may not exist on the current system.\n* ...\n\n## Principles\n\n## Simplicity and intuitiveness\n\nThe test format should be simple, easy to read, and easy to understand.\n\nWe base the test format on Markdown, which is well understood among the\ntarget developer community. We leverage features of markdown - code fences\nand HTML processing instructions - which are generally known or familiar\nto many developers.\n\n## Embrace Coreutils\n\nWe embed a copy of the [uutils](https://github.com/uutils/coreutils) Rust\nimplementation of GNU coreutils. This allows users to harness as little or\nmuch power from these common utilities (like `grep`, `sed`, and `awk`)\nfor additional text processing / analysis as they want.\n\nBy leaning on these common utilities we defer the onus of having to reinvent\nthis complexity in our test format and test harness. Our test harness can focus\non executing processes and not complex text stream processing.\n\nBy using the Rust coreutils implementation, test execution uses a\ndeterministic version of these tools, not whatever version or variant you\nhave on the system. On Windows you don't need to install Cygwin, msys, or\nrun in WSL to get access to these utilities.\n\n## Commitment to Backwards Compatibility\n\nWe strive to not require end-users to rewrite test files when upgrading. If you\nneed to spend hours updating tests when upgrading the version of this project\nyou are using, we've failed.\n\nThe test file format should evolve to be backwards and forwards compatible.\n\nThe test execution semantics should also be highly backwards compatible.\n\nWhen you upgrade the version of this project you are using hopefully the\nworst thing that happens is you need to run a command to migrate to slightly\ndifferent file syntax or accept machine proposed changes to expectations\ndefined in the test files.\n\n## Embrace the Concept of the Shell Without an Actual Shell\n\nWhile it could be useful to execute tests in a shell (like bash, zsh,\nor even nushell), we reject the existence of an explicit shell during test\nexecution (unless end-users opt into it).\n\nPractical experience with Mercurial and cram demonstrated there are too\nmany sharp edges with actual shells. At the least you need a shell binary\non every machine under test. And the shell version/features needs to behave\nthe same on all systems. This can be difficult, especially on Windows.\n\nA fully embeddable shell (like Nushell) could be a viable alternative. But\nnow we're externalizing knowledge of a specific shell onto our end-users.\n\nEnd-users are opinionated about shells. Bash. Zsh. Powershell. We don't want\nto pick sides in this debate.\n\nBut shells are useful. Variables. Pipes. Redirection. Control flow and loops.\nBuiltin commands (like `cd` and `echo`). Background process execution.\n\nWe want to expose some of the features of shells without using an actual\nshell. Make the user think they have a shell for common functionality to\ntest command invocations but don't let them juggle chainsaws.\n\n## TODO\n\nHere are some ideas for the test format and execution semantics that we'd\nlike to flush out more. Many are potential future features we could add to\nthe test runner. We should focus on defining an extensible test format\nso future features don't require new syntax.\n\n* Define syntax for regex matching of single lines.\n* Define syntax for eliding multiple lines of expected output.\n* Define escaping / alternative directive encoding mechanism so expected\n  process output colliding with our special syntax can be worked around and\n  allow expression of all outputs in the test format.\n* Define syntax for matching common / repeated patterns. Logically allow\n  expansion of tokens in expected output. We'll want to define some tokens\n  by default, such as the current executable name and working directory.\n* Define mechanism to verifying binary output. Maybe a directive / info block\n  to escape output as hex or base64? Consider humans wanting to add arbitrary\n  line breaks so 1 byte insertions/removals don't invalidate all following\n  lines.\n* Define additional minimal environment variables. (e.g. `TZ`, `PATH`.) Stuff\n  that is in most environments.\n* Syntax for setting additional environment variables.\n* Consider `%` and possibly other line-leading tokens inside code fences to\n  influence operations. e.g. `% cd foo` to change the CWD for future command\n  invocations.\n* Consider shell-like file redirection syntax for sending output of a command to\n  a file, including to `/dev/null` (or its equivalent).\n* Consider some magical commands or syntax (like `$ cat \u003cpath\u003e`) to allow\n  verification of produced file content.\n* Mechanism for conditional execution. e.g. only execute on Windows or POSIX.\n  Only execute if allowed to access a network. Maybe allow tags to be specified\n  to the test runner so tests can be filtered. This can allow expression and\n  skipping of *slow* tests. See also `requires` and keywords mechanisms in\n  Mercurial's test harness.\n* Command / test timeout support. How do you express that?\n* Support for delivering a signal to a process. e.g. simulate a ^C to test\n  error handling.\n* Consider semantics for automatic drift overwrites when the test runner\n  executes. We want to expose a turnkey mechanism where new/different/drifted\n  test output can be recorded in the original test file without humans having\n  to edit the file. The more custom syntax we add to the file format the harder\n  this becomes. e.g. if you support regular expression matching of lines, how\n  do you preserve those expressions or tokens when overwriting expected output?\n  This is a known wort in Mercurial's and cram's more expressive output\n  processing world.\n* Define semantics around stdio file descriptor buffers. Could be nice to\n  test differences when stdout is buffered/unbuffered.\n* Consider ability to implement an interactive execution mode where you can\n  step through executions. This allows developers to attach debuggers or\n  inspect the filesystem sandbox when they want. It could also allow selectively\n  accepting/rejecting/modifying output drift.\n* Consider mechanism to define stdin/stdout in separate files. Useful for\n  large content, like verifying behavior over input data sets. Can also be\n  useful for sharing inputs across files and test cases.\n* Consider mechanism for reusing a series of common commands. e.g. common\n  test fixture setup code. How can we enable developers to minimize the\n  overhead of authoring tests by minimizing DRY violations.\n  * Idea: directories containing seeds for filesystem sandboxes. Test files or\n    cases can specify directories to use to populate content of the filesystem\n    sandbox.\n  * Idea: Directive so test files or cases can specify another `.clitest` file\n    whose single test case can be prepended to cases.\n  * Idea: code fence info block to name a test case so it can be included in\n    another one.\n  * Idea: code fence info block to denote the prepending of content to all\n    test cases.\n* Consider allowing test cases to execute within a shell. Maybe a shell\n  implemented in pure Rust for portability.\n\n### Unresolved Grammar / Parsing Issues\n\nAssuming that command strings are denoted by lines beginning with `$` and\nexpected process output is following lines, there are some ambiguities with\nparsing the expected output.\n\nHow do we know we've hit the end of process output? In Mercurial and cram,\nlines indented by 2 spaces constituted output. So you could just look for\nthe first non-indented line to find EOF. But we don't propose indenting\nlines. (It is annoying - if you have blank lines you need to insert empty\nlines with leading whitespace, which editors like to strip. Some editors\nmay not clearly display the whitespace only lines. Behavior is not\nintuitive and not developer friendly!)\n\nYou can't say an empty line indicates end of output because there could be\nempty lines in process output. You also don't want to strip trailing lines\nbecause that whitespace could be relevant and you want to test it is there!\n(Don't you want to know if your program starts printing an extra trailing\nnewline?)\n\nWe also have to consider the scenario that a command could print output\nthat conflicts with our own file syntax.\n\nThe following scenarios are all ambiguous:\n\n~~~\n\nIs the actual output \"foo\" \"foo\\n\" or \"foo\\n\\n\"?\n```\n$ myapp\nfoo\n\n$ irrelevant\n```\n\nHow do we validate output beginning with \"$\"?\nDoes the first command print nothing or \"$ hello\"?\n```\n$ echo '$ hello'\n$ hello\n```\n\nWhat about commands that print code fence delimiters?\n```\n$ echo '```'\n```\n```\n~~~\n\nMercurial solves the no trailing newline problem by annotating the line with\na `(no-eol)` annotation. e.g.\n\n~~~\n```\n$ echo -n hello\nhello (no-eol)\n```\n~~~\n\nFor output that conflicts with our own syntax, the obvious solution is\nescaping. e.g.\n\n~~~\n```\n$ echo '$ hello'\n\\$ hello\n\n$ echo '```'\n\\`\\`\\`\n```\n~~~\n\nExperience with Mercurial tells us that escaping can be annoying. Especially\nif your program domain contains a lot of output conflicting with our syntax.\nImagine testing a program that emits Markdown using our tool! Or imagine\nus testing this tool using its own syntax! All that escaping could be pretty\nannoying.\n\nOne potential solution is alternative delimiters. For example a processing\ninstruction or code fence info block could denote an alternative delimiter.\n\n~~~\n\u003c%clitest command_delimiter=@ %\u003e\n\n```\n@ echo '$ hello'\n$ hello\n```\n~~~\n\nThen end-users could select delimiters that make sense for their domain.\n\nThis doesn't solve the problem of code fence delimiters though!\n\nMaybe we could leverage a heredoc style syntax for explicitly delimiting\noutput?\n\n~~~\n```\n$ echo '```' \u003e\u003c EOF\n```\nEOF\n```\n~~~\n\nThis could work. But it does make the file parser more complicated. Without\nthis syntax you can scan the file and pair up code fence tokens to isolate\nall the lines defining code fences. Then you could parse each code fence\nlater. But if you use a heredoc style syntax and have lines within the\nheredoc sharing the syntax as code fences, now your parser has to recognize\nthe heredoc syntax so it knows to ignore a code fence token within a code\nfence. Not very desirable! Maybe code fence tokens deserve a one-off solution\nor escaping?\n\n## Project History\n\nThe goal of this project is to facilitate [literate testing](https://arrenbrecht.ch/testing/)\nof command line applications. Command line applications are complex\nand are often designed with user interaction in mind. Their behavior is\nimportant to test and understand. Changes/differences in behavior are\nimportant to detect and capture to ensure consistent user experiences.\n\nWe believe that literate testing of CLI applications results in higher\nquality CLIs by increasing the surface area under test and forcing developers\nto confront their CLI UX. This belief is grounded in the experience of\nproject contributors, namely around the use of the practice in the Mercurial\nversion control tool. We want to lower the barrier to literate CLI\ntesting in the Rust ecosystem and beyond to encourage its broader use.\n\nAt the time of this project's inception (mid 2023), literate CLI testing in\nthe Rust ecosystem was not very mature. Popular CLI testing crates relied on\nspawning processes and inspecting their behavior from Rust code. See\n[assert_cmd](https://crates.io/crates/assert_cmd),\n[insta-cmd](https://crates.io/crates/insta-cmd), and\n[rexpect](https://crates.io/crates/rexpect). The [trycmd](https://github.com/assert-rs/trycmd)\nseemed to be the lone attempt at higher-order literate CLI testing in Rust.\nBut it had various limitations (see below).\n\nThis project is inspired by [Mercurial's](https://www.mercurial-scm.org/) custom\ntest harness. Mercurial inspired [cram](https://bitheap.org/cram/). And cram\ninspired [trycmd](https://github.com/assert-rs/trycmd).\n\nThis project came about due to various limitations with all of the above tools.\nWe wanted to invent a modern, powerful literate command testing tool that\nexceeded capabilities of tools before while hopefully avoiding many pitfalls\nof earlier tools.\n\nHere are some limitations with Mercurial and cram:\n\n* Format was completely custom. Developers had to learn a new DSL for writing\n  tests. It was relatively intuitive. But IDEs lacked parsing / highlighting.\n* Indent of 2 spaces was kinda annoying to manually paste output differences.\n  You were constantly doing multi-line indents in your editor.\n* Line output processing features (like `(re)`, `(glob)`, `(esc)` `(no-eol)`)\n  often got swallowed when outputs changed. The interaction of these directives\n  often had unexpected consequences. You often needed to define these on\n  several lines, which was annoying.\n* Test files were effectively normalized to an auto-generated shell script which\n  was executed on the current system. Commands were literal shell expressions.\n  You were constantly fighting portability issues. e.g. on Windows you needed\n  to install cygwin or msys to provide a shell and other common commands. You\n  could have all the common problems with shell programming, including esoteric\n  escaping and variable referencing issues. State leaking between commands.\n  Poor debugging story. The full power of the shell could be useful but it was\n  difficult to wield.\n* Implemented in Python. Test harness was relatively slow. Required a Python\n  interpreter to execute. Standalone binaries much easier for end-users.\n\nAnd limitations with trycmd:\n\n* Public library API does not facilitate advanced use. You can't construct test\n  cases via the Rust API. See [issue 217](https://github.com/assert-rs/trycmd/issues/217).\n* Extensions to file format not possible. Related to above. But the low-level\n  test format parser doesn't seem designed with an extensibility mechanism in\n  place. See [issue 218](https://github.com/assert-rs/trycmd/issues/218).\n* Currently lacking a lot of features that Mercurial and cram have. See their\n  [enhancements list](https://github.com/assert-rs/trycmd/issues?q=label%3Aenhancement+)\n  for them all. Examples include: lack of piping, lack of output redirection,\n  lack of dynamic capture/matching, turnkey support for coreutils commands,\n  filesystem sandboxing for `.trycmd` tests, handling escaping when there is\n  a conflict between command output and trycmd test file syntax.\n* No standalone test runner/binary. Scope seems limited to embedding a test runner\n  in Rust crates.\n\nTrycmd feels like the most modern command testing tool in the Rust space right\nnow since it supports a literate test format and is relatively easy to use (e.g.\nno Python dependency). But it is still a far cry from the features of Mercurial\nand cram. The crate maintainer doesn't seem interested in opening up the crate's\npublic API to support additional customization. This means we'll need to build\nsomething ourselves. We should hopefully be able to leverage some of trycmd's\nprior art, such as the [snapbox](https://crates.io/crates/snapbox) crate.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Findygreg%2Fclitest-rs","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Findygreg%2Fclitest-rs","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Findygreg%2Fclitest-rs/lists"}