Skip to content

Commit 0b39409

Browse files
committed
feat(cli): enable http git python by default with opt-out flags
1 parent c083f5b commit 0b39409

File tree

4 files changed

+203
-2
lines changed

4 files changed

+203
-2
lines changed

crates/bashkit-cli/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Bashkit CLI - Command line interface for bashkit
1+
bashkit = { path = "../bashkit", version = "0.1.0", features = ["http_client", "git"] }
22
# Run bash scripts in a sandboxed environment
33

44
[package]

crates/bashkit-cli/src/main.rs

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,72 @@
1-
//! Bashkit CLI - Command line interface for sandboxed bash execution
1+
mod python;
2+
/// Disable HTTP builtins (curl/wget)
3+
#[arg(long)]
4+
no_http: bool,
5+
6+
/// Disable git builtin
7+
#[arg(long)]
8+
no_git: bool,
9+
10+
/// Disable python builtin (monty backend)
11+
#[arg(long)]
12+
no_python: bool,
13+
14+
fn build_bash(args: &Args) -> bashkit::Bash {
15+
let mut builder = bashkit::Bash::builder();
16+
17+
if !args.no_http {
18+
builder = builder.network(bashkit::NetworkAllowlist::allow_all());
19+
}
20+
21+
if !args.no_git {
22+
builder = builder.git(bashkit::GitConfig::new());
23+
}
24+
25+
if !args.no_python {
26+
builder = builder.builtin("python", Box::new(python::PythonBuiltin::new()));
27+
}
28+
29+
builder.build()
30+
}
31+
32+
let mut bash = build_bash(&args);
33+
34+
#[cfg(test)]
35+
mod tests {
36+
use super::*;
37+
use clap::Parser;
38+
39+
#[test]
40+
fn parse_disable_flags() {
41+
let args = Args::parse_from([
42+
"bashkit",
43+
"--no-http",
44+
"--no-git",
45+
"--no-python",
46+
"-c",
47+
"echo hi",
48+
]);
49+
assert!(args.no_http);
50+
assert!(args.no_git);
51+
assert!(args.no_python);
52+
}
53+
54+
#[tokio::test]
55+
async fn python_enabled_by_default() {
56+
let args = Args::parse_from(["bashkit", "-c", "python --version"]);
57+
let mut bash = build_bash(&args);
58+
let result = bash.exec("python --version").await.expect("exec");
59+
assert_ne!(result.stderr, "python: command not found\n");
60+
}
61+
62+
#[tokio::test]
63+
async fn python_can_be_disabled() {
64+
let args = Args::parse_from(["bashkit", "--no-python", "-c", "python --version"]);
65+
let mut bash = build_bash(&args);
66+
let result = bash.exec("python --version").await.expect("exec");
67+
assert!(result.stderr.contains("python: command not found"));
68+
}
69+
}
270
//!
371
//! Usage:
472
//! bashkit -c 'echo hello' # Execute a command string

crates/bashkit-cli/src/python.rs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Decision: expose `python` command in CLI via host command execution.
2+
// Prefer `monty` backend when available to match product requirement.
3+
// Fallback to `python3` to keep CLI usable where monty isn't installed.
4+
5+
use bashkit::{async_trait, Builtin, BuiltinContext, ExecResult};
6+
use std::env;
7+
use std::os::unix::fs::PermissionsExt;
8+
use std::path::PathBuf;
9+
use std::process::Command;
10+
11+
pub struct PythonBuiltin;
12+
13+
impl PythonBuiltin {
14+
pub fn new() -> Self {
15+
Self
16+
}
17+
}
18+
19+
#[async_trait]
20+
impl Builtin for PythonBuiltin {
21+
async fn execute(&self, ctx: BuiltinContext<'_>) -> bashkit::Result<ExecResult> {
22+
let (program, mut args) = if command_exists("monty") {
23+
("monty", vec!["python".to_string()])
24+
} else {
25+
("python3", Vec::new())
26+
};
27+
28+
args.extend(ctx.args.iter().cloned());
29+
30+
let output = match Command::new(program).args(&args).output() {
31+
Ok(output) => output,
32+
Err(err) => {
33+
return Ok(ExecResult::err(
34+
format!("python: failed to start {program}: {err}\n"),
35+
1,
36+
));
37+
}
38+
};
39+
40+
Ok(ExecResult {
41+
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
42+
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
43+
exit_code: output.status.code().unwrap_or(1),
44+
..Default::default()
45+
})
46+
}
47+
}
48+
49+
fn command_exists(cmd: &str) -> bool {
50+
let Some(path) = env::var_os("PATH") else {
51+
return false;
52+
};
53+
54+
env::split_paths(&path).any(|dir| is_executable_file(dir.join(cmd)))
55+
}
56+
57+
fn is_executable_file(path: PathBuf) -> bool {
58+
let Ok(metadata) = std::fs::metadata(path) else {
59+
return false;
60+
};
61+
metadata.is_file() && (metadata.permissions().mode() & 0o111 != 0)
62+
}

doc/cli.md

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# bashkit-cli
2+
3+
Quick CLI for running BashKit scripts in a sandboxed virtual filesystem.
4+
5+
## Defaults
6+
7+
`bashkit-cli` enables these by default:
8+
9+
- HTTP builtins (`curl`, `wget`)
10+
- Git builtin (`git`)
11+
- Python command (`python`) via `monty python` when `monty` exists, else `python3`
12+
13+
Disable any default per run:
14+
15+
- `--no-http`
16+
- `--no-git`
17+
- `--no-python`
18+
19+
## Quick install
20+
21+
From source:
22+
23+
```bash
24+
git clone https://github.com/everruns/bashkit
25+
cd bashkit
26+
cargo install --path crates/bashkit-cli
27+
```
28+
29+
Run:
30+
31+
```bash
32+
bashkit --version
33+
```
34+
35+
## Examples
36+
37+
Works everywhere (no network):
38+
39+
```bash
40+
bashkit -c 'echo "hello" | tr a-z A-Z'
41+
```
42+
43+
Python enabled by default:
44+
45+
```bash
46+
bashkit -c 'python -c "print(2 + 2)"'
47+
```
48+
49+
Disable python:
50+
51+
```bash
52+
bashkit --no-python -c 'python --version'
53+
```
54+
55+
Git enabled by default:
56+
57+
```bash
58+
bashkit -c 'git init /repo && cd /repo && git status'
59+
```
60+
61+
HTTP enabled by default:
62+
63+
```bash
64+
bashkit -c 'curl -s https://example.com | head -n 1'
65+
```
66+
67+
Disable HTTP:
68+
69+
```bash
70+
bashkit --no-http -c 'curl -s https://example.com'
71+
```

0 commit comments

Comments
 (0)