use super::clap_cache::cached_command;
use async_trait::async_trait;
use std::ffi::OsString;
use std::path::Path;
use super::{Builtin, Context, STREAM_CHUNK_BYTES};
use crate::error::Result;
use crate::fs::vfs_join;
use crate::interpreter::ExecResult;
pub struct Cat;
cached_command!(cat_cmd, super::generated::cat_args::cat_command());
#[async_trait]
impl Builtin for Cat {
async fn execute(&self, ctx: Context<'_>) -> Result<ExecResult> {
let argv: Vec<OsString> = std::iter::once(OsString::from("cat"))
.chain(ctx.args.iter().map(OsString::from))
.collect();
let matches = match cat_cmd().try_get_matches_from(argv) {
Ok(m) => m,
Err(e) => {
let kind = e.kind();
let rendered = e.render().to_string();
if matches!(
kind,
clap::error::ErrorKind::DisplayHelp | clap::error::ErrorKind::DisplayVersion
) {
return Ok(ExecResult::ok(rendered));
}
return Ok(ExecResult::err(rendered, 2));
}
};
let g = |k: &str| matches.get_flag(k);
let show_all = g("show-all");
let number_nonblank = g("number-nonblank");
let nonprint_ends = g("e");
let nonprint_tabs = g("t");
let show_ends = g("show-ends") || show_all || nonprint_ends;
let show_tabs = g("show-tabs") || show_all || nonprint_tabs;
let show_nonprinting = g("show-nonprinting") || show_all || nonprint_ends || nonprint_tabs;
let number_all = g("number") && !number_nonblank;
let squeeze = g("squeeze-blank");
let files: Vec<String> = matches
.get_many::<OsString>("file")
.map(|vs| vs.map(|v| v.to_string_lossy().into_owned()).collect())
.unwrap_or_default();
let plain = !(show_ends
|| show_tabs
|| show_nonprinting
|| number_all
|| number_nonblank
|| squeeze);
if plain && let Some(stream) = ctx.stdout_stream() {
return stream_plain(&ctx, &files, &stream).await;
}
let budget = ctx.shell.as_ref().map(|s| s.execution_budget);
let mut raw = crate::limits::BudgetedBytes::new(budget)?;
let mut stderr = String::new();
let mut stdin_used = false;
for file in &files {
if file == "-" {
if let Some(stdin) = ctx.stdin
&& !std::mem::replace(&mut stdin_used, true)
{
raw.try_extend_from_slice(stdin.as_bytes())?;
}
if let Some(input) = ctx.stdin_stream() {
raw.try_extend_from_slice(&input.read_to_end().await?)?;
}
} else {
let path = if Path::new(file).is_absolute() {
file.clone()
} else {
vfs_join(ctx.cwd, file).to_string_lossy().into_owned()
};
match ctx.fs.read_file(Path::new(&path)).await {
Ok(bytes) => raw.try_extend_from_slice(&bytes)?,
Err(e) => {
let reason = crate::error::io_error_reason(&e);
stderr.push_str(&format!("cat: {file}: {reason}\n"));
}
}
}
}
let (raw, _raw_storage) = raw.into_parts();
let mut result = if plain {
ExecResult::ok_bytes(raw)
} else {
ExecResult::ok_bytes(render(
&raw,
show_ends,
show_tabs,
show_nonprinting,
number_all,
number_nonblank,
squeeze,
))
};
if !stderr.is_empty() {
result.stderr = stderr.into();
result.exit_code = 1;
}
Ok(result)
}
}
async fn stream_plain(
ctx: &Context<'_>,
files: &[String],
stream: &super::StdoutStream,
) -> Result<ExecResult> {
async fn send(ctx: &Context<'_>, stream: &super::StdoutStream, data: &[u8]) -> Result<bool> {
for chunk in data.chunks(STREAM_CHUNK_BYTES) {
ctx.consume_budget_work(1)?;
if !stream.write(chunk).await? {
return Ok(false);
}
}
Ok(true)
}
let mut stderr = String::new();
let mut code = 0;
let mut stdin_used = false;
for file in files {
let sent = if file == "-" {
let mut ok = match ctx.stdin {
Some(stdin) if !std::mem::replace(&mut stdin_used, true) => {
send(ctx, stream, stdin.as_bytes()).await?
}
_ => true,
};
if let Some(input) = ctx.stdin_stream() {
while ok {
let chunk = input.read().await?;
if chunk.is_empty() {
break;
}
ok = send(ctx, stream, &chunk).await?;
}
}
ok
} else {
let path = if Path::new(file).is_absolute() {
file.clone()
} else {
vfs_join(ctx.cwd, file).to_string_lossy().into_owned()
};
match ctx.fs.read_file(Path::new(&path)).await {
Ok(bytes) => send(ctx, stream, &bytes).await?,
Err(e) => {
let reason = crate::error::io_error_reason(&e);
stderr.push_str(&format!("cat: {file}: {reason}\n"));
code = 1;
true
}
}
};
if !sent {
return Ok(ExecResult::err(stderr, 141));
}
}
Ok(ExecResult::err(stderr, code))
}
fn render(
raw: &[u8],
show_ends: bool,
show_tabs: bool,
show_nonprinting: bool,
number_all: bool,
number_nonblank: bool,
squeeze: bool,
) -> Vec<u8> {
let mut out = Vec::with_capacity(raw.len());
let mut counter: u64 = 0;
let mut prev_blank = false;
let mut iter = raw.split_inclusive(|byte| *byte == b'\n').peekable();
if iter.peek().is_none() {
return out;
}
for chunk in iter {
let (body, has_newline): (&[u8], bool) = match chunk.strip_suffix(b"\n") {
Some(body) => (body, true),
None => (chunk, false),
};
let is_blank = body.is_empty();
if squeeze && is_blank && prev_blank {
continue;
}
prev_blank = is_blank;
if number_all || (number_nonblank && !is_blank) {
counter += 1;
out.extend_from_slice(format!("{counter:>6}\t").as_bytes());
}
let (body, cr_end) = match body.strip_suffix(b"\r") {
Some(rest) if show_ends && has_newline && !show_nonprinting => (rest, true),
_ => (body, false),
};
if show_nonprinting || show_tabs {
for &b in body {
emit_byte(&mut out, b, show_tabs, show_nonprinting);
}
} else {
out.extend_from_slice(body);
}
if cr_end {
out.extend_from_slice(b"^M");
}
if show_ends && has_newline {
out.push(b'$');
}
if has_newline {
out.push(b'\n');
}
}
out
}
fn emit_byte(out: &mut Vec<u8>, b: u8, show_tabs: bool, show_nonprinting: bool) {
match b {
b'\t' if show_tabs => {
out.extend_from_slice(b"^I");
}
b'\t' => out.push(b'\t'),
b'\n' => out.push(b'\n'),
0..=31 if show_nonprinting => {
out.push(b'^');
out.push(b + 64);
}
0x7f if show_nonprinting => {
out.extend_from_slice(b"^?");
}
128..=255 if show_nonprinting => {
out.extend_from_slice(b"M-");
let low = b & 0x7f;
if (0..32).contains(&low) {
out.push(b'^');
out.push(low + 64);
} else if low == 0x7f {
out.extend_from_slice(b"^?");
} else {
out.push(low);
}
}
_ => out.push(b),
}
}