mod alias;
mod archive;
pub(crate) mod arg_parser;
mod assert;
mod awk;
mod base64;
mod bc;
mod caller;
mod cat;
mod checksum;
mod clear;
mod column;
mod comm;
mod compgen;
mod csv;
mod curl;
mod cuttr;
mod date;
mod diff;
mod dirstack;
mod disk;
mod dotenv;
mod echo;
mod environ;
mod envsubst;
mod expand;
mod export;
mod expr;
mod fc;
mod fileops;
mod flow;
mod fold;
mod glob_cmd;
mod grep;
mod headtail;
mod help;
mod hextools;
mod http;
mod iconv;
mod inspect;
mod introspect;
mod join;
mod jq;
mod json;
mod log;
mod ls;
mod mapfile;
mod mkfifo;
mod navigation;
mod nl;
mod parallel;
mod paste;
mod patch;
mod path;
mod pipeline;
mod printf;
mod read;
mod retry;
mod rg;
pub(crate) mod search_common;
mod sed;
mod semver;
mod seq;
mod sleep;
mod sortuniq;
mod source;
mod split;
mod strings;
mod system;
mod template;
mod test;
mod textrev;
pub(crate) mod timeout;
mod tomlq;
mod trap;
mod tree;
mod vars;
mod verify;
mod wait;
mod wc;
mod yaml;
mod yes;
mod zip_cmd;
#[cfg(feature = "git")]
mod git;
#[cfg(feature = "python")]
mod python;
pub use alias::{Alias, Unalias};
pub use archive::{Gunzip, Gzip, Tar};
pub use assert::Assert;
pub use awk::Awk;
pub use base64::Base64;
pub use bc::Bc;
pub use caller::Caller;
pub use cat::Cat;
pub use checksum::{Md5sum, Sha1sum, Sha256sum};
pub use clear::Clear;
pub use column::Column;
pub use comm::Comm;
pub use compgen::Compgen;
pub use csv::Csv;
pub use curl::{Curl, Wget};
pub use cuttr::{Cut, Tr};
pub use date::Date;
pub use diff::Diff;
pub use dirstack::{Dirs, Popd, Pushd};
pub use disk::{Df, Du};
pub use dotenv::Dotenv;
pub use echo::Echo;
pub use environ::{Env, History, Printenv};
pub use envsubst::Envsubst;
pub use expand::{Expand, Unexpand};
pub use export::Export;
pub use expr::Expr;
pub use fc::Fc;
pub use fileops::{Chmod, Chown, Cp, Kill, Ln, Mkdir, Mktemp, Mv, Rm, Touch};
pub use flow::{Break, Colon, Continue, Exit, False, Return, True};
pub use fold::Fold;
pub use glob_cmd::GlobCmd;
pub use grep::Grep;
pub use headtail::{Head, Tail};
pub use help::Help;
pub use hextools::{Hexdump, Od, Xxd};
pub use http::Http;
pub use iconv::Iconv;
pub use inspect::{File, Less, Stat};
pub use introspect::{Hash, Type, Which};
pub use join::Join;
pub use jq::Jq;
pub use json::Json;
pub use log::Log;
pub(crate) use ls::glob_match;
pub use ls::{Find, Ls, Rmdir};
pub use mapfile::Mapfile;
pub use mkfifo::Mkfifo;
pub use navigation::{Cd, Pwd};
pub use nl::Nl;
pub use parallel::Parallel;
pub use paste::Paste;
pub use patch::Patch;
pub use path::{Basename, Dirname, Readlink, Realpath};
pub use pipeline::{Tee, Watch, Xargs};
pub use printf::Printf;
pub use read::Read;
pub use retry::Retry;
pub use rg::Rg;
pub use sed::Sed;
pub use semver::Semver;
pub use seq::Seq;
pub use sleep::Sleep;
pub use sortuniq::{Sort, Uniq};
pub use source::Source;
pub use split::Split;
pub use strings::Strings;
pub use system::{DEFAULT_HOSTNAME, DEFAULT_USERNAME, Hostname, Id, Uname, Whoami};
pub use template::Template;
pub use test::{Bracket, Test};
pub use textrev::{Rev, Tac};
pub use timeout::Timeout;
pub use tomlq::Tomlq;
pub use trap::Trap;
pub use tree::Tree;
pub use vars::{Eval, Local, Readonly, Set, Shift, Shopt, Times, Unset};
pub use verify::Verify;
pub use wait::Wait;
pub use wc::Wc;
pub use yaml::Yaml;
pub use yes::Yes;
pub use zip_cmd::{Unzip, Zip};
#[cfg(feature = "git")]
pub use git::Git;
#[cfg(feature = "python")]
pub use python::{Python, PythonExternalFnHandler, PythonExternalFns, PythonLimits};
use async_trait::async_trait;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use crate::error::Result;
use crate::fs::FileSystem;
use crate::interpreter::ExecResult;
pub(crate) async fn read_text_file(
fs: &dyn FileSystem,
path: &Path,
cmd_name: &str,
) -> std::result::Result<String, ExecResult> {
let content = fs
.read_file(path)
.await
.map_err(|e| ExecResult::err(format!("{cmd_name}: {}: {e}\n", path.display()), 1))?;
if path == Path::new("/dev/urandom") || path == Path::new("/dev/random") {
return Ok(content.iter().map(|&b| b as char).collect());
}
Ok(String::from_utf8_lossy(&content).into_owned())
}
pub(crate) use crate::interpreter::ShellRef;
pub use crate::interpreter::BuiltinSideEffect;
#[derive(Debug, Clone)]
pub struct SubCommand {
pub name: String,
pub args: Vec<String>,
pub stdin: Option<String>,
}
#[derive(Debug)]
pub enum ExecutionPlan {
Timeout {
duration: std::time::Duration,
preserve_status: bool,
command: SubCommand,
},
Batch {
commands: Vec<SubCommand>,
},
}
pub fn resolve_path(cwd: &Path, path_str: &str) -> PathBuf {
let path = Path::new(path_str);
let joined = if path.is_absolute() {
path.to_path_buf()
} else {
cwd.join(path)
};
normalize_path(&joined)
}
use crate::fs::normalize_path;
pub struct Context<'a> {
pub args: &'a [String],
pub env: &'a HashMap<String, String>,
#[allow(dead_code)] pub variables: &'a mut HashMap<String, String>,
pub cwd: &'a mut PathBuf,
pub fs: Arc<dyn FileSystem>,
pub stdin: Option<&'a str>,
#[cfg(feature = "http_client")]
pub http_client: Option<&'a crate::network::HttpClient>,
#[cfg(feature = "git")]
pub git_client: Option<&'a crate::git::GitClient>,
pub(crate) shell: Option<ShellRef<'a>>,
}
impl<'a> Context<'a> {
#[cfg(test)]
pub fn new_for_test(
args: &'a [String],
env: &'a std::collections::HashMap<String, String>,
variables: &'a mut std::collections::HashMap<String, String>,
cwd: &'a mut std::path::PathBuf,
fs: std::sync::Arc<dyn crate::fs::FileSystem>,
stdin: Option<&'a str>,
) -> Self {
Self {
args,
env,
variables,
cwd,
fs,
stdin,
#[cfg(feature = "http_client")]
http_client: None,
#[cfg(feature = "git")]
git_client: None,
shell: None,
}
}
}
#[async_trait]
pub trait Builtin: Send + Sync {
async fn execute(&self, ctx: Context<'_>) -> Result<ExecResult>;
async fn execution_plan(&self, _ctx: &Context<'_>) -> Result<Option<ExecutionPlan>> {
Ok(None)
}
fn llm_hint(&self) -> Option<&'static str> {
None
}
}
#[async_trait]
impl Builtin for std::sync::Arc<dyn Builtin> {
async fn execute(&self, ctx: Context<'_>) -> Result<ExecResult> {
(**self).execute(ctx).await
}
async fn execution_plan(&self, ctx: &Context<'_>) -> Result<Option<ExecutionPlan>> {
(**self).execution_plan(ctx).await
}
fn llm_hint(&self) -> Option<&'static str> {
(**self).llm_hint()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fs::{FileSystem, InMemoryFs};
#[test]
fn test_resolve_path_absolute() {
let cwd = PathBuf::from("/home/user");
let result = resolve_path(&cwd, "/tmp/file.txt");
assert_eq!(result, PathBuf::from("/tmp/file.txt"));
}
#[test]
fn test_resolve_path_relative() {
let cwd = PathBuf::from("/home/user");
let result = resolve_path(&cwd, "downloads/file.txt");
assert_eq!(result, PathBuf::from("/home/user/downloads/file.txt"));
}
#[test]
fn test_resolve_path_dot_from_root() {
let cwd = PathBuf::from("/");
let result = resolve_path(&cwd, ".");
assert_eq!(result, PathBuf::from("/"));
}
#[test]
fn test_resolve_path_dot_from_normal_dir() {
let cwd = PathBuf::from("/home/user");
let result = resolve_path(&cwd, ".");
assert_eq!(result, PathBuf::from("/home/user"));
}
#[test]
fn test_resolve_path_dotdot() {
let cwd = PathBuf::from("/home/user");
let result = resolve_path(&cwd, "..");
assert_eq!(result, PathBuf::from("/home"));
}
#[test]
fn test_resolve_path_dotdot_from_root() {
let cwd = PathBuf::from("/");
let result = resolve_path(&cwd, "..");
assert_eq!(result, PathBuf::from("/"));
}
#[test]
fn test_resolve_path_complex() {
let cwd = PathBuf::from("/home/user");
let result = resolve_path(&cwd, "./downloads/../documents/./file.txt");
assert_eq!(result, PathBuf::from("/home/user/documents/file.txt"));
}
#[tokio::test]
async fn read_text_file_returns_lossy_utf8() {
let fs = InMemoryFs::new();
fs.write_file(Path::new("/tmp/data.bin"), b"hi\xffthere")
.await
.unwrap();
let text = read_text_file(&fs, Path::new("/tmp/data.bin"), "cat")
.await
.unwrap();
assert_eq!(text, "hi\u{fffd}there");
}
#[tokio::test]
async fn read_text_file_formats_missing_file_errors() {
let fs = InMemoryFs::new();
let err = read_text_file(&fs, Path::new("/tmp/missing.txt"), "cat")
.await
.unwrap_err();
assert_eq!(err.exit_code, 1);
assert!(err.stderr.contains("cat: /tmp/missing.txt:"));
}
}