From 5aa4e77e53f4364d8552ee52ad95d265e752f021 Mon Sep 17 00:00:00 2001 From: timzhong2000 Date: Sat, 22 Aug 2026 22:17:21 +0800 Subject: [PATCH] Run source snippets in isolated templates --- Cargo.toml | 2 +- README.md | 15 ++++++++-- README_zh.md | 15 ++++++++-- npm/README.md | 6 ++++ skills/run-code-snippet/SKILL.md | 12 +++++++- src/cli.rs | 31 +++++++++++++++++++-- src/main.rs | 17 ++++++++---- src/runner/dotnet.rs | 12 +++++++- src/runner/go.rs | 9 +++++- src/runner/mod.rs | 42 ++++++++++++++++++++++++---- src/runner/node.rs | 47 ++++++++++++++++++++++++++------ src/runner/python.rs | 12 ++++++-- src/runner/rust.rs | 12 +++++++- src/source.rs | 37 +++++++++++++++++++++++++ src/util.rs | 2 +- 15 files changed, 238 insertions(+), 33 deletions(-) create mode 100644 src/source.rs diff --git a/Cargo.toml b/Cargo.toml index ec4ea43..aebaa85 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ name = "run-code" version = "0.1.0" edition = "2024" rust-version = "1.85" -description = "Run code from stdin with selected toolchains and temporary dependencies" +description = "Run code snippets with selected toolchains and temporary dependencies" license = "MIT" repository = "https://github.com/timzhong1024/run-code" homepage = "https://github.com/timzhong1024/run-code" diff --git a/README.md b/README.md index ce45a83..7c21c19 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [简体中文](README_zh.md) -Run Python, TypeScript, JavaScript, Rust, Go, or C# snippets with a selected runtime/toolchain version and temporary dependencies in an isolated environment. +Run Python, TypeScript, JavaScript, Rust, Go, or C# snippets from stdin or a source file with a selected runtime/toolchain version and temporary dependencies in an isolated environment. ## Installation @@ -38,6 +38,14 @@ GitHub Releases also provide standalone binaries for macOS, Linux, and Windows. ## Examples +### Source file + +```bash +run-code node@20 snippet.ts -- first --verbose +``` + +The source file is read and its contents are copied into a newly created isolated template project before execution. `run-code` does not execute inside the source file's existing project, discover that project's dependencies, or copy sibling files. Add everything the snippet needs with `--package`; arguments after `--` are passed to the snippet. + ### TypeScript ```bash @@ -141,10 +149,13 @@ Report vulnerabilities privately through GitHub private vulnerability reporting. ```text run-code [OPTIONS] TOOLCHAIN[@VERSION] +run-code [OPTIONS] TOOLCHAIN[@VERSION] FILE [-- ARG ...] run-code skill ``` - `TOOLCHAIN[@VERSION]`: Select a language and optional version. Supported toolchains are `python`, `node`, `rust`, `go`, and `dotnet`; `javascript` and `typescript` are aliases for `node`, while `csharp` and `cs` are aliases for `dotnet`. +- `FILE`: Read a source file and copy its contents into a new isolated template project. The file's existing project and sibling files are not used. When omitted, source code is read from stdin. +- `ARG`: Pass arguments after `--` to the snippet process. This also works with stdin input. - `-p, --package SPEC`: Add a temporary dependency. Repeat the option to install multiple packages. Specs follow each ecosystem: Python uses `NAME==VERSION`; Node, Rust, Go, and .NET use `NAME@VERSION`. Rust also supports `NAME[@VERSION][FEATURE,...]`, such as `'tokio@1[full]'`. - `--commonjs`: Run Node code as CommonJS. The default is ESM with top-level `await` support. - `--clean`: Delete the generated project after execution. Without this option, the project is retained and its path appears in the displayed command. @@ -153,4 +164,4 @@ run-code skill - `-h, --help`: Show help. - `-V, --version`: Show the version. -When the version is omitted, built-in defaults are used: Python 3.14, Node latest, Rust stable, Go latest, and .NET 10. C# runs as a .NET 10+ file-based app. Python and Node execute directly without creating a project when no `--package` option is provided; an isolated project is created only when dependencies are needed. Source code is read from stdin, and package-manager download caches remain enabled. By default, `run-code` displays only dependency installation and final execution commands while streaming their stdout/stderr; project initialization output is shown only when initialization fails. +When the version is omitted, built-in defaults are used: Python 3.14, Node latest, Rust stable, Go latest, and .NET 10. C# runs as a .NET 10+ file-based app. For stdin input, Python and Node execute directly when no `--package` option is provided. File input always creates an isolated template project, even without dependencies. Package-manager download caches remain enabled. By default, `run-code` displays only dependency installation and final execution commands while streaming their stdout/stderr; project initialization output is shown only when initialization fails. diff --git a/README_zh.md b/README_zh.md index b3b2654..c9f8ff3 100644 --- a/README_zh.md +++ b/README_zh.md @@ -2,7 +2,7 @@ [English](README.md) -在隔离的临时环境中,用指定版本的 runtime/toolchain 和依赖快速运行一段 Python、TypeScript、JavaScript、Rust、Go 或 C# 代码。 +在隔离的临时环境中,用指定版本的 runtime/toolchain 和依赖快速运行来自 stdin 或源文件的一段 Python、TypeScript、JavaScript、Rust、Go 或 C# 代码。 ## 安装 @@ -38,6 +38,14 @@ GitHub Release 还会提供 macOS、Linux 和 Windows 的独立 binary。 ## 示例 +### 源文件 + +```bash +run-code node@20 snippet.ts -- first --verbose +``` + +执行前,`run-code` 会读取源文件,并将内容复制到新建的隔离模板项目中。它不会在源文件所属的已有工程里运行,不会读取该工程的依赖,也不会复制同目录的其他文件。代码片段需要的依赖应通过 `--package` 明确添加;`--` 后的参数会传给代码进程。 + ### TypeScript ```bash @@ -141,10 +149,13 @@ Codex 会自动发现这些目录中的 skill;详细约定见 [Codex Skills ```text run-code [OPTIONS] TOOLCHAIN[@VERSION] +run-code [OPTIONS] TOOLCHAIN[@VERSION] FILE [-- ARG ...] run-code skill ``` - `TOOLCHAIN[@VERSION]`:选择语言及版本。支持 `python`、`node`、`rust`、`go` 和 `dotnet`;`javascript`、`typescript` 是 `node` 的别名,`csharp`、`cs` 是 `dotnet` 的别名,版本可以省略。 +- `FILE`:读取源文件,并将内容复制进新的隔离模板项目;不会使用文件所在的已有工程或同目录文件。省略时从 stdin 读取代码。 +- `ARG`:在 `--` 后提供并传给代码进程;stdin 输入同样可以传参。 - `-p, --package SPEC`:添加临时依赖,可重复使用以安装多个包。依赖格式遵循对应生态;Python 版本使用 `NAME==VERSION`,Node、Rust、Go 和 .NET 使用 `NAME@VERSION`。Rust 还支持 `NAME[@VERSION][FEATURE,...]`,例如 `'tokio@1[full]'`。 - `--commonjs`:让 Node 以 CommonJS 方式运行;默认使用支持顶层 `await` 的 ESM。 - `--clean`:运行结束后删除临时项目;不指定时保留项目,默认输出的执行命令中会包含项目路径。 @@ -153,4 +164,4 @@ run-code skill - `-h, --help`:显示帮助。 - `-V, --version`:显示版本。 -未指定版本时使用内置默认值:Python 3.14、Node latest、Rust stable、Go latest、.NET 10。C# 通过 .NET 10+ file-based app 运行。Python 和 Node 未指定 `--package` 时直接执行,不创建临时项目;需要依赖时才创建隔离项目。代码通过 stdin 输入,包管理器的下载缓存保持启用。默认只显示依赖安装和最终代码运行命令,并实时透传它们的 stdout/stderr;项目初始化仅在失败时输出诊断信息。 +未指定版本时使用内置默认值:Python 3.14、Node latest、Rust stable、Go latest、.NET 10。C# 通过 .NET 10+ file-based app 运行。使用 stdin 且未指定 `--package` 时,Python 和 Node 直接执行;使用文件输入时,即使没有依赖也始终创建隔离模板项目。包管理器的下载缓存保持启用。默认只显示依赖安装和最终代码运行命令,并实时透传它们的 stdout/stderr;项目初始化仅在失败时输出诊断信息。 diff --git a/npm/README.md b/npm/README.md index ec2bedd..e771768 100644 --- a/npm/README.md +++ b/npm/README.md @@ -6,4 +6,10 @@ Prebuilt npm distribution of [`run-code`](https://github.com/timzhong1024/run-co echo 'print("hello")' | npx @timzhong2000/run-code python ``` +Or copy a source snippet into an isolated template project and pass arguments: + +```bash +npx @timzhong2000/run-code node@20 snippet.ts -- first --verbose +``` + The package selects the bundled binary for the current operating system and CPU architecture. diff --git a/skills/run-code-snippet/SKILL.md b/skills/run-code-snippet/SKILL.md index f7312c1..e309a1d 100644 --- a/skills/run-code-snippet/SKILL.md +++ b/skills/run-code-snippet/SKILL.md @@ -13,6 +13,14 @@ CODE LANG ``` +也可以读取已有代码片段文件,并在 `--` 后传入参数: + +```bash +run-code TOOLCHAIN[@VERSION] [--package SPEC ...] [--commonjs] [--clean] [--quiet] FILE [-- ARG ...] +``` + +文件模式只读取 `FILE` 的内容,再将其复制进新建的隔离模板项目运行;不会在 `FILE` 所属的已有工程中执行,不会读取该工程的依赖,也不会复制相邻文件。缺少的依赖仍用 `--package` 明确添加。 + Fish 不支持 heredoc;用 `printf` 将每行代码送入 stdin: ```fish @@ -30,11 +38,13 @@ CODE '@ | run-code TOOLCHAIN[@VERSION] [--package SPEC ...] [--commonjs] [--clean] [--quiet] ``` -使用 `python`、`node`、`rust`、`go` 或 `dotnet`;`javascript` 和 `typescript` 是 `node` 的别名,`csharp` 和 `cs` 是 `dotnet` 的别名。省略版本时使用内置的最新稳定版本。Python 依赖版本使用 `NAME==VERSION`;Rust 依赖可用 `NAME[@VERSION][FEATURE,...]` 指定 Cargo features;.NET 依赖使用 NuGet 的 `NAME[@VERSION]`。Node 默认以 ESM 统一运行 JavaScript/TypeScript,仅在代码明确依赖 CommonJS 时添加 `--commonjs`。C# 使用 .NET 10+ file-based app。Python 和 Node 未指定 `--package` 时直接执行且不创建项目;指定依赖时才创建隔离项目。需要项目时默认保留;仅在明确只需一次结果时添加 `--clean`,仅需代码本身的输出时添加 `--quiet`。直接调用工具,不自行创建项目或检查运行环境。 +使用 `python`、`node`、`rust`、`go` 或 `dotnet`;`javascript` 和 `typescript` 是 `node` 的别名,`csharp` 和 `cs` 是 `dotnet` 的别名。省略版本时使用内置的最新稳定版本。Python 依赖版本使用 `NAME==VERSION`;Rust 依赖可用 `NAME[@VERSION][FEATURE,...]` 指定 Cargo features;.NET 依赖使用 NuGet 的 `NAME[@VERSION]`。Node 默认以 ESM 统一运行 JavaScript/TypeScript,仅在代码明确依赖 CommonJS 时添加 `--commonjs`。C# 使用 .NET 10+ file-based app。stdin 模式下,Python 和 Node 未指定 `--package` 时直接执行;文件模式始终创建隔离模板项目。需要项目时默认保留;仅在明确只需一次结果时添加 `--clean`,仅需代码本身的输出时添加 `--quiet`。直接调用工具,不自行创建项目或检查运行环境。 ## Examples ```bash +run-code node@20 snippet.ts -- first --verbose + run-code node@20 --package zod@4 --clean <<'TS' import { z } from "zod"; console.log(z.object({ id: z.number() }).parse({ id: 1 })); diff --git a/src/cli.rs b/src/cli.rs index c7cdb84..8ea1e75 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,6 +1,7 @@ #[cfg(test)] use clap::CommandFactory; use clap::{Parser, Subcommand}; +use std::path::PathBuf; use std::str::FromStr; pub const DEFAULT_PYTHON_TOOLCHAIN: &str = "3.14"; @@ -82,7 +83,7 @@ impl FromStr for ToolchainSpec { #[command( name = "run-code", version, - about = "Run stdin with a selected toolchain and temporary dependencies" + about = "Run a code snippet with a selected toolchain and temporary dependencies" )] pub struct Cli { #[command(subcommand)] @@ -92,6 +93,14 @@ pub struct Cli { #[arg(value_name = "TOOLCHAIN[@VERSION]")] pub toolchain: Option, + /// Source file to copy into an isolated template project; reads stdin when omitted + #[arg(value_name = "FILE")] + pub source: Option, + + /// Arguments passed to the code snippet + #[arg(last = true, value_name = "ARG")] + pub args: Vec, + /// Dependency specification; Python supports NAME==VERSION #[arg(short = 'p', long = "package", value_name = "SPEC")] pub packages: Vec, @@ -129,6 +138,8 @@ impl Cli { || self.commonjs || self.clean || self.quiet + || self.source.is_some() + || !self.args.is_empty() { Some("the skill command does not accept execution arguments".into()) } else { @@ -153,7 +164,7 @@ mod tests { .get_arguments() .filter(|arg| arg.get_id() != "help" && arg.get_id() != "version") .count(); - assert_eq!(visible, 5); + assert_eq!(visible, 7); } #[test] @@ -163,6 +174,22 @@ mod tests { assert!(cli.commonjs); } + #[test] + fn source_file_and_trailing_arguments_are_distinct() { + let cli = + Cli::try_parse_from(["run-code", "node@20", "snippet.ts", "--", "first", "--flag"]) + .unwrap(); + assert_eq!(cli.source, Some(PathBuf::from("snippet.ts"))); + assert_eq!(cli.args, ["first", "--flag"]); + } + + #[test] + fn stdin_can_also_receive_trailing_arguments() { + let cli = Cli::try_parse_from(["run-code", "python", "--", "first"]).unwrap(); + assert!(cli.source.is_none()); + assert_eq!(cli.args, ["first"]); + } + #[test] fn skill_is_a_command_without_a_toolchain() { let cli = Cli::try_parse_from(["run-code", "skill"]).unwrap(); diff --git a/src/main.rs b/src/main.rs index 73feb95..6f89d5e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,11 +2,11 @@ mod cli; mod process; mod runner; mod skill; +mod source; mod util; use clap::{Parser, error::ErrorKind}; use cli::{Cli, Command}; -use std::io::{self, Read}; fn main() { let cli = match Cli::try_parse() { @@ -28,12 +28,17 @@ fn main() { return; } - let mut code = String::new(); - if let Err(error) = io::stdin().read_to_string(&mut code) { - return exit_with_error(&format!("failed to read stdin: {error}"), 1); - } + let code = match source::read(cli.source.as_deref()) { + Ok(code) => code, + Err(error) => return exit_with_error(&error, 1), + }; if code.is_empty() { - return exit_with_error("stdin did not contain source code", 2); + let input = if cli.source.is_some() { + "source file" + } else { + "stdin" + }; + return exit_with_error(&format!("{input} did not contain source code"), 2); } match runner::run_snippet(&cli, &code) { diff --git a/src/runner/dotnet.rs b/src/runner/dotnet.rs index 53656e6..61f62ac 100644 --- a/src/runner/dotnet.rs +++ b/src/runner/dotnet.rs @@ -19,13 +19,23 @@ impl<'a> DotnetBackend<'a> { } impl Backend for DotnetBackend<'_> { - fn prepare(&self, dir: &Path, code: &str, quiet: bool) -> Result { + fn prepare( + &self, + dir: &Path, + code: &str, + arguments: &[String], + quiet: bool, + ) -> Result { let source = source_with_packages(code, self.packages)?; write_source(&dir.join("snippet.cs"), &source)?; let toolchain = format!("dotnet@{}", self.toolchain.version); let mut args = vec!["exec".into(), toolchain]; args.extend(strings(&["--", "dotnet", "run", "snippet.cs"])); + if !arguments.is_empty() { + args.push("--".into()); + args.extend(arguments.iter().cloned()); + } let result = run_final("mise", &args, Some(dir), &[], quiet)?; Ok(result.exit_code.unwrap_or(1)) } diff --git a/src/runner/go.rs b/src/runner/go.rs index 37108c9..8bd1585 100644 --- a/src/runner/go.rs +++ b/src/runner/go.rs @@ -17,7 +17,13 @@ impl<'a> GoBackend<'a> { } } impl Backend for GoBackend<'_> { - fn prepare(&self, dir: &Path, code: &str, quiet: bool) -> Result { + fn prepare( + &self, + dir: &Path, + code: &str, + arguments: &[String], + quiet: bool, + ) -> Result { let toolchain = format!("go@{}", self.toolchain.version); let env = [("GOWORK".into(), "off".into())]; run_checked_hidden( @@ -52,6 +58,7 @@ impl Backend for GoBackend<'_> { } let mut args = vec!["exec".into(), toolchain.clone()]; args.extend(strings(&["--", "go", "run", "."])); + args.extend(arguments.iter().cloned()); let result = run_final("mise", &args, Some(dir), &env, quiet)?; Ok(result.exit_code.unwrap_or(1)) } diff --git a/src/runner/mod.rs b/src/runner/mod.rs index f788481..4b358c7 100644 --- a/src/runner/mod.rs +++ b/src/runner/mod.rs @@ -13,12 +13,23 @@ trait Backend { fn runs_without_project(&self) -> bool { false } - fn run_direct(&self, _code: &str, _quiet: bool) -> Result { + fn run_direct( + &self, + _code: &str, + _arguments: &[String], + _quiet: bool, + ) -> Result { Err(RunFailure::message( "this backend does not support direct execution", )) } - fn prepare(&self, project_dir: &Path, code: &str, quiet: bool) -> Result; + fn prepare( + &self, + project_dir: &Path, + code: &str, + arguments: &[String], + quiet: bool, + ) -> Result; } fn backend_for(cli: &Cli) -> Result, String> { @@ -74,8 +85,12 @@ pub fn run_snippet(cli: &Cli, code: &str) -> Result { hint: None, exit_code: Some(2), })?; - if backend.runs_without_project() { - return backend.run_direct(code, cli.quiet).map_err(Into::into); + // File input is always copied into an isolated template project. It never + // runs in, or discovers dependencies from, the source file's project. + if should_run_direct(cli, backend.as_ref()) { + return backend + .run_direct(code, &cli.args, cli.quiet) + .map_err(Into::into); } let temp_dir = Builder::new() @@ -93,10 +108,14 @@ pub fn run_snippet(cli: &Cli, code: &str) -> Result { }; backend - .prepare(&project_dir, code, cli.quiet) + .prepare(&project_dir, code, &cli.args, cli.quiet) .map_err(Into::into) } +fn should_run_direct(cli: &Cli, backend: &dyn Backend) -> bool { + cli.source.is_none() && backend.runs_without_project() +} + #[cfg(test)] mod tests { use super::*; @@ -119,4 +138,17 @@ mod tests { assert!(!backend_for(&with_package).unwrap().runs_without_project()); } } + + #[test] + fn source_files_force_an_isolated_template_project() { + for toolchain in ["python", "node"] { + let stdin = Cli::try_parse_from(["run-code", toolchain]).unwrap(); + let stdin_backend = backend_for(&stdin).unwrap(); + assert!(should_run_direct(&stdin, stdin_backend.as_ref())); + + let cli = Cli::try_parse_from(["run-code", toolchain, "snippet.txt"]).unwrap(); + let backend = backend_for(&cli).unwrap(); + assert!(!should_run_direct(&cli, backend.as_ref())); + } + } } diff --git a/src/runner/node.rs b/src/runner/node.rs index 2819527..86b14fa 100644 --- a/src/runner/node.rs +++ b/src/runner/node.rs @@ -36,7 +36,7 @@ impl Backend for NodeBackend<'_> { self.packages.is_empty() } - fn run_direct(&self, code: &str, quiet: bool) -> Result { + fn run_direct(&self, code: &str, arguments: &[String], quiet: bool) -> Result { let suffix = if self.commonjs { ".cts" } else { ".mts" }; let source = Builder::new() .prefix("run-code-") @@ -49,7 +49,7 @@ impl Backend for NodeBackend<'_> { })?; write_source(source.path(), code)?; - let args = vec![ + let mut args = vec![ "env".into(), "exec".into(), "--node".into(), @@ -59,12 +59,19 @@ impl Backend for NodeBackend<'_> { format!("tsx@{TSX_VERSION}"), source.path().to_string_lossy().into_owned(), ]; + args.extend(arguments.iter().cloned()); let cwd = std::env::temp_dir(); let result = run_final("vp", &args, Some(&cwd), &[], quiet)?; Ok(result.exit_code.unwrap_or(1)) } - fn prepare(&self, dir: &Path, code: &str, quiet: bool) -> Result { + fn prepare( + &self, + dir: &Path, + code: &str, + arguments: &[String], + quiet: bool, + ) -> Result { let file = self.file_name(); let manifest = serde_json::to_string_pretty(&json!({ "name": "run-code-snippet", @@ -114,12 +121,36 @@ impl Backend for NodeBackend<'_> { quiet, )?; - let final_args = if quiet { - strings(&["exec", "tsx", file]) - } else { - strings(&["run", "start"]) - }; + let final_args = final_command(file, arguments, quiet); let result = run_final("vp", &final_args, Some(dir), &[], quiet)?; Ok(result.exit_code.unwrap_or(1)) } } + +fn final_command(file: &str, arguments: &[String], quiet: bool) -> Vec { + let mut command = if quiet { + strings(&["exec", "--", "tsx", file]) + } else { + strings(&["run", "start"]) + }; + command.extend(arguments.iter().cloned()); + command +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn snippet_arguments_do_not_include_the_cli_separator() { + let arguments = vec!["first".into(), "--flag".into()]; + assert_eq!( + final_command("snippet.ts", &arguments, false), + ["run", "start", "first", "--flag"] + ); + assert_eq!( + final_command("snippet.ts", &arguments, true), + ["exec", "--", "tsx", "snippet.ts", "first", "--flag"] + ); + } +} diff --git a/src/runner/python.rs b/src/runner/python.rs index 42757af..e102140 100644 --- a/src/runner/python.rs +++ b/src/runner/python.rs @@ -22,18 +22,25 @@ impl Backend for PythonBackend<'_> { self.packages.is_empty() } - fn run_direct(&self, code: &str, quiet: bool) -> Result { + fn run_direct(&self, code: &str, arguments: &[String], quiet: bool) -> Result { let mut args = strings(&["run", "--quiet"]); args.extend(strings(&["--isolated", "--managed-python", "--python"])); args.push(self.toolchain.version.clone()); args.extend(strings(&["python", "-c"])); args.push(code.into()); + args.extend(arguments.iter().cloned()); let cwd = std::env::temp_dir(); let result = run_final("uv", &args, Some(&cwd), &[], quiet)?; Ok(result.exit_code.unwrap_or(1)) } - fn prepare(&self, dir: &Path, code: &str, quiet: bool) -> Result { + fn prepare( + &self, + dir: &Path, + code: &str, + arguments: &[String], + quiet: bool, + ) -> Result { let version = &self.toolchain.version; let mut init = strings(&[ "init", @@ -71,6 +78,7 @@ impl Backend for PythonBackend<'_> { args.extend(strings(&["--managed-python", "--python"])); args.push(version.clone()); args.extend(strings(&["python", "main.py"])); + args.extend(arguments.iter().cloned()); let result = run_final("uv", &args, Some(dir), &[], quiet)?; Ok(result.exit_code.unwrap_or(1)) } diff --git a/src/runner/rust.rs b/src/runner/rust.rs index 22ee896..acf3e3a 100644 --- a/src/runner/rust.rs +++ b/src/runner/rust.rs @@ -19,7 +19,13 @@ impl<'a> RustBackend<'a> { } } impl Backend for RustBackend<'_> { - fn prepare(&self, dir: &Path, code: &str, quiet: bool) -> Result { + fn prepare( + &self, + dir: &Path, + code: &str, + arguments: &[String], + quiet: bool, + ) -> Result { let version = &self.toolchain.version; let mut init = strings(&["run", "--install"]); init.push(version.clone()); @@ -61,6 +67,10 @@ impl Backend for RustBackend<'_> { let mut args = strings(&["run", "--install"]); args.push(version.clone()); args.extend(strings(&["cargo", "run", "--quiet"])); + if !arguments.is_empty() { + args.push("--".into()); + args.extend(arguments.iter().cloned()); + } let result = run_final("rustup", &args, Some(dir), &env, quiet)?; Ok(result.exit_code.unwrap_or(1)) } diff --git a/src/source.rs b/src/source.rs new file mode 100644 index 0000000..fe0d8a9 --- /dev/null +++ b/src/source.rs @@ -0,0 +1,37 @@ +use std::fs; +use std::io::{self, Read}; +use std::path::Path; + +pub fn read(source: Option<&Path>) -> Result { + match source { + Some(path) => fs::read_to_string(path) + .map_err(|error| format!("failed to read source file {}: {error}", path.display())), + None => { + let mut code = String::new(); + io::stdin() + .read_to_string(&mut code) + .map_err(|error| format!("failed to read stdin: {error}"))?; + Ok(code) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Write; + + #[test] + fn reads_utf8_source_file() { + let mut source = tempfile::NamedTempFile::new().unwrap(); + write!(source, "print('copied')").unwrap(); + assert_eq!(read(Some(source.path())).unwrap(), "print('copied')"); + } + + #[test] + fn reports_the_source_path_on_failure() { + let path = Path::new("missing-snippet.py"); + let error = read(Some(path)).unwrap_err(); + assert!(error.contains("missing-snippet.py")); + } +} diff --git a/src/util.rs b/src/util.rs index baa9a51..9041bd3 100644 --- a/src/util.rs +++ b/src/util.rs @@ -8,7 +8,7 @@ pub fn write_source(path: &Path, code: &str) -> Result<(), RunFailure> { .map_err(|e| RunFailure::message(format!("failed to create source directory: {e}")))?; } fs::write(path, code) - .map_err(|e| RunFailure::message(format!("failed to store stdin source: {e}"))) + .map_err(|e| RunFailure::message(format!("failed to store snippet source: {e}"))) } pub fn path_text(path: &Path) -> String {