From 37bbc0ccf6ca34c1163384742f172741a5322c75 Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Thu, 2 Jul 2026 10:33:38 +0800 Subject: feat: add cross-platform script runner with run.sh and run.ps1 --- .run/.gitignore | 3 + .run/src/bin/csharp-script.cs | 8 +++ .run/src/bin/powrshell-script.ps1 | 8 +++ .run/src/bin/python-script.py | 11 +++ .run/src/bin/rust-script.rs | 12 ++++ .run/src/bin/shell-script.sh | 14 ++++ LICENSE | 13 ++++ README.md | 30 ++++++++ install/install.ps1 | 32 +++++++++ install/install.sh | 26 +++++++ run.ps1 | 147 +++++++++++++++++++++++++++++++++++++ run.sh | 148 ++++++++++++++++++++++++++++++++++++++ 12 files changed, 452 insertions(+) create mode 100644 .run/.gitignore create mode 100644 .run/src/bin/csharp-script.cs create mode 100644 .run/src/bin/powrshell-script.ps1 create mode 100644 .run/src/bin/python-script.py create mode 100644 .run/src/bin/rust-script.rs create mode 100644 .run/src/bin/shell-script.sh create mode 100644 LICENSE create mode 100644 README.md create mode 100644 install/install.ps1 create mode 100644 install/install.sh create mode 100644 run.ps1 create mode 100644 run.sh diff --git a/.run/.gitignore b/.run/.gitignore new file mode 100644 index 0000000..78d04c9 --- /dev/null +++ b/.run/.gitignore @@ -0,0 +1,3 @@ +/.temp +/target +/Cargo.* diff --git a/.run/src/bin/csharp-script.cs b/.run/src/bin/csharp-script.cs new file mode 100644 index 0000000..1e7a3b7 --- /dev/null +++ b/.run/src/bin/csharp-script.cs @@ -0,0 +1,8 @@ +Console.WriteLine(""); +Console.WriteLine(" Welcome to use `run.sh` / `run.ps1`"); +Console.WriteLine(""); +Console.WriteLine(" > \"Hello World\""); +Console.WriteLine(" > cwd : \"{0}\"", Environment.CurrentDirectory); +var argStr = args.Length > 0 ? string.Join(", ", args.Select(a => $"\"{a}\"")) : ""; +Console.WriteLine(" > args : {0}", argStr); +Console.WriteLine(""); diff --git a/.run/src/bin/powrshell-script.ps1 b/.run/src/bin/powrshell-script.ps1 new file mode 100644 index 0000000..70f3bae --- /dev/null +++ b/.run/src/bin/powrshell-script.ps1 @@ -0,0 +1,8 @@ +Write-Host "" +Write-Host ' Welcome to use `run.sh` / `run.ps1`' +Write-Host "" +Write-Host ' > "Hello World"' +Write-Host " > cwd : `"$(Get-Location)`"" +$items = ($args | ForEach-Object { "`"$_`"" }) -join ", " +Write-Host " > args : $items" +Write-Host "" diff --git a/.run/src/bin/python-script.py b/.run/src/bin/python-script.py new file mode 100644 index 0000000..9f4ff7b --- /dev/null +++ b/.run/src/bin/python-script.py @@ -0,0 +1,11 @@ +import os +import sys + +print() +print(" Welcome to use `run.sh` / `run.ps1`") +print() +print(' > "Hello World"') +print(f' > cwd : "{os.getcwd()}"') +args = ", ".join(f'"{a}"' for a in sys.argv[1:]) +print(f" > args : {args}") +print() diff --git a/.run/src/bin/rust-script.rs b/.run/src/bin/rust-script.rs new file mode 100644 index 0000000..19f4bcb --- /dev/null +++ b/.run/src/bin/rust-script.rs @@ -0,0 +1,12 @@ +fn main() { + let args: Vec = std::env::args().skip(1).collect(); + let cwd = std::env::current_dir().unwrap(); + println!(); + println!(" Welcome to use `run.sh` / `run.ps1`"); + println!(); + println!(" > \"Hello World\""); + println!(" > cwd : \"{}\"", cwd.display()); + let args_str: Vec = args.iter().map(|a| format!("\"{}\"", a)).collect(); + println!(" > args : {}", args_str.join(", ")); + println!(); +} diff --git a/.run/src/bin/shell-script.sh b/.run/src/bin/shell-script.sh new file mode 100644 index 0000000..306afb5 --- /dev/null +++ b/.run/src/bin/shell-script.sh @@ -0,0 +1,14 @@ +#!/bin/sh +echo "" +echo ' Welcome to use `run.sh` / `run.ps1`' +echo "" +echo ' > "Hello World"' +echo " > cwd : \"$(pwd)\"" +items="" +sep="" +for arg in "$@"; do + items="${items}${sep}\"${arg}\"" + sep=", " +done +echo " > args : ${items}" +echo "" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b3e27d9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + Version 2, December 2004 + + Copyright (C) 2026 Weicao-CatilGrass + + Everyone is permitted to copy and distribute verbatim or modified + copies of this license document, and changing it is allowed as long + as the name is changed. + + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. You just DO WHAT THE FUCK YOU WANT TO. diff --git a/README.md b/README.md new file mode 100644 index 0000000..291f3d8 --- /dev/null +++ b/README.md @@ -0,0 +1,30 @@ +# Screw "make", I just want a script runner + +Sometimes when working on projects, I need cross-platform operation. I don't want to install something like `just` or `make`. I just want to write some scripts, drop them in, and run them. That's it. + +This is the solution: + +- Rust program? Drop it in `.run/src/bin/myscript.rs` +- C#? Drop it in `.run/src/bin/myscript.cs` +- Python? Drop it in `.run/src/bin/myscript.py` +- Shell? PowerShell? Same! + +Any script can be dropped in. After that, just run `.\run.ps1 script-name` or `./run.sh script-name`. + +## Install + +For Linux: + +```bash +curl -fsSL https://raw.githubusercontent.com/catilgrass/run/refs/heads/master/install/install.sh | bash +``` + +For Windows: + +```bash +iwr -useb https://raw.githubusercontent.com/catilgrass/run/refs/heads/master/install/install.ps1 | iex +``` + +## License + +WTFPL — drop it into your project and use it. diff --git a/install/install.ps1 b/install/install.ps1 new file mode 100644 index 0000000..f9853ff --- /dev/null +++ b/install/install.ps1 @@ -0,0 +1,32 @@ +$tempDir = "./run_script_temp" +$repoDir = "$tempDir/run" + +Write-Host "Creating temporary directory..." +$null = New-Item -ItemType Directory -Path $tempDir -Force + +Write-Host "Cloning https://github.com/catilgrass/run ..." +git clone "https://github.com/catilgrass/run" $repoDir + +Write-Host "Removing unwanted files..." +Remove-Item -Path "$repoDir/README.md", "$repoDir/LICENSE" -Force -ErrorAction SilentlyContinue +Remove-Item -Path "$repoDir/install" -Recurse -Force -ErrorAction SilentlyContinue + +Write-Host "Removing git data..." +Remove-Item -Path "$repoDir/.git" -Recurse -Force -ErrorAction SilentlyContinue + +Write-Host "Installing files..." +Get-ChildItem -Path $repoDir -Force | ForEach-Object { + $dest = Join-Path (Get-Location) $_.Name + if ($_.PSIsContainer) { + Copy-Item -Path $_.FullName -Destination $dest -Recurse -Force + } else { + Copy-Item -Path $_.FullName -Destination $dest -Force + } +} + +Write-Host "Cleaning up temporary directory..." +[System.GC]::Collect() +Start-Sleep -Milliseconds 200 +Remove-Item -Path $tempDir -Recurse -Force + +Write-Host "Done!" diff --git a/install/install.sh b/install/install.sh new file mode 100644 index 0000000..3dece5f --- /dev/null +++ b/install/install.sh @@ -0,0 +1,26 @@ +#!/bin/sh +set -e + +TEMP_DIR="./run_script_temp" +REPO_DIR="$TEMP_DIR/run" + +echo "Creating temporary directory..." +mkdir -p "$TEMP_DIR" + +echo "Cloning https://github.com/catilgrass/run ..." +git clone https://github.com/catilgrass/run "$REPO_DIR" + +echo "Removing unwanted files..." +rm -f "$REPO_DIR/README.md" "$REPO_DIR/LICENSE" +rm -rf "$REPO_DIR/install" + +echo "Removing git data..." +rm -rf "$REPO_DIR/.git" + +echo "Installing files..." +cp -a "$REPO_DIR/." . + +echo "Cleaning up temporary directory..." +rm -rf "$TEMP_DIR" + +echo "Done!" diff --git a/run.ps1 b/run.ps1 new file mode 100644 index 0000000..17b85e1 --- /dev/null +++ b/run.ps1 @@ -0,0 +1,147 @@ +Set-Location -Path (Split-Path -Parent $MyInvocation.MyCommand.Path) -ErrorAction Stop + +$tools = @{} + +if (Test-Path ".run/src/bin/*.cs") { + Get-ChildItem -Path ".run/src/bin/*.cs" | ForEach-Object { + $tools[$_.BaseName] = @{ Type = "cs"; Path = $_.FullName } + } +} + +if (Test-Path ".run/src/bin/*.rs") { + Get-ChildItem -Path ".run/src/bin/*.rs" | ForEach-Object { + $tools[$_.BaseName] = @{ Type = "rs"; Path = $_.FullName } + } +} + +if (Test-Path ".run/src/bin/*.py") { + Get-ChildItem -Path ".run/src/bin/*.py" | ForEach-Object { + $tools[$_.BaseName] = @{ Type = "py"; Path = $_.FullName } + } +} + +if (Test-Path ".run/src/bin/*.ps1") { + Get-ChildItem -Path ".run/src/bin/*.ps1" | ForEach-Object { + $tools[$_.BaseName] = @{ Type = "ps1"; Path = $_.FullName } + } +} + +if ($args.Count -eq 0) { + $sorted = $tools.Keys | Sort-Object + $total = $sorted.Count + $num_w = $total.ToString().Length + + $max_name = ($sorted | ForEach-Object { $_.Length } | Measure-Object -Maximum).Maximum + + $inner_w = 2 + $num_w + 1 + 1 + $max_name + 2 + 10 + 1 + 2 + if ($inner_w -lt 38) { $inner_w = 38 } + + $title = '.\run.ps1 [ARGS...]' + $title_len = $title.Length + $dash_total = $inner_w - 2 - $title_len + $dash_left = [Math]::Floor($dash_total / 2) + $dash_right = $dash_total - $dash_left + + Write-Host ("┌" + ("─" * $dash_left) + " " + $title + " " + ("─" * $dash_right) + "┐") + Write-Host ("│" + (" " * $inner_w) + "│") + + $i = 1 + foreach ($name in $sorted) { + $type = $tools[$name].Type + $lang = switch ($type) { + "cs" { "C#" } + "py" { "Python" } + "rs" { "Rust" } + "ps1" { "PowerShell" } + } + $entry = " " + $i.ToString().PadRight($num_w) + ". " + $name.PadRight($max_name) + " [$lang]" + Write-Host ("│" + $entry.PadRight($inner_w) + "│") + $i++ + } + + Write-Host ("│" + (" " * $inner_w) + "│") + Write-Host ("└" + ("─" * $inner_w) + "┘") + exit 1 +} + +$target_name = $args[0] + +if ($target_name -match '^\d+$') { + $sorted = $tools.Keys | Sort-Object + $idx = [int]$target_name - 1 + if ($idx -ge 0 -and $idx -lt $sorted.Count) { + $target_name = $sorted[$idx] + } else { + Write-Host "Error: invalid number '$target_name', valid range is 1-$($sorted.Count)" + exit 1 + } +} + +if (-not $tools.ContainsKey($target_name)) { + Write-Host "Error: target '$target_name' does not exist" + exit 1 +} + +$script_args = $args[1..$args.Count] +$info = $tools[$target_name] + +switch ($info.Type) { + "ps1" { + & $info.Path @script_args + } + "py" { + python $info.Path $script_args + } + "rs" { + if (-not (Test-Path ".run/Cargo.toml")) { +@" +[package] +name = "run_rust" +version = "0.1.0" +edition = "2024" + +[workspace] + +[dependencies] +"@ | Set-Content -Path ".run/Cargo.toml" + } + cargo build --manifest-path ".run/Cargo.toml" --target-dir ".run/target" --bin $target_name --quiet + $binary = ".run/target/debug/$target_name.exe" + if (-not (Test-Path $binary)) { + $binary = ".run/target/debug/$target_name" + } + & $binary $script_args + } + "cs" { + $temp_dir = ".run/target/csproj/$target_name" + $null = New-Item -ItemType Directory -Path $temp_dir -Force + + $props_content = @' + + + $(MSBuildThisFileDirectory)../../csharp/bin + $(MSBuildThisFileDirectory)../../csharp/obj + + +'@ + Set-Content -Path "$temp_dir/Directory.Build.props" -Value $props_content + + $csproj_content = @" + + + + Exe + net8.0 + enable + enable + + + +"@ + Set-Content -Path "$temp_dir/$target_name.csproj" -Value $csproj_content + Copy-Item -Path $info.Path -Destination "$temp_dir/Program.cs" -Force + + dotnet run --project "$temp_dir/$target_name.csproj" -- $script_args + } +} + diff --git a/run.sh b/run.sh new file mode 100644 index 0000000..62e08ac --- /dev/null +++ b/run.sh @@ -0,0 +1,148 @@ +#!/bin/bash + +cd "$(dirname "$0")" || exit 1 + +declare -A tools + +for file in .run/src/bin/*.cs; do + if [ -f "$file" ]; then + name=$(basename "$file" .cs) + tools["$name"]="cs" + fi +done + +for file in .run/src/bin/*.rs; do + if [ -f "$file" ]; then + name=$(basename "$file" .rs) + tools["$name"]="rs" + fi +done + +for file in .run/src/bin/*.py; do + if [ -f "$file" ]; then + name=$(basename "$file" .py) + tools["$name"]="py" + fi +done + +for file in .run/src/bin/*.sh; do + if [ -f "$file" ]; then + name=$(basename "$file" .sh) + tools["$name"]="sh" + fi +done + +if [ $# -eq 0 ]; then + sorted_names=($(echo "${!tools[@]}" | tr ' ' '\n' | sort)) + total=${#sorted_names[@]} + num_w=${#total} + + max_name=0 + for name in "${sorted_names[@]}"; do + len=${#name} + ((len > max_name)) && max_name=$len + done + + inner_w=$((2 + num_w + 1 + 1 + max_name + 2 + 6 + 1 + 2)) + ((inner_w < 38)) && inner_w=38 + + title="./run.sh [ARGS...]" + title_len=${#title} + dash_total=$((inner_w - 2 - title_len)) + dash_left=$((dash_total / 2)) + dash_right=$((dash_total - dash_left)) + + echo "┌$(printf '─%.0s' $(seq 1 $dash_left)) $title $(printf '─%.0s' $(seq 1 $dash_right))┐" + printf "│%*s│\n" $inner_w "" + + i=1 + for name in "${sorted_names[@]}"; do + type="${tools[$name]}" + case "$type" in + cs) lang="C#";; + py) lang="Python";; + rs) lang="Rust";; + sh) lang="Shell";; + esac + entry=$(printf " %-*d) %-*s [%s]" $num_w $i $max_name $name $lang) + printf "│%-*s│\n" $inner_w "$entry" + i=$((i + 1)) + done + + printf "│%*s│\n" $inner_w "" + echo "└$(printf '─%.0s' $(seq 1 $inner_w))┘" + exit 1 +fi + +target_name="$1" +shift + +if [[ "$target_name" =~ ^[0-9]+$ ]]; then + sorted=($(echo "${!tools[@]}" | tr ' ' '\n' | sort)) + idx=$((target_name - 1)) + if [ "$idx" -ge 0 ] && [ "$idx" -lt "${#sorted[@]}" ]; then + target_name="${sorted[$idx]}" + else + echo "Error: invalid number '$target_name', valid range is 1-${#sorted[@]}" + exit 1 + fi +fi + +if [ -z "${tools[$target_name]}" ]; then + echo "Error: target '$target_name' does not exist" + exit 1 +fi + +type="${tools[$target_name]}" + +case "$type" in + sh) + chmod +x ".run/src/bin/$target_name.sh" + ".run/src/bin/$target_name.sh" "$@" + ;; + py) + python ".run/src/bin/$target_name.py" "$@" + ;; + rs) + if [ ! -f ".run/Cargo.toml" ]; then + cat > ".run/Cargo.toml" <<'EOF' +[package] +name = "run_rust" +version = "0.1.0" +edition = "2024" + +[workspace] + +[dependencies] +EOF + fi + cargo build --manifest-path ".run/Cargo.toml" --target-dir ".run/target" --bin "$target_name" --quiet + ".run/target/debug/$target_name" "$@" + ;; + cs) + temp_dir=".run/target/csproj/$target_name" + mkdir -p "$temp_dir" + cat > "$temp_dir/Directory.Build.props" <<'PROPS' + + + $(MSBuildThisFileDirectory)../../csharp/bin + $(MSBuildThisFileDirectory)../../csharp/obj + + +PROPS + cat > "$temp_dir/$target_name.csproj" <<'CSPROJ' + + + + Exe + net8.0 + enable + enable + + + +CSPROJ + cp ".run/src/bin/$target_name.cs" "$temp_dir/Program.cs" + dotnet run --project "$temp_dir/$target_name.csproj" -- "$@" + ;; +esac -- cgit