Merge master

This commit is contained in:
Jonathan Turner 2019-06-22 13:38:17 +12:00
commit baeb192f12
58 changed files with 1743 additions and 732 deletions

View file

@ -0,0 +1,35 @@
variables:
lkg-rust-nightly: "2019-06-16"
trigger:
- master
strategy:
matrix:
linux-nightly:
image: ubuntu-16.04
toolchain: nightly-$(lkg-rust-nightly)
macos-nightly:
image: macos-10.13
toolchain: nightly-$(lkg-rust-nightly)
windows-nightly:
image: vs2017-win2016
toolchain: nightly-$(lkg-rust-nightly)
pool:
vmImage: $(image)
steps:
- bash: |
set -e
curl https://sh.rustup.rs -sSf | sh -s -- -y --no-modify-path --default-toolchain none
export PATH=$HOME/.cargo/bin:$PATH
rustup install --no-self-update $(toolchain)
rustup default $(toolchain)
rustc -Vv
echo "##vso[task.prependpath]$HOME/.cargo/bin"
displayName: Install Rust
- bash: cargo build
displayName: Build source
- bash: cargo test
displayName: Run tests

520
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -11,7 +11,7 @@ edition = "2018"
[dependencies]
#rustyline = "4.1.0"
rustyline = { git ="https://github.com/kkawakam/rustyline.git" }
sysinfo = "0.8.4"
sysinfo = "0.8.5"
chrono = { version = "0.4.6", features = ["serde"] }
chrono-tz = "0.5.1"
derive-new = "0.5.6"
@ -25,7 +25,7 @@ indexmap = { version = "1.0.2", features = ["serde-1"] }
chrono-humanize = "0.0.11"
byte-unit = "2.1.0"
ordered-float = "1.0.2"
prettyprint = "0.6.0"
prettyprint = "0.7.0"
cursive = { version = "0.12.0", features = ["pancurses-backend"], default-features = false }
futures-preview = { version = "0.3.0-alpha.16", features = ["compat", "io-compat"] }
futures-sink-preview = "0.3.0-alpha.16"
@ -37,11 +37,11 @@ log = "0.4.6"
pretty_env_logger = "0.3.0"
lalrpop-util = "0.17.0"
regex = "1.1.6"
serde = "1.0.91"
serde = "1.0.92"
serde_json = "1.0.39"
serde-hjson = "0.9.0"
serde_yaml = "0.8"
serde_derive = "1.0.91"
serde_derive = "1.0.92"
getset = "0.0.7"
logos = "0.10.0-rc2"
logos-derive = "0.10.0-rc2"
@ -53,7 +53,7 @@ clap = "2.33.0"
enum_derive = "0.1.7"
adhoc_derive = "0.1.2"
lazy_static = "1.3.0"
git2 = "0.8.0"
git2 = "0.9.1"
dirs = "2.0.1"
ctrlc = "3.1.3"
ptree = "0.2"
@ -66,14 +66,13 @@ derive_more = "0.15.0"
enum-utils = "0.1.0"
unicode-xid = "0.1.0"
nom-trace = { version = "0.1.0", git = "https://github.com/pythondude325/nom-trace.git" }
serde_ini = "0.2.0"
subprocess = "0.1.18"
sys-info = "0.5.7"
[dependencies.pancurses]
version = "0.16"
features = ["win32a"]
[dependencies.subprocess]
git = "https://github.com/jonathandturner/rust-subprocess.git"
branch = "is_already_escaped"
[dev-dependencies]
pretty_assertions = "0.6.1"

321
README.md
View file

@ -1,24 +1,174 @@
[![Build Status](https://dev.azure.com/nushell/nushell/_apis/build/status/nushell.nushell?branchName=master)](https://dev.azure.com/nushell/nushell/_build/latest?definitionId=2&branchName=master)
# Nu Shell
A shell for the GitHub era. Like having a playground for a shell.
Like having a shell in a playground.
# Status
This project has little of what will eventually be necessary for Nu to serve as your day-to-day shell. It already works well enough for contributors to dogfood it as their daily driver, but there are too many basic deficiencies for it to be useful for most people.
This project is currently in its early stages, though it already works well enough for contributors to dogfood it as their daily driver. Its design is subject to change as it matures.
At the moment, executing a command that isn't identified as a built-in new command will fall back to running it as a shell command (using cmd on Windows or bash on Linux and MacOS), correctly passing through stdin, stdout and stderr, so things like your daily git workflows and even `vim` will work just fine.
Nu has a list of built-in commands (listed below). If a command is unknown, the command will shell-out and execute it (using cmd on Windows or bash on Linux and MacOS), correctly passing through stdin, stdout and stderr, so things like your daily git workflows and even `vim` will work just fine.
## Commands
# Philosophy
Nu draws heavy inspiration from projects like PowerShell. Rather than thinking of you filesystem and services as raw streams of text, Nu looks at each input as something with structure. For example, when you list the contents of a directory, what you get back in a list of objects, where each object represents an item in that directory.
## Pipelines
Nu takes this a step further and builds heavily on the idea of _pipelines_. Just as the Unix philosophy, Nu allows commands to output from stdout and read from stdin. Additionally, commands can output structured data (you can think of this as a third kind of stream). Commands that work in the pipeline fit into one of three categories
* Commands that produce a stream (eg, `ls`)
* Commands that filter a stream (eg, `where "file type" == "Directory"`)
* Commands that consumes the output of the pipeline (eg, `autoview`)
Commands are separated by the pipe symbol (`|`) to denote a pipeline flowing left to right.
```
/home/jonathan/Source/nushell(master)> ls | where "file type" == "Directory" | autoview
-----------+-----------+----------+--------+--------------+----------------
file name | file type | readonly | size | accessed | modified
-----------+-----------+----------+--------+--------------+----------------
target | Directory | | 4.1 KB | 19 hours ago | 19 hours ago
images | Directory | | 4.1 KB | 2 weeks ago | a week ago
tests | Directory | | 4.1 KB | 2 weeks ago | 18 minutes ago
docs | Directory | | 4.1 KB | a week ago | a week ago
.git | Directory | | 4.1 KB | 2 weeks ago | 25 minutes ago
src | Directory | | 4.1 KB | 2 weeks ago | 25 minutes ago
.cargo | Directory | | 4.1 KB | 2 weeks ago | 2 weeks ago
-----------+-----------+----------+--------+--------------+----------------
```
Because most of the time you'll want to see the output of a pipeline, `autoview` is assumed. We could have also written the above:
```
/home/jonathan/Source/nushell(master)> ls | where "file type" == "Directory"
```
Being able to use the same commands and compose them differently is an important philosophy in Nu. For example, we could use the built-in `ps` command as well to get a list of the running processes, using the same `where` as above.
```text
C:\Code\nushell(master)> ps | where cpu > 0
------------------ +-----+-------+-------+----------
name | cmd | cpu | pid | status
------------------ +-----+-------+-------+----------
msedge.exe | - | 0.77 | 26472 | Runnable
nu.exe | - | 7.83 | 15473 | Runnable
SearchIndexer.exe | - | 82.17 | 23476 | Runnable
BlueJeans.exe | - | 4.54 | 10000 | Runnable
-------------------+-----+-------+-------+----------
```
## Opening files
Nu can load file and URL contents as raw text or as structured data (if it recognizes the format). For example, you can load a .toml file as structured data and explore it:
```
/home/jonathan/Source/nushell(master)> open Cargo.toml
-----------------+------------------+-----------------
dependencies | dev-dependencies | package
-----------------+------------------+-----------------
[object Object] | [object Object] | [object Object]
-----------------+------------------+-----------------
```
We can pipeline this into a command that gets the contents of one of the columns:
```
/home/jonathan/Source/nushell(master)> open Cargo.toml | get package
-------------+----------------------------+---------+---------+------+---------
authors | description | edition | license | name | version
-------------+----------------------------+---------+---------+------+---------
[list List] | A shell for the GitHub era | 2018 | MIT | nu | 0.1.2
-------------+----------------------------+---------+---------+------+---------
```
Finally, we can use commands outside of Nu once we have the data we want:
```
/home/jonathan/Source/nushell(master)> open Cargo.toml | get package.version | echo $it
0.1.2
```
Here we use the variable `$it` to refer to the value being piped to the external command.
## Navigation
By default, Nu opens up into your filesystem and the current working directory. One way to think of this is a pair: the current object and the current path in the object. The filesystem is our first object, and the path is the cwd.
| object | path |
| ------ | ---- |
| Filesystem | /home/jonathan/Source/nushell |
Using the `cd` command allows you to change the path from the current path to a new path, just as you might expect. Using `ls` allows you to view the contents of the filesystem at the current path (or at the path of your choosing).
In addition to `cd` and `ls`, we can `enter` an object. Entering an object makes it the current object to navigate (similar to the concept of mounting a filesystem in Unix systems).
```
/home/jonathan/Source/nushell(master)> enter Cargo.toml
object/>
```
As we enter, we create a stack of objects we're navigating:
| object | path |
| ------ | ---- |
| Filesystem | /home/jonathan/Source/nushell |
| object (from Cargo.toml) | / |
Commands `cd` and `ls` now work on the object being navigated.
```
object/> ls
-----------------+------------------+-----------------
dependencies | dev-dependencies | package
-----------------+------------------+-----------------
[object Object] | [object Object] | [object Object]
-----------------+------------------+-----------------
```
```
object/> cd package/version
object/package/version> ls
-------
value
-------
0.1.2
-------
```
The `exit` command will pop the stack and get us back to a previous object we were navigating.
# Goals
Nu adheres closely to a set of goals that make up its design philosophy. As features are added, they are checked against these goals.
* First and foremost, Nu is cross-platform. Commands and techniques should carry between platforms and offer first-class consistent support for Windows, macOS, and Linux.
* Nu ensures direct compatibility with existing platform-specific executables that make up people's workflows.
* Nu's workflow and tools should have the usability in day-to-day experience of using a shell in 2019 (and beyond).
* Nu views data as both structured and unstructured. It is an object shell like PowerShell.
These goals are all critical, project-defining priorities. Priority #1 is "direct compatibility" because any new shell absolutely needs a way to use existing executables in a direct and natural way.
# Commands
## Initial commands
| command | description |
| ------------- | ------------- |
| cd directory | Change to the given directory |
| ls | View current directory contents |
| ------------- | ------------- |
| cd path | Change to a new path |
| ls (path) | View the contents of the current or given path |
| ps | View current processes |
| open filename | Load a file into a cell, convert to table if possible (avoid by appending '--raw') |
| sysinfo | View information about the current system |
| open {filename or url} | Load a file into a cell, convert to table if possible (avoid by appending '--raw') |
| enter {filename or url} | Enter (mount) the given contents as the current object |
| exit | Leave/pop from the current object (exits if in filesystem object) |
## Commands on tables
## Filters on tables (structured data)
| command | description |
| ------------- | ------------- |
| ------------- | ------------- |
| pick ...columns | Down-select table to only these columns |
| reject ...columns | Remove the given columns from the table |
| get column-or-column-path | Open given cells as text |
@ -29,10 +179,12 @@ At the moment, executing a command that isn't identified as a built-in new comma
| to-array | Collapse rows into a single list |
| to-json | Convert table into .json text |
| to-toml | Convert table into .toml text |
| to-ini | Convert table into .ini text |
## Commands on text
## Filters on text (unstructured data)
| command | description |
| ------------- | ------------- |
| ------------- | ------------- |
| from-ini | Parse text as .ini and create table |
| from-json | Parse text as .json and create table |
| from-toml | Parse text as .toml and create table |
| from-xml | Parse text as .xml and create a table |
@ -40,144 +192,19 @@ At the moment, executing a command that isn't identified as a built-in new comma
| split-column sep ...fields | Split row contents across multiple columns via the separator |
| split-row sep | Split row contents over multiple rows via the separator |
| trim | Trim leading and following whitespace from text data |
| {external-command} $it | Run external command with given arguments, replacing $it with each row text |
| {external-command} $it | Run external command with given arguments, replacing $it with each row text |
# Goals
Prime Directive: Cross platform workflows, with first-class consistent support for Windows, OSX and Linux.
Priority #1: direct compatibility with existing platform-specific executables that make up people's workflows
Priority #2: Create workflow tools that more closely match the day-to-day experience of using a shell in 2019 (and beyond)
Priority #3: It's an object shell like PowerShell.
> These goals are all critical, project-defining priorities. Priority #1 is "direct compatibility" because any new shell absolutely needs a way to use existing executables in a direct and natural way.
## Consuming commands
| command | description |
| ------------- | ------------- |
| autoview | View the contents of the pipeline as a table or list |
| clip | Copy the contents of the pipeline to the copy/paste buffer |
| save filename | Save the contents of the pipeline to a file |
| table | View the contents of the pipeline as a table |
| tree | View the contents of the pipeline as a tree |
| vtable | View the contents of the pipeline as a vertical (rotated) table |
# License
The project is made available under the MIT license. See "LICENSE" for more information.
# A Taste of Nu
Nu has built-in commands for ls and ps, loading these results into a table you can work with.
```text
~\Code\nushell> ps | where cpu > 0
------------------------------------------------
name cmd cpu pid status
------------------------------------------------
msedge.exe - 0.77 26472 Runnable
------------------------------------------------
nu.exe - 7.83 15473 Runnable
------------------------------------------------
SearchIndexer.exe - 82.17 23476 Runnable
------------------------------------------------
BlueJeans.exe - 4.54 10000 Runnable
------------------------------------------------
```
Commands are linked together with pipes, allowing you to select the data you want to use.
```text
~\Code\nushell> ps | where name == chrome.exe | first 5
----------------------------------------
name cmd cpu pid status
----------------------------------------
chrome.exe - 0.00 22092 Runnable
----------------------------------------
chrome.exe - 0.00 17324 Runnable
----------------------------------------
chrome.exe - 0.00 16376 Runnable
----------------------------------------
chrome.exe - 0.00 21876 Runnable
----------------------------------------
chrome.exe - 0.00 13432 Runnable
----------------------------------------
```
The name of the columns in the table can be used to sort the table.
```text
~\Code\nushell> ls | sort-by "file type" size
----------------------------------------------------------------------------------------
file name file type readonly size created accessed modified
----------------------------------------------------------------------------------------
.cargo Directory Empty a week ago a week ago a week ago
----------------------------------------------------------------------------------------
.git Directory Empty 2 weeks ago 9 hours ago 9 hours ago
----------------------------------------------------------------------------------------
images Directory Empty 2 weeks ago 2 weeks ago 2 weeks ago
----------------------------------------------------------------------------------------
src Directory Empty 2 weeks ago 10 hours ago 10 hours ago
----------------------------------------------------------------------------------------
target Directory Empty 10 hours ago 10 hours ago 10 hours ago
----------------------------------------------------------------------------------------
tests Directory Empty 14 hours ago 10 hours ago 10 hours ago
----------------------------------------------------------------------------------------
tmp Directory Empty 2 days ago 2 days ago 2 days ago
----------------------------------------------------------------------------------------
rustfmt.toml File 16 B a week ago a week ago a week ago
----------------------------------------------------------------------------------------
.gitignore File 32 B 2 weeks ago 2 weeks ago 2 weeks ago
----------------------------------------------------------------------------------------
.editorconfig File 156 B 2 weeks ago 2 weeks ago 2 weeks ago
----------------------------------------------------------------------------------------
```
You can also use the names of the columns to down-select to only the data you want.
```text
~\Code\nushell> ls | pick "file name" "file type" size | sort-by "file type"
------------------------------------
file name file type size
------------------------------------
.cargo Directory Empty
------------------------------------
.git Directory Empty
------------------------------------
images Directory Empty
------------------------------------
src Directory Empty
------------------------------------
target Directory Empty
------------------------------------
tests Directory Empty
------------------------------------
rustfmt.toml File 16 B
------------------------------------
.gitignore File 32 B
------------------------------------
.editorconfig File 156 B
------------------------------------
```
Some file types can be loaded as tables.
```text
~\Code\nushell> open Cargo.toml
----------------------------------------------------
dependencies dev-dependencies package
----------------------------------------------------
[object Object] [object Object] [object Object]
----------------------------------------------------
~\Code\nushell> open Cargo.toml | get package
--------------------------------------------------------------------------
authors description edition license name version
--------------------------------------------------------------------------
[list List] A shell for the GitHub era 2018 MIT nu 0.1.1
--------------------------------------------------------------------------
```
Once you've found the data, you can call out to external applications and use it.
```text
~\Code\nushell> open Cargo.toml | get package.version | echo $it
0.1.1
```
Nu currently has fish-style completion of previous commands, as well ctrl-r reverse search.
![autocompletion][fish-style]
[fish-style]: ./images/nushell-autocomplete3.gif "Fish-style autocomplete"

31
appveyor.yml Normal file
View file

@ -0,0 +1,31 @@
image: Visual Studio 2017
environment:
global:
PROJECT_NAME: nushell
RUST_BACKTRACE: 1
matrix:
- TARGET: x86_64-pc-windows-msvc
CHANNEL: nightly
BITS: 64
install:
- set PATH=C:\msys64\mingw%BITS%\bin;C:\msys64\usr\bin;%PATH%
- curl -sSf -o rustup-init.exe https://win.rustup.rs
# Install rust
- rustup-init.exe -y --default-host %TARGET% --default-toolchain %CHANNEL%-%TARGET%
- set PATH=%PATH%;C:\Users\appveyor\.cargo\bin
# Required for Racer autoconfiguration
- call "C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Auxiliary\Build\vcvars64.bat"
build: false
test_script:
# compile #[cfg(not(test))] code
- cargo build --verbose
- cargo test --all --verbose
cache:
- target -> Cargo.lock
- C:\Users\appveyor\.cargo\registry -> Cargo.lock

View file

@ -15,6 +15,7 @@ use crate::evaluate::Scope;
use crate::git::current_branch;
use crate::object::Value;
use crate::parser::ast::{Expression, Leaf, RawExpression};
use crate::parser::lexer::Spanned;
use crate::parser::{Args, Pipeline};
use log::debug;
@ -49,11 +50,13 @@ pub async fn cli() -> Result<(), Box<dyn Error>> {
context.add_commands(vec![
command("ps", ps::ps),
command("ls", ls::ls),
command("sysinfo", sysinfo::sysinfo),
command("cd", cd::cd),
command("view", view::view),
command("skip", skip::skip),
command("first", first::first),
command("size", size::size),
command("from-ini", from_ini::from_ini),
command("from-json", from_json::from_json),
command("from-toml", from_toml::from_toml),
command("from-xml", from_xml::from_xml),
@ -62,16 +65,19 @@ pub async fn cli() -> Result<(), Box<dyn Error>> {
command("open", open::open),
command("enter", enter::enter),
command("exit", exit::exit),
command("lines", lines::lines),
command("pick", pick::pick),
command("split-column", split_column::split_column),
command("split-row", split_row::split_row),
command("reject", reject::reject),
command("trim", trim::trim),
command("to-array", to_array::to_array),
command("to-ini", to_ini::to_ini),
command("to-json", to_json::to_json),
command("to-toml", to_toml::to_toml),
Arc::new(Where),
Arc::new(Config),
Arc::new(SkipWhile),
command("sort-by", sort_by::sort_by),
]);
@ -79,7 +85,9 @@ pub async fn cli() -> Result<(), Box<dyn Error>> {
sink("autoview", autoview::autoview),
sink("clip", clip::clip),
sink("save", save::save),
sink("table", table::table),
sink("tree", tree::tree),
sink("vtable", vtable::vtable),
]);
}
@ -101,19 +109,16 @@ pub async fn cli() -> Result<(), Box<dyn Error>> {
cc.store(true, Ordering::SeqCst);
})
.expect("Error setting Ctrl-C handler");
let mut ctrlcbreak = false;
loop {
if ctrl_c.load(Ordering::SeqCst) {
ctrl_c.store(false, Ordering::SeqCst);
if let ShellError::String(s) = ShellError::string("CTRL-C") {
context.host.lock().unwrap().stdout(&format!("{:?}", s));
}
continue;
}
let (obj, cwd) = {
let env = context.env.lock().unwrap();
let last = env.last().unwrap();
let last = env.back().unwrap();
(last.obj().clone(), last.path().display().to_string())
};
let readline = match obj {
@ -133,36 +138,55 @@ pub async fn cli() -> Result<(), Box<dyn Error>> {
rl.add_history_entry(line.clone());
}
LineResult::Error(mut line, err) => match err {
ShellError::Diagnostic(diag) => {
let host = context.host.lock().unwrap();
let writer = host.err_termcolor();
line.push_str(" ");
let files = crate::parser::span::Files::new(line);
language_reporting::emit(
&mut writer.lock(),
&files,
&diag.diagnostic,
&language_reporting::DefaultConfig,
)
.unwrap();
LineResult::CtrlC => {
if ctrlcbreak {
std::process::exit(0);
} else {
context
.host
.lock()
.unwrap()
.stdout("CTRL-C pressed (again to quit)");
ctrlcbreak = true;
continue;
}
}
ShellError::TypeError(desc) => context
.host
.lock()
.unwrap()
.stdout(&format!("TypeError: {}", desc)),
LineResult::Error(mut line, err) => {
rl.add_history_entry(line.clone());
match err {
ShellError::Diagnostic(diag) => {
let host = context.host.lock().unwrap();
let writer = host.err_termcolor();
line.push_str(" ");
let files = crate::parser::span::Files::new(line);
ShellError::MissingProperty { subpath, .. } => context
.host
.lock()
.unwrap()
.stdout(&format!("Missing property {}", subpath)),
language_reporting::emit(
&mut writer.lock(),
&files,
&diag.diagnostic,
&language_reporting::DefaultConfig,
)
.unwrap();
}
ShellError::String(_) => context.host.lock().unwrap().stdout(&format!("{}", err)),
},
ShellError::TypeError(desc) => context
.host
.lock()
.unwrap()
.stdout(&format!("TypeError: {}", desc)),
ShellError::MissingProperty { subpath, .. } => context
.host
.lock()
.unwrap()
.stdout(&format!("Missing property {}", subpath)),
ShellError::String(_) => {
context.host.lock().unwrap().stdout(&format!("{}", err))
}
}
}
LineResult::Break => {
break;
@ -176,6 +200,7 @@ pub async fn cli() -> Result<(), Box<dyn Error>> {
.stdout(&format!("A surprising fatal error occurred.\n{:?}", err));
}
}
ctrlcbreak = false;
}
rl.save_history("history.txt").unwrap();
@ -185,6 +210,7 @@ pub async fn cli() -> Result<(), Box<dyn Error>> {
enum LineResult {
Success(String),
Error(String, ShellError),
CtrlC,
Break,
#[allow(unused)]
@ -200,6 +226,7 @@ impl std::ops::Try for LineResult {
LineResult::Success(s) => Ok(Some(s)),
LineResult::Error(_, s) => Err(s),
LineResult::Break => Ok(None),
LineResult::CtrlC => Ok(None),
LineResult::FatalError(err) => Err(err),
}
}
@ -258,27 +285,28 @@ async fn process_line(readline: Result<String, ReadlineError>, ctx: &mut Context
(None, _) => break,
(Some(ClassifiedCommand::Expr(_)), _) => {
return LineResult::Error(line.clone(), ShellError::unimplemented(
"Expression-only commands",
))
return LineResult::Error(
line.clone(),
ShellError::unimplemented("Expression-only commands"),
)
}
(_, Some(ClassifiedCommand::Expr(_))) => {
return LineResult::Error(line.clone(), ShellError::unimplemented(
"Expression-only commands",
))
return LineResult::Error(
line.clone(),
ShellError::unimplemented("Expression-only commands"),
)
}
(Some(ClassifiedCommand::Sink(_)), Some(_)) => {
return LineResult::Error(line.clone(), ShellError::string("Commands like table, save, and autoview must come last in the pipeline"))
(Some(ClassifiedCommand::Sink(SinkCommand { name_span, .. })), Some(_)) => {
return LineResult::Error(line.clone(), ShellError::maybe_labeled_error("Commands like table, save, and autoview must come last in the pipeline", "must come last", name_span));
}
(Some(ClassifiedCommand::Sink(left)), None) => {
let input_vec: Vec<Value> = input.objects.collect().await;
left.run(
ctx,
input_vec,
)?;
if let Err(err) = left.run(ctx, input_vec) {
return LineResult::Error(line.clone(), err);
}
break;
}
@ -290,13 +318,12 @@ async fn process_line(readline: Result<String, ReadlineError>, ctx: &mut Context
Err(err) => return LineResult::Error(line.clone(), err),
},
(
Some(ClassifiedCommand::Internal(left)),
Some(_),
) => match left.run(ctx, input).await {
Ok(val) => ClassifiedInputStream::from_input_stream(val),
Err(err) => return LineResult::Error(line.clone(), err),
},
(Some(ClassifiedCommand::Internal(left)), Some(_)) => {
match left.run(ctx, input).await {
Ok(val) => ClassifiedInputStream::from_input_stream(val),
Err(err) => return LineResult::Error(line.clone(), err),
}
}
(Some(ClassifiedCommand::Internal(left)), None) => {
match left.run(ctx, input).await {
@ -313,13 +340,12 @@ async fn process_line(readline: Result<String, ReadlineError>, ctx: &mut Context
Err(err) => return LineResult::Error(line.clone(), err),
},
(
Some(ClassifiedCommand::External(left)),
Some(_),
) => match left.run(ctx, input, StreamNext::Internal).await {
Ok(val) => val,
Err(err) => return LineResult::Error(line.clone(), err),
},
(Some(ClassifiedCommand::External(left)), Some(_)) => {
match left.run(ctx, input, StreamNext::Internal).await {
Ok(val) => val,
Err(err) => return LineResult::Error(line.clone(), err),
}
}
(Some(ClassifiedCommand::External(left)), None) => {
match left.run(ctx, input, StreamNext::Last).await {
@ -332,9 +358,7 @@ async fn process_line(readline: Result<String, ReadlineError>, ctx: &mut Context
LineResult::Success(line.clone())
}
Err(ReadlineError::Interrupted) => {
LineResult::Error("".to_string(), ShellError::string("CTRL-C"))
}
Err(ReadlineError::Interrupted) => LineResult::CtrlC,
Err(ReadlineError::Eof) => {
println!("CTRL-D");
LineResult::Break
@ -416,13 +440,17 @@ fn classify_command(
}))
}
false => {
let arg_list_strings: Vec<String> = match args {
Some(args) => args.iter().map(|i| i.as_external_arg()).collect(),
let arg_list_strings: Vec<Spanned<String>> = match args {
Some(args) => args
.iter()
.map(|i| Spanned::from_item(i.as_external_arg(), i.span))
.collect(),
None => vec![],
};
Ok(ClassifiedCommand::External(ExternalCommand {
name: name.to_string(),
name_span: Some(span.clone()),
args: arg_list_strings,
}))
}

View file

@ -8,11 +8,13 @@ crate mod config;
crate mod enter;
crate mod exit;
crate mod first;
crate mod from_ini;
crate mod from_json;
crate mod from_toml;
crate mod from_xml;
crate mod from_yaml;
crate mod get;
crate mod lines;
crate mod ls;
crate mod open;
crate mod pick;
@ -21,18 +23,24 @@ crate mod reject;
crate mod save;
crate mod size;
crate mod skip;
crate mod skip_while;
crate mod sort_by;
crate mod split_column;
crate mod split_row;
crate mod sysinfo;
crate mod table;
crate mod to_array;
crate mod to_ini;
crate mod to_json;
crate mod to_toml;
crate mod tree;
crate mod trim;
crate mod view;
crate mod vtable;
crate mod where_;
crate use command::command;
crate use config::Config;
crate use where_::Where;
crate use skip_while::SkipWhile;

View file

@ -5,7 +5,7 @@ use std::path::PathBuf;
pub fn cd(args: CommandArgs) -> Result<OutputStream, ShellError> {
let env = args.env.lock().unwrap();
let latest = env.last().unwrap();
let latest = env.back().unwrap();
match latest.obj {
Value::Filesystem => {
@ -13,17 +13,23 @@ pub fn cd(args: CommandArgs) -> Result<OutputStream, ShellError> {
let path = match args.positional.first() {
None => match dirs::home_dir() {
Some(o) => o,
_ => return Err(ShellError::string("Can not change to home directory")),
_ => {
return Err(ShellError::maybe_labeled_error(
"Can not change to home directory",
"can not go to home",
args.name_span,
))
}
},
Some(v) => {
let target = v.as_string()?.clone();
match dunce::canonicalize(cwd.join(&target).as_path()) {
Ok(p) => p,
Err(_) => {
return Err(ShellError::labeled_error(
return Err(ShellError::maybe_labeled_error(
"Can not change to directory",
"directory not found",
args.positional[0].span.clone(),
Some(args.positional[0].span.clone()),
));
}
}
@ -35,10 +41,10 @@ pub fn cd(args: CommandArgs) -> Result<OutputStream, ShellError> {
Ok(_) => {}
Err(_) => {
if args.positional.len() > 0 {
return Err(ShellError::labeled_error(
return Err(ShellError::maybe_labeled_error(
"Can not change to directory",
"directory not found",
args.positional[0].span.clone(),
Some(args.positional[0].span.clone()),
));
} else {
return Err(ShellError::string("Can not change to directory"));

View file

@ -1,9 +1,10 @@
use crate::commands::command::Sink;
use crate::parser::ast::Expression;
use crate::parser::lexer::Span;
use crate::parser::lexer::{Span, Spanned};
use crate::parser::registry::Args;
use crate::prelude::*;
use bytes::{BufMut, BytesMut};
use futures::stream::StreamExt;
use futures_codec::{Decoder, Encoder, Framed};
use std::io::{Error, ErrorKind};
use std::path::PathBuf;
@ -109,51 +110,56 @@ impl InternalCommand {
context: &mut Context,
input: ClassifiedInputStream,
) -> Result<InputStream, ShellError> {
let result = context.run_command(
let mut result = context.run_command(
self.command,
self.name_span.clone(),
self.args,
input.objects,
)?;
let env = context.env.clone();
let stream = result.filter_map(move |v| match v {
ReturnValue::Action(action) => match action {
CommandAction::ChangePath(path) => {
env.lock().unwrap().last_mut().map(|x| {
x.path = path;
x
});
futures::future::ready(None)
let mut stream = VecDeque::new();
while let Some(item) = result.next().await {
match item {
ReturnValue::Value(Value::Error(err)) => {
return Err(*err);
}
CommandAction::Enter(obj) => {
let new_env = Environment {
obj: obj,
path: PathBuf::from("/"),
};
env.lock().unwrap().push(new_env);
futures::future::ready(None)
}
CommandAction::Exit => {
let mut v = env.lock().unwrap();
if v.len() == 1 {
std::process::exit(0);
ReturnValue::Action(action) => match action {
CommandAction::ChangePath(path) => {
context.env.lock().unwrap().back_mut().map(|x| {
x.path = path;
x
});
}
v.pop();
futures::future::ready(None)
CommandAction::Enter(obj) => {
let new_env = Environment {
obj: obj,
path: PathBuf::from("/"),
};
context.env.lock().unwrap().push_back(new_env);
}
CommandAction::Exit => match context.env.lock().unwrap().pop_back() {
Some(Environment {
obj: Value::Filesystem,
..
}) => std::process::exit(0),
None => std::process::exit(-1),
_ => {}
},
},
ReturnValue::Value(v) => {
stream.push_back(v);
}
},
ReturnValue::Value(v) => futures::future::ready(Some(v)),
});
}
}
Ok(stream.boxed() as InputStream)
}
}
crate struct ExternalCommand {
crate name: String,
crate args: Vec<String>,
#[allow(unused)]
crate name_span: Option<Span>,
crate args: Vec<Spanned<String>>,
}
crate enum StreamNext {
@ -174,10 +180,11 @@ impl ExternalCommand {
let mut arg_string = format!("{}", self.name);
for arg in &self.args {
arg_string.push_str(" ");
arg_string.push_str(&arg);
arg_string.push_str(&arg.item);
}
let mut process;
#[cfg(windows)]
{
process = Exec::shell(&self.name);
@ -185,6 +192,23 @@ impl ExternalCommand {
if arg_string.contains("$it") {
let mut first = true;
for i in &inputs {
if i.as_string().is_err() {
let mut span = None;
for arg in &self.args {
if arg.item.contains("$it") {
span = Some(arg.span);
}
}
if let Some(span) = span {
return Err(ShellError::labeled_error(
"External $it needs string data",
"given object instead of string data",
span,
));
} else {
return Err(ShellError::string("Error: $it needs string data"));
}
}
if !first {
process = process.arg("&&");
process = process.arg(&self.name);
@ -198,7 +222,7 @@ impl ExternalCommand {
}
} else {
for arg in &self.args {
process = process.arg(arg);
process = process.arg(arg.item.clone());
}
}
}
@ -209,6 +233,19 @@ impl ExternalCommand {
if arg_string.contains("$it") {
let mut first = true;
for i in &inputs {
if i.as_string().is_err() {
let mut span = None;
for arg in &self.args {
if arg.item.contains("$it") {
span = Some(arg.span);
}
}
return Err(ShellError::maybe_labeled_error(
"External $it needs string data",
"given object instead of string data",
span,
));
}
if !first {
new_arg_string.push_str("&&");
new_arg_string.push_str(&self.name);
@ -227,9 +264,10 @@ impl ExternalCommand {
new_arg_string.push_str(&arg);
}
}
process = Exec::shell(new_arg_string);
}
process = process.cwd(context.env.lock().unwrap().first().unwrap().path());
process = process.cwd(context.env.lock().unwrap().front().unwrap().path());
let mut process = match stream_next {
StreamNext::Last => process,

View file

@ -13,7 +13,16 @@ pub fn clip(args: SinkCommandArgs) -> Result<(), ShellError> {
} else {
first = false;
}
new_copy_data.push_str(&i.as_string().unwrap());
match i.as_string() {
Ok(s) => new_copy_data.push_str(&s),
Err(_) => {
return Err(ShellError::maybe_labeled_error(
"Given non-string data",
"expected strings from pipeline",
args.name_span,
))
}
}
}
}
clip_context.set_contents(new_copy_data).unwrap();

View file

@ -8,7 +8,7 @@ use std::path::PathBuf;
pub struct CommandArgs {
pub host: Arc<Mutex<dyn Host + Send>>,
pub env: Arc<Mutex<Vec<Environment>>>,
pub env: Arc<Mutex<VecDeque<Environment>>>,
pub name_span: Option<Span>,
pub positional: Vec<Spanned<Value>>,
pub named: indexmap::IndexMap<String, Value>,

View file

@ -7,14 +7,20 @@ use std::path::{Path, PathBuf};
pub fn enter(args: CommandArgs) -> Result<OutputStream, ShellError> {
if args.positional.len() == 0 {
return Err(ShellError::string("open requires a filepath or url"));
return Err(ShellError::maybe_labeled_error(
"open requires a path or url",
"missing path",
args.name_span,
));
}
let span = args.name_span;
let cwd = args
.env
.lock()
.unwrap()
.first()
.front()
.unwrap()
.path()
.to_path_buf();
@ -85,45 +91,111 @@ pub fn enter(args: CommandArgs) -> Result<OutputStream, ShellError> {
let mut stream = VecDeque::new();
let open_raw = match args.positional.get(1) {
let file_extension = match args.positional.get(1) {
Some(Spanned {
item: Value::Primitive(Primitive::String(s)),
..
}) if s == "--raw" => true,
Some(v) => {
return Err(ShellError::labeled_error(
"Unknown flag for open",
"unknown flag",
v.span,
))
span,
}) => {
if s == "--raw" {
None
} else if s == "--json" {
Some("json".to_string())
} else if s == "--xml" {
Some("xml".to_string())
} else if s == "--ini" {
Some("ini".to_string())
} else if s == "--yaml" {
Some("yaml".to_string())
} else if s == "--toml" {
Some("toml".to_string())
} else {
return Err(ShellError::labeled_error(
"Unknown flag for open",
"unknown flag",
span.clone(),
));
}
}
_ => false,
_ => file_extension,
};
match file_extension {
Some(x) if x == "toml" && !open_raw => {
Some(x) if x == "toml" => {
stream.push_back(ReturnValue::Action(CommandAction::Enter(
crate::commands::from_toml::from_toml_string_to_value(contents),
crate::commands::from_toml::from_toml_string_to_value(contents).map_err(
move |_| {
ShellError::maybe_labeled_error(
"Could not load as TOML",
"could not load as TOML",
span,
)
},
)?,
)));
}
Some(x) if x == "json" && !open_raw => {
Some(x) if x == "json" => {
stream.push_back(ReturnValue::Action(CommandAction::Enter(
crate::commands::from_json::from_json_string_to_value(contents),
crate::commands::from_json::from_json_string_to_value(contents).map_err(
move |_| {
ShellError::maybe_labeled_error(
"Could not load as JSON",
"could not load as JSON",
span,
)
},
)?,
)));
}
Some(x) if x == "xml" && !open_raw => {
Some(x) if x == "xml" => {
stream.push_back(ReturnValue::Action(CommandAction::Enter(
crate::commands::from_xml::from_xml_string_to_value(contents),
crate::commands::from_xml::from_xml_string_to_value(contents).map_err(
move |_| {
ShellError::maybe_labeled_error(
"Could not load as XML",
"could not load as XML",
span,
)
},
)?,
)));
}
Some(x) if x == "yml" && !open_raw => {
Some(x) if x == "ini" => {
stream.push_back(ReturnValue::Action(CommandAction::Enter(
crate::commands::from_yaml::from_yaml_string_to_value(contents),
crate::commands::from_ini::from_ini_string_to_value(contents).map_err(
move |_| {
ShellError::maybe_labeled_error(
"Could not load as INI",
"could not load as INI",
span,
)
},
)?,
)));
}
Some(x) if x == "yaml" && !open_raw => {
Some(x) if x == "yml" => {
stream.push_back(ReturnValue::Action(CommandAction::Enter(
crate::commands::from_yaml::from_yaml_string_to_value(contents),
crate::commands::from_yaml::from_yaml_string_to_value(contents).map_err(
move |_| {
ShellError::maybe_labeled_error(
"Could not load as YAML",
"could not load as YAML",
span,
)
},
)?,
)));
}
Some(x) if x == "yaml" => {
stream.push_back(ReturnValue::Action(CommandAction::Enter(
crate::commands::from_yaml::from_yaml_string_to_value(contents).map_err(
move |_| {
ShellError::maybe_labeled_error(
"Could not load as YAML",
"could not load as YAML",
span,
)
},
)?,
)));
}
_ => {

View file

@ -1,11 +1,8 @@
use crate::commands::command::CommandAction;
use crate::errors::ShellError;
use crate::object::{Primitive, Value};
use crate::parser::lexer::Spanned;
use crate::prelude::*;
use std::path::{Path, PathBuf};
pub fn exit(args: CommandArgs) -> Result<OutputStream, ShellError> {
pub fn exit(_args: CommandArgs) -> Result<OutputStream, ShellError> {
let mut stream = VecDeque::new();
stream.push_back(ReturnValue::Action(CommandAction::Exit));
Ok(stream.boxed())

View file

@ -5,15 +5,11 @@ use crate::prelude::*;
pub fn first(args: CommandArgs) -> Result<OutputStream, ShellError> {
if args.positional.len() == 0 {
if let Some(span) = args.name_span {
return Err(ShellError::labeled_error(
"First requires an amount",
"needs parameter",
span,
));
} else {
return Err(ShellError::string("first requires an amount."));
}
return Err(ShellError::maybe_labeled_error(
"First requires an amount",
"needs parameter",
args.name_span,
));
}
let amount = args.positional[0].as_i64();

54
src/commands/from_ini.rs Normal file
View file

@ -0,0 +1,54 @@
use crate::object::{DataDescriptor, Dictionary, Primitive, Value};
use crate::prelude::*;
use indexmap::IndexMap;
use std::collections::HashMap;
fn convert_ini_second_to_nu_value(v: &HashMap<String, String>) -> Value {
let mut second = Dictionary::new(IndexMap::new());
for (key, value) in v.into_iter() {
second.add(
DataDescriptor::from(key.as_str()),
Value::Primitive(Primitive::String(value.clone())),
);
}
Value::Object(second)
}
fn convert_ini_top_to_nu_value(v: &HashMap<String, HashMap<String, String>>) -> Value {
let mut top_level = Dictionary::new(IndexMap::new());
for (key, value) in v.iter() {
top_level.add(
DataDescriptor::from(key.as_str()),
convert_ini_second_to_nu_value(value),
);
}
Value::Object(top_level)
}
pub fn from_ini_string_to_value(s: String) -> Result<Value, Box<dyn std::error::Error>> {
let v: HashMap<String, HashMap<String, String>> = serde_ini::from_str(&s)?;
Ok(convert_ini_top_to_nu_value(&v))
}
pub fn from_ini(args: CommandArgs) -> Result<OutputStream, ShellError> {
let out = args.input;
let span = args.name_span;
Ok(out
.map(move |a| match a {
Value::Primitive(Primitive::String(s)) => match from_ini_string_to_value(s) {
Ok(x) => ReturnValue::Value(x),
Err(e) => {
ReturnValue::Value(Value::Error(Box::new(ShellError::maybe_labeled_error(
"Could not parse as INI",
format!("{:#?}", e),
span,
))))
}
},
_ => ReturnValue::Value(Value::Error(Box::new(ShellError::maybe_labeled_error(
"Expected string values from pipeline",
"expects strings from pipeline",
span,
)))),
})
.boxed())
}

View file

@ -28,19 +28,31 @@ fn convert_json_value_to_nu_value(v: &serde_hjson::Value) -> Value {
}
}
pub fn from_json_string_to_value(s: String) -> Value {
let v: serde_hjson::Value = serde_hjson::from_str(&s).unwrap();
convert_json_value_to_nu_value(&v)
pub fn from_json_string_to_value(s: String) -> serde_hjson::Result<Value> {
let v: serde_hjson::Value = serde_hjson::from_str(&s)?;
Ok(convert_json_value_to_nu_value(&v))
}
pub fn from_json(args: CommandArgs) -> Result<OutputStream, ShellError> {
let out = args.input;
let span = args.name_span;
Ok(out
.map(|a| match a {
Value::Primitive(Primitive::String(s)) => {
ReturnValue::Value(from_json_string_to_value(s))
}
_ => ReturnValue::Value(Value::Primitive(Primitive::String("".to_string()))),
.map(move |a| match a {
Value::Primitive(Primitive::String(s)) => match from_json_string_to_value(s) {
Ok(x) => ReturnValue::Value(x),
Err(_) => {
ReturnValue::Value(Value::Error(Box::new(ShellError::maybe_labeled_error(
"Could not parse as JSON",
"piped data failed JSON parse",
span,
))))
}
},
_ => ReturnValue::Value(Value::Error(Box::new(ShellError::maybe_labeled_error(
"Expected string values from pipeline",
"expects strings from pipeline",
span,
)))),
})
.boxed())
}

View file

@ -1,5 +1,5 @@
use crate::object::{Primitive, Value, Dictionary, DataDescriptor};
use crate::object::base::OF64;
use crate::object::{DataDescriptor, Dictionary, Primitive, Value};
use crate::prelude::*;
fn convert_toml_value_to_nu_value(v: &toml::Value) -> Value {
@ -8,31 +8,50 @@ fn convert_toml_value_to_nu_value(v: &toml::Value) -> Value {
toml::Value::Integer(n) => Value::Primitive(Primitive::Int(*n)),
toml::Value::Float(n) => Value::Primitive(Primitive::Float(OF64::from(*n))),
toml::Value::String(s) => Value::Primitive(Primitive::String(s.clone())),
toml::Value::Array(a) => Value::List(a.iter().map(|x| convert_toml_value_to_nu_value(x)).collect()),
toml::Value::Array(a) => Value::List(
a.iter()
.map(|x| convert_toml_value_to_nu_value(x))
.collect(),
),
toml::Value::Datetime(dt) => Value::Primitive(Primitive::String(dt.to_string())),
toml::Value::Table(t) => {
let mut collected = Dictionary::default();
for (k, v) in t.iter() {
collected.add(DataDescriptor::from(k.clone()), convert_toml_value_to_nu_value(v));
collected.add(
DataDescriptor::from(k.clone()),
convert_toml_value_to_nu_value(v),
);
}
Value::Object(collected)
}
}
}
pub fn from_toml_string_to_value(s: String) -> Value {
let v: toml::Value = s.parse::<toml::Value>().unwrap();
convert_toml_value_to_nu_value(&v)
pub fn from_toml_string_to_value(s: String) -> Result<Value, Box<dyn std::error::Error>> {
let v: toml::Value = s.parse::<toml::Value>()?;
Ok(convert_toml_value_to_nu_value(&v))
}
pub fn from_toml(args: CommandArgs) -> Result<OutputStream, ShellError> {
let out = args.input;
let span = args.name_span;
Ok(out
.map(|a| match a {
Value::Primitive(Primitive::String(s)) => {
ReturnValue::Value(from_toml_string_to_value(s))
}
_ => ReturnValue::Value(Value::Primitive(Primitive::String("".to_string()))),
.map(move |a| match a {
Value::Primitive(Primitive::String(s)) => match from_toml_string_to_value(s) {
Ok(x) => ReturnValue::Value(x),
Err(_) => {
ReturnValue::Value(Value::Error(Box::new(ShellError::maybe_labeled_error(
"Could not parse as TOML",
"piped data failed TOML parse",
span,
))))
}
},
_ => ReturnValue::Value(Value::Error(Box::new(ShellError::maybe_labeled_error(
"Expected string values from pipeline",
"expects strings from pipeline",
span,
)))),
})
.boxed())
}

View file

@ -15,7 +15,7 @@ fn from_node_to_value<'a, 'd>(n: &roxmltree::Node<'a, 'd>) -> Value {
.filter(|x| match x {
Value::Primitive(Primitive::String(f)) => {
if f.trim() == "" {
false
false
} else {
true
}
@ -46,22 +46,30 @@ fn from_document_to_value(d: &roxmltree::Document) -> Value {
from_node_to_value(&d.root_element())
}
pub fn from_xml_string_to_value(s: String) -> Value {
match roxmltree::Document::parse(&s) {
Ok(doc) => from_document_to_value(&doc),
Err(_) => Value::Error(Box::new(ShellError::string(
"Can't convert string from xml".to_string(),
))),
}
pub fn from_xml_string_to_value(s: String) -> Result<Value, Box<dyn std::error::Error>> {
let parsed = roxmltree::Document::parse(&s)?;
Ok(from_document_to_value(&parsed))
}
pub fn from_xml(args: CommandArgs) -> Result<OutputStream, ShellError> {
let out = args.input;
let span = args.name_span;
Ok(out
.map(|a| match a {
Value::Primitive(Primitive::String(s)) => ReturnValue::Value(from_xml_string_to_value(s)),
_ => ReturnValue::Value(Value::Error(Box::new(ShellError::string(
"Trying to convert XML from non-string".to_string(),
.map(move |a| match a {
Value::Primitive(Primitive::String(s)) => match from_xml_string_to_value(s) {
Ok(x) => ReturnValue::Value(x),
Err(_) => {
ReturnValue::Value(Value::Error(Box::new(ShellError::maybe_labeled_error(
"Could not parse as XML",
"piped data failed XML parse",
span,
))))
}
},
_ => ReturnValue::Value(Value::Error(Box::new(ShellError::maybe_labeled_error(
"Expected string values from pipeline",
"expects strings from pipeline",
span,
)))),
})
.boxed())

View file

@ -36,19 +36,31 @@ fn convert_yaml_value_to_nu_value(v: &serde_yaml::Value) -> Value {
}
}
pub fn from_yaml_string_to_value(s: String) -> Value {
let v: serde_yaml::Value = serde_yaml::from_str(&s).unwrap();
convert_yaml_value_to_nu_value(&v)
pub fn from_yaml_string_to_value(s: String) -> serde_yaml::Result<Value> {
let v: serde_yaml::Value = serde_yaml::from_str(&s)?;
Ok(convert_yaml_value_to_nu_value(&v))
}
pub fn from_yaml(args: CommandArgs) -> Result<OutputStream, ShellError> {
let out = args.input;
let span = args.name_span;
Ok(out
.map(|a| match a {
Value::Primitive(Primitive::String(s)) => {
ReturnValue::Value(from_yaml_string_to_value(s))
}
_ => ReturnValue::Value(Value::Primitive(Primitive::String("".to_string()))),
.map(move |a| match a {
Value::Primitive(Primitive::String(s)) => match from_yaml_string_to_value(s) {
Ok(x) => ReturnValue::Value(x),
Err(_) => {
ReturnValue::Value(Value::Error(Box::new(ShellError::maybe_labeled_error(
"Could not parse as YAML",
"piped data failed YAML parse",
span,
))))
}
},
_ => ReturnValue::Value(Value::Error(Box::new(ShellError::maybe_labeled_error(
"Expected string values from pipeline",
"expects strings from pipeline",
span,
)))),
})
.boxed())
}

View file

@ -1,17 +1,19 @@
use crate::errors::ShellError;
use crate::object::Value;
use crate::parser::lexer::Span;
use crate::prelude::*;
fn get_member(path: &str, obj: &Value) -> Option<Value> {
fn get_member(path: &str, span: Span, obj: &Value) -> Option<Value> {
let mut current = obj;
for p in path.split(".") {
match current.get_data_by_key(p) {
Some(v) => current = v,
None => {
return Some(Value::Error(Box::new(ShellError::string(format!(
"Object field name not found: {}",
p
)))))
return Some(Value::Error(Box::new(ShellError::labeled_error(
"Unknown field",
"object missing field",
span,
))));
}
}
}
@ -21,15 +23,11 @@ fn get_member(path: &str, obj: &Value) -> Option<Value> {
pub fn get(args: CommandArgs) -> Result<OutputStream, ShellError> {
if args.positional.len() == 0 {
if let Some(span) = args.name_span {
return Err(ShellError::labeled_error(
"Get requires a field or field path",
"needs parameter",
span,
));
} else {
return Err(ShellError::string("get requires fields."));
}
return Err(ShellError::maybe_labeled_error(
"Get requires a field or field path",
"needs parameter",
args.name_span,
));
}
let amount = args.positional[0].as_i64();
@ -44,7 +42,11 @@ pub fn get(args: CommandArgs) -> Result<OutputStream, ShellError> {
.boxed());
}
let fields: Result<Vec<String>, _> = args.positional.iter().map(|a| a.as_string()).collect();
let fields: Result<Vec<(String, Span)>, _> = args
.positional
.iter()
.map(|a| (a.as_string().map(|x| (x, a.span))))
.collect();
let fields = fields?;
let stream = args
@ -52,7 +54,7 @@ pub fn get(args: CommandArgs) -> Result<OutputStream, ShellError> {
.map(move |item| {
let mut result = VecDeque::new();
for field in &fields {
match get_member(field, &item) {
match get_member(&field.0, field.1, &item) {
Some(Value::List(l)) => {
for item in l {
result.push_back(ReturnValue::Value(item.copy()));

37
src/commands/lines.rs Normal file
View file

@ -0,0 +1,37 @@
use crate::errors::ShellError;
use crate::object::{Primitive, Value};
use crate::prelude::*;
pub fn lines(args: CommandArgs) -> Result<OutputStream, ShellError> {
let input = args.input;
let span = args.name_span;
let stream = input
.map(move |v| match v {
Value::Primitive(Primitive::String(s)) => {
let split_result: Vec<_> = s.lines().filter(|s| s.trim() != "").collect();
let mut result = VecDeque::new();
for s in split_result {
result.push_back(ReturnValue::Value(Value::Primitive(Primitive::String(
s.to_string(),
))));
}
result
}
_ => {
let mut result = VecDeque::new();
result.push_back(ReturnValue::Value(Value::Error(Box::new(
ShellError::maybe_labeled_error(
"Expected string values from pipeline",
"expects strings from pipeline",
span,
),
))));
result
}
})
.flatten();
Ok(stream.boxed())
}

View file

@ -7,8 +7,8 @@ use std::path::{Path, PathBuf};
pub fn ls(args: CommandArgs) -> Result<OutputStream, ShellError> {
let env = args.env.lock().unwrap();
let path = env.last().unwrap().path.to_path_buf();
let obj = &env.last().unwrap().obj;
let path = env.back().unwrap().path.to_path_buf();
let obj = &env.back().unwrap().obj;
let mut full_path = PathBuf::from(path);
match &args.positional.get(0) {
Some(Spanned {
@ -31,7 +31,11 @@ pub fn ls(args: CommandArgs) -> Result<OutputStream, ShellError> {
s.span,
));
} else {
return Err(ShellError::string(e.to_string()));
return Err(ShellError::maybe_labeled_error(
e.to_string(),
e.to_string(),
args.name_span,
));
}
}
Ok(o) => o,
@ -69,27 +73,46 @@ pub fn ls(args: CommandArgs) -> Result<OutputStream, ShellError> {
Some(v) => {
viewed = v;
}
_ => println!("Idx not found"),
_ => {
return Err(ShellError::maybe_labeled_error(
"Given incorrect index",
format!("path given bad index: {}", idx),
args.name_span,
))
}
},
_ => println!("idx not a number"),
_ => {
return Err(ShellError::maybe_labeled_error(
"Given incorrect index",
format!(
"path index not a number: {}",
&s[0..finish]
),
args.name_span,
))
}
}
}
_ => println!("obj not some"),
/*
_ => match viewed.get_data_by_key(s) {
Some(v) => {
viewed = v;
}
_ => println!("Obj not Some"),
},
*/
_ => {
return Err(ShellError::maybe_labeled_error(
"Index not closed",
format!("path missing closing ']'"),
if args.positional.len() > 0 { Some(args.positional[0].span) } else { args.name_span },
))
}
}
} else {
match viewed.get_data_by_key(s) {
Some(v) => {
viewed = v;
}
_ => println!("Obj not Some"),
_ => {
return Err(ShellError::maybe_labeled_error(
"Could not find key",
format!("could not find: {}", s),
args.name_span,
))
}
}
first = false;
}

View file

@ -6,14 +6,20 @@ use std::path::{Path, PathBuf};
pub fn open(args: CommandArgs) -> Result<OutputStream, ShellError> {
if args.positional.len() == 0 {
return Err(ShellError::string("open requires a filepath or url"));
return Err(ShellError::maybe_labeled_error(
"Open requires a path or url",
"needs path or url",
args.name_span,
));
}
let span = args.name_span;
let cwd = args
.env
.lock()
.unwrap()
.first()
.front()
.unwrap()
.path()
.to_path_buf();
@ -65,8 +71,8 @@ pub fn open(args: CommandArgs) -> Result<OutputStream, ShellError> {
),
Err(_) => {
return Err(ShellError::labeled_error(
"File cound not be opened",
"file not found",
"File could not be opened",
"could not be opened",
args.positional[0].span,
));
}
@ -84,45 +90,111 @@ pub fn open(args: CommandArgs) -> Result<OutputStream, ShellError> {
let mut stream = VecDeque::new();
let open_raw = match args.positional.get(1) {
let file_extension = match args.positional.get(1) {
Some(Spanned {
item: Value::Primitive(Primitive::String(s)),
..
}) if s == "--raw" => true,
Some(v) => {
return Err(ShellError::labeled_error(
"Unknown flag for open",
"unknown flag",
v.span,
))
span,
}) => {
if s == "--raw" {
None
} else if s == "--json" {
Some("json".to_string())
} else if s == "--xml" {
Some("xml".to_string())
} else if s == "--ini" {
Some("ini".to_string())
} else if s == "--yaml" {
Some("yaml".to_string())
} else if s == "--toml" {
Some("toml".to_string())
} else {
return Err(ShellError::labeled_error(
"Unknown flag for open",
"unknown flag",
span.clone(),
));
}
}
_ => false,
_ => file_extension,
};
match file_extension {
Some(x) if x == "toml" && !open_raw => {
Some(x) if x == "toml" => {
stream.push_back(ReturnValue::Value(
crate::commands::from_toml::from_toml_string_to_value(contents),
crate::commands::from_toml::from_toml_string_to_value(contents).map_err(
move |_| {
ShellError::maybe_labeled_error(
"Could not open as TOML",
"could not open as TOML",
span,
)
},
)?,
));
}
Some(x) if x == "json" && !open_raw => {
Some(x) if x == "json" => {
stream.push_back(ReturnValue::Value(
crate::commands::from_json::from_json_string_to_value(contents),
crate::commands::from_json::from_json_string_to_value(contents).map_err(
move |_| {
ShellError::maybe_labeled_error(
"Could not open as JSON",
"could not open as JSON",
span,
)
},
)?,
));
}
Some(x) if x == "xml" && !open_raw => {
Some(x) if x == "ini" => {
stream.push_back(ReturnValue::Value(
crate::commands::from_xml::from_xml_string_to_value(contents),
crate::commands::from_ini::from_ini_string_to_value(contents).map_err(
move |_| {
ShellError::maybe_labeled_error(
"Could not open as INI",
"could not open as INI",
span,
)
},
)?,
));
}
Some(x) if x == "yml" && !open_raw => {
Some(x) if x == "xml" => {
stream.push_back(ReturnValue::Value(
crate::commands::from_yaml::from_yaml_string_to_value(contents),
crate::commands::from_xml::from_xml_string_to_value(contents).map_err(
move |_| {
ShellError::maybe_labeled_error(
"Could not open as XML",
"could not open as XML",
span,
)
},
)?,
));
}
Some(x) if x == "yaml" && !open_raw => {
Some(x) if x == "yml" => {
stream.push_back(ReturnValue::Value(
crate::commands::from_yaml::from_yaml_string_to_value(contents),
crate::commands::from_yaml::from_yaml_string_to_value(contents).map_err(
move |_| {
ShellError::maybe_labeled_error(
"Could not open as YAML",
"could not open as YAML",
span,
)
},
)?,
));
}
Some(x) if x == "yaml" => {
stream.push_back(ReturnValue::Value(
crate::commands::from_yaml::from_yaml_string_to_value(contents).map_err(
move |_| {
ShellError::maybe_labeled_error(
"Could not open as YAML",
"could not open as YAML",
span,
)
},
)?,
));
}
_ => {

View file

@ -5,15 +5,11 @@ use crate::prelude::*;
pub fn pick(args: CommandArgs) -> Result<OutputStream, ShellError> {
if args.positional.len() == 0 {
if let Some(span) = args.name_span {
return Err(ShellError::labeled_error(
"Pick requires fields",
"needs parameter",
span,
));
} else {
return Err(ShellError::string("pick requires fields."));
}
return Err(ShellError::maybe_labeled_error(
"Pick requires fields",
"needs parameter",
args.name_span,
));
}
let fields: Result<Vec<String>, _> = args.positional.iter().map(|a| a.as_string()).collect();

View file

@ -5,15 +5,11 @@ use crate::prelude::*;
pub fn reject(args: CommandArgs) -> Result<OutputStream, ShellError> {
if args.positional.len() == 0 {
if let Some(span) = args.name_span {
return Err(ShellError::labeled_error(
"Reject requires fields",
"needs parameter",
span,
));
} else {
return Err(ShellError::string("reject requires fields."));
}
return Err(ShellError::maybe_labeled_error(
"Reject requires fields",
"needs parameter",
args.name_span,
));
}
let fields: Result<Vec<String>, _> = args.positional.iter().map(|a| a.as_string()).collect();

View file

@ -6,7 +6,11 @@ use std::path::{Path, PathBuf};
pub fn save(args: SinkCommandArgs) -> Result<(), ShellError> {
if args.positional.len() == 0 {
return Err(ShellError::string("save requires a filepath"));
return Err(ShellError::maybe_labeled_error(
"Save requires a filepath",
"needs path",
args.name_span,
));
}
let cwd = args
@ -14,7 +18,7 @@ pub fn save(args: SinkCommandArgs) -> Result<(), ShellError> {
.env
.lock()
.unwrap()
.first()
.front()
.unwrap()
.path()
.to_path_buf();
@ -41,6 +45,14 @@ pub fn save(args: SinkCommandArgs) -> Result<(), ShellError> {
}
toml::to_string(&args.input[0]).unwrap()
}
Some(x) if x == "ini" && !save_raw => {
if args.input.len() != 1 {
return Err(ShellError::string(
"saving to ini requires a single object (or use --raw)",
));
}
serde_ini::to_string(&args.input[0]).unwrap()
}
Some(x) if x == "json" && !save_raw => {
if args.input.len() != 1 {
return Err(ShellError::string(

View file

@ -6,14 +6,18 @@ use std::fs::File;
use std::io::prelude::*;
pub fn size(args: CommandArgs) -> Result<OutputStream, ShellError> {
if args.positional.is_empty() {
return Err(ShellError::string("size requires at least one file"));
if args.positional.len() == 0 {
return Err(ShellError::maybe_labeled_error(
"Size requires a filepath",
"needs path",
args.name_span,
));
}
let cwd = args
.env
.lock()
.unwrap()
.first()
.front()
.unwrap()
.path()
.to_path_buf();

View file

@ -3,15 +3,11 @@ use crate::prelude::*;
pub fn skip(args: CommandArgs) -> Result<OutputStream, ShellError> {
if args.positional.len() == 0 {
if let Some(span) = args.name_span {
return Err(ShellError::labeled_error(
"Skip requires an amount",
"needs parameter",
span,
));
} else {
return Err(ShellError::string("skip requires an amount."));
}
return Err(ShellError::maybe_labeled_error(
"Skip requires an amount",
"needs parameter",
args.name_span,
));
}
let amount = args.positional[0].as_i64();

View file

@ -0,0 +1,51 @@
use crate::errors::ShellError;
use crate::parser::registry::PositionalType;
use crate::parser::CommandConfig;
use crate::prelude::*;
pub struct SkipWhile;
impl Command for SkipWhile {
fn run(&self, args: CommandArgs) -> Result<OutputStream, ShellError> {
skip_while(args)
}
fn name(&self) -> &str {
"skip-while"
}
fn config(&self) -> CommandConfig {
CommandConfig {
name: self.name().to_string(),
mandatory_positional: vec![PositionalType::Block("condition".to_string())],
optional_positional: vec![],
rest_positional: false,
named: indexmap::IndexMap::new(),
}
}
}
pub fn skip_while(args: CommandArgs) -> Result<OutputStream, ShellError> {
if args.positional.len() == 0 {
return Err(ShellError::maybe_labeled_error(
"Where requires a condition",
"needs condition",
args.name_span,
));
}
let block = args.positional[0].as_block()?;
let input = args.input;
let objects = input.skip_while(move |item| {
let result = block.invoke(&item);
let return_value = match result {
Ok(v) if v.is_true() => true,
_ => false,
};
futures::future::ready(return_value)
});
Ok(objects.map(|x| ReturnValue::Value(x)).boxed())
}

View file

@ -6,7 +6,15 @@ use log::trace;
// TODO: "Amount remaining" wrapper
pub fn split_column(args: CommandArgs) -> Result<OutputStream, ShellError> {
if args.positional.len() == 0 {
return Err(ShellError::maybe_labeled_error(
"Split-column needs more information",
"needs parameter (eg split-column \",\")",
args.name_span,
));
}
let input = args.input;
let span = args.name_span;
let args = args.positional;
Ok(input
@ -53,7 +61,11 @@ pub fn split_column(args: CommandArgs) -> Result<OutputStream, ShellError> {
ReturnValue::Value(Value::Object(dict))
}
}
_ => ReturnValue::Value(Value::Object(crate::object::Dictionary::default())),
_ => ReturnValue::Value(Value::Error(Box::new(ShellError::maybe_labeled_error(
"Expected string values from pipeline",
"expects strings from pipeline",
span,
)))),
})
.boxed())
}

View file

@ -6,7 +6,16 @@ use log::trace;
// TODO: "Amount remaining" wrapper
pub fn split_row(args: CommandArgs) -> Result<OutputStream, ShellError> {
if args.positional.len() == 0 {
return Err(ShellError::maybe_labeled_error(
"Split-row needs more information",
"needs parameter (eg split-row \"\\n\")",
args.name_span,
));
}
let input = args.input;
let span = args.name_span;
let args = args.positional;
let stream = input
@ -27,7 +36,14 @@ pub fn split_row(args: CommandArgs) -> Result<OutputStream, ShellError> {
result
}
_ => {
let result = VecDeque::new();
let mut result = VecDeque::new();
result.push_back(ReturnValue::Value(Value::Error(Box::new(
ShellError::maybe_labeled_error(
"Expected string values from pipeline",
"expects strings from pipeline",
span,
),
))));
result
}
})

205
src/commands/sysinfo.rs Normal file
View file

@ -0,0 +1,205 @@
use crate::errors::ShellError;
use crate::object::base::OF64;
use crate::object::Dictionary;
use crate::object::{Primitive, Value};
use crate::prelude::*;
use sys_info::*;
use sysinfo::{ComponentExt, DiskExt, NetworkExt, SystemExt};
pub fn sysinfo(_args: CommandArgs) -> Result<OutputStream, ShellError> {
let mut idx = indexmap::IndexMap::new();
if let (Ok(name), Ok(version)) = (os_type(), os_release()) {
let mut os_idx = indexmap::IndexMap::new();
os_idx.insert(
"name".to_string(),
Value::Primitive(Primitive::String(name)),
);
os_idx.insert(
"version".to_string(),
Value::Primitive(Primitive::String(version)),
);
idx.insert("os".to_string(), Value::Object(Dictionary::from(os_idx)));
}
if let (Ok(num_cpu), Ok(cpu_speed)) = (cpu_num(), cpu_speed()) {
let mut cpu_idx = indexmap::IndexMap::new();
cpu_idx.insert(
"num".to_string(),
Value::Primitive(Primitive::Int(num_cpu as i64)),
);
cpu_idx.insert(
"speed".to_string(),
Value::Primitive(Primitive::Int(cpu_speed as i64)),
);
idx.insert("cpu".to_string(), Value::Object(Dictionary::from(cpu_idx)));
}
if let Ok(x) = loadavg() {
let mut load_idx = indexmap::IndexMap::new();
load_idx.insert(
"1min".to_string(),
Value::Primitive(Primitive::Float(OF64::from(x.one))),
);
load_idx.insert(
"5min".to_string(),
Value::Primitive(Primitive::Float(OF64::from(x.five))),
);
load_idx.insert(
"15min".to_string(),
Value::Primitive(Primitive::Float(OF64::from(x.fifteen))),
);
idx.insert(
"load avg".to_string(),
Value::Object(Dictionary::from(load_idx)),
);
}
if let Ok(x) = mem_info() {
let mut mem_idx = indexmap::IndexMap::new();
mem_idx.insert(
"total".to_string(),
Value::Primitive(Primitive::Bytes(x.total as u128 * 1024)),
);
mem_idx.insert(
"free".to_string(),
Value::Primitive(Primitive::Bytes(x.free as u128 * 1024)),
);
mem_idx.insert(
"avail".to_string(),
Value::Primitive(Primitive::Bytes(x.avail as u128 * 1024)),
);
mem_idx.insert(
"buffers".to_string(),
Value::Primitive(Primitive::Bytes(x.buffers as u128 * 1024)),
);
mem_idx.insert(
"cached".to_string(),
Value::Primitive(Primitive::Bytes(x.cached as u128 * 1024)),
);
mem_idx.insert(
"swap total".to_string(),
Value::Primitive(Primitive::Bytes(x.swap_total as u128 * 1024)),
);
mem_idx.insert(
"swap free".to_string(),
Value::Primitive(Primitive::Bytes(x.swap_free as u128 * 1024)),
);
idx.insert("mem".to_string(), Value::Object(Dictionary::from(mem_idx)));
}
/*
if let Ok(x) = disk_info() {
let mut disk_idx = indexmap::IndexMap::new();
disk_idx.insert(
"total".to_string(),
Value::Primitive(Primitive::Bytes(x.total as u128 * 1024)),
);
disk_idx.insert(
"free".to_string(),
Value::Primitive(Primitive::Bytes(x.free as u128 * 1024)),
);
}
*/
if let Ok(x) = hostname() {
idx.insert(
"hostname".to_string(),
Value::Primitive(Primitive::String(x)),
);
}
#[cfg(not(windows))]
{
if let Ok(x) = boottime() {
let mut boottime_idx = indexmap::IndexMap::new();
boottime_idx.insert(
"days".to_string(),
Value::Primitive(Primitive::Int(x.tv_sec / (24 * 3600))),
);
boottime_idx.insert(
"hours".to_string(),
Value::Primitive(Primitive::Int((x.tv_sec / 3600) % 24)),
);
boottime_idx.insert(
"mins".to_string(),
Value::Primitive(Primitive::Int((x.tv_sec / 60) % 60)),
);
idx.insert(
"uptime".to_string(),
Value::Object(Dictionary::from(boottime_idx)),
);
}
}
let system = sysinfo::System::new();
let components_list = system.get_components_list();
if components_list.len() > 0 {
let mut v = vec![];
for component in components_list {
let mut component_idx = indexmap::IndexMap::new();
component_idx.insert(
"name".to_string(),
Value::string(component.get_label().to_string()),
);
component_idx.insert(
"temp".to_string(),
Value::float(component.get_temperature() as f64),
);
component_idx.insert(
"max".to_string(),
Value::float(component.get_max() as f64),
);
if let Some(critical) = component.get_critical() {
component_idx.insert("critical".to_string(), Value::float(critical as f64));
}
v.push(Value::Object(Dictionary::from(component_idx)));
}
idx.insert("temps".to_string(), Value::List(v));
}
let disks = system.get_disks();
if disks.len() > 0 {
let mut v = vec![];
for disk in disks {
let mut disk_idx = indexmap::IndexMap::new();
disk_idx.insert(
"name".to_string(),
Value::string(disk.get_name().to_string_lossy()),
);
disk_idx.insert(
"available".to_string(),
Value::bytes(disk.get_available_space()),
);
disk_idx.insert(
"total".to_string(),
Value::bytes(disk.get_total_space()),
);
v.push(Value::Object(Dictionary::from(disk_idx)));
}
idx.insert("disks".to_string(), Value::List(v));
}
let network = system.get_network();
let incoming = network.get_income();
let outgoing = network.get_outcome();
let mut network_idx = indexmap::IndexMap::new();
network_idx.insert("incoming".to_string(), Value::bytes(incoming));
network_idx.insert("outgoing".to_string(), Value::bytes(outgoing));
idx.insert("network".to_string(), Value::Object(Dictionary::from(network_idx)));
// println!("{:#?}", system.get_network());
let mut stream = VecDeque::new();
stream.push_back(ReturnValue::Value(Value::Object(Dictionary::from(idx))));
Ok(stream.boxed())
}

16
src/commands/table.rs Normal file
View file

@ -0,0 +1,16 @@
use crate::commands::command::SinkCommandArgs;
use crate::errors::ShellError;
use crate::format::TableView;
use crate::prelude::*;
pub fn table(args: SinkCommandArgs) -> Result<(), ShellError> {
if args.input.len() > 0 {
let mut host = args.ctx.host.lock().unwrap();
let view = TableView::from_list(&args.input);
if let Some(view) = view {
handle_unexpected(&mut *host, |host| crate::format::print_view(&view, host));
}
}
Ok(())
}

17
src/commands/to_ini.rs Normal file
View file

@ -0,0 +1,17 @@
use crate::object::{Primitive, Value};
use crate::prelude::*;
pub fn to_ini(args: CommandArgs) -> Result<OutputStream, ShellError> {
let out = args.input;
let span = args.name_span;
Ok(out
.map(move |a| match serde_ini::to_string(&a) {
Ok(x) => ReturnValue::Value(Value::Primitive(Primitive::String(x))),
Err(_) => ReturnValue::Value(Value::Error(Box::new(ShellError::maybe_labeled_error(
"Can not convert to INI string",
"can not convert piped data to INI string",
span,
)))),
})
.boxed())
}

View file

@ -3,7 +3,15 @@ use crate::prelude::*;
pub fn to_json(args: CommandArgs) -> Result<OutputStream, ShellError> {
let out = args.input;
let span = args.name_span;
Ok(out
.map(|a| ReturnValue::Value(Value::Primitive(Primitive::String(serde_json::to_string(&a).unwrap()))))
.map(move |a| match serde_json::to_string(&a) {
Ok(x) => ReturnValue::Value(Value::Primitive(Primitive::String(x))),
Err(_) => ReturnValue::Value(Value::Error(Box::new(ShellError::maybe_labeled_error(
"Can not convert to JSON string",
"can not convert piped data to JSON string",
span,
)))),
})
.boxed())
}

View file

@ -3,7 +3,15 @@ use crate::prelude::*;
pub fn to_toml(args: CommandArgs) -> Result<OutputStream, ShellError> {
let out = args.input;
let span = args.name_span;
Ok(out
.map(|a| ReturnValue::Value(Value::Primitive(Primitive::String(toml::to_string(&a).unwrap()))))
.map(move |a| match toml::to_string(&a) {
Ok(x) => ReturnValue::Value(Value::Primitive(Primitive::String(x))),
Err(_) => ReturnValue::Value(Value::Error(Box::new(ShellError::maybe_labeled_error(
"Can not convert to TOML string",
"can not convert piped data to TOML string",
span,
)))),
})
.boxed())
}

View file

@ -6,13 +6,18 @@ use crate::prelude::*;
pub fn trim(args: CommandArgs) -> Result<OutputStream, ShellError> {
let input = args.input;
let span = args.name_span;
Ok(input
.map(move |v| match v {
Value::Primitive(Primitive::String(s)) => {
ReturnValue::Value(Value::Primitive(Primitive::String(s.trim().to_string())))
}
x => ReturnValue::Value(x),
_ => ReturnValue::Value(Value::Error(Box::new(ShellError::maybe_labeled_error(
"Expected string values from pipeline",
"expects strings from pipeline",
span,
)))),
})
.boxed())
}

View file

@ -4,15 +4,11 @@ use prettyprint::PrettyPrinter;
pub fn view(args: CommandArgs) -> Result<OutputStream, ShellError> {
if args.positional.len() == 0 {
if let Some(span) = args.name_span {
return Err(ShellError::labeled_error(
"View requires a filename",
"needs parameter",
span,
));
} else {
return Err(ShellError::string("view requires a filename."));
}
return Err(ShellError::maybe_labeled_error(
"View requires a filename",
"needs parameter",
args.name_span,
));
}
let target = match args.positional[0].as_string() {
@ -34,7 +30,7 @@ pub fn view(args: CommandArgs) -> Result<OutputStream, ShellError> {
.env
.lock()
.unwrap()
.first()
.front()
.unwrap()
.path()
.to_path_buf();

16
src/commands/vtable.rs Normal file
View file

@ -0,0 +1,16 @@
use crate::commands::command::SinkCommandArgs;
use crate::errors::ShellError;
use crate::format::VTableView;
use crate::prelude::*;
pub fn vtable(args: SinkCommandArgs) -> Result<(), ShellError> {
if args.input.len() > 0 {
let mut host = args.ctx.host.lock().unwrap();
let view = VTableView::from_list(&args.input);
if let Some(view) = view {
handle_unexpected(&mut *host, |host| crate::format::print_view(&view, host));
}
}
Ok(())
}

View file

@ -25,8 +25,12 @@ impl Command for Where {
}
pub fn r#where(args: CommandArgs) -> Result<OutputStream, ShellError> {
if args.positional.is_empty() {
return Err(ShellError::string("select requires a field"));
if args.positional.len() == 0 {
return Err(ShellError::maybe_labeled_error(
"Where requires a condition",
"needs condition",
args.name_span,
));
}
let block = args.positional[0].as_block()?;

View file

@ -13,16 +13,18 @@ pub struct Context {
commands: IndexMap<String, Arc<dyn Command>>,
sinks: IndexMap<String, Arc<dyn Sink>>,
crate host: Arc<Mutex<dyn Host + Send>>,
crate env: Arc<Mutex<Vec<Environment>>>,
crate env: Arc<Mutex<VecDeque<Environment>>>,
}
impl Context {
crate fn basic() -> Result<Context, Box<dyn Error>> {
let mut env = VecDeque::new();
env.push_back(Environment::basic()?);
Ok(Context {
commands: indexmap::IndexMap::new(),
sinks: indexmap::IndexMap::new(),
host: Arc::new(Mutex::new(crate::env::host::BasicHost)),
env: Arc::new(Mutex::new(vec![Environment::basic()?])),
env: Arc::new(Mutex::new(env)),
})
}

View file

@ -53,6 +53,20 @@ impl ShellError {
)
}
crate fn maybe_labeled_error(
msg: impl Into<String>,
label: impl Into<String>,
span: Option<Span>,
) -> ShellError {
match span {
Some(span) => ShellError::diagnostic(
Diagnostic::new(Severity::Error, msg.into())
.with_label(Label::new_primary(span).with_message(label.into())),
),
None => ShellError::string(msg),
}
}
crate fn string(title: impl Into<String>) -> ShellError {
ShellError::String(StringError::new(title.into(), Value::nothing()))
}

View file

@ -3,6 +3,7 @@ crate mod generic;
crate mod list;
crate mod table;
crate mod tree;
crate mod vtable;
use crate::prelude::*;
@ -10,6 +11,7 @@ crate use entries::EntriesView;
crate use generic::GenericView;
crate use table::TableView;
crate use tree::TreeView;
crate use vtable::VTableView;
crate trait RenderView {
fn render_view(&self, host: &mut dyn Host) -> Result<(), ShellError>;

View file

@ -6,11 +6,6 @@ use prettytable::format::{FormatBuilder, LinePosition, LineSeparator};
use prettytable::{color, Attr, Cell, Row, Table};
// An entries list is printed like this:
//
// name : ...
// name2 : ...
// another_name : ...
#[derive(new)]
pub struct TableView {
headers: Vec<DataDescriptor>,

84
src/format/vtable.rs Normal file
View file

@ -0,0 +1,84 @@
use crate::format::RenderView;
use crate::object::{DescriptorName, Value};
use crate::prelude::*;
use derive_new::new;
use prettytable::format::{FormatBuilder, LinePosition, LineSeparator};
use prettytable::{color, Attr, Cell, Row, Table};
#[derive(new)]
pub struct VTableView {
entries: Vec<Vec<String>>,
}
impl VTableView {
pub fn from_list(values: &[Value]) -> Option<VTableView> {
if values.len() == 0 {
return None;
}
let item = &values[0];
let headers = item.data_descriptors();
if headers.len() == 0 {
return None;
}
let mut entries = vec![];
for header in headers {
let mut row = vec![];
if let DescriptorName::String(s) = &header.name {
row.push(s.clone());
} else {
row.push("value".to_string());
}
for value in values {
row.push(value.get_data(&header).borrow().format_leaf(Some(&header)));
}
entries.push(row);
}
Some(VTableView { entries })
}
}
impl RenderView for VTableView {
fn render_view(&self, host: &mut dyn Host) -> Result<(), ShellError> {
if self.entries.len() == 0 {
return Ok(());
}
let mut table = Table::new();
let fb = FormatBuilder::new()
.separator(LinePosition::Top, LineSeparator::new('-', '+', ' ', ' '))
.separator(LinePosition::Bottom, LineSeparator::new('-', '+', ' ', ' '))
.column_separator('|')
.padding(1, 1);
table.set_format(fb.build());
for row in &self.entries {
table.add_row(Row::new(
row.iter()
.enumerate()
.map(|(idx, h)| {
if idx == 0 {
Cell::new(h)
.with_style(Attr::ForegroundColor(color::GREEN))
.with_style(Attr::Bold)
} else {
Cell::new(h)
}
})
.collect(),
));
}
table.print_term(&mut *host.out_terminal()).unwrap();
Ok(())
}
}

View file

@ -291,7 +291,12 @@ impl Value {
crate fn as_string(&self) -> Result<String, ShellError> {
match self {
Value::Primitive(Primitive::String(s)) => Ok(s.clone()),
Value::Primitive(Primitive::String(x)) => Ok(format!("{}", x)),
Value::Primitive(Primitive::Boolean(x)) => Ok(format!("{}", x)),
Value::Primitive(Primitive::Float(x)) => Ok(format!("{}", x.into_inner())),
Value::Primitive(Primitive::Int(x)) => Ok(format!("{}", x)),
Value::Primitive(Primitive::Bytes(x)) => Ok(format!("{}", x)),
//Value::Primitive(Primitive::String(s)) => Ok(s.clone()),
// TODO: this should definitely be more general with better errors
other => Err(ShellError::string(format!(
"Expected string, got {:?}",

View file

@ -383,7 +383,16 @@ impl Leaf {
fn as_external_arg(&self) -> String {
match self {
Leaf::String(s) => format!("\"{}\"", s),
Leaf::String(s) => {
#[cfg(windows)]
{
format!("{}", s)
}
#[cfg(not(windows))]
{
format!("\"{}\"", s)
}
}
Leaf::Bare(path) => format!("{}", path.to_string()),
Leaf::Boolean(b) => format!("{}", b),
Leaf::Int(i) => format!("{}", i),

1
tests/enter.out Normal file
View file

@ -0,0 +1 @@
markup

10
tests/enter.txt Normal file
View file

@ -0,0 +1,10 @@
cd tests
enter test.json
cd glossary
cd GlossDiv
cd GlossList
cd GlossEntry
cd GlossSee
ls | echo $it
exit
exit

1
tests/external_num.out Normal file
View file

@ -0,0 +1 @@
10

3
tests/external_num.txt Normal file
View file

@ -0,0 +1,3 @@
cd tests
open test.json | get glossary.GlossDiv.GlossList.GlossEntry.Height | echo $it
exit

1
tests/lines.out Normal file
View file

@ -0,0 +1 @@
rustyline

3
tests/lines.txt Normal file
View file

@ -0,0 +1,3 @@
cd tests
open test.toml --raw | lines | skip-while $it != "[dependencies]" | skip 1 | first 1 | split-column "=" | get Column1 | trim | echo $it
exit

1
tests/open_ini.out Normal file
View file

@ -0,0 +1 @@
1234

3
tests/open_ini.txt Normal file
View file

@ -0,0 +1,3 @@
cd tests
open test.ini | get SectionOne.integer | echo $it
exit

19
tests/test.ini Normal file
View file

@ -0,0 +1,19 @@
[SectionOne]
key = value
integer = 1234
real = 3.14
string1 = 'Case 1'
string2 = "Case 2"
[SectionTwo]
; comment line
key = new value
integer = 1234
real = 3.14
string1 = 'Case 1'
string2 = "Case 2"
string3 = 'Case 3'

View file

@ -1,20 +1,24 @@
{
"glossary": {
"title": "example glossary",
"GlossDiv": {
"GlossDiv": {
"title": "S",
"GlossList": {
"GlossList": {
"GlossEntry": {
"ID": "SGML",
"SortAs": "SGML",
"GlossTerm": "Standard Generalized Markup Language",
"Acronym": "SGML",
"Abbrev": "ISO 8879:1986",
"GlossDef": {
"SortAs": "SGML",
"GlossTerm": "Standard Generalized Markup Language",
"Acronym": "SGML",
"Abbrev": "ISO 8879:1986",
"Height": 10,
"GlossDef": {
"para": "A meta-markup language, used to create markup languages such as DocBook.",
"GlossSeeAlso": ["GML", "XML"]
"GlossSeeAlso": [
"GML",
"XML"
]
},
"GlossSee": "markup"
"GlossSee": "markup"
}
}
}

View file

@ -1,9 +1,9 @@
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use std::io::prelude::*;
use std::process::{Command, Stdio};
use std::error::Error;
use std::io::prelude::*;
use std::path::PathBuf;
use std::process::{Command, Stdio};
fn test_helper(test_name: &str) {
let mut baseline_path = PathBuf::new();
@ -27,10 +27,10 @@ mod tests {
let process = match Command::new(executable)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn() {
.spawn()
{
Ok(process) => process,
Err(why) => panic!("Can't run test {}", why.description())
Err(why) => panic!("Can't run test {}", why.description()),
};
let baseline_out = std::fs::read_to_string(baseline_path).unwrap();
@ -38,15 +38,13 @@ mod tests {
let input_commands = std::fs::read_to_string(txt_path).unwrap();
match process.stdin.unwrap().write_all(input_commands.as_bytes()) {
Err(why) => panic!("couldn't write to wc stdin: {}",
why.description()),
Ok(_) => {},
Err(why) => panic!("couldn't write to wc stdin: {}", why.description()),
Ok(_) => {}
}
let mut s = String::new();
match process.stdout.unwrap().read_to_string(&mut s) {
Err(why) => panic!("couldn't read stdout: {}",
why.description()),
Err(why) => panic!("couldn't read stdout: {}", why.description()),
Ok(_) => {
let s = s.replace("\r\n", "\n");
assert_eq!(s, baseline_out);
@ -69,6 +67,11 @@ mod tests {
test_helper("open_xml");
}
#[test]
fn open_ini() {
test_helper("open_ini");
}
#[test]
fn json_roundtrip() {
test_helper("json_roundtrip");
@ -88,4 +91,20 @@ mod tests {
fn split() {
test_helper("split");
}
#[test]
fn enter() {
test_helper("enter");
}
#[test]
fn lines() {
test_helper("lines");
}
#[test]
fn external_num() {
test_helper("external_num");
}
}