sshscript 3.1.4__tar.gz → 3.1.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. {sshscript-3.1.4 → sshscript-3.1.5}/CHANGELOG.md +47 -1
  2. sshscript-3.1.5/CODE_OF_CONDUCT.md +61 -0
  3. sshscript-3.1.5/CONTRIBUTING.md +87 -0
  4. sshscript-3.1.5/EXCEPTIONS.md +54 -0
  5. {sshscript-3.1.4 → sshscript-3.1.5}/MANIFEST.in +1 -1
  6. sshscript-3.1.5/PKG-INFO +375 -0
  7. sshscript-3.1.5/README.md +344 -0
  8. sshscript-3.1.5/SECURITY.md +111 -0
  9. sshscript-3.1.5/SUPPORT.md +86 -0
  10. {sshscript-3.1.4 → sshscript-3.1.5}/pyproject.toml +14 -0
  11. {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/__init__.py +6 -2
  12. {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/_version.py +1 -1
  13. sshscript-3.1.5/src/sshscript/commandresult.py +28 -0
  14. {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/dollar.py +3 -2
  15. {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/dollarchanger.py +1 -1
  16. {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/dollarparser.py +4 -0
  17. {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/session.py +86 -17
  18. sshscript-3.1.5/src/sshscript/sshconfig.py +127 -0
  19. {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/sshscript.py +36 -10
  20. {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/unittest/dollar_syntax.spy +7 -13
  21. {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/unittest/language.spy +2 -2
  22. sshscript-3.1.5/src/sshscript/unittest/test_check_file.py +60 -0
  23. sshscript-3.1.5/src/sshscript/unittest/test_command_api.py +128 -0
  24. {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/unittest/test_logger_integration.py +2 -1
  25. {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/unittest/test_openssh_integration.py +6 -3
  26. sshscript-3.1.5/src/sshscript/unittest/test_ssh_config.py +128 -0
  27. {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/unittest/test_ssh_security.py +2 -1
  28. {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/unittest/test_sshscript_module.py +8 -8
  29. sshscript-3.1.5/src/sshscript.egg-info/PKG-INFO +375 -0
  30. {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript.egg-info/SOURCES.txt +6 -0
  31. {sshscript-3.1.4 → sshscript-3.1.5}/tools/check_release.py +1 -1
  32. {sshscript-3.1.4 → sshscript-3.1.5}/tools/prepare_release.py +7 -2
  33. sshscript-3.1.4/CONTRIBUTING.md +0 -56
  34. sshscript-3.1.4/EXCEPTIONS.md +0 -45
  35. sshscript-3.1.4/PKG-INFO +0 -178
  36. sshscript-3.1.4/README.md +0 -153
  37. sshscript-3.1.4/SECURITY.md +0 -37
  38. sshscript-3.1.4/SUPPORT.md +0 -23
  39. sshscript-3.1.4/src/sshscript.egg-info/PKG-INFO +0 -178
  40. {sshscript-3.1.4 → sshscript-3.1.5}/.github/dependabot.yml +0 -0
  41. {sshscript-3.1.4 → sshscript-3.1.5}/.github/workflows/ci.yml +0 -0
  42. {sshscript-3.1.4 → sshscript-3.1.5}/.github/workflows/codeql.yml +0 -0
  43. {sshscript-3.1.4 → sshscript-3.1.5}/.github/workflows/release.yml +0 -0
  44. {sshscript-3.1.4 → sshscript-3.1.5}/LICENSE.txt +0 -0
  45. {sshscript-3.1.4 → sshscript-3.1.5}/RELEASING.md +0 -0
  46. {sshscript-3.1.4 → sshscript-3.1.5}/setup.cfg +0 -0
  47. {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/channelgeneric.py +0 -0
  48. {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/channelssh.py +0 -0
  49. {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/channelsubprocess.py +0 -0
  50. {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/channelutils.py +0 -0
  51. {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/errorutils.py +0 -0
  52. {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/patching.py +0 -0
  53. {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/sessionwrapper.py +0 -0
  54. {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/spyimporter.py +0 -0
  55. {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/stdio.py +0 -0
  56. {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/tokenparser.py +0 -0
  57. {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/unittest/check_package_asserts.py +0 -0
  58. {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/unittest/dollar_syntax_fixture.spy +0 -0
  59. {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/unittest/language_fixture.spy +0 -0
  60. {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/unittest/test_channelgeneric_expect.py +0 -0
  61. {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/unittest/test_file_transfer.py +0 -0
  62. {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/unittest/test_logger_api.py +0 -0
  63. {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/unittest/test_production_contract.py +0 -0
  64. {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/unittest/test_session_close_reporting.py +0 -0
  65. {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/unittest/test_session_proxy_cleanup.py +0 -0
  66. {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/unittest/test_spy_source_mapping.py +0 -0
  67. {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/unittest/test_spy_thread_session.py +0 -0
  68. {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/unittest/test_sshscript_dollar_syntax.py +0 -0
  69. {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/unittest/test_stdio_dynamic_string.py +0 -0
  70. {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/unittest/test_syntax_error.spy +0 -0
  71. {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/unittest/test_update_check.py +0 -0
  72. {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript.egg-info/dependency_links.txt +0 -0
  73. {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript.egg-info/entry_points.txt +0 -0
  74. {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript.egg-info/requires.txt +0 -0
  75. {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript.egg-info/top_level.txt +0 -0
  76. {sshscript-3.1.4 → sshscript-3.1.5}/tools/publish_release.py +0 -0
  77. {sshscript-3.1.4 → sshscript-3.1.5}/tools/run_checks.py +0 -0
  78. {sshscript-3.1.4 → sshscript-3.1.5}/tools/setup_openssh_ci.sh +0 -0
@@ -1,6 +1,49 @@
1
1
  # Changelog
2
2
 
3
- ## 3.1.4 - 2026-09-23
3
+ This project records user-visible changes here. Release artifacts and their
4
+ provenance are available from the linked GitHub Release and PyPI pages.
5
+
6
+ ## [3.1.5] - 2026-09-27
7
+
8
+ ### Command API and SSH configuration
9
+
10
+ - Accept argv lists/tuples for a single command, with local direct execution
11
+ and POSIX quoting over SSH; shell mode remains explicit for strings.
12
+ - Return immutable CommandResult snapshots with stdout, stderr, exitcode,
13
+ host, duration, and command. Unpacking/indexing now yields three values:
14
+ stdout, stderr, exitcode. Two-value unpacking must be updated.
15
+ Output values are now strings rather than live buffer objects.
16
+ - Apply check=True consistently to local and remote one-shot commands after
17
+ preserving results; CalledProcessError carries text output and result.
18
+ - Resolve common ~/.ssh/config settings with explicit API overrides; add
19
+ config opt-out, alternate files, and a connection-free settings preview.
20
+ Support ProxyCommand and ProxyJump (the latter uses local OpenSSH).
21
+ - Reject unsupported Match/Include/canonicalization rules; warn about other
22
+ unapplied config settings. ProxyJump requires noninteractive authentication
23
+ and verified jump-host keys. Existing host-key checks remain enabled.
24
+
25
+ ### Script validation
26
+
27
+ - Add check_file(path) and --check FILE to compile Python/dollar syntax without
28
+ executing user code, imports, or commands. The no-file --check update alias
29
+ remains compatible; --check-updates is the explicit update command.
30
+ - Map tokenizer indentation errors to the original source file; retain source
31
+ mapping coverage for nested commands, interpolation, multiline expressions,
32
+ and imported .spy modules. Editor integration is deferred.
33
+
34
+ ### Documentation and community
35
+
36
+ - Reorganize the README around installation, secure first use, compatibility,
37
+ support, and release provenance.
38
+ - Add structured issue forms, a pull request template, and a Code of Conduct.
39
+ - Clarify supported environments, security boundaries, and public test paths.
40
+
41
+ ## [3.1.4] - 2026-09-24
42
+
43
+ First Production/Stable release in the 3.1 line. Published through
44
+ [PyPI Trusted Publishing](https://pypi.org/project/sshscript/3.1.4/) with
45
+ verified artifacts in the
46
+ [GitHub Release](https://github.com/iapyeh/sshscript/releases/tag/v3.1.4).
4
47
 
5
48
  ### Production hardening
6
49
 
@@ -53,3 +96,6 @@
53
96
  - Add packaging metadata, license, contributor guidance, and CI configuration.
54
97
  - Add a disposable loopback OpenSSH integration gate covering host keys, SFTP,
55
98
  PTY behavior, sudo/su, and timeout handling.
99
+
100
+ [Unreleased]: https://github.com/iapyeh/sshscript/compare/v3.1.4...release
101
+ [3.1.4]: https://github.com/iapyeh/sshscript/releases/tag/v3.1.4
@@ -0,0 +1,61 @@
1
+ # Code of Conduct
2
+
3
+ ## Our commitment
4
+
5
+ SSHScript welcomes people of every background and experience level. We are
6
+ committed to a collaborative environment in which participation is respectful,
7
+ constructive, and free from harassment.
8
+
9
+ ## Expected behavior
10
+
11
+ - Be respectful and assume good faith while still discussing technical risks
12
+ directly.
13
+ - Give actionable feedback about work rather than making personal judgments.
14
+ - Acknowledge different experience levels, environments, and communication
15
+ styles.
16
+ - Protect credentials, personal information, internal infrastructure details,
17
+ and embargoed vulnerability information.
18
+ - Accept moderation decisions and correct harmful behavior promptly.
19
+
20
+ ## Unacceptable behavior
21
+
22
+ - Harassment, intimidation, threats, stalking, or discriminatory language.
23
+ - Sexualized language, imagery, or unwelcome attention.
24
+ - Personal attacks, deliberate humiliation, or sustained disruption.
25
+ - Publishing another person's private information without permission.
26
+ - Retaliation against someone who reports a concern in good faith.
27
+ - Using project spaces to distribute malware, credentials, or knowingly unsafe
28
+ instructions.
29
+
30
+ Reasonable technical disagreement, security review, and criticism of code are
31
+ not harassment when expressed professionally and focused on the work.
32
+
33
+ ## Scope
34
+
35
+ This policy applies to repository issues, pull requests, reviews, discussions,
36
+ release and documentation spaces, and other public interactions in which a
37
+ person represents the SSHScript project.
38
+
39
+ ## Reporting conduct concerns
40
+
41
+ Do not post a conduct report publicly. Contact the maintainer at
42
+ [iapyeh@gmail.com](mailto:iapyeh@gmail.com) with links or screenshots, the
43
+ relevant dates, a factual description, and any immediate safety concern. Share
44
+ only information needed to evaluate the report.
45
+
46
+ Suspected software vulnerabilities belong in
47
+ [GitHub private vulnerability reporting](https://github.com/iapyeh/sshscript/security/advisories/new),
48
+ as described in [SECURITY.md](SECURITY.md).
49
+
50
+ Reports will be reviewed as privately as practical. A maintainer named in or
51
+ directly involved with a report should not make the enforcement decision when a
52
+ reasonable alternative reviewer is available. Knowingly false reports may
53
+ themselves violate this policy; an unproven good-faith report does not.
54
+
55
+ ## Enforcement
56
+
57
+ Project maintainers may edit or remove content, request a correction, issue a
58
+ warning, limit participation, or permanently exclude a participant. Responses
59
+ will be based on severity, impact, pattern of behavior, and willingness to
60
+ correct the problem. Illegal activity or a credible threat may be referred to
61
+ the appropriate service provider or authority.
@@ -0,0 +1,87 @@
1
+ # Contributing
2
+
3
+ Thank you for helping improve SSHScript. Participation is governed by the
4
+ [Code of Conduct](CODE_OF_CONDUCT.md). Please use a private security advisory,
5
+ not an issue or pull request, for a suspected vulnerability.
6
+
7
+ ## Development setup
8
+
9
+ Use Python 3.11 or newer. The public release checkout uses a `src/` package
10
+ layout. From the repository root:
11
+
12
+ ```sh
13
+ python3 -m venv .venv
14
+ . .venv/bin/activate
15
+ python3 -m pip install --upgrade pip
16
+ python3 -m pip install -e .
17
+ python3 -m pip install build twine
18
+ ```
19
+
20
+ Keep test credentials, host inventories, local configuration, build output,
21
+ and editor state untracked.
22
+
23
+ ## Required checks
24
+
25
+ Run the complete credential-free gate before proposing a change:
26
+
27
+ ```sh
28
+ python3 tools/run_checks.py
29
+ python3 tools/check_release.py --output /tmp/sshscript-candidate-UNIQUE
30
+ ```
31
+
32
+ The output path must not already exist. The first command finds the public test
33
+ suite under `src/sshscript/unittest/` and runs normal and optimized tests,
34
+ compile checks, the package assertion scan, and dollar-syntax smoke tests. The
35
+ second command builds the allowlisted wheel and source distribution, checks
36
+ their metadata, installs the wheel in an isolated environment, and performs an
37
+ installed-package smoke test.
38
+
39
+ Tests under `src/sshscript/unittest/` must not require network access, SSH
40
+ agents, private keys, passwords, or host-specific configuration unless they are
41
+ part of the isolated OpenSSH fixture. A regression fix should add a
42
+ credential-free test whenever the behavior can be reproduced with a fake
43
+ Paramiko client or local subprocess.
44
+
45
+ ## SSH integration tests
46
+
47
+ Public CI provisions a disposable loopback OpenSSH server and runs
48
+ `src/sshscript/unittest/test_openssh_integration.py` against the built wheel.
49
+ The setup uses ephemeral users, passwords, and keys generated inside the CI
50
+ runner; it must never target a persistent host. See
51
+ `tools/setup_openssh_ci.sh` for the exact environment contract.
52
+
53
+ Historical site-specific and credentialed tests are intentionally not shipped
54
+ in the public release checkout. Do not add production credentials or internal
55
+ host scenarios to a pull request. Convert a failure into a disposable fixture,
56
+ redacted fake, or credential-free unit test first.
57
+
58
+ ## Compatibility and public APIs
59
+
60
+ Changes to `Session`, `run_file()`, CLI exit statuses, dollar syntax, logging,
61
+ or SSH security defaults require synchronized implementation, tests, README,
62
+ and changelog updates. Avoid silently accepting insecure behavior.
63
+
64
+ The module API is the primary public interface. Keep dollar syntax behavior
65
+ aligned with it, and document compatibility aliases or deprecations explicitly.
66
+ Tests should cover both normal Python and `python -O` where the exception
67
+ contract is involved. Do not use runtime `assert` statements for package input
68
+ or lifecycle validation.
69
+
70
+ Before release, run all normal, optimized, compile, and AST gates listed in
71
+ [EXCEPTIONS.md](EXCEPTIONS.md). CI targets Python 3.11 through 3.14.
72
+
73
+ ## Pull requests
74
+
75
+ Target the protected `release` branch from a topic branch or fork. Keep each
76
+ pull request focused and include:
77
+
78
+ - the problem and intended behavior;
79
+ - tests that fail before the change and pass afterward when practical;
80
+ - documentation and changelog updates for user-visible behavior;
81
+ - the commands used to validate the change;
82
+ - any compatibility, security, logging, or migration impact.
83
+
84
+ Complete the pull request template and respond to review feedback. A passing CI
85
+ run is required but does not guarantee acceptance. By submitting a
86
+ contribution, you agree that it may be distributed under the repository's MIT
87
+ License.
@@ -0,0 +1,54 @@
1
+ # Stable exception contract
2
+
3
+ The following contract applies with and without `python -O`.
4
+
5
+ | Failure | Exception |
6
+ | --- | --- |
7
+ | Wrong argument type | `TypeError` |
8
+ | Correct type but invalid value, content, or combination | `ValueError` |
9
+ | Invalid session, console, or channel lifecycle state | `RuntimeError` |
10
+ | Operation on a closed channel or transport | `BrokenPipeError` |
11
+ | Channel ends while waiting | `EOFError` |
12
+ | Timeout | `TimeoutError` |
13
+ | Disconnected `Session.sftp`, upload, or download | `SSHScriptException` |
14
+ | Filesystem failure | Appropriate `OSError` subclass |
15
+ | Paramiko failure | Original Paramiko exception and traceback |
16
+ | Internal AST or data-structure invariant failure | Descriptive `RuntimeError` |
17
+ | Stack indexing outside its bounds | `IndexError`, following `deque` |
18
+
19
+ Nonzero command exit status remains data by default. One-shot local and remote
20
+ `Session.exec_command(..., check=True)` raise `subprocess.CalledProcessError`
21
+ after saving `session.last_result` and channel output/status. The exception's
22
+ `output`/`stdout` and `stderr` are text snapshots on both backends; `cmd` is a
23
+ string or argument tuple, and `result` is the complete immutable CommandResult.
24
+ CommandResult unpacks as exactly three values: stdout, stderr, exitcode; indexing
25
+ and slicing use the same order. Persistent consoles retain two-buffer returns.
26
+ This replaces the local backend's former raw subprocess exception (byte
27
+ output and generated argv). `AssertionError` was never a supported API contract.
28
+
29
+ User-written `assert` in `.spy` files is preserved as Python syntax. Python's
30
+ optimized mode removes these statements, including any calls inside them.
31
+ Production scripts must not depend on `assert` for command-success handling.
32
+ For local or remote one-shot execution, use `session.exec_command(command, check=True)`, or
33
+ explicitly inspect `session.exitcode` and raise an application exception when
34
+ appropriate. Persistent consoles retain their separate command/prompt contract.
35
+
36
+ `for_with` must be strictly bool. `get_pty` must be None or bool. Commands must
37
+ be nonempty strings or nonempty lists/tuples of string arguments. Argument
38
+ sequences disallow NUL, an empty executable, and shell mode. Persistent
39
+ commands must contain only one line.
40
+ Compiled bytes regular expressions are not accepted by text-output matching.
41
+ Listener removal requires the identical top listener; failed removal, duplicate
42
+ hijack/release, and last-layer removal leave their associated state unchanged.
43
+
44
+ Run the canonical credential-free gates from the public release repository root
45
+ using a supported interpreter:
46
+
47
+ ```sh
48
+ python3 tools/run_checks.py
49
+ ```
50
+
51
+ The runner locates the package and tests under `src/sshscript/`, then executes
52
+ normal and optimized test suites, the dollar-syntax smoke suite, compile checks,
53
+ and the package-assertion gate. CI runs these checks on Python 3.11–3.14 on
54
+ Linux and macOS. The AST gate scans package runtime modules and excludes tests.
@@ -1,5 +1,5 @@
1
1
  include README.md LICENSE.txt RELEASING.md
2
- include CHANGELOG.md CONTRIBUTING.md EXCEPTIONS.md SECURITY.md SUPPORT.md
2
+ include CHANGELOG.md CODE_OF_CONDUCT.md CONTRIBUTING.md EXCEPTIONS.md SECURITY.md SUPPORT.md
3
3
  recursive-include tools *.py
4
4
  include tools/setup_openssh_ci.sh
5
5
  include .github/dependabot.yml
@@ -0,0 +1,375 @@
1
+ Metadata-Version: 2.4
2
+ Name: sshscript
3
+ Version: 3.1.5
4
+ Summary: Python automation for local processes, SSH, and SSHScript .spy files
5
+ Author-email: "Yeh, Hsin-Yuan" <iapyeh@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/iapyeh/sshscript
8
+ Project-URL: Documentation, https://iapyeh.github.io/sshscript/v3a/
9
+ Project-URL: Changelog, https://github.com/iapyeh/sshscript/blob/release/CHANGELOG.md
10
+ Project-URL: Releases, https://github.com/iapyeh/sshscript/releases
11
+ Project-URL: Source, https://github.com/iapyeh/sshscript
12
+ Project-URL: Issues, https://github.com/iapyeh/sshscript/issues
13
+ Project-URL: Security, https://github.com/iapyeh/sshscript/security/policy
14
+ Project-URL: Support, https://github.com/iapyeh/sshscript/discussions
15
+ Keywords: automation,paramiko,remote-execution,sftp,ssh,subprocess,system-administration
16
+ Classifier: Development Status :: 5 - Production/Stable
17
+ Classifier: Operating System :: MacOS
18
+ Classifier: Operating System :: POSIX :: Linux
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Programming Language :: Python :: 3.14
24
+ Classifier: Topic :: System :: Systems Administration
25
+ Requires-Python: >=3.11
26
+ Description-Content-Type: text/markdown
27
+ License-File: LICENSE.txt
28
+ Requires-Dist: paramiko<5,>=2.11
29
+ Requires-Dist: packaging>=21
30
+ Dynamic: license-file
31
+
32
+ # SSHScript
33
+
34
+ [![PyPI](https://img.shields.io/pypi/v/sshscript)](https://pypi.org/project/sshscript/)
35
+ [![Python](https://img.shields.io/pypi/pyversions/sshscript)](https://pypi.org/project/sshscript/)
36
+ [![CI](https://github.com/iapyeh/sshscript/actions/workflows/ci.yml/badge.svg?branch=release)](https://github.com/iapyeh/sshscript/actions/workflows/ci.yml)
37
+ [![CodeQL](https://github.com/iapyeh/sshscript/actions/workflows/codeql.yml/badge.svg?branch=release)](https://github.com/iapyeh/sshscript/actions/workflows/codeql.yml)
38
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/iapyeh/sshscript/blob/release/LICENSE.txt)
39
+
40
+ SSHScript is a Python automation library for running commands locally and over
41
+ SSH through one `Session` API. It also provides optional dollar syntax for
42
+ compact `.spy` automation files.
43
+
44
+ **Current release:** [3.1.5](https://github.com/iapyeh/sshscript/releases/tag/v3.1.5)
45
+ (Production/Stable) · **Python:** 3.11 or newer · **Tested:** Python
46
+ 3.11–3.14 on Linux and macOS
47
+
48
+ [Documentation](https://iapyeh.github.io/sshscript/v3a/) ·
49
+ [PyPI](https://pypi.org/project/sshscript/) ·
50
+ [Changelog](https://github.com/iapyeh/sshscript/blob/release/CHANGELOG.md) ·
51
+ [Security](https://github.com/iapyeh/sshscript/security/policy) ·
52
+ [Support](https://github.com/iapyeh/sshscript/blob/release/SUPPORT.md)
53
+
54
+ ## Why SSHScript?
55
+
56
+ - Use the same interface for local subprocesses and remote SSH commands.
57
+ - Traverse nested SSH connections without rebuilding connection logic.
58
+ - Keep ordinary Python functions, packages, exceptions, data processing, and
59
+ threading around your automation.
60
+ - Scope connections, privilege changes, persistent shells, and interactive
61
+ programs with context managers.
62
+ - Read stdout, stderr, and exit status directly after each command.
63
+ - Add concise dollar syntax only where command-shaped notation improves a
64
+ script.
65
+
66
+ ## Install
67
+
68
+ SSHScript requires Python 3.11 or newer. Installing in a virtual environment is
69
+ recommended:
70
+
71
+ ```sh
72
+ python3 -m venv .venv
73
+ . .venv/bin/activate
74
+ python3 -m pip install --upgrade pip
75
+ python3 -m pip install sshscript
76
+ sshscript --version
77
+ ```
78
+
79
+ For a production deployment that requires repeatable dependency resolution,
80
+ pin SSHScript and all transitive dependencies in your application's lock file.
81
+ To install this release explicitly:
82
+
83
+ ```sh
84
+ python3 -m pip install "sshscript==3.1.5"
85
+ ```
86
+
87
+ Use `python3 -m pip install --upgrade sshscript` to upgrade. The optional
88
+ `sshscript --check-updates` command queries PyPI and prints an upgrade command;
89
+ it never installs an update by itself.
90
+
91
+ ## 60-second local quickstart
92
+
93
+ ```python
94
+ import shlex
95
+ import sys
96
+
97
+ from sshscript import Session
98
+
99
+ command = shlex.join([
100
+ sys.executable,
101
+ "-c",
102
+ "print('sshscript is ready')",
103
+ ])
104
+
105
+ with Session() as local:
106
+ stdout, stderr, exitcode = local.exec_command(
107
+ command,
108
+ shell=False,
109
+ check=True,
110
+ )
111
+ print(str(stdout).strip())
112
+ print(f"exit code: {local.exitcode}")
113
+ ```
114
+
115
+ Expected output:
116
+
117
+ ```text
118
+ sshscript is ready
119
+ exit code: 0
120
+ ```
121
+
122
+ `exec_command()` accepts one nonempty command string or a nonempty list/tuple
123
+ of string arguments. An argument sequence means **one command**, not a batch.
124
+ Sequences execute directly on the local host and are quoted for a POSIX login
125
+ shell over SSH; shell operators inside them are literal arguments. They accept
126
+ only `shell=None` or `shell=False`, without `shell_executable`. Use a string
127
+ with `shell=True` when you intentionally need shell operators.
128
+
129
+ ```python
130
+ with Session() as local:
131
+ result = local.exec_command(
132
+ [sys.executable, "-c", "import sys; print(sys.argv[1])", "a; b"],
133
+ check=True,
134
+ timeout=30,
135
+ )
136
+ print(result.stdout, result.exitcode, result.duration)
137
+ stdout, stderr, exitcode = result # three-value unpacking
138
+ ```
139
+
140
+ Each call returns an immutable `CommandResult` containing text `stdout` and
141
+ `stderr`, `exitcode`, `host`, `duration`, and `command`. `host` snapshots
142
+ `session.host` (`None` locally); it does not execute `hostname`. `duration` is
143
+ elapsed monotonic seconds for command execution and output collection,
144
+ including communication and worker cleanup but excluding connection setup.
145
+ `command` is the normalized string or an immutable tuple of arguments.
146
+ `session.last_result` references the most recently completed command; saved
147
+ results remain valid after later commands or session closure. Validation
148
+ failures leave it alone; starting a command clears it until completion.
149
+
150
+ Both local and remote commands accept `check=True` to raise
151
+ `subprocess.CalledProcessError` for a nonzero status **after** preserving the
152
+ result. The exception has text `stdout`/`stderr`, the normalized `cmd`, and a
153
+ `result` attribute. With the default `check=False`, a nonzero status is result
154
+ data. Connection errors and timeouts propagate unchanged.
155
+
156
+ Compatibility: the returned object is no longer a tuple of live output
157
+ buffers. Unpack exactly three values: stdout, stderr, exitcode. Indexing and
158
+ slicing use that same three-value order; old two-value unpacking must change.
159
+ Use `session.stdout`/`session.stderr` for the existing buffer interface.
160
+ Persistent shell and interactive console APIs retain their existing buffer
161
+ and prompt semantics; this result/check contract applies to one-shot Session
162
+ commands, including `$` commands outside persistent consoles.
163
+
164
+ ## First secure SSH connection
165
+
166
+ SSHScript uses Paramiko and verifies system host keys by default. Before the
167
+ first connection, place the server key in the account's standard
168
+ `known_hosts` file and verify its fingerprint through an independent trusted
169
+ channel. Prefer an SSH agent, managed private key, or secret manager over a
170
+ password embedded in source code.
171
+
172
+ ```python
173
+ from sshscript import Session
174
+
175
+ with Session() as local:
176
+ with local.connect(
177
+ "ops@example.net",
178
+ timeout=30,
179
+ banner_timeout=30,
180
+ auth_timeout=30,
181
+ ) as remote:
182
+ result = remote.exec_command(
183
+ ["uname", "-s"],
184
+ check=True,
185
+ timeout=30,
186
+ )
187
+ print(result.stdout.strip())
188
+ ```
189
+
190
+ Unknown and changed host keys are rejected unless the caller explicitly
191
+ supplies a different Paramiko policy. Do not use automatic key acceptance in a
192
+ production workflow unless a separate trusted bootstrap process has already
193
+ verified the key. See the
194
+ [SSHScript v3 documentation](https://iapyeh.github.io/sshscript/v3a/) for
195
+ nested connections, timeouts, file transfer, `sudo`, `su`, and interactive
196
+ programs.
197
+
198
+ ## Reusing SSH configuration
199
+
200
+ Connections from a local session read `~/.ssh/config` if it exists. Supported
201
+ settings are `Host` patterns, `HostName`, `User`, `Port`, `IdentityFile`,
202
+ `ProxyCommand`, and `ProxyJump`. Explicit API arguments override config, then
203
+ built-in defaults apply. `port=None` means unspecified; an explicit `port=22`
204
+ overrides a configured port. Explicit `pkey`, `pkey_path`, or `key_filename`
205
+ overrides configured identity files. Host-key verification remains enabled.
206
+
207
+ ```python
208
+ # Inspect effective settings without connecting or starting a proxy process.
209
+ settings = Session.resolve_connection("production", port=2222)
210
+
211
+ with Session() as local:
212
+ with local.connect("production") as remote:
213
+ result = remote(["uname", "-s"], check=True)
214
+ ```
215
+
216
+ Pass `ssh_config=False` to disable config lookup, or `ssh_config="/path/config"`
217
+ to require a particular file. Nested connections do not read local config
218
+ unless an explicit file is supplied; proxy options remain unsupported on
219
+ nested sessions. `session.host` and `result.host` use the resolved HostName.
220
+ `resolve_connection()` returns effective Paramiko keyword arguments plus
221
+ `proxyCommand`, when applicable; it performs no network operations.
222
+
223
+ Config tokens `%h`, `%n`, `%p`, `%r`, `%u`, `%d`, and `%%` are expanded after
224
+ explicit overrides for identity paths and configured proxy commands. Other
225
+ tokens fail clearly. Explicit `proxyCommand` strings retain their historical
226
+ verbatim behavior. Explicit `proxyCommand=None` disables configured proxies.
227
+ You can also supply `proxyJump="user@bastion:2222"` or a comma-separated chain.
228
+ When both configured proxy types are active, select one explicitly or remove
229
+ the conflict; SSHScript does not implement OpenSSH's first-proxy-wins rule.
230
+
231
+ `ProxyJump` uses the local `ssh` executable to forward to the target, with
232
+ batch authentication and strict host-key checks on jump hosts. It requires
233
+ known host keys and noninteractive authentication for those hops. The target
234
+ connection remains managed and verified by Paramiko. A custom config file is
235
+ also passed to `ssh`; otherwise its user config is used when present.
236
+
237
+ This is a subset of OpenSSH configuration. `Match`, `Include`, and hostname
238
+ canonicalization are rejected before lookup; other unapplied options produce
239
+ a warning. In particular, alternate known-hosts files and identity-agent
240
+ settings are not imported. Treat config and proxy commands as trusted local
241
+ inputs; connecting may execute configured proxy programs.
242
+
243
+ ## Optional dollar syntax
244
+
245
+ Dollar syntax is not ordinary Python syntax. It is normally stored in `.spy`
246
+ files; `run_script(source)` also accepts Dollar syntax from an in-memory string.
247
+ Both forms use the same session and transport implementation as the module API:
248
+
249
+ ```python
250
+ # health.spy
251
+ $hostname
252
+ if $.exitcode != 0:
253
+ raise RuntimeError("hostname failed")
254
+ print($.stdout.strip())
255
+
256
+ with $.connect("ops@example.net"):
257
+ $uname -s
258
+ if $.exitcode != 0:
259
+ raise RuntimeError("remote uname failed")
260
+ ```
261
+
262
+ Run exactly one file with:
263
+
264
+ ```sh
265
+ sshscript health.spy
266
+ ```
267
+
268
+ Check one file without executing its Python, imports, or commands:
269
+
270
+ ```sh
271
+ sshscript --check health.spy
272
+ ```
273
+
274
+ The exit status is 0 for valid syntax, 1 for a syntax/read failure, and 2 for
275
+ invalid CLI usage. Syntax diagnostics show the original file, line, source,
276
+ and caret. `sshscript.check_file(path)` provides the same compile-only check
277
+ and raises source-located `SyntaxError` or filesystem errors. It validates
278
+ Python/dollar syntax, not shell commands, imported modules, or remote hosts.
279
+ `--script` remains available to inspect generated Python without execution.
280
+ For compatibility, **`--check` without a file still checks PyPI for updates**;
281
+ use `--check-updates` explicitly for that purpose.
282
+
283
+ In version 3, a single `$` supports direct commands and shell features such as
284
+ pipelines and redirection. The old `$$` form is retained for compatibility but
285
+ is deprecated. New applications should start with the regular Python module
286
+ API and adopt dollar syntax only when its notation is useful.
287
+
288
+ The CLI and `run_file()` execute one file. Directories, globs, and multiple
289
+ paths are intentionally unsupported. Importing SSHScript does not globally
290
+ enable imports of `.spy` modules; use the temporary `sshscript.spy_imports()`
291
+ context manager when a regular Python program needs that behavior.
292
+
293
+ ## Security model
294
+
295
+ SSHScript executes commands and Python code; it is not a sandbox. Treat every
296
+ `.spy` file, Python module, command string, remote host, and command output as a
297
+ trust boundary. In particular:
298
+
299
+ - never run unreviewed automation with production credentials;
300
+ - avoid shell interpolation of external data;
301
+ - keep credentials, private keys, inventories, and captured production output
302
+ out of source control;
303
+ - verify host keys independently and retain timeouts around network operations;
304
+ - validate site-specific PAM, `sudoers`, shell, and network policy in a
305
+ disposable environment before rollout.
306
+
307
+ See the [security policy](https://github.com/iapyeh/sshscript/security/policy)
308
+ for the complete reporting and security model, and the
309
+ [stable exception contract](https://github.com/iapyeh/sshscript/blob/release/EXCEPTIONS.md)
310
+ for runtime behavior.
311
+
312
+ ## Release confidence and provenance
313
+
314
+ The 3.1 release line uses the following public controls:
315
+
316
+ - CI on Linux and macOS with Python 3.11, 3.12, 3.13, and 3.14;
317
+ - normal and optimized-mode tests, syntax smoke tests, compile checks, and a
318
+ runtime-assertion gate;
319
+ - disposable loopback OpenSSH integration tests for host keys, SFTP, PTY,
320
+ `sudo`/`su`, and timeout behavior;
321
+ - CodeQL and Dependabot;
322
+ - PyPI Trusted Publishing with short-lived OIDC credentials;
323
+ - GitHub build-provenance attestations and a SHA-256 `verified.json` manifest
324
+ attached to the GitHub Release.
325
+
326
+ Download distributions from [PyPI](https://pypi.org/project/sshscript/) or the
327
+ [GitHub Release](https://github.com/iapyeh/sshscript/releases/tag/v3.1.5), not
328
+ from unverified mirrors. These controls establish tested behavior, artifact
329
+ integrity, and release provenance; they are not a substitute for reviewing the
330
+ automation you run or for validating your production environment.
331
+
332
+ ## Compatibility and support
333
+
334
+ | Component | Current policy |
335
+ | --- | --- |
336
+ | SSHScript | Latest 3.1.x receives bug and security fixes |
337
+ | Python | 3.11–3.14 are continuously tested |
338
+ | Platforms | Current Linux and macOS releases |
339
+ | SSH | OpenSSH integration is tested on Ubuntu; other servers are best effort |
340
+ | Paramiko | Runtime dependency is `>=2.11,<5`; CI resolves a compatible release |
341
+ | Windows | Not currently tested or supported |
342
+
343
+ See the [support policy](https://github.com/iapyeh/sshscript/blob/release/SUPPORT.md)
344
+ for scope, support channels, and the information needed in a useful bug report.
345
+
346
+ ## Development and verification
347
+
348
+ A release checkout uses the `src/sshscript/` package layout:
349
+
350
+ ```sh
351
+ python3 -m pip install 'paramiko>=2.11,<5' 'packaging>=21' build twine
352
+ python3 tools/run_checks.py
353
+ python3 tools/check_release.py --output /tmp/sshscript-candidate-UNIQUE
354
+ ```
355
+
356
+ The output directory must not already exist. `run_checks.py` locates the test
357
+ suite under `src/sshscript/unittest/` automatically. See the
358
+ [contributing guide](https://github.com/iapyeh/sshscript/blob/release/CONTRIBUTING.md)
359
+ before proposing a change and the
360
+ [release guide](https://github.com/iapyeh/sshscript/blob/release/RELEASING.md)
361
+ for maintainer-only release steps.
362
+
363
+ ## Project policies
364
+
365
+ - [Changelog](https://github.com/iapyeh/sshscript/blob/release/CHANGELOG.md)
366
+ - [Support policy](https://github.com/iapyeh/sshscript/blob/release/SUPPORT.md)
367
+ - [Security policy](https://github.com/iapyeh/sshscript/security/policy)
368
+ - [Stable exception contract](https://github.com/iapyeh/sshscript/blob/release/EXCEPTIONS.md)
369
+ - [Contributing guide](https://github.com/iapyeh/sshscript/blob/release/CONTRIBUTING.md)
370
+ - [Code of Conduct](https://github.com/iapyeh/sshscript/blob/release/CODE_OF_CONDUCT.md)
371
+
372
+ ## License
373
+
374
+ SSHScript is released under the
375
+ [MIT License](https://github.com/iapyeh/sshscript/blob/release/LICENSE.txt).