{"id":37123700,"url":"https://github.com/ngrsoftlab/rexec","last_synced_at":"2026-01-14T14:17:08.700Z","repository":{"id":296426912,"uuid":"992520551","full_name":"NGRsoftlab/rexec","owner":"NGRsoftlab","description":"Lightweight Go library for templated shell command execution (local \u0026 SSH), with structured results parsing and file transfer support","archived":false,"fork":false,"pushed_at":"2025-06-12T15:59:30.000Z","size":78,"stargazers_count":3,"open_issues_count":0,"forks_count":1,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-01-12T19:34:20.927Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/NGRsoftlab.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2025-05-29T09:37:15.000Z","updated_at":"2025-11-11T12:44:32.000Z","dependencies_parsed_at":"2025-05-30T23:35:48.500Z","dependency_job_id":"230b2d15-17d5-4e24-bc38-c29c16227da9","html_url":"https://github.com/NGRsoftlab/rexec","commit_stats":null,"previous_names":["ngrsoftlab/rexec"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/NGRsoftlab/rexec","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/NGRsoftlab%2Frexec","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/NGRsoftlab%2Frexec/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/NGRsoftlab%2Frexec/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/NGRsoftlab%2Frexec/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/NGRsoftlab","download_url":"https://codeload.github.com/NGRsoftlab/rexec/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/NGRsoftlab%2Frexec/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28422516,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-14T13:30:50.153Z","status":"ssl_error","status_checked_at":"2026-01-14T13:29:08.907Z","response_time":107,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"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":[],"created_at":"2026-01-14T14:17:07.998Z","updated_at":"2026-01-14T14:17:08.692Z","avatar_url":"https://github.com/NGRsoftlab.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# rexec\n\n-`rexec` is a Go library that executes shell commands locally or over SSH, parses outputs into Go types, transfers files, and manages SSH connection retries and timeouts—without requiring agents on target machines.\n\nBy running ad-hoc commands and interpreting their results, you receive immediate feedback from remote hosts, when you can’t deploy or maintain persistent agents on remote hosts.\n\n## Quick start\n1. connection via SSH,\n2. uploading the file via SFTP,\n3. checking the existence of the file,\n4. parsing the rights/owner/date of the file attribute via `ls`,\n5. outputting the result from a Go structure.\n\n```go\npackage main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"path\"\n\t\"time\"\n\t\n\t\"github.com/ngrsoftlab/rexec\"\n\t\"github.com/ngrsoftlab/rexec/command\"\n\t\"github.com/ngrsoftlab/rexec/parser/examples\"\n\t\"github.com/ngrsoftlab/rexec/ssh\"\n)\n\nfunc main() {\n\t// 1. setting up ssh client\n\tsshCfg, err := ssh.NewConfig(\n\t\t\"alice\", \"example.com\", 22,\n\t\tssh.WithPasswordAuth(\"secret\"),\n\t\tssh.WithRetry(3, 5*time.Second),\n\t\tssh.WithKeepAlive(30*time.Second),\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tclient, err := ssh.NewClient(sshCfg)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tdefer client.Close()\n\t\n\tctx := context.Background()\n\t\n\t// 2. upload file by SFTP\n\tdata := []byte(\"Hello, rexec!\")\n\tremoteDir := \"/tmp/rexec\"\n\tfileName := \"hello.txt\"\n\tspec := \u0026rexec.FileSpec{\n\t\tTargetDir:  remoteDir,\n\t\tFilename:   fileName,\n\t\tMode:       0644,\n\t\tFolderMode: 0755,\n\t\tContent:    \u0026rexec.FileContent{Data: data},\n\t}\n\t// scp := ssh.NewSCPTransfer(client) // switch protocol is so simple\n\tsftp := ssh.NewSFTPTransfer(client)\n\tif err := sftp.Copy(ctx, spec); err != nil {\n\t\tpanic(err)\n\t}\n\t\n\t// 3. check uploaded file existence\n\tvar exists bool\n\tremotePath := path.Join(remoteDir, fileName)\n\tcmdExist := command.New(\n\t\t\"test -f %s \u0026\u0026 echo true || echo false\",\n\t\tcommand.WithArgs(remotePath),\n\t\tcommand.WithParser(\u0026examples.PathExistence{}),\n\t)\n\texists, err = rexec.RunParse[ssh.RunOption, bool](ctx, client, cmdExist)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tfmt.Printf(\"Exists: %v\\n\", exists)\n\t\n\t// 4. gathering details of uploaded file\n\tvar entries []examples.LsEntry\n\tcmdLs := command.New(\n\t\t\"ls -la %s\",\n\t\tcommand.WithArgs(remotePath),\n\t\tcommand.WithParser(\u0026examples.LsParser{}),\n\t)\n\tentries, err = rexec.RunParse[ssh.RunOption, []examples.LsEntry](ctx, client, cmdLs)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\t\n\t// 5. print result\n\tif len(entries) \u003e 0 {\n\t\te := entries[0]\n\t\tfmt.Printf(\"File: %s\\n\", e.Name)\n\t\tfmt.Printf(\"Owner: %s\\n\", e.Owner)\n\t\tfmt.Printf(\"Created: %s %s %s\\n\", e.Month, e.Day, e.TimeOrYear)\n\t}\n}\n```\n\n## Features\n\n\n- **Unified API**: Local and SSH execution via `Client[O any]` interface. Where `O` is `ssh.RunOption` or `local.RunOption`\n- **Structured Parsing**: Convert command output into Go structs with `parser.Parser`\n- **File Transfers**: Copy files using `FileSpec` over local FS, SCP, or SFTP\n- **SSH Connection Retries**: Automatic dial retries on SSH connection failures\n- **TCP Keep-Alive**: Prevent idle disconnections\n- **Automatic PTY**: Allocate a pseudo-TTY for interactive commands (e.g. `sudo`, `passwd`)\n- **Context-Aware**: Timeouts and cancellations via `context.Context`\n- **Custom I/O Streams**: Override `stdin`, `stdout`, `stderr` for using in websockets, logs. Includes support for real-time streaming of output\n- **Concurrency Safety**: Respects SSH server’s `MaxSessions` limit\n\n---\n\n## Installation\n\n```bash\n# Install the library\ngo get github.com/ngrsoftlab/rexec\n```\n\n## Configuration\n\n### Local Client\n\n```go\nimport \"github.com/ngrsoftlab/rexec/local\"\n\ncfg := local.NewConfig().\n  WithWorkDir(\"/tmp\").       // default workdir\n  WithEnvVars(map[string]string{ // environment for every run\n    \"GREETING\": \"Hello\",\n  })\nclient := local.NewClient(cfg)\ndefer client.Close()\n```\n\n- WithWorkDir(path string): set default workdir.\n- WithEnvVars(map[string]string): set default environment.\n\n### SSH Client\n\n```go\nimport (\n  \"time\"\n  \"github.com/ngrsoftlab/rexec/ssh\"\n)\n\nsshCfg, err := ssh.NewConfig(\n  \"alice\", \"example.com\", 22,\n  ssh.WithPasswordAuth(\"secret\"),           // password auth\n  ssh.WithKnownHosts(\"~/.ssh/known_hosts\"),\n  ssh.WithRetry(3, 5*time.Second),          // SSH dial retry\n  ssh.WithKeepAlive(30*time.Second),        // TCP keep-alive\n  ssh.WithSudoPassword(\"sudoPass\"),         // automatic sudo prompt\n  ssh.WithWorkdir(\"/home/alice\"),           // default remote dir\n  ssh.WithMaxSessions(2),                   // concurrent sessions\n)\nif err != nil {\n  // handle error\n}\nclient, err := ssh.NewClient(sshCfg)\ndefer client.Close()\n```\n\n#### SSH Config Options\n- `WithPort(int)`\n- `WithTimeout(time.Duration)`\n- `WithRetry(count int, interval time.Duration)` (SSH dial only)\n- `WithKeepAlive(time.Duration)`\n- `WithKnownHosts(path string)`\n- `WithSudoPassword(string)`\n- `WithEnvVars(map[string]string)`\n- `WithWorkdir(string)`\n- `WithMaxSessions(int)`\n- Auth: \n    - `WithPasswordAuth(password string)`\n    - `WithAgentAuth()`\n    - `WithPrivateKeyPathAuth(path, passphrase string)`\n    - `WithKeyBytesAuth([]byte, passphrase string)`\n\n\n## Executing Commands\n\n### Constructing Commands\n```go\nimport \"github.com/ngrsoftlab/rexec/command\"\n\nconst listTpl = \"ls -la %s\"\ncmd := command.New(\n  listTpl,\n  command.WithArgs(\"/var/log\"),      // fmt.Sprintf args\n  command.WithParser(\u0026parser.LsParser{}), // parser\n)\n```\n\n- `WithArgs(...any)`: append positional parameters.\n- `WithParser(parser.Parser)`: attach parsing logic.\n\n### Client.Run\n\n```go\n// dst is optional – pass nil to ignore parsing\n\n// Example: local execution with override options\nres, err := localClient.Run(ctx, cmd, \u0026dst, local.WithWorkdir(\"/data\"))\nif err != nil {\n// handle error; res.Stderr contains stderr\n}\n// Example: SSH execution with env var override\nres, err = sshClient.Run(ctx, cmd, \u0026dst, ssh.WithEnvVar(\"KEY\", \"value\"))\nif err != nil {\n// handle error; res.ExitCode holds exit status\n}\n```\nLocal (local.RunOption):\n- `WithWorkdir(string)`\n- `WithEnvVar(key, value)`\n- `WithStdout(io.Writer)`\n- `WithStderr(io.Writer)`\n- `WithStdin(io.Reader)`\n\nSSH (ssh.RunOption):\n- `WithEnvVar(key, value)`\n- `WithStdout(io.Writer)`\n- `WithStderr(io.Writer)`\n- `WithStdin(io.Reader)`\n- `WithStreaming()`: real-time output\n- `WithoutBuffering()`: disable internal buffers\n\n\n### Helpers \u0026 Generics\n\n```go\nimport \"github.com/ngrsoftlab/rexec\"\n\n// ignore parsing and return error only\nerr := rexec.RunNoResult[O](ctx, client, cmd, opts...)\n\n// get raw outputs:\nout, errOut, exit, err := rexec.RunRaw[O](ctx, client, cmd, opts...)\n\n// parse into T:\ndst, err := rexec.RunParse[O, T](ctx, client, cmd, opts...)\n```\n\n- O = local.RunOption or ssh.RunOption; \n- T = result type.\n\n### Parsers\nImplement parser.Parser to handle any command:\n\n```go\ntype Parser interface {\n  Parse(raw *RawResult, dst any) error\n}\n```\n#### Built-in Parsers\n\n- Located under parser/examples:\n  - PathExistence: stdout \"true\"/\"false\" → bool\n  - LsParser: parse ls -la → []LsEntry\n\n#### Custom Parsers\n\n```go\nconst uptimeTpl = \"uptime -p\" // create template\n\ntype UptimeInfo struct { Since string }\n\ntype UptimeParser struct{}\n\nfunc (p *UptimeParser) Parse(raw *parser.RawResult, dst any) error {\n  info, ok := dst.(*UptimeInfo)\n  if !ok { return fmt.Errorf(\"dst must be *UptimeInfo\") }\n  info.Since = strings.TrimPrefix(raw.Stdout, \"up \")\n  return raw.Err\n}\n\nvar info UptimeInfo\ncmd := command.New(uptimeTpl, command.WithParser(\u0026UptimeParser{}))\n_, err := client.Run(ctx, cmd, \u0026info)\n```\n## File Transfers\n\nUse rexec.FileSpec:\n\n```go\ntype FileSpec struct {\n  TargetDir  string\n  Filename   string\n  Mode       os.FileMode\n  FolderMode os.FileMode\n  Content    *FileContent\n}\n```\n\n### FileContent\n\n`FileContent` supports three source types; choose one per `FileSpec`:\n\n1. **In-Memory Data**  \n   ```go\n   content := \u0026rexec.FileContent{Data: []byte(\"small payload\")}\n   ```\n\n2. **Filesystem Path**  \n   ```go\n   content := \u0026rexec.FileContent{SourcePath: \"/path/to/file.txt\"}\n   ```\n\n3. **Reader**  \n   ```go\n   f, _ := os.Open(\"/var/log/stream.log\")\n   content := \u0026rexec.FileContent{Reader: f}\n   ```\n   • Use for large files or runtime-generated streams to avoid buffering overhead.\n\nInternally, `ReaderAndSize()` returns:\n```go\nreader, size, err := content.ReaderAndSize()\n```\n\nThe `FileContent.ReaderAndSize()` method encapsulates logic to produce an `io.ReadCloser` and its length. Its behavior depends on which field is set:\n\n1. **`Data []byte`**\n   - Returns `io.NopCloser(bytes.NewReader(Data))` and `int64(len(Data))`.\n   - Zero-seeking overhead; length is known immediately.\n\n2. **`SourcePath string`**\n   - Opens the file via `os.Open(SourcePath)`.\n   - Calls `File.Stat()` to get size, then returns file handle and size.\n   - Errors if file does not exist or is inaccessible.\n\n3. **`Reader io.Reader`**\n   - If `Reader` implements `io.Seeker`, it seeks to determine current position and end to calculate \n   - If `Reader` is not seekable, returns error: \"reader is not seekable\".\n   - Use this when you have a stream that supports seeking (e.g., `bytes.Reader`) or accept unknown size.\n\n**Importance of Seekable Readers**\n   - Seekable readers allow accurate `size` reporting, necessary for protocols like SCP that require upfront length.\n   - Non-seekable streams must implement custom logic or be wrapped if size is needed\n\n**Use Cases**\n   - **In-memory**: when `Data` is small and performance matters.\n   - **File path**: for large existing files; OS handles buffering.\n   - **Seekable stream**: for random-access buffers or replayable streams.\n\n### Local\n\n```go\ntransfer := local.NewTransfer()\nerr := transfer.Copy(ctx, \u0026rexec.FileSpec{...})\n```\n\n### SCP\n\n```go\nscp := ssh.NewSCPTransfer(sshClient)\nerr := scp.Copy(ctx, spec)\n```\n\n### SFTP\n\n```go\nsftp := ssh.NewSFTPTransfer(sshClient)\nerr := sftp.Copy(ctx, spec)\n```\n\n## SSH Connection Management \u0026 PTY\n\n### Connection Options (SSH-only)\n- `ssh.WithRetry(count int, interval time.Duration)`: retry SSH dialing up to count times with interval delay on connection failures; does not retry failed commands.\n- `ssh.WithKeepAlive(duration time.Duration)`: send TCP keep-alive messages at the specified interval to keep the SSH connection alive.\n\n### PTY Allocation \u0026 Sudo Handling\n- `Automatic PTY`: commands containing keywords like `sudo`, `ssh`, or `docker login` trigger a pseudo-terminal allocation, enabling interactive prompts.\n-` Sudo Password`: if `ssh.WithSudoPassword(password)` when set, the client monitors stdout for password: prompts and writes the provided password to stdin automatically.\n\n\n## Session Limits\n\nEnsures the SSH client never exceeds the host’s allowed concurrent sessions.\nRecommended range 1-4 concurrent sessions. if problems occur, reduce the value\nConfigured via:\n```go\n    sshCfg, _ := ssh.NewConfig(\n      \"user\", \"host\", 22,\n      ssh.WithMaxSessions(3), // allow up to 3 simultaneous sessions\n    )\n```\n- If the limit is reached, `OpenSession` blocks until a slot frees or the context expires.\n\n## Stream Overrides and internal buffer control\n\nCustomize command I/O for live integrations and buffering control:\n\n- `ssh.WithStreaming()`: immediately writes each chunk of stdout/stderr to your WithStdout/WithStderr writers as it arrives, rather than waiting for command completion. Use-cases:\n- Live logs in a web dashboard or CLI progress indicators\n- Pushing real-time output over WebSockets\n- Interactive feedback loops in GUIs or monitoring tools \n\nWithout streaming, output is accumulated internally and made available only after the command finishes.\n\n- `ssh.WithoutBuffering()`: turns off the internal output buffers entirely; all data is sent directly to your writers. Benefits include:\n- Reduced memory usage when handling large or continuous streams\n- Predictable delivery in streaming pipelines or when chaining commands\n      \nIf buffering is disabled and streaming is not enabled, the library will not store any output in `res.Stdout/res.Stderr` — all data goes to your writers.\n\nExample of real-time WebSocket forwarding with minimal memory overhead:\n\n```go\nwsWriter := NewWebSocketWriter(conn)\nres, err := sshClient.Run(\n  ctx,\n  cmd,\n  nil,\n  ssh.WithStdout(wsWriter),\n  ssh.WithStderr(wsWriter),\n  ssh.WithStreaming(),\n  ssh.WithoutBuffering(),\n)\nif err != nil {\n  // handle error; live output streamed via wsWriter\n}\n```\n\n### Error Code Mapping\n\nBy default, `ExitCodeMapper` is applied automatically within every `Run` call, translating known exit codes into descriptive errors.  \nFor advanced use cases (e.g., custom error analysis), you can manually invoke the mapper:\n\n```go\nimport \"github.com/ngrsoftlab/rexec/utils\"\n\n// Run and receive raw result\nres, err := sshClient.Run(ctx, cmd, nil)\n\n// Default behavior: err includes mapped message\nif err != nil {\n  // err.Error() contains human-readable exit description\n}\n\n// Manual mapping example\nmapper := utils.NewDefaultExitCodeMapper()\nif res.ExitCode != 0 {\n  desc := mapper.Lookup(res.ExitCode)\n  log.Printf(\"Custom error mapping: code %d =\u003e %s\", res.ExitCode, desc)\n}\n```\n\nUse manual mapping when you need to log, categorize, or transform exit statuses beyond the default error message.\n\n\n\n© 2025 NGRSOFTLAB\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fngrsoftlab%2Frexec","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fngrsoftlab%2Frexec","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fngrsoftlab%2Frexec/lists"}