{"id":32366961,"url":"https://github.com/byllyfish/shellous","last_synced_at":"2025-10-24T18:54:48.717Z","repository":{"id":37965297,"uuid":"394543447","full_name":"byllyfish/shellous","owner":"byllyfish","description":"asyncio library that provides an API for running subprocesses","archived":false,"fork":false,"pushed_at":"2025-10-23T21:27:56.000Z","size":4066,"stargazers_count":21,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-10-23T22:24:38.125Z","etag":null,"topics":["asyncio","expect","pty","python","subprocess"],"latest_commit_sha":null,"homepage":"","language":"Python","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/byllyfish.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2021-08-10T06:06:21.000Z","updated_at":"2025-10-23T21:24:52.000Z","dependencies_parsed_at":"2024-01-10T05:16:19.354Z","dependency_job_id":"80dcaaee-57e5-4978-ac4b-07958d155e41","html_url":"https://github.com/byllyfish/shellous","commit_stats":{"total_commits":798,"total_committers":2,"mean_commits":399.0,"dds":"0.022556390977443663","last_synced_commit":"103f00fbc9c9ff3d5ca13e687f3de6d537690427"},"previous_names":[],"tags_count":44,"template":false,"template_full_name":null,"purl":"pkg:github/byllyfish/shellous","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/byllyfish%2Fshellous","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/byllyfish%2Fshellous/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/byllyfish%2Fshellous/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/byllyfish%2Fshellous/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/byllyfish","download_url":"https://codeload.github.com/byllyfish/shellous/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/byllyfish%2Fshellous/sbom","scorecard":{"id":260032,"data":{"date":"2025-08-12T07:36:46Z","repo":{"name":"github.com/byllyfish/shellous","commit":"248b5799407d6722e46dadfdc0731fc694fb433d"},"scorecard":{"version":"v5.2.1","commit":"ab2f6e92482462fe66246d9e32f642855a691dc1"},"score":8.1,"checks":[{"name":"Maintained","score":10,"reason":"26 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 10","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/ab2f6e92482462fe66246d9e32f642855a691dc1/docs/checks.md#maintained"}},{"name":"Dependency-Update-Tool","score":10,"reason":"update tool detected","details":["Info: detected update tool: Dependabot: .github/dependabot.yml:1"],"documentation":{"short":"Determines if the project uses a dependency update tool.","url":"https://github.com/ossf/scorecard/blob/ab2f6e92482462fe66246d9e32f642855a691dc1/docs/checks.md#dependency-update-tool"}},{"name":"Security-Policy","score":10,"reason":"security policy file detected","details":["Info: security policy file detected: SECURITY.md:1","Info: Found linked content: SECURITY.md:1","Info: Found disclosure, vulnerability, and/or timelines in security policy: SECURITY.md:1","Info: Found text in security policy: SECURITY.md:1"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/ab2f6e92482462fe66246d9e32f642855a691dc1/docs/checks.md#security-policy"}},{"name":"Code-Review","score":0,"reason":"Found 0/7 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/ab2f6e92482462fe66246d9e32f642855a691dc1/docs/checks.md#code-review"}},{"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/ab2f6e92482462fe66246d9e32f642855a691dc1/docs/checks.md#binary-artifacts"}},{"name":"Dangerous-Workflow","score":10,"reason":"no dangerous workflow patterns detected","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/ab2f6e92482462fe66246d9e32f642855a691dc1/docs/checks.md#dangerous-workflow"}},{"name":"Token-Permissions","score":10,"reason":"GitHub workflow tokens follow principle of least privilege","details":["Info: jobLevel 'contents' permission set to 'read': .github/workflows/codeql.yml:32","Info: jobLevel 'actions' permission set to 'read': .github/workflows/codeql.yml:31","Info: jobLevel 'actions' permission set to 'read': .github/workflows/scorecards.yml:30","Info: jobLevel 'contents' permission set to 'read': .github/workflows/scorecards.yml:29","Info: topLevel 'contents' permission set to 'read': .github/workflows/ci.yml:14","Info: topLevel 'contents' permission set to 'read': .github/workflows/codeql.yml:24","Info: topLevel 'contents' permission set to 'read': .github/workflows/dependency-review.yml:13","Info: topLevel 'contents' permission set to 'read': .github/workflows/docs.yml:8","Info: topLevel 'contents' permission set to 'read': .github/workflows/publish.yml:9","Info: topLevel permissions set to 'read-all': .github/workflows/scorecards.yml:18","Info: no jobLevel write permissions found"],"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/ab2f6e92482462fe66246d9e32f642855a691dc1/docs/checks.md#token-permissions"}},{"name":"Pinned-Dependencies","score":10,"reason":"all dependencies are pinned","details":["Info:  23 out of  23 GitHub-owned GitHubAction dependencies pinned","Info:  13 out of  13 third-party GitHubAction dependencies pinned","Info:   6 out of   6 pipCommand dependencies pinned"],"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/ab2f6e92482462fe66246d9e32f642855a691dc1/docs/checks.md#pinned-dependencies"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Info: FSF or OSI recognized license: Apache License 2.0: LICENSE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/ab2f6e92482462fe66246d9e32f642855a691dc1/docs/checks.md#license"}},{"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/ab2f6e92482462fe66246d9e32f642855a691dc1/docs/checks.md#cii-best-practices"}},{"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/ab2f6e92482462fe66246d9e32f642855a691dc1/docs/checks.md#vulnerabilities"}},{"name":"Branch-Protection","score":-1,"reason":"internal error: error during branchesHandler.setup: internal error: githubv4.Query: Resource not accessible by integration","details":null,"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/ab2f6e92482462fe66246d9e32f642855a691dc1/docs/checks.md#branch-protection"}},{"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/ab2f6e92482462fe66246d9e32f642855a691dc1/docs/checks.md#signed-releases"}},{"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/ab2f6e92482462fe66246d9e32f642855a691dc1/docs/checks.md#fuzzing"}},{"name":"SAST","score":10,"reason":"SAST tool is run on all commits","details":["Info: SAST configuration detected: CodeQL","Info: all commits (30) are checked with a SAST tool"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/ab2f6e92482462fe66246d9e32f642855a691dc1/docs/checks.md#sast"}},{"name":"Packaging","score":10,"reason":"packaging workflow detected","details":["Info: Project packages its releases by way of GitHub Actions.: .github/workflows/publish.yml:36"],"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/ab2f6e92482462fe66246d9e32f642855a691dc1/docs/checks.md#packaging"}},{"name":"Contributors","score":0,"reason":"project has 0 contributing companies or organizations -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project has a set of contributors from multiple organizations (e.g., companies).","url":"https://github.com/ossf/scorecard/blob/ab2f6e92482462fe66246d9e32f642855a691dc1/docs/checks.md#contributors"}},{"name":"CI-Tests","score":10,"reason":"7 out of 7 merged PRs checked by a CI test -- score normalized to 10","details":null,"documentation":{"short":"Determines if the project runs tests before pull requests are merged.","url":"https://github.com/ossf/scorecard/blob/ab2f6e92482462fe66246d9e32f642855a691dc1/docs/checks.md#ci-tests"}}]},"last_synced_at":"2025-08-17T10:37:36.036Z","repository_id":37965297,"created_at":"2025-08-17T10:37:36.036Z","updated_at":"2025-08-17T10:37:36.036Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":280849757,"owners_count":26401813,"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-24T02:00:06.418Z","response_time":73,"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":["asyncio","expect","pty","python","subprocess"],"created_at":"2025-10-24T18:54:45.474Z","updated_at":"2025-10-24T18:54:48.704Z","avatar_url":"https://github.com/byllyfish.png","language":"Python","readme":"# Async Processes and Pipelines\n\n[![PyPI](https://img.shields.io/pypi/v/shellous)](https://pypi.org/project/shellous/) [![docs](https://img.shields.io/badge/-documentation-informational)](https://byllyfish.github.io/shellous/shellous.html) [![CI](https://github.com/byllyfish/shellous/actions/workflows/ci.yml/badge.svg)](https://github.com/byllyfish/shellous/actions/workflows/ci.yml) [![codecov](https://codecov.io/gh/byllyfish/shellous/branch/main/graph/badge.svg?token=W44NZE89AW)](https://codecov.io/gh/byllyfish/shellous) [![Downloads](https://static.pepy.tech/badge/shellous)](https://pepy.tech/project/shellous)\n\n**shellous** provides a concise API for running subprocesses using [asyncio](https://docs.python.org/3/library/asyncio.html). It is \nsimilar to and inspired by [sh](https://pypi.org/project/sh/).\n\n```python\nimport asyncio\nfrom shellous import sh\n\nasync def main():\n    result = await sh(\"echo\", \"hello\")\n    print(result)\n\nasyncio.run(main())\n```\n\n## Benefits\n\n- Run programs asynchronously in a single line.\n- Redirect stdin, stdout and stderr to files, memory buffers, async streams or loggers.\n- Iterate asynchronously over subprocess output.\n- Set timeouts and reliably cancel running processes.\n- Run a program with a pseudo-terminal (pty).\n- Use send() and expect() to manually control a subprocess.\n- Construct [pipelines](https://en.wikipedia.org/wiki/Pipeline_(Unix)) and use [process substitution](https://en.wikipedia.org/wiki/Process_substitution) directly from Python (no shell required).\n- Runs on Linux, MacOS, FreeBSD and Windows.\n- Monitor processes being started and stopped with `audit_callback` API.\n\n## Requirements\n\n- Requires Python 3.9 or later.\n- Requires an asyncio event loop.\n- Pseudo-terminals require a Unix system.\n- Process substitution requires a Unix system with /dev/fd support.\n\n## Running a Command\n\nThe tutorial in this README uses the asyncio REPL built into Python. In these examples, `\u003e\u003e\u003e`\nis the REPL prompt.\n\nStart the asyncio REPL by typing `python3 -m asyncio`, and import **sh** from the **shellous** module:\n\n```pycon\n\u003e\u003e\u003e from shellous import sh\n```\n\nHere's a command that runs `echo \"hello, world\"`.\n\n```pycon\n\u003e\u003e\u003e await sh(\"echo\", \"hello, world\")\n'hello, world\\n'\n```\n\nThe first argument to `sh` is the program name. It is followed by zero or more arguments. Each argument will be\nconverted to a string. If an argument is a list or tuple, it is flattened recursively.\n\n```pycon\n\u003e\u003e\u003e await sh(\"echo\", 1, 2, [3, 4, (5, 6)])\n'1 2 3 4 5 6\\n'\n```\n\nA command does not run until you `await` it. When you run a command using `await`, it returns the value of the standard output interpreted as a UTF-8 string.\nIt is safe to `await` the same command object more than once.\n\nHere, we create our own echo command with \"-n\" to omit the newline. Note, `echo(\"abc\")` will run the same command as `echo -n \"abc\"`.\n\n```pycon\n\u003e\u003e\u003e echo = sh(\"echo\", \"-n\")\n\u003e\u003e\u003e await echo(\"abc\")\n'abc'\n```\n\nCommands are **immutable** objects that represent a program invocation: program name, arguments, environment\nvariables, redirection operators and other settings. When you use a method to modify a `Command`, you are\nreturning a new `Command` object. The original object is unchanged.\n\nYou can wrap your commands in a function to improve type safety:\n\n```pycon\n\u003e\u003e\u003e from shellous import Command\n\u003e\u003e\u003e def exclaim(word: str) -\u003e Command[str]:\n...   return sh(\"echo\", \"-n\", f\"{word}!!\")\n... \n\u003e\u003e\u003e await exclaim(\"Oh\")\n'Oh!!'\n```\n\nThe type hint `Command[str]` indicates that the command returns a `str`.\n\n### Arguments\n\nCommands use positional arguments only; keyword arguments are not supported.\n\nIn most cases, shellous automatically converts Python objects passed as command arguments to `str` or `bytes`. As described above, the `list` and `tuple` types are an exception; they are recursively flattened before their elements are converted to strings.\n\nDicts, sets, and generator types are **not supported** as arguments. Their string format doesn't make sense as\na command line argument.\n\n### Results\n\nWhen a command completes successfully, it returns the standard output (or \"\" if stdout is redirected). For a more detailed response, you can specify that the command should return a `Result` object by using the `.result` modifier:\n\n```pycon\n\u003e\u003e\u003e await echo.result(\"abc\")\nResult(exit_code=0, output_bytes=b'abc', error_bytes=b'', cancelled=False, encoding='utf-8')\n```\n\nA `Result` object contains the command's `exit_code` in addition to its output. A `Result` is True if \nthe command's exit_code is zero. You can access the string value of the output using the `.output` property:\n\n```python\nif result := await sh.result(\"cat\", \"some-file\"):\n    output = result.output\nelse:\n    print(f\"Command failed with exit_code={result.exit_code})\n```\n\nYou can retrieve the string value of the standard error using the `.error` property. (By default, only the \nfirst 1024 bytes of standard error is stored.)\n\nIf a command was terminated by a signal, the `exit_code` will be the negative *signal* number.\n\nThe return value of `sh.result(\"cmd\", ...)` uses the type hint `Command[Result]`.\n\n### ResultError\n\nIf you are not using the `.result` modifier and a command fails, it raises a `ResultError` exception:\n\n```pycon\n\u003e\u003e\u003e await sh(\"cat\", \"does_not_exist\")\nTraceback (most recent call last):\n  ...\nshellous.result.ResultError: Result(exit_code=1, output_bytes=b'', error_bytes=b'cat: does_not_exist: No such file or directory\\n', cancelled=False, encoding='utf-8')\n```\n\nThe `ResultError` exception contains a `Result` object with the exit_code and the first 1024 bytes of standard error.\n\nIn some cases, you want to ignore certain exit code values. That is, you want to treat them as if they are \nnormal. To do this, you can set the `exit_codes` option:\n\n```pycon\n\u003e\u003e\u003e await sh(\"cat\", \"does_not_exist\").set(exit_codes={0,1})\n''\n```\n\nIf there is a problem launching a process, shellous can also raise a separate `FileNotFoundError` or  `PermissionError` exception.\n\n### Async For\n\nUsing `await` to run a command collects the entire output of the command in memory before returning it. You \ncan also iterate over the output lines as they arrive using `async for`.\n\n```pycon\n\u003e\u003e\u003e [line async for line in echo(\"hi\\n\", \"there\")]\n['hi\\n', ' there']\n```\n\nUse an `async for` loop when you want to examine the stream of output from a command, line by line. For example, suppose you want to run tail on a log file.\n\n```python\nasync for line in sh(\"tail\", \"-f\", \"/var/log/syslog\"):\n    if \"ERROR\" in line:\n        print(line.rstrip())\n```\n\n### Async With\n\nYou can use a command as an asynchronous context manager. There are two ways to run a program using\na context manager: a low-level API and a high-level API.\n\n#### Byte-by-Byte (Low Level)\n\nUse `async with` directly when you need byte-by-byte\ncontrol over the individual streams: stdin, stdout and stderr. To control a standard stream, you\nmust tell shellous to \"capture\" it (For more on this, see [Redirection](#redirection).)\n\n```python\ncmd = sh(\"cat\").stdin(sh.CAPTURE).stdout(sh.CAPTURE)\nasync with cmd as run:\n    run.stdin.write(b\"abc\")\n    run.stdin.close()\n    print(await run.stdout.readline())\n\nresult = run.result()\n```\n\nThe streams `run.stdout` and `run.stderr` are `asyncio.StreamReader` objects. The stream `run.stdin`\nis an `asyncio.StreamWriter` object. If we didn't specify that stdin/stdout are `sh.CAPTURE`, the \nstreams `run.stdin` and `run.stdout` would be \n`None`. \n\nThe return value of `run.result()` is a `Result` object. Depending on the command settings, this \nfunction may raise a `ResultError` on a non-zero exit code.\n\n\u003e :warning: When reading or writing individual streams, you are responsible for managing reads and writes so they don't\ndeadlock. You may use `run.create_task` to schedule a concurrent task.\n\nYou can also use `async with` to run a server. When you do so, you must tell the server\nto stop using `run.cancel()`. Otherwise, the context manager will wait forever for the process to exit.\n\n```python\nasync with sh(\"some-server\") as run:\n    # Send commands to the server here...\n    # Manually signal the server to stop.\n    run.cancel()\n```\n\n#### Prompt with Send/Expect (High Level API)\n\nUse the `prompt()` method to control a process using `send` and `expect`. The `prompt()`\nmethod returns an asynchronous context manager (the `Prompt` class) that facilitates reading and \nwriting strings and matching regular expressions.\n\n```python\ncmd = sh(\"cat\").set(pty=True)\n\nasync with cmd.prompt() as client:\n  await client.send(\"abc\")\n  output, _ = await client.expect(\"\\r\\n\")\n  print(output)\n```\n\nThe `Prompt` API automatically captures `stdin` and `stdout`.\n\nHere is another example of controlling a bash co-process running in a docker container.\n\n```python\nasync def list_packages():\n    \"Run bash in an ubuntu docker container and list packages.\"\n    bash_prompt = re.compile(\"root@[0-9a-f]+:/[^#]*# \")\n    cmd = sh(\"docker\", \"run\", \"-it\", \"--rm\", \"-e\", \"TERM=dumb\", \"ubuntu\")\n\n    async with cmd.set(pty=True).prompt(bash_prompt, timeout=3) as cli:\n        # Read up to first prompt.\n        await cli.expect()\n\n        # Disable echo. The `command()` method combines send *and* expect methods.\n        await cli.command(\"stty -echo\")\n\n        # Return list of packages.\n        result = await cli.command(\"apt-cache pkgnames\")\n        return result.strip().split(\"\\r\\n\")\n\n    # You can check the result object's exit code. You can only\n    # access `cli.result` outside the `async with` block.\n    assert cli.result.exit_code == 0\n```\n\nThe `prompt()` API does not raise a `ResultError` when a command exits with an error status. \nTypically, you'll see an EOFError when you were expecting to read a response. You can check the\nexit status by retrieving the Prompt's `result` property outside of the `async with` block.\n\n## Redirection\n\nshellous supports the redirection operators `|` and `\u003e\u003e`. They work similar to how they work in \nthe unix shell. Shellous does not support use of `\u003c` or `\u003e` for redirection. Instead, replace these \nwith `|`.\n\nTo redirect to or from a file, use a `pathlib.Path` object. Alternatively, you can redirect input/output\nto a StringIO object, an open file, a Logger, or use a special redirection constant like `sh.DEVNULL`.\n\n\u003e :warning: When combining the redirect operators with `await`, you must use parentheses; `await` has higher\nprecedence than `|` and `\u003e\u003e`.\n\n### Redirecting Standard Input\n\nTo redirect standard input, use the pipe operator `|` with the argument on the **left-side**.\nHere is an example that passes the string \"abc\" as standard input.\n\n```pycon\n\u003e\u003e\u003e cmd = \"abc\" | sh(\"wc\", \"-c\")\n\u003e\u003e\u003e await cmd\n'       3\\n'\n```\n\nTo read input from a file, use a `Path` object from `pathlib`.\n\n```pycon\n\u003e\u003e\u003e from pathlib import Path\n\u003e\u003e\u003e cmd = Path(\"LICENSE\") | sh(\"wc\", \"-l\")\n\u003e\u003e\u003e await cmd\n'     201\\n'\n```\n\nShellous supports different STDIN behavior when using different Python types.\n\n| Python Type | Behavior as STDIN |\n| ----------- | --------------- |\n| str | Read input from string object. |\n| bytes, bytearray | Read input from bytes object. |\n| Path | Read input from file specified by `Path`. |\n| File, StringIO, ByteIO | Read input from open file object. |\n| int | Read input from existing file descriptor. |\n| asyncio.StreamReader | Read input from `StreamReader`. |\n| sh.DEVNULL | Read input from `/dev/null`. |\n| sh.INHERIT  | Read input from existing `sys.stdin`. |\n| sh.CAPTURE | You will write to stdin interactively. |\n\n### Redirecting Standard Output\n\nTo redirect standard output, use the pipe operator `|` with the argument on the **right-side**. Here is an \nexample that writes to a temporary file.\n\n```pycon\n\u003e\u003e\u003e output_file = Path(\"/tmp/output_file\")\n\u003e\u003e\u003e cmd = sh(\"echo\", \"abc\") | output_file\n\u003e\u003e\u003e await cmd\n''\n\u003e\u003e\u003e output_file.read_bytes()\nb'abc\\n'\n```\n\nTo redirect standard output with append, use the `\u003e\u003e` operator.\n\n```pycon\n\u003e\u003e\u003e cmd = sh(\"echo\", \"def\") \u003e\u003e output_file\n\u003e\u003e\u003e await cmd\n''\n\u003e\u003e\u003e output_file.read_bytes()\nb'abc\\ndef\\n'\n```\n\nShellous supports different STDOUT behavior when using different Python types.\n\n| Python Type | Behavior as STDOUT/STDERR | \n| ----------- | --------------- | \n| Path | Write output to the file path specified by `Path`.  | \n| bytearray | Write output to a mutable byte array. | \n| File, StringIO, ByteIO | Write output to an open file object. | \n| int | Write output to existing file descriptor at its current position. ◆ | \n| logging.Logger | Log each line of output. ◆ | \n| asyncio.StreamWriter | Write output to `StreamWriter`. ◆ | \n| sh.CAPTURE | Capture output for `async with`. ◆ | \n| sh.DEVNULL | Write output to `/dev/null`. ◆ | \n| sh.INHERIT  | Write output to existing `sys.stdout` or `sys.stderr`. ◆ | \n\n◆ For these types, there is no difference between using `|` and `\u003e\u003e`.\n\nShellous does **not** support redirecting standard output/error to a plain `str` or `bytes` object. \nIf you intend to redirect output to a file, you must use a `pathlib.Path` object.\n\n### Redirecting Standard Error\n\nBy default, the first 1024 bytes read from standard error are stored in the Result object.\nAny further bytes are discarded. You can change the 1024 byte limit using the `error_limit`\noption.\n\nTo redirect standard error, use the `stderr` method. Standard error supports the\nsame Python types as standard output. To append, set `append=True` in the `stderr` method.\n\nTo redirect stderr to the same place as stdout, use the `sh.STDOUT` constant. If you also\nredirect stdout to `sh.DEVNULL`, you will only receive the standard error.\n\n```pycon\n\u003e\u003e\u003e cmd = sh(\"cat\", \"does_not_exist\").stderr(sh.STDOUT)\n\u003e\u003e\u003e await cmd.set(exit_codes={0,1})\n'cat: does_not_exist: No such file or directory\\n'\n```\n\nTo redirect standard error to the hosting program's `sys.stderr`, use the `sh.INHERIT` redirect\noption.\n\n```pycon\n\u003e\u003e\u003e cmd = sh(\"cat\", \"does_not_exist\").stderr(sh.INHERIT)\n\u003e\u003e\u003e await cmd\ncat: does_not_exist: No such file or directory\nTraceback (most recent call last):\n  ...\nshellous.result.ResultError: Result(exit_code=1, output_bytes=b'', error_bytes=b'', cancelled=False, encoding='utf-8')\n```\n\nIf you redirect stderr, it will no longer be stored in the Result object, and the `error_limit` option\nwill not apply.\n\n### Default Redirections\n\nFor regular commands, the default redirections are:\n\n- Standard input is read from the empty string (\"\").\n- Standard out is buffered and stored in the Result object (BUFFER).\n- First 1024 bytes of standard error is buffered and stored in the Result object (BUFFER).\n\nHowever, the default redirections are adjusted when using a pseudo-terminal (pty):\n\n- Standard input is captured and ignored (CAPTURE).\n- Standard out is buffered and stored in the Result object (BUFFER).\n- Standard error is redirected to standard output (STDOUT).\n\nWhen you use the `Prompt` API, the standard input and standard output are automatically redirected to CAPTURE.\n\n## Pipelines\n\nYou can create a pipeline by combining commands using the `|` operator. A pipeline feeds the standard out of one process into the next process as standard input. Here is the shellous\nequivalent to the bash command: `ls | grep README`\n\n```pycon\n\u003e\u003e\u003e pipe = sh(\"ls\") | sh(\"grep\", \"README\")\n\u003e\u003e\u003e await pipe\n'README.md\\n'\n```\n\nA pipeline returns a `Result` if the last command in the pipeline has the `.result` modifier. To set other\noptions like `encoding` for a Pipeline, set them on the last command.\n\n```pycon\n\u003e\u003e\u003e pipe = sh(\"ls\") | sh(\"grep\", \"README\").result\n\u003e\u003e\u003e await pipe\nResult(exit_code=0, output_bytes=b'README.md\\n', error_bytes=b'', cancelled=False, encoding='utf-8')\n```\n\nError reporting for a pipeline is implemented similar to using the `-o pipefail` shell option.\n\nPipelines support the same `await/async for/async with` operations that work on a single command, including\nthe `Prompt` API.\n\n```pycon\n\u003e\u003e\u003e [line.strip() async for line in pipe]\n['README.md']\n```\n\n## Process Substitution (Unix Only)\n\nYou can pass a shell command as an argument to another. Here is the shellous equivalent to the bash\ncommand: `grep README \u003c(ls)`.\n\n```pycon\n\u003e\u003e\u003e cmd = sh(\"grep\", \"README\", sh(\"ls\"))\n\u003e\u003e\u003e await cmd\n'README.md\\n'\n```\n\nUse `.writable` to write to a command instead.\n\n```pycon\n\u003e\u003e\u003e buf = bytearray()\n\u003e\u003e\u003e cmd = sh(\"ls\") | sh(\"tee\", sh(\"grep\", \"README\").writable | buf) | sh.DEVNULL\n\u003e\u003e\u003e await cmd\n''\n\u003e\u003e\u003e buf\nbytearray(b'README.md\\n')\n```\n\nThe above example is equivalent to `ls | tee \u003e(grep README \u003e buf) \u003e /dev/null`.\n\n## Timeouts\n\nYou can specify a timeout using the `timeout` option. If the timeout expires, shellous will raise\na `TimeoutError`.\n\n```pycon\n\u003e\u003e\u003e await sh(\"sleep\", 60).set(timeout=0.1)\nTraceback (most recent call last):\n  ...\nTimeoutError\n```\n\nTimeouts are just a special case of **cancellation**. When a command is cancelled, shellous terminates \nthe running process and raises a `CancelledError`.\n\n```pycon\n\u003e\u003e\u003e t = asyncio.create_task(sh(\"sleep\", 60).coro())\n\u003e\u003e\u003e t.cancel()\nTrue\n\u003e\u003e\u003e await t\nTraceback (most recent call last):\n  ...\nCancelledError\n```\n\nBy default, shellous will send a SIGTERM signal to the process to tell it to exit. If the process does not\nexit within 3 seconds, shellous will send a SIGKILL signal. You can change these defaults with the\n`cancel_signal` and `cancel_timeout` settings. A command is not considered fully cancelled until the \nprocess exits.\n\n## Pseudo-Terminal Support (Unix Only)\n\nTo run a command through a pseudo-terminal, set the `pty` option to True. \n\n```pycon\n\u003e\u003e\u003e await sh(\"echo\", \"in a pty\").set(pty=True)\n'in a pty\\r\\n'\n```\n\nAlternatively, you can pass a `pty` function to configure the tty mode and size.\n\n```pycon\n\u003e\u003e\u003e ls = sh(\"ls\").set(pty=shellous.cooked(cols=40, rows=10, echo=False))\n\u003e\u003e\u003e await ls(\"README.md\", \"CHANGELOG.md\")\n'CHANGELOG.md\\tREADME.md\\r\\n'\n```\n\nShellous provides three built-in helper functions: `shellous.cooked()`, `shellous.raw()` and `shellous.cbreak()`.\n\n## Context Objects\n\nYou can store shared command settings in an immutable context object (CmdContext). To create a new \ncontext object, specify your changes to the default context **sh**:\n\n```pycon\n\u003e\u003e\u003e auditor = lambda phase, info: print(phase, info[\"runner\"].name)\n\u003e\u003e\u003e sh_audit = sh.set(audit_callback=auditor)\n```\n\nNow all commands created with `sh_audit` will log their progress using the audit callback.\n\n```pycon\n\u003e\u003e\u003e await sh_audit(\"echo\", \"goodbye\")\nstart echo\nstop echo\n'goodbye\\n'\n```\n\nYou can also create a context object that specifies all return values are `Result` objects.\n\n```pycon\n\u003e\u003e\u003e rsh = sh.result\n\u003e\u003e\u003e await rsh(\"echo\", \"whatever\")\nResult(exit_code=0, output_bytes=b'whatever\\n', error_bytes=b'', cancelled=False, encoding='utf-8')\n```\n\n## Options\n\nBoth `Command` and `CmdContext` support options to control their runtime behavior. Some of these options (timeout,\npty, audit_callback, and exit_codes) have been described above. See the `shellous.Options` class for\nmore information.\n\nYou can retrieve an option from `cmd` with `cmd.options.\u003coption\u003e`. For example, use `cmd.options.encoding` to obtain the encoding:\n\n```pycon\n\u003e\u003e\u003e cmd = sh(\"echo\").set(encoding=\"latin1\")\n\u003e\u003e\u003e cmd.options.encoding\n'latin1'\n```\n\n`Command` and `CmdContext` use the `.set()` method to specify most options:\n\n| Option | Description |\n| --- | --- |\n| path | Search path to use instead of the `PATH` environment variable. (Default=None) |\n| env | Additional environment variables to pass to the command. (Default={}) |\n| inherit_env | True if command should inherit the environment variables from the current process. (Default=True) |\n| encoding | Text encoding of input/output streams. You can specify an error handling scheme by including it after a space, e.g. \"ascii backslashreplace\". (Default=\"utf-8 strict\") |\n| exit_codes | Set of exit codes that do not raise a `ResultError`. (Default={0}) |\n| timeout | Timeout in seconds to wait before cancelling the process. (Default=None) |\n| cancel_timeout | Timeout in seconds to wait for a cancelled process to exit before forcefully terminating it. (Default=3s) |\n| cancel_signal | The signal sent to a process when it is cancelled. (Default=SIGTERM) |\n| alt_name | Alternate name for the process used for debug logging. (Default=None) |\n| pass_fds | Additional file descriptors to pass to the process. (Default={}) |\n| pass_fds_close | True if descriptors in `pass_fds` should be closed after the child process is launched. (Default=False) |\n| pty | Used to allocate a pseudo-terminal (PTY). (Default=False) |\n| close_fds | True if process should close all file descriptors when it starts. This setting defaults to False to align with `posix_spawn` requirements. (Default=False) |\n| audit_callback | Provide function to audit stages of process execution. (Default=None) |\n| coerce_arg | Provide function to coerce `Command` arguments to strings when `str()` is not sufficient. For example, you can provide your own function that converts a dictionary argument to a sequence of strings. (Default=None) |\n| error_limit | Maximum number of initial bytes of STDERR to store in `Result` object. (Default=1024) |\n\n### env\n\nUse the `env()` method to **add** to the list of environment variables. The `env()` method supports keyword parameters.\nYou can call `env()` more than once and the effect is additive.\n\n```pycon\n\u003e\u003e\u003e cmd = sh(\"echo\").env(ENV1=\"a\", ENV2=\"b\").env(ENV2=3)\n\u003e\u003e\u003e cmd.options.env\n{'ENV1': 'a', 'ENV2': '3'}\n```\n\nUse the `env` option with `set()` when you want to **replace all** the environment variables. \n\n### input, output, error\n\nWhen you apply a redirection operator to a `Command` or `CmdContext`, the redirection targets\nare also stored in the `Options` object. To change these, use the `.stdin()`, `.stdout()`, or `.stderr()` methods or the redirection operator `|`.\n\n| Option | Description |\n| --- | --- |\n| input | The redirection target for standard input. |\n| input_close | True if standard input should be closed after the process is launched. |\n| output | The redirection target for standard output. |\n| output_append | True if standard output should be open for append. |\n| output_close | True if standard output should be closed after the process is launched. |\n| error | The redirection target for standard error. |\n| error_append | True if standard error should be open for append. |\n| error_close | True if standard error should be closed after the process is launched. |\n\n## Type Checking\n\nShellous fully supports PEP 484 type hints.\n\n### Commands\n\nCommands are generic on the return type, either `str` or `Result`. You will specify the\ntype of a command object as `Command[str]` or `Command[Result]`.\n\nUse the `result` modifier to obtain a `Command[Result]` from a `Command[str]`.\n\n```python\nfrom shellous import sh, Command, Result\n\ncmd1: Command[str] = sh(\"echo\", \"abc\")\n# When you `await cmd1`, the result is a `str` object.\n\ncmd2: Command[Result] = sh.result(\"echo\", \"abc\")\n# When you `await cmd2`, the result is a `Result` object.\n```\n\n### CmdContext\n\nThe `CmdContext` class is also generic on either `str` or `Result`.\n\n```python\nfrom shellous import sh, CmdContext, Result\n\nsh1: CmdContext[str] = sh.set(path=\"/bin:/usr/bin\")\n# When you use `sh1` to create commands, it produces `Command[str]` object with the given path.\n\nsh2: CmdContext[Result] = sh.result.set(path=\"/bin:/usr/bin\")\n# When you use `sh2` to create commands, it produces `Command[Result]` objects with the given path.\n```\n\n## Logging\n\nFor verbose logging, shellous supports a `SHELLOUS_TRACE` environment variable. Set the\nvalue of `SHELLOUS_TRACE` to a comma-delimited list of options:\n\n- **detail**:  Enables detailed logging used to trace the steps of running a command.\n\n- **prompt**: Enables logging in the `Prompt` class when controlling a program\nusing send/expect.\n\n- **all**: Enables all logging options.\n\nShellous uses the built-in Python `logging` module. After enabling these options, \nthe `shellous` logger will display log messages at the `INFO` level.\n\nWithout these options enabled, Shellous generates almost no log messages.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbyllyfish%2Fshellous","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbyllyfish%2Fshellous","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbyllyfish%2Fshellous/lists"}