Add string/binary type color to ByteStream (#12897)

# Description

This PR allows byte streams to optionally be colored as being
specifically binary or string data, which guarantees that they'll be
converted to `Binary` or `String` appropriately on `into_value()`,
making them compatible with `Type` guarantees. This makes them
significantly more broadly usable for command input and output.

There is still an `Unknown` type for byte streams coming from external
commands, which uses the same behavior as we previously did where it's a
string if it's UTF-8.

A small number of commands were updated to take advantage of this, just
to prove the point. I will be adding more after this merges.

# User-Facing Changes
- New types in `describe`: `string (stream)`, `binary (stream)`
- These commands now return a stream if their input was a stream:
  - `into binary`
  - `into string`
  - `bytes collect`
  - `str join`
  - `first` (binary)
  - `last` (binary)
  - `take` (binary)
  - `skip` (binary)
- Streams that are explicitly binary colored will print as a streaming
hexdump
  - example:
    ```nushell
    1.. | each { into binary } | bytes collect
    ```

# Tests + Formatting
I've added some tests to cover it at a basic level, and it doesn't break
anything existing, but I do think more would be nice. Some of those will
come when I modify more commands to stream.

# After Submitting
There are a few things I'm not quite satisfied with:

- **String trimming behavior.** We automatically trim newlines from
streams from external commands, but I don't think we should do this with
internal commands. If I call a command that happens to turn my string
into a stream, I don't want the newline to suddenly disappear. I changed
this to specifically do it only on `Child` and `File`, but I don't know
if this is quite right, and maybe we should bring back the old flag for
`trim_end_newline`
- **Known binary always resulting in a hexdump.** It would be nice to
have a `print --raw`, so that we can put binary data on stdout
explicitly if we want to. This PR doesn't change how external commands
work though - they still dump straight to stdout.

Otherwise, here's the normal checklist:

- [ ] release notes
- [ ] docs update for plugin protocol changes (added `type` field)

---------

Co-authored-by: Ian Manske <ian.manske@pm.me>
This commit is contained in:
Devyn Cairns
2024-05-19 17:35:32 -07:00
committed by GitHub
parent baeba19b22
commit c61075e20e
42 changed files with 1107 additions and 416 deletions

View File

@ -1,3 +1,4 @@
use itertools::Itertools;
use nu_engine::command_prelude::*;
#[derive(Clone, Copy)]
@ -35,46 +36,33 @@ impl Command for BytesCollect {
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let separator: Option<Vec<u8>> = call.opt(engine_state, stack, 0)?;
let span = call.head;
// input should be a list of binary data.
let mut output_binary = vec![];
for value in input {
match value {
Value::Binary { mut val, .. } => {
output_binary.append(&mut val);
// manually concat
// TODO: make use of std::slice::Join when it's available in stable.
if let Some(sep) = &separator {
let mut work_sep = sep.clone();
output_binary.append(&mut work_sep)
}
}
// Explicitly propagate errors instead of dropping them.
Value::Error { error, .. } => return Err(*error),
other => {
return Err(ShellError::OnlySupportsThisInputType {
let metadata = input.metadata();
let iter = Itertools::intersperse(
input.into_iter_strict(span)?.map(move |value| {
// Everything is wrapped in Some in case there's a separator, so we can flatten
Some(match value {
// Explicitly propagate errors instead of dropping them.
Value::Error { error, .. } => Err(*error),
Value::Binary { val, .. } => Ok(val),
other => Err(ShellError::OnlySupportsThisInputType {
exp_input_type: "binary".into(),
wrong_type: other.get_type().to_string(),
dst_span: call.head,
dst_span: span,
src_span: other.span(),
});
}
}
}
}),
})
}),
Ok(separator).transpose(),
)
.flatten();
match separator {
None => Ok(Value::binary(output_binary, call.head).into_pipeline_data()),
Some(sep) => {
if output_binary.is_empty() {
Ok(Value::binary(output_binary, call.head).into_pipeline_data())
} else {
// have push one extra separator in previous step, pop them out.
for _ in sep {
let _ = output_binary.pop();
}
Ok(Value::binary(output_binary, call.head).into_pipeline_data())
}
}
}
let output = ByteStream::from_result_iter(iter, span, None, ByteStreamType::Binary);
Ok(PipelineData::ByteStream(output, metadata))
}
fn examples(&self) -> Vec<Example> {

View File

@ -127,15 +127,18 @@ fn into_binary(
let cell_paths = call.rest(engine_state, stack, 0)?;
let cell_paths = (!cell_paths.is_empty()).then_some(cell_paths);
if let PipelineData::ByteStream(stream, ..) = input {
// TODO: in the future, we may want this to stream out, converting each to bytes
Ok(Value::binary(stream.into_bytes()?, head).into_pipeline_data())
if let PipelineData::ByteStream(stream, metadata) = input {
// Just set the type - that should be good enough
Ok(PipelineData::ByteStream(
stream.with_type(ByteStreamType::Binary),
metadata,
))
} else {
let args = Arguments {
cell_paths,
compact: call.has_flag(engine_state, stack, "compact")?,
};
operate(action, args, input, call.head, engine_state.ctrlc.clone())
operate(action, args, input, head, engine_state.ctrlc.clone())
}
}

View File

@ -103,7 +103,7 @@ fn into_cell_path(call: &Call, input: PipelineData) -> Result<PipelineData, Shel
}
PipelineData::ByteStream(stream, ..) => Err(ShellError::OnlySupportsThisInputType {
exp_input_type: "list, int".into(),
wrong_type: "byte stream".into(),
wrong_type: stream.type_().describe().into(),
dst_span: head,
src_span: stream.span(),
}),

View File

@ -156,9 +156,23 @@ fn string_helper(
let cell_paths = call.rest(engine_state, stack, 0)?;
let cell_paths = (!cell_paths.is_empty()).then_some(cell_paths);
if let PipelineData::ByteStream(stream, ..) = input {
// TODO: in the future, we may want this to stream out, converting each to bytes
Ok(Value::string(stream.into_string()?, head).into_pipeline_data())
if let PipelineData::ByteStream(stream, metadata) = input {
// Just set the type - that should be good enough. There is no guarantee that the data
// within a string stream is actually valid UTF-8. But refuse to do it if it was already set
// to binary
if stream.type_() != ByteStreamType::Binary {
Ok(PipelineData::ByteStream(
stream.with_type(ByteStreamType::String),
metadata,
))
} else {
Err(ShellError::CantConvert {
to_type: "string".into(),
from_type: "binary".into(),
span: stream.span(),
help: Some("try using the `decode` command".into()),
})
}
} else {
let config = engine_state.get_config().clone();
let args = Arguments {

View File

@ -135,7 +135,7 @@ fn drop_cols(
PipelineData::Empty => Ok(PipelineData::Empty),
PipelineData::ByteStream(stream, ..) => Err(ShellError::OnlySupportsThisInputType {
exp_input_type: "table or record".into(),
wrong_type: "byte stream".into(),
wrong_type: stream.type_().describe().into(),
dst_span: head,
src_span: stream.span(),
}),

View File

@ -170,12 +170,43 @@ fn first_helper(
))
}
}
PipelineData::ByteStream(stream, ..) => Err(ShellError::OnlySupportsThisInputType {
exp_input_type: "list, binary or range".into(),
wrong_type: "byte stream".into(),
dst_span: head,
src_span: stream.span(),
}),
PipelineData::ByteStream(stream, metadata) => {
if stream.type_() == ByteStreamType::Binary {
let span = stream.span();
if let Some(mut reader) = stream.reader() {
use std::io::Read;
if return_single_element {
// Take a single byte
let mut byte = [0u8];
if reader.read(&mut byte).err_span(span)? > 0 {
Ok(Value::int(byte[0] as i64, head).into_pipeline_data())
} else {
Err(ShellError::AccessEmptyContent { span: head })
}
} else {
// Just take 'rows' bytes off the stream, mimicking the binary behavior
Ok(PipelineData::ByteStream(
ByteStream::read(
reader.take(rows as u64),
head,
None,
ByteStreamType::Binary,
),
metadata,
))
}
} else {
Ok(PipelineData::Empty)
}
} else {
Err(ShellError::OnlySupportsThisInputType {
exp_input_type: "list, binary or range".into(),
wrong_type: stream.type_().describe().into(),
dst_span: head,
src_span: stream.span(),
})
}
}
PipelineData::Empty => Err(ShellError::OnlySupportsThisInputType {
exp_input_type: "list, binary or range".into(),
wrong_type: "null".into(),

View File

@ -261,8 +261,8 @@ fn insert(
type_name: "empty pipeline".to_string(),
span: head,
}),
PipelineData::ByteStream(..) => Err(ShellError::IncompatiblePathAccess {
type_name: "byte stream".to_string(),
PipelineData::ByteStream(stream, ..) => Err(ShellError::IncompatiblePathAccess {
type_name: stream.type_().describe().into(),
span: head,
}),
}

View File

@ -86,7 +86,7 @@ impl Command for Items {
}),
PipelineData::ByteStream(stream, ..) => Err(ShellError::OnlySupportsThisInputType {
exp_input_type: "record".into(),
wrong_type: "byte stream".into(),
wrong_type: stream.type_().describe().into(),
dst_span: call.head,
src_span: stream.span(),
}),

View File

@ -160,12 +160,48 @@ impl Command for Last {
}),
}
}
PipelineData::ByteStream(stream, ..) => Err(ShellError::OnlySupportsThisInputType {
exp_input_type: "list, binary or range".into(),
wrong_type: "byte stream".into(),
dst_span: head,
src_span: stream.span(),
}),
PipelineData::ByteStream(stream, ..) => {
if stream.type_() == ByteStreamType::Binary {
let span = stream.span();
if let Some(mut reader) = stream.reader() {
use std::io::Read;
// Have to be a bit tricky here, but just consume into a VecDeque that we
// shrink to fit each time
const TAKE: u64 = 8192;
let mut buf = VecDeque::with_capacity(rows + TAKE as usize);
loop {
let taken = std::io::copy(&mut (&mut reader).take(TAKE), &mut buf)
.err_span(span)?;
if buf.len() > rows {
buf.drain(..(buf.len() - rows));
}
if taken < TAKE {
// This must be EOF.
if return_single_element {
if !buf.is_empty() {
return Ok(
Value::int(buf[0] as i64, head).into_pipeline_data()
);
} else {
return Err(ShellError::AccessEmptyContent { span: head });
}
} else {
return Ok(Value::binary(buf, head).into_pipeline_data());
}
}
}
} else {
Ok(PipelineData::Empty)
}
} else {
Err(ShellError::OnlySupportsThisInputType {
exp_input_type: "list, binary or range".into(),
wrong_type: stream.type_().describe().into(),
dst_span: head,
src_span: stream.span(),
})
}
}
PipelineData::Empty => Err(ShellError::OnlySupportsThisInputType {
exp_input_type: "list, binary or range".into(),
wrong_type: "null".into(),

View File

@ -12,6 +12,7 @@ impl Command for Skip {
Signature::build(self.name())
.input_output_types(vec![
(Type::table(), Type::table()),
(Type::Binary, Type::Binary),
(
Type::List(Box::new(Type::Any)),
Type::List(Box::new(Type::Any)),
@ -51,6 +52,11 @@ impl Command for Skip {
"editions" => Value::test_int(2021),
})])),
},
Example {
description: "Skip 2 bytes of a binary value",
example: "0x[01 23 45 67] | skip 2",
result: Some(Value::test_binary(vec![0x45, 0x67])),
},
]
}
fn run(
@ -87,12 +93,30 @@ impl Command for Skip {
let ctrlc = engine_state.ctrlc.clone();
let input_span = input.span().unwrap_or(call.head);
match input {
PipelineData::ByteStream(stream, ..) => Err(ShellError::OnlySupportsThisInputType {
exp_input_type: "list, binary or range".into(),
wrong_type: "byte stream".into(),
dst_span: call.head,
src_span: stream.span(),
}),
PipelineData::ByteStream(stream, metadata) => {
if stream.type_() == ByteStreamType::Binary {
let span = stream.span();
if let Some(mut reader) = stream.reader() {
use std::io::Read;
// Copy the number of skipped bytes into the sink before proceeding
std::io::copy(&mut (&mut reader).take(n as u64), &mut std::io::sink())
.err_span(span)?;
Ok(PipelineData::ByteStream(
ByteStream::read(reader, call.head, None, ByteStreamType::Binary),
metadata,
))
} else {
Ok(PipelineData::Empty)
}
} else {
Err(ShellError::OnlySupportsThisInputType {
exp_input_type: "list, binary or range".into(),
wrong_type: stream.type_().describe().into(),
dst_span: call.head,
src_span: stream.span(),
})
}
}
PipelineData::Value(Value::Binary { val, .. }, metadata) => {
let bytes = val.into_iter().skip(n).collect::<Vec<_>>();
Ok(Value::binary(bytes, input_span).into_pipeline_data_with_metadata(metadata))

View File

@ -78,12 +78,32 @@ impl Command for Take {
stream.modify(|iter| iter.take(rows_desired)),
metadata,
)),
PipelineData::ByteStream(stream, ..) => Err(ShellError::OnlySupportsThisInputType {
exp_input_type: "list, binary or range".into(),
wrong_type: "byte stream".into(),
dst_span: head,
src_span: stream.span(),
}),
PipelineData::ByteStream(stream, metadata) => {
if stream.type_() == ByteStreamType::Binary {
if let Some(reader) = stream.reader() {
use std::io::Read;
// Just take 'rows' bytes off the stream, mimicking the binary behavior
Ok(PipelineData::ByteStream(
ByteStream::read(
reader.take(rows_desired as u64),
head,
None,
ByteStreamType::Binary,
),
metadata,
))
} else {
Ok(PipelineData::Empty)
}
} else {
Err(ShellError::OnlySupportsThisInputType {
exp_input_type: "list, binary or range".into(),
wrong_type: stream.type_().describe().into(),
dst_span: head,
src_span: stream.span(),
})
}
}
PipelineData::Empty => Err(ShellError::OnlySupportsThisInputType {
exp_input_type: "list, binary or range".into(),
wrong_type: "null".into(),

View File

@ -1,7 +1,7 @@
use nu_engine::{command_prelude::*, get_eval_block_with_early_return};
use nu_protocol::{
byte_stream::copy_with_interrupt, engine::Closure, process::ChildPipe, ByteStream,
ByteStreamSource, OutDest,
ByteStreamSource, OutDest, PipelineMetadata,
};
use std::{
io::{self, Read, Write},
@ -104,9 +104,13 @@ use it in your pipeline."#
if let PipelineData::ByteStream(stream, metadata) = input {
let span = stream.span();
let ctrlc = engine_state.ctrlc.clone();
let eval_block = {
let metadata = metadata.clone();
move |stream| eval_block(PipelineData::ByteStream(stream, metadata))
let type_ = stream.type_();
let info = StreamInfo {
span,
ctrlc: ctrlc.clone(),
type_,
metadata: metadata.clone(),
};
match stream.into_source() {
@ -115,10 +119,11 @@ use it in your pipeline."#
return stderr_misuse(span, head);
}
let tee = IoTee::new(read, span, eval_block)?;
let tee_thread = spawn_tee(info, eval_block)?;
let tee = IoTee::new(read, tee_thread);
Ok(PipelineData::ByteStream(
ByteStream::read(tee, span, ctrlc),
ByteStream::read(tee, span, ctrlc, type_),
metadata,
))
}
@ -127,44 +132,32 @@ use it in your pipeline."#
return stderr_misuse(span, head);
}
let tee = IoTee::new(file, span, eval_block)?;
let tee_thread = spawn_tee(info, eval_block)?;
let tee = IoTee::new(file, tee_thread);
Ok(PipelineData::ByteStream(
ByteStream::read(tee, span, ctrlc),
ByteStream::read(tee, span, ctrlc, type_),
metadata,
))
}
ByteStreamSource::Child(mut child) => {
let stderr_thread = if use_stderr {
let stderr_thread = if let Some(stderr) = child.stderr.take() {
let tee_thread = spawn_tee(info.clone(), eval_block)?;
let tee = IoTee::new(stderr, tee_thread);
match stack.stderr() {
OutDest::Pipe | OutDest::Capture => {
let tee = IoTee::new(stderr, span, eval_block)?;
child.stderr = Some(ChildPipe::Tee(Box::new(tee)));
None
Ok(None)
}
OutDest::Null => Some(tee_pipe_on_thread(
stderr,
io::sink(),
span,
ctrlc.as_ref(),
eval_block,
)?),
OutDest::Inherit => Some(tee_pipe_on_thread(
stderr,
io::stderr(),
span,
ctrlc.as_ref(),
eval_block,
)?),
OutDest::File(file) => Some(tee_pipe_on_thread(
stderr,
file.clone(),
span,
ctrlc.as_ref(),
eval_block,
)?),
}
OutDest::Null => copy_on_thread(tee, io::sink(), &info).map(Some),
OutDest::Inherit => {
copy_on_thread(tee, io::stderr(), &info).map(Some)
}
OutDest::File(file) => {
copy_on_thread(tee, file.clone(), &info).map(Some)
}
}?
} else {
None
};
@ -175,37 +168,29 @@ use it in your pipeline."#
child.stdout = Some(stdout);
Ok(())
}
OutDest::Null => {
copy_pipe(stdout, io::sink(), span, ctrlc.as_deref())
}
OutDest::Inherit => {
copy_pipe(stdout, io::stdout(), span, ctrlc.as_deref())
}
OutDest::File(file) => {
copy_pipe(stdout, file.as_ref(), span, ctrlc.as_deref())
}
OutDest::Null => copy_pipe(stdout, io::sink(), &info),
OutDest::Inherit => copy_pipe(stdout, io::stdout(), &info),
OutDest::File(file) => copy_pipe(stdout, file.as_ref(), &info),
}?;
}
stderr_thread
} else {
let stderr_thread = if let Some(stderr) = child.stderr.take() {
let info = info.clone();
match stack.stderr() {
OutDest::Pipe | OutDest::Capture => {
child.stderr = Some(stderr);
Ok(None)
}
OutDest::Null => {
copy_pipe_on_thread(stderr, io::sink(), span, ctrlc.as_ref())
.map(Some)
copy_pipe_on_thread(stderr, io::sink(), &info).map(Some)
}
OutDest::Inherit => {
copy_pipe_on_thread(stderr, io::stderr(), span, ctrlc.as_ref())
.map(Some)
copy_pipe_on_thread(stderr, io::stderr(), &info).map(Some)
}
OutDest::File(file) => {
copy_pipe_on_thread(stderr, file.clone(), span, ctrlc.as_ref())
.map(Some)
copy_pipe_on_thread(stderr, file.clone(), &info).map(Some)
}
}?
} else {
@ -213,29 +198,16 @@ use it in your pipeline."#
};
if let Some(stdout) = child.stdout.take() {
let tee_thread = spawn_tee(info.clone(), eval_block)?;
let tee = IoTee::new(stdout, tee_thread);
match stack.stdout() {
OutDest::Pipe | OutDest::Capture => {
let tee = IoTee::new(stdout, span, eval_block)?;
child.stdout = Some(ChildPipe::Tee(Box::new(tee)));
Ok(())
}
OutDest::Null => {
tee_pipe(stdout, io::sink(), span, ctrlc.as_deref(), eval_block)
}
OutDest::Inherit => tee_pipe(
stdout,
io::stdout(),
span,
ctrlc.as_deref(),
eval_block,
),
OutDest::File(file) => tee_pipe(
stdout,
file.as_ref(),
span,
ctrlc.as_deref(),
eval_block,
),
OutDest::Null => copy(tee, io::sink(), &info),
OutDest::Inherit => copy(tee, io::stdout(), &info),
OutDest::File(file) => copy(tee, file.as_ref(), &info),
}?;
}
@ -350,7 +322,7 @@ where
fn stderr_misuse<T>(span: Span, head: Span) -> Result<T, ShellError> {
Err(ShellError::UnsupportedInput {
msg: "--stderr can only be used on external commands".into(),
input: "the input to `tee` is not an external commands".into(),
input: "the input to `tee` is not an external command".into(),
msg_span: head,
input_span: span,
})
@ -363,23 +335,12 @@ struct IoTee<R: Read> {
}
impl<R: Read> IoTee<R> {
fn new(
reader: R,
span: Span,
eval_block: impl FnOnce(ByteStream) -> Result<(), ShellError> + Send + 'static,
) -> Result<Self, ShellError> {
let (sender, receiver) = mpsc::channel();
let thread = thread::Builder::new()
.name("tee".into())
.spawn(move || eval_block(ByteStream::from_iter(receiver, span, None)))
.err_span(span)?;
Ok(Self {
fn new(reader: R, tee: TeeThread) -> Self {
Self {
reader,
sender: Some(sender),
thread: Some(thread),
})
sender: Some(tee.sender),
thread: Some(tee.thread),
}
}
}
@ -411,68 +372,74 @@ impl<R: Read> Read for IoTee<R> {
}
}
fn tee_pipe(
pipe: ChildPipe,
mut dest: impl Write,
struct TeeThread {
sender: Sender<Vec<u8>>,
thread: JoinHandle<Result<(), ShellError>>,
}
fn spawn_tee(
info: StreamInfo,
mut eval_block: impl FnMut(PipelineData) -> Result<(), ShellError> + Send + 'static,
) -> Result<TeeThread, ShellError> {
let (sender, receiver) = mpsc::channel();
let thread = thread::Builder::new()
.name("tee".into())
.spawn(move || {
// We don't use ctrlc here because we assume it already has it on the other side
let stream = ByteStream::from_iter(receiver.into_iter(), info.span, None, info.type_);
eval_block(PipelineData::ByteStream(stream, info.metadata))
})
.err_span(info.span)?;
Ok(TeeThread { sender, thread })
}
#[derive(Clone)]
struct StreamInfo {
span: Span,
ctrlc: Option<&AtomicBool>,
eval_block: impl FnOnce(ByteStream) -> Result<(), ShellError> + Send + 'static,
) -> Result<(), ShellError> {
match pipe {
ChildPipe::Pipe(pipe) => {
let mut tee = IoTee::new(pipe, span, eval_block)?;
copy_with_interrupt(&mut tee, &mut dest, span, ctrlc)?;
}
ChildPipe::Tee(tee) => {
let mut tee = IoTee::new(tee, span, eval_block)?;
copy_with_interrupt(&mut tee, &mut dest, span, ctrlc)?;
}
}
ctrlc: Option<Arc<AtomicBool>>,
type_: ByteStreamType,
metadata: Option<PipelineMetadata>,
}
fn copy(mut src: impl Read, mut dest: impl Write, info: &StreamInfo) -> Result<(), ShellError> {
copy_with_interrupt(&mut src, &mut dest, info.span, info.ctrlc.as_deref())?;
Ok(())
}
fn tee_pipe_on_thread(
pipe: ChildPipe,
dest: impl Write + Send + 'static,
span: Span,
ctrlc: Option<&Arc<AtomicBool>>,
eval_block: impl FnOnce(ByteStream) -> Result<(), ShellError> + Send + 'static,
fn copy_pipe(pipe: ChildPipe, dest: impl Write, info: &StreamInfo) -> Result<(), ShellError> {
match pipe {
ChildPipe::Pipe(pipe) => copy(pipe, dest, info),
ChildPipe::Tee(tee) => copy(tee, dest, info),
}
}
fn copy_on_thread(
mut src: impl Read + Send + 'static,
mut dest: impl Write + Send + 'static,
info: &StreamInfo,
) -> Result<JoinHandle<Result<(), ShellError>>, ShellError> {
let ctrlc = ctrlc.cloned();
let span = info.span;
let ctrlc = info.ctrlc.clone();
thread::Builder::new()
.name("stderr tee".into())
.spawn(move || tee_pipe(pipe, dest, span, ctrlc.as_deref(), eval_block))
.name("stderr copier".into())
.spawn(move || {
copy_with_interrupt(&mut src, &mut dest, span, ctrlc.as_deref())?;
Ok(())
})
.map_err(|e| e.into_spanned(span).into())
}
fn copy_pipe(
pipe: ChildPipe,
mut dest: impl Write,
span: Span,
ctrlc: Option<&AtomicBool>,
) -> Result<(), ShellError> {
match pipe {
ChildPipe::Pipe(mut pipe) => {
copy_with_interrupt(&mut pipe, &mut dest, span, ctrlc)?;
}
ChildPipe::Tee(mut tee) => {
copy_with_interrupt(&mut tee, &mut dest, span, ctrlc)?;
}
}
Ok(())
}
fn copy_pipe_on_thread(
pipe: ChildPipe,
dest: impl Write + Send + 'static,
span: Span,
ctrlc: Option<&Arc<AtomicBool>>,
info: &StreamInfo,
) -> Result<JoinHandle<Result<(), ShellError>>, ShellError> {
let ctrlc = ctrlc.cloned();
thread::Builder::new()
.name("stderr copier".into())
.spawn(move || copy_pipe(pipe, dest, span, ctrlc.as_deref()))
.map_err(|e| e.into_spanned(span).into())
match pipe {
ChildPipe::Pipe(pipe) => copy_on_thread(pipe, dest, info),
ChildPipe::Tee(tee) => copy_on_thread(tee, dest, info),
}
}
#[test]

View File

@ -225,8 +225,8 @@ fn update(
type_name: "empty pipeline".to_string(),
span: head,
}),
PipelineData::ByteStream(..) => Err(ShellError::IncompatiblePathAccess {
type_name: "byte stream".to_string(),
PipelineData::ByteStream(stream, ..) => Err(ShellError::IncompatiblePathAccess {
type_name: stream.type_().describe().into(),
span: head,
}),
}

View File

@ -285,8 +285,8 @@ fn upsert(
type_name: "empty pipeline".to_string(),
span: head,
}),
PipelineData::ByteStream(..) => Err(ShellError::IncompatiblePathAccess {
type_name: "byte stream".to_string(),
PipelineData::ByteStream(stream, ..) => Err(ShellError::IncompatiblePathAccess {
type_name: stream.type_().describe().into(),
span: head,
}),
}

View File

@ -182,7 +182,7 @@ fn values(
}
PipelineData::ByteStream(stream, ..) => Err(ShellError::OnlySupportsThisInputType {
exp_input_type: "record or table".into(),
wrong_type: "byte stream".into(),
wrong_type: stream.type_().describe().into(),
dst_span: head,
src_span: stream.span(),
}),

View File

@ -51,7 +51,12 @@ impl Command for ToText {
str
});
Ok(PipelineData::ByteStream(
ByteStream::from_iter(iter, span, engine_state.ctrlc.clone()),
ByteStream::from_iter(
iter,
span,
engine_state.ctrlc.clone(),
ByteStreamType::String,
),
meta,
))
}

View File

@ -117,10 +117,20 @@ pub fn response_to_buffer(
_ => None,
};
// Try to guess whether the response is definitely intended to binary or definitely intended to
// be UTF-8 text. Otherwise specify `None` and just guess. This doesn't have to be thorough.
let content_type_lowercase = response.header("content-type").map(|s| s.to_lowercase());
let response_type = match content_type_lowercase.as_deref() {
Some("application/octet-stream") => ByteStreamType::Binary,
Some(h) if h.contains("charset=utf-8") => ByteStreamType::String,
_ => ByteStreamType::Unknown,
};
let reader = response.into_reader();
PipelineData::ByteStream(
ByteStream::read(reader, span, engine_state.ctrlc.clone()).with_known_size(buffer_size),
ByteStream::read(reader, span, engine_state.ctrlc.clone(), response_type)
.with_known_size(buffer_size),
None,
)
}

View File

@ -1,4 +1,5 @@
use nu_engine::command_prelude::*;
use std::io::Write;
#[derive(Clone)]
pub struct StrJoin;
@ -40,31 +41,40 @@ impl Command for StrJoin {
) -> Result<PipelineData, ShellError> {
let separator: Option<String> = call.opt(engine_state, stack, 0)?;
let config = engine_state.get_config();
let config = engine_state.config.clone();
// let output = input.collect_string(&separator.unwrap_or_default(), &config)?;
// Hmm, not sure what we actually want.
// `to_formatted_string` formats dates as human readable which feels funny.
let mut strings: Vec<String> = vec![];
let span = call.head;
for value in input {
let str = match value {
Value::Error { error, .. } => {
return Err(*error);
let metadata = input.metadata();
let mut iter = input.into_iter();
let mut first = true;
let output = ByteStream::from_fn(span, None, ByteStreamType::String, move |buffer| {
// Write each input to the buffer
if let Some(value) = iter.next() {
// Write the separator if this is not the first
if first {
first = false;
} else if let Some(separator) = &separator {
write!(buffer, "{}", separator)?;
}
Value::Date { val, .. } => format!("{val:?}"),
value => value.to_expanded_string("\n", config),
};
strings.push(str);
}
let output = if let Some(separator) = separator {
strings.join(&separator)
} else {
strings.join("")
};
match value {
Value::Error { error, .. } => {
return Err(*error);
}
// Hmm, not sure what we actually want.
// `to_expanded_string` formats dates as human readable which feels funny.
Value::Date { val, .. } => write!(buffer, "{val:?}")?,
value => write!(buffer, "{}", value.to_expanded_string("\n", &config))?,
}
Ok(true)
} else {
Ok(false)
}
});
Ok(Value::string(output, call.head).into_pipeline_data())
Ok(PipelineData::ByteStream(output, metadata))
}
fn examples(&self) -> Vec<Example> {

View File

@ -416,6 +416,7 @@ impl ExternalCommand {
.name("external stdin worker".to_string())
.spawn(move || {
let input = match input {
// Don't touch binary input or byte streams
input @ PipelineData::ByteStream(..) => input,
input @ PipelineData::Value(Value::Binary { .. }, ..) => input,
input => {

View File

@ -5,6 +5,7 @@
use lscolors::{LsColors, Style};
use nu_color_config::{color_from_hex, StyleComputer, TextStyle};
use nu_engine::{command_prelude::*, env::get_config, env_to_string};
use nu_pretty_hex::HexConfig;
use nu_protocol::{
ByteStream, Config, DataSource, ListStream, PipelineMetadata, TableMode, ValueIterator,
};
@ -15,7 +16,7 @@ use nu_table::{
use nu_utils::get_ls_colors;
use std::{
collections::VecDeque,
io::{Cursor, IsTerminal},
io::{IsTerminal, Read},
path::PathBuf,
str::FromStr,
sync::{atomic::AtomicBool, Arc},
@ -364,16 +365,18 @@ fn handle_table_command(
) -> Result<PipelineData, ShellError> {
let span = input.data.span().unwrap_or(input.call.head);
match input.data {
// Binary streams should behave as if they really are `binary` data, and printed as hex
PipelineData::ByteStream(stream, _) if stream.type_() == ByteStreamType::Binary => Ok(
PipelineData::ByteStream(pretty_hex_stream(stream, input.call.head), None),
),
PipelineData::ByteStream(..) => Ok(input.data),
PipelineData::Value(Value::Binary { val, .. }, ..) => {
let bytes = {
let mut str = nu_pretty_hex::pretty_hex(&val);
str.push('\n');
str.into_bytes()
};
let ctrlc = input.engine_state.ctrlc.clone();
let stream = ByteStream::read(Cursor::new(bytes), input.call.head, ctrlc);
Ok(PipelineData::ByteStream(stream, None))
let stream = ByteStream::read_binary(val, input.call.head, ctrlc);
Ok(PipelineData::ByteStream(
pretty_hex_stream(stream, input.call.head),
None,
))
}
// None of these two receive a StyleComputer because handle_row_stream() can produce it by itself using engine_state and stack.
PipelineData::Value(Value::List { vals, .. }, metadata) => {
@ -410,6 +413,70 @@ fn handle_table_command(
}
}
fn pretty_hex_stream(stream: ByteStream, span: Span) -> ByteStream {
let mut cfg = HexConfig {
// We are going to render the title manually first
title: true,
// If building on 32-bit, the stream size might be bigger than a usize
length: stream.known_size().and_then(|sz| sz.try_into().ok()),
..HexConfig::default()
};
// This won't really work for us
debug_assert!(cfg.width > 0, "the default hex config width was zero");
let mut read_buf = Vec::with_capacity(cfg.width);
let mut reader = if let Some(reader) = stream.reader() {
reader
} else {
// No stream to read from
return ByteStream::read_string("".into(), span, None);
};
ByteStream::from_fn(span, None, ByteStreamType::String, move |buffer| {
// Turn the buffer into a String we can write to
let mut write_buf = std::mem::take(buffer);
write_buf.clear();
// SAFETY: we just truncated it empty
let mut write_buf = unsafe { String::from_utf8_unchecked(write_buf) };
// Write the title at the beginning
if cfg.title {
nu_pretty_hex::write_title(&mut write_buf, cfg, true).expect("format error");
cfg.title = false;
// Put the write_buf back into buffer
*buffer = write_buf.into_bytes();
Ok(true)
} else {
// Read up to `cfg.width` bytes
read_buf.clear();
(&mut reader)
.take(cfg.width as u64)
.read_to_end(&mut read_buf)
.err_span(span)?;
if !read_buf.is_empty() {
nu_pretty_hex::hex_write(&mut write_buf, &read_buf, cfg, Some(true))
.expect("format error");
write_buf.push('\n');
// Advance the address offset for next time
cfg.address_offset += read_buf.len();
// Put the write_buf back into buffer
*buffer = write_buf.into_bytes();
Ok(true)
} else {
Ok(false)
}
}
})
}
fn handle_record(
input: CmdInput,
cfg: TableConfig,
@ -608,7 +675,8 @@ fn handle_row_stream(
ctrlc.clone(),
cfg,
);
let stream = ByteStream::from_result_iter(paginator, input.call.head, None);
let stream =
ByteStream::from_result_iter(paginator, input.call.head, None, ByteStreamType::String);
Ok(PipelineData::ByteStream(stream, None))
}