use async_trait::async_trait;
use std::path::Path;
use super::{Builtin, Context};
use crate::error::Result;
use crate::interpreter::ExecResult;
pub struct Basename;
#[async_trait]
impl Builtin for Basename {
async fn execute(&self, ctx: Context<'_>) -> Result<ExecResult> {
if let Some(r) = super::check_help_version(
ctx.args,
"Usage: basename NAME [SUFFIX]\nPrint NAME with leading directory components removed.\nIf SUFFIX is specified, also remove a trailing SUFFIX.\n\n --help\tdisplay this help and exit\n --version\toutput version information and exit\n",
Some("basename (bashkit) 0.1"),
) {
return Ok(r);
}
if ctx.args.is_empty() {
return Ok(ExecResult::err(
"basename: missing operand\n".to_string(),
1,
));
}
let mut output = String::new();
let mut args_iter = ctx.args.iter();
let path_arg = args_iter
.next()
.expect("args_iter.next() valid: guarded by is_empty() check above");
let path = Path::new(path_arg);
let basename = path
.file_name()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_else(|| {
if path_arg == "/" {
"/".to_string()
} else if path_arg.is_empty() {
String::new()
} else {
path_arg.clone()
}
});
let result = if let Some(suffix) = args_iter.next() {
if let Some(stripped) = basename.strip_suffix(suffix.as_str()) {
stripped.to_string()
} else {
basename
}
} else {
basename
};
output.push_str(&result);
output.push('\n');
Ok(ExecResult::ok(output))
}
}
pub struct Dirname;
#[async_trait]
impl Builtin for Dirname {
async fn execute(&self, ctx: Context<'_>) -> Result<ExecResult> {
if let Some(r) = super::check_help_version(
ctx.args,
"Usage: dirname NAME...\nOutput each NAME with its last non-slash component and trailing slashes removed.\nIf NAME contains no slashes, output '.' (current directory).\n\n --help\tdisplay this help and exit\n --version\toutput version information and exit\n",
Some("dirname (bashkit) 0.1"),
) {
return Ok(r);
}
if ctx.args.is_empty() {
return Ok(ExecResult::err("dirname: missing operand\n".to_string(), 1));
}
let mut output = String::new();
for (i, arg) in ctx.args.iter().enumerate() {
if i > 0 {
output.push('\n');
}
let path = Path::new(arg);
let dirname = path
.parent()
.map(|p| {
let s = p.to_string_lossy();
if s.is_empty() {
".".to_string()
} else {
s.to_string()
}
})
.unwrap_or_else(|| {
if arg == "/" {
"/".to_string()
} else {
".".to_string()
}
});
output.push_str(&dirname);
}
output.push('\n');
Ok(ExecResult::ok(output))
}
}
pub struct Realpath;
#[async_trait]
impl Builtin for Realpath {
async fn execute(&self, ctx: Context<'_>) -> Result<ExecResult> {
if let Some(r) = super::check_help_version(
ctx.args,
"Usage: realpath [PATH...]\nPrint the resolved absolute pathname.\n\n --help\tdisplay this help and exit\n --version\toutput version information and exit\n",
Some("realpath (bashkit) 0.1"),
) {
return Ok(r);
}
if ctx.args.is_empty() {
return Ok(ExecResult::err(
"realpath: missing operand\n".to_string(),
1,
));
}
let mut output = String::new();
for arg in ctx.args {
if arg.starts_with('-') {
continue; }
let resolved = super::resolve_path(ctx.cwd, arg);
output.push_str(&resolved.to_string_lossy());
output.push('\n');
}
Ok(ExecResult::ok(output))
}
}
pub struct Readlink;
#[async_trait]
impl Builtin for Readlink {
#[allow(clippy::collapsible_if)]
async fn execute(&self, ctx: Context<'_>) -> Result<ExecResult> {
if let Some(r) = super::check_help_version(
ctx.args,
"Usage: readlink [-f|-m|-e] FILE...\nPrint resolved symbolic links or canonical file names.\n\n -f\tcanonicalize by following every symlink; all but last component must exist\n -m\tcanonicalize without requiring components to exist\n -e\tcanonicalize; all components must exist\n -n\tdo not output the trailing newline\n --help\tdisplay this help and exit\n --version\toutput version information and exit\n",
Some("readlink (bashkit) 0.1"),
) {
return Ok(r);
}
if ctx.args.is_empty() {
return Ok(ExecResult::err(
"readlink: missing operand\n".to_string(),
1,
));
}
let mut mode = ReadlinkMode::Raw;
let mut files: Vec<&str> = Vec::new();
for arg in ctx.args {
match arg.as_str() {
"-f" => mode = ReadlinkMode::Canonicalize,
"-m" => mode = ReadlinkMode::CanonicalizeMissing,
"-e" => mode = ReadlinkMode::CanonicalizeExisting,
"-n" | "-v" | "-q" | "-s" | "--no-newline" => { }
s if s.starts_with('-') && s.len() > 1 && !s.starts_with("--") => {
for ch in s[1..].chars() {
match ch {
'f' => mode = ReadlinkMode::Canonicalize,
'm' => mode = ReadlinkMode::CanonicalizeMissing,
'e' => mode = ReadlinkMode::CanonicalizeExisting,
'n' | 'v' | 'q' | 's' => {}
_ => {
return Ok(ExecResult::err(
format!("readlink: invalid option -- '{}'\n", ch),
1,
));
}
}
}
}
_ => files.push(arg),
}
}
if files.is_empty() {
return Ok(ExecResult::err(
"readlink: missing operand\n".to_string(),
1,
));
}
let mut output = String::new();
let mut exit_code = 0;
for file in &files {
let resolved = super::resolve_path(ctx.cwd, file);
match mode {
ReadlinkMode::Raw => {
match ctx.fs.read_link(&resolved).await {
Ok(target) => {
output.push_str(&target.to_string_lossy());
output.push('\n');
}
Err(_) => {
exit_code = 1;
}
}
}
ReadlinkMode::Canonicalize | ReadlinkMode::CanonicalizeMissing => {
let parent_missing = if mode == ReadlinkMode::Canonicalize {
resolved
.parent()
.filter(|p| !p.as_os_str().is_empty())
.map(|p| ctx.fs.exists(p))
} else {
None
};
if let Some(fut) = parent_missing {
if !fut.await.unwrap_or(false) {
exit_code = 1;
continue;
}
}
output.push_str(&resolved.to_string_lossy());
output.push('\n');
}
ReadlinkMode::CanonicalizeExisting => {
if ctx.fs.exists(&resolved).await.unwrap_or(false) {
output.push_str(&resolved.to_string_lossy());
output.push('\n');
} else {
exit_code = 1;
}
}
}
}
if exit_code != 0 && output.is_empty() {
Ok(ExecResult::err(String::new(), exit_code))
} else if exit_code != 0 {
let mut result = ExecResult::with_code(output, exit_code);
result.exit_code = exit_code;
Ok(result)
} else {
Ok(ExecResult::ok(output))
}
}
}
#[derive(PartialEq)]
enum ReadlinkMode {
Raw,
Canonicalize,
CanonicalizeMissing,
CanonicalizeExisting,
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use crate::fs::InMemoryFs;
async fn run_basename(args: &[&str]) -> ExecResult {
let fs = Arc::new(InMemoryFs::new());
let mut variables = HashMap::new();
let env = HashMap::new();
let mut cwd = PathBuf::from("/");
let args: Vec<String> = args.iter().map(|s| s.to_string()).collect();
let ctx = Context {
args: &args,
env: &env,
variables: &mut variables,
cwd: &mut cwd,
fs,
stdin: None,
#[cfg(feature = "http_client")]
http_client: None,
#[cfg(feature = "git")]
git_client: None,
#[cfg(feature = "ssh")]
ssh_client: None,
shell: None,
};
Basename.execute(ctx).await.unwrap()
}
async fn run_dirname(args: &[&str]) -> ExecResult {
let fs = Arc::new(InMemoryFs::new());
let mut variables = HashMap::new();
let env = HashMap::new();
let mut cwd = PathBuf::from("/");
let args: Vec<String> = args.iter().map(|s| s.to_string()).collect();
let ctx = Context {
args: &args,
env: &env,
variables: &mut variables,
cwd: &mut cwd,
fs,
stdin: None,
#[cfg(feature = "http_client")]
http_client: None,
#[cfg(feature = "git")]
git_client: None,
#[cfg(feature = "ssh")]
ssh_client: None,
shell: None,
};
Dirname.execute(ctx).await.unwrap()
}
#[tokio::test]
async fn test_basename_simple() {
let result = run_basename(&["/usr/bin/sort"]).await;
assert_eq!(result.exit_code, 0);
assert_eq!(result.stdout, "sort\n");
}
#[tokio::test]
async fn test_basename_with_suffix() {
let result = run_basename(&["file.txt", ".txt"]).await;
assert_eq!(result.exit_code, 0);
assert_eq!(result.stdout, "file\n");
}
#[tokio::test]
async fn test_basename_no_suffix_match() {
let result = run_basename(&["file.txt", ".doc"]).await;
assert_eq!(result.exit_code, 0);
assert_eq!(result.stdout, "file.txt\n");
}
#[tokio::test]
async fn test_basename_no_dir() {
let result = run_basename(&["filename"]).await;
assert_eq!(result.exit_code, 0);
assert_eq!(result.stdout, "filename\n");
}
#[tokio::test]
async fn test_basename_trailing_slash() {
let result = run_basename(&["/usr/bin/"]).await;
assert_eq!(result.exit_code, 0);
assert_eq!(result.stdout, "bin\n");
}
#[tokio::test]
async fn test_basename_missing_operand() {
let result = run_basename(&[]).await;
assert_eq!(result.exit_code, 1);
assert!(result.stderr.contains("missing operand"));
}
#[tokio::test]
async fn test_dirname_simple() {
let result = run_dirname(&["/usr/bin/sort"]).await;
assert_eq!(result.exit_code, 0);
assert_eq!(result.stdout, "/usr/bin\n");
}
#[tokio::test]
async fn test_dirname_no_dir() {
let result = run_dirname(&["filename"]).await;
assert_eq!(result.exit_code, 0);
assert_eq!(result.stdout, ".\n");
}
#[tokio::test]
async fn test_dirname_root() {
let result = run_dirname(&["/"]).await;
assert_eq!(result.exit_code, 0);
assert_eq!(result.stdout, "/\n");
}
#[tokio::test]
async fn test_dirname_trailing_slash() {
let result = run_dirname(&["/usr/bin/"]).await;
assert_eq!(result.exit_code, 0);
assert_eq!(result.stdout, "/usr\n");
}
#[tokio::test]
async fn test_dirname_missing_operand() {
let result = run_dirname(&[]).await;
assert_eq!(result.exit_code, 1);
assert!(result.stderr.contains("missing operand"));
}
use crate::fs::FileSystem;
async fn run_readlink_with_fs(args: &[&str], fs: Arc<dyn FileSystem>) -> ExecResult {
let mut variables = HashMap::new();
let env = HashMap::new();
let mut cwd = PathBuf::from("/");
let args: Vec<String> = args.iter().map(|s| s.to_string()).collect();
let ctx = Context {
args: &args,
env: &env,
variables: &mut variables,
cwd: &mut cwd,
fs,
stdin: None,
#[cfg(feature = "http_client")]
http_client: None,
#[cfg(feature = "git")]
git_client: None,
#[cfg(feature = "ssh")]
ssh_client: None,
shell: None,
};
Readlink.execute(ctx).await.unwrap()
}
#[tokio::test]
async fn test_readlink_missing_operand() {
let fs = Arc::new(InMemoryFs::new()) as Arc<dyn FileSystem>;
let result = run_readlink_with_fs(&[], fs).await;
assert_eq!(result.exit_code, 1);
assert!(result.stderr.contains("missing operand"));
}
#[tokio::test]
async fn test_readlink_raw_symlink() {
let fs = Arc::new(InMemoryFs::new()) as Arc<dyn FileSystem>;
fs.symlink(Path::new("/target"), Path::new("/link"))
.await
.unwrap();
let result = run_readlink_with_fs(&["/link"], fs).await;
assert_eq!(result.exit_code, 0);
assert_eq!(result.stdout, "/target\n");
}
#[tokio::test]
async fn test_readlink_raw_not_symlink() {
let fs = Arc::new(InMemoryFs::new()) as Arc<dyn FileSystem>;
fs.write_file(Path::new("/file"), b"data").await.unwrap(); let result = run_readlink_with_fs(&["/file"], fs).await;
assert_eq!(result.exit_code, 1);
assert!(result.stdout.is_empty());
}
#[tokio::test]
async fn test_readlink_raw_nonexistent() {
let fs = Arc::new(InMemoryFs::new()) as Arc<dyn FileSystem>;
let result = run_readlink_with_fs(&["/nonexistent"], fs).await;
assert_eq!(result.exit_code, 1);
}
#[tokio::test]
async fn test_readlink_f_canonicalize() {
let fs = Arc::new(InMemoryFs::new()) as Arc<dyn FileSystem>;
fs.mkdir(Path::new("/home"), true).await.unwrap();
fs.mkdir(Path::new("/home/user"), true).await.unwrap();
let result = run_readlink_with_fs(&["-f", "/home/user/../user/./file"], fs).await;
assert_eq!(result.exit_code, 0);
assert_eq!(result.stdout, "/home/user/file\n");
}
#[tokio::test]
async fn test_readlink_m_canonicalize_missing() {
let fs = Arc::new(InMemoryFs::new()) as Arc<dyn FileSystem>;
let result = run_readlink_with_fs(&["-m", "/a/b/../c"], fs).await;
assert_eq!(result.exit_code, 0);
assert_eq!(result.stdout, "/a/c\n");
}
#[tokio::test]
async fn test_readlink_e_existing() {
let fs = Arc::new(InMemoryFs::new()) as Arc<dyn FileSystem>;
fs.mkdir(Path::new("/existing"), false).await.unwrap();
let result = run_readlink_with_fs(&["-e", "/existing"], fs).await;
assert_eq!(result.exit_code, 0);
assert_eq!(result.stdout, "/existing\n");
}
#[tokio::test]
async fn test_readlink_e_nonexistent() {
let fs = Arc::new(InMemoryFs::new()) as Arc<dyn FileSystem>;
let result = run_readlink_with_fs(&["-e", "/nonexistent"], fs).await;
assert_eq!(result.exit_code, 1);
assert!(result.stdout.is_empty());
}
#[tokio::test]
async fn test_readlink_invalid_option() {
let fs = Arc::new(InMemoryFs::new()) as Arc<dyn FileSystem>;
let result = run_readlink_with_fs(&["-z", "/file"], fs).await;
assert_eq!(result.exit_code, 1);
assert!(result.stderr.contains("invalid option"));
}
}