diff --git a/README.md b/README.md index 0d6080d..9e7c6aa 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,11 @@ relay discover vite # aliases grouped by target program relay snippet add goback "cd ../" relay snippet run goback --dry-run +# Snippets support {{0}} {{1}} … placeholders — pass args at runtime. +relay snippet add killport --shell powershell "Get-NetTCPConnection -LocalPort {{0}} ^| ForEach-Object { Stop-Process -Id `$_.OwningProcess -Force }" +relay snippet run killport --dry-run 4600 +# → Get-NetTCPConnection -LocalPort 4600 | ... + # Diagnose. relay doctor # check PATH, shims, config relay doctor --fix # auto-repair missing shims and PATH entries @@ -180,6 +185,17 @@ relay snippet run goback relay snippet run goback --dry-run ``` +**Placeholders:** Use `{{0}}` `{{1}}` … in snippet content and pass arguments at runtime: + +```bash +relay snippet add greet --shell powershell "Write-Host Hello {{0}}" +relay snippet run greet --dry-run World # → Write-Host Hello World +relay snippet add killport --shell powershell "Get-NetTCPConnection -LocalPort {{0}} ^| ForEach-Object { Stop-Process -Id `$_.OwningProcess -Force }" +killport 4600 # shim forwarding works too +``` + +If a placeholder index exceeds the number of arguments provided, relay returns a clear error. + **Why snippets?** Commands like `cd`, `export`, complex pipes, and shell built-ins can't work through relay's direct-execution model. Snippets fill that gap while keeping cross-shell portability. --- @@ -239,9 +255,9 @@ relay snippet run goback --dry-run | `relay snippet edit --content ` | Update a snippet's content | | `relay snippet edit --desc ` | Update description (pass `""` to clear) | | `relay snippet edit --shell ` | Change the shell dialect | -| `relay snippet run ` | Execute a snippet (auto-translates to current shell) | -| `relay snippet run --dry-run` | Print the translated command without executing | -| `relay snippet run --no-translate` | Run as-is, skip cross-shell translation | +| `relay snippet run [args...]` | Execute a snippet (auto-translates to current shell). `args` replace `{{0}}` `{{1}}` … placeholders | +| `relay snippet run --dry-run [args...]` | Print the substituted & translated command without executing | +| `relay snippet run --no-translate [args...]` | Run as-is, skip cross-shell translation (placeholders still apply) | | `relay snippet clear` | Remove all snippets (asks for confirmation) | | `relay snippet clear --yes` | Same, no confirmation | diff --git a/README.zh.md b/README.zh.md index 5106af3..7ab7418 100644 --- a/README.zh.md +++ b/README.zh.md @@ -117,6 +117,11 @@ relay discover vite # 按目标程序聚合查看 relay snippet add goback "cd ../" relay snippet run goback --dry-run +# Snippet 支持 {{0}} {{1}} … 占位符,运行时传入参数自动替换 +relay snippet add killport --shell powershell "Get-NetTCPConnection -LocalPort {{0}} ^| ForEach-Object { Stop-Process -Id `$_.OwningProcess -Force }" +relay snippet run killport --dry-run 4600 +# → Get-NetTCPConnection -LocalPort 4600 | ... + # 诊断 relay doctor # 检查 PATH / shim / 配置 relay doctor --fix # 自动修复缺失的 shim 和 PATH @@ -174,6 +179,17 @@ relay snippet run goback relay snippet run goback --dry-run ``` +**占位符:** Snippet 内容中可用 `{{0}}` `{{1}}` … 占位符,运行时传入的参数会依次替换: + +```bash +relay snippet add greet --shell powershell "Write-Host Hello {{0}}" +relay snippet run greet --dry-run World # → Write-Host Hello World +relay snippet add killport --shell powershell "Get-NetTCPConnection -LocalPort {{0}} ^| ForEach-Object { Stop-Process -Id `$_.OwningProcess -Force }" +killport 4600 # 通过 shim 传参同样有效 +``` + +如果占位符索引超出传入参数数量,relay 会报错并提示缺少哪个参数。 + **为什么需要 snippet?** `cd`、`export`、复杂管道、Shell 内置命令等无法通过 relay 的直接执行模型工作。Snippet 填补了这一空白,同时保持跨 Shell 的移植性。 --- @@ -233,9 +249,9 @@ relay snippet run goback --dry-run | `relay snippet edit --content ` | 修改内容 | | `relay snippet edit --desc ` | 修改描述(传 `""` 清除) | | `relay snippet edit --shell ` | 修改 Shell 方言 | -| `relay snippet run ` | 执行 snippet(自动翻译到当前 Shell) | -| `relay snippet run --dry-run` | 只打印翻译后的命令,不执行 | -| `relay snippet run --no-translate` | 跳过翻译,原样执行 | +| `relay snippet run [args...]` | 执行 snippet(自动翻译到当前 Shell)。`args` 按序替换 `{{0}}` `{{1}}` … 占位符 | +| `relay snippet run --dry-run [args...]` | 只打印替换和翻译后的命令,不执行 | +| `relay snippet run --no-translate [args...]` | 跳过翻译,原样执行(仍支持占位符替换) | | `relay snippet clear` | 删除所有 snippets(会先确认) | | `relay snippet clear --yes` | 同上,但不确认 | diff --git a/src/cli.rs b/src/cli.rs index 3784c9c..a0e8e3a 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -221,6 +221,10 @@ pub enum SnippetAction { shell: Option, }, /// Execute a snippet, translating it to the current shell if needed. + /// + /// Use `{{0}}`, `{{1}}`, … placeholders in the snippet content and pass + /// trailing arguments to substitute them at runtime: + /// relay snippet run killport 4000 Run { name: String, /// Print the translated command without executing it. @@ -238,6 +242,10 @@ pub enum SnippetAction { /// Overrides auto-detection — useful for testing cross-shell results. #[arg(long)] target: Option, + /// Arguments to substitute for `{{0}}`, `{{1}}`, … placeholders in + /// the snippet content. + #[arg(trailing_var_arg = true, allow_hyphen_values = true)] + args: Vec, }, /// Remove all snippets. Clear { @@ -330,7 +338,8 @@ fn dispatch_with_root(command: Command, root: Option) -> Res no_translate, force, target, - } => snippet::run(&paths, name, *dry_run, *no_translate, target.as_deref(), *force)?, + args, + } => snippet::run(&paths, name, *dry_run, *no_translate, target.as_deref(), *force, args)?, SnippetAction::Clear { yes } => snippet::clear(&paths, *yes)?, }, } diff --git a/src/error.rs b/src/error.rs index df40aa8..10ab0d3 100644 --- a/src/error.rs +++ b/src/error.rs @@ -66,4 +66,7 @@ pub enum RelayError { #[error("snippet translation failed: {0}")] SnippetTranslateError(String), + + #[error("placeholder {{{{ {0} }}}} has no corresponding argument (only {1} arg(s) provided)")] + MissingPlaceholderArg(usize, usize), } diff --git a/src/snippet/mod.rs b/src/snippet/mod.rs index 1a02e29..bedcf70 100644 --- a/src/snippet/mod.rs +++ b/src/snippet/mod.rs @@ -195,14 +195,17 @@ pub fn clear(paths: &Paths, auto_yes: bool) -> Result<()> { // ─── Run (cross-shell execution) ───────────────────────────────────────── -/// `relay snippet run ` — execute a snippet, optionally translating -/// it to the current shell dialect via polysh. +/// `relay snippet run [args...]` — execute a snippet, optionally +/// translating it to the current shell dialect via polysh. +/// +/// Placeholders `{{0}}`, `{{1}}`, … in the snippet content are substituted +/// with the corresponding positional arguments before execution. /// /// Pass `target` to override the auto-detected current dialect — useful /// for testing cross-shell translation results (e.g. `--target unix`). /// Pass `force` to run a best-effort translation even when polysh cannot /// fully translate all segments. -pub fn run(paths: &Paths, name: &str, dry_run: bool, no_translate: bool, target: Option<&str>, force: bool) -> Result<()> { +pub fn run(paths: &Paths, name: &str, dry_run: bool, no_translate: bool, target: Option<&str>, force: bool, args: &[String]) -> Result<()> { let config = config::load(paths)?; let snip = config .snippets @@ -215,10 +218,13 @@ pub fn run(paths: &Paths, name: &str, dry_run: bool, no_translate: bool, target: }; let stored_dialect = snip.shell; + // Substitute {{0}}, {{1}}, … placeholders with positional args. + let content = substitute_placeholders(&snip.content, args)?; + let command = if no_translate || current_dialect == stored_dialect { - snip.content.clone() + content } else { - translate_content(&snip.content, stored_dialect, current_dialect, force)? + translate_content(&content, stored_dialect, current_dialect, force)? }; if dry_run { @@ -252,6 +258,54 @@ fn validate_name(name: &str) -> Result<()> { Ok(()) } +/// Substitute `{{0}}`, `{{1}}`, … placeholders in `content` with the +/// corresponding values from `args`. +/// +/// Returns the original content unchanged when there are no `{{…}}` markers. +/// Non-numeric placeholders (e.g. `{{name}}`) are left as-is. Unclosed `{{` +/// is also left as-is. If a numeric placeholder index exceeds `args.len()` +/// an [`RelayError::MissingPlaceholderArg`] error is returned. +fn substitute_placeholders(content: &str, args: &[String]) -> Result { + // Fast path: no placeholder markers at all. + if !content.contains("{{") { + return Ok(content.to_string()); + } + + let mut result = String::with_capacity(content.len()); + let mut rest = content; + + while let Some(start) = rest.find("{{") { + result.push_str(&rest[..start]); + rest = &rest[start + 2..]; + + if let Some(end) = rest.find("}}") { + let idx_str = rest[..end].trim(); + match idx_str.parse::() { + Ok(idx) => { + if idx >= args.len() { + return Err(RelayError::MissingPlaceholderArg(idx, args.len())); + } + result.push_str(&args[idx]); + } + Err(_) => { + // Not a number — leave the marker as-is so users can have + // literal `{{name}}` in their snippets when needed. + result.push_str("{{"); + result.push_str(&rest[..=end]); + } + } + rest = &rest[end + 2..]; + } else { + // Unclosed `{{` — leave it as-is. + result.push_str("{{"); + result.push_str(rest); + rest = ""; + } + } + result.push_str(rest); + Ok(result) +} + /// Detect the current shell dialect via polysh. pub fn detect_current_dialect() -> ShellDialect { let info = polysh::detector::detect_shell();