Merge master

This commit is contained in:
Jonathan Turner 2019-11-17 06:17:05 +13:00
commit 07db14f72e
195 changed files with 19791 additions and 5402 deletions

View file

@ -3,12 +3,27 @@ trigger:
strategy:
matrix:
linux-nightly:
linux-stable:
image: ubuntu-16.04
macos-nightly:
style: 'unflagged'
macos-stable:
image: macos-10.14
windows-nightly:
style: 'unflagged'
windows-stable:
image: vs2017-win2016
style: 'unflagged'
linux-nightly-canary:
image: ubuntu-16.04
style: 'canary'
macos-nightly-canary:
image: macos-10.14
style: 'canary'
windows-nightly-canary:
image: vs2017-win2016
style: 'canary'
fmt:
image: ubuntu-16.04
style: 'fmt'
pool:
vmImage: $(image)
@ -20,13 +35,19 @@ steps:
then
sudo apt-get -y install libxcb-composite0-dev libx11-dev
fi
curl https://sh.rustup.rs -sSf | sh -s -- -y --no-modify-path --default-toolchain `cat rust-toolchain`
curl https://sh.rustup.rs -sSf | sh -s -- -y --no-modify-path --default-toolchain "stable"
export PATH=$HOME/.cargo/bin:$PATH
rustup update
rustc -Vv
echo "##vso[task.prependpath]$HOME/.cargo/bin"
rustup component add rustfmt --toolchain `cat rust-toolchain`
rustup component add rustfmt --toolchain "stable"
displayName: Install Rust
- bash: RUSTFLAGS="-D warnings" cargo test --all-features
condition: eq(variables['style'], 'unflagged')
displayName: Run tests
- bash: NUSHELL_ENABLE_ALL_FLAGS=1 RUSTFLAGS="-D warnings" cargo test --all-features
condition: eq(variables['style'], 'canary')
displayName: Run tests
- bash: cargo fmt --all -- --check
condition: eq(variables['style'], 'fmt')
displayName: Lint

View file

@ -0,0 +1,3 @@
[build]
rustflags = "--cfg coloring_in_tokens"

30
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View file

@ -0,0 +1,30 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1.
2.
3.
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Configuration (please complete the following information):**
- OS: [e.g. Windows]
- Version [e.g. 0.4.0]
- Optional features (if any)
Add any other context about the problem here.

View file

@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View file

@ -2,7 +2,7 @@ name: Publish consumable Docker images
on:
push:
tags: ['*.*.*']
tags: ['v?[0-9]+.[0-9]+.[0-9]+*']
jobs:
compile:
@ -14,7 +14,11 @@ jobs:
- x86_64-unknown-linux-gnu
steps:
- uses: actions/checkout@v1
- run: cargo install cross
- name: Install rust-embedded/cross
env: { VERSION: v0.1.16 }
run: >-
wget -nv https://github.com/rust-embedded/cross/releases/download/${VERSION}/cross-${VERSION}-x86_64-unknown-linux-gnu.tar.gz
-O- | sudo tar xz -C /usr/local/bin/
- name: compile for specific target
env: { arch: '${{ matrix.arch }}' }
run: |
@ -31,6 +35,10 @@ jobs:
name: Build and publish docker images
needs: compile
runs-on: ubuntu-latest
env:
DOCKER_REGISTRY: quay.io/nushell
DOCKER_PASSWORD: ${{ secrets.DOCKER_REGISTRY }}
DOCKER_USER: ${{ secrets.DOCKER_USER }}
strategy:
matrix:
tag:
@ -58,41 +66,53 @@ jobs:
- uses: actions/download-artifact@master
with: { name: '${{ matrix.arch }}', path: target/release }
- name: Build and publish exact version
run: |
REGISTRY=${REGISTRY,,}; export TAG=${GITHUB_REF##*/}-${{ matrix.tag }};
run: |-
export DOCKER_TAG=${GITHUB_REF##*/}-${{ matrix.tag }}
export NU_BINS=target/release/$( [ ${{ matrix.plugin }} = true ] && echo nu* || echo nu )
export PATCH=$([ ${{ matrix.use-patch }} = true ] && echo .${{ matrix.tag }} || echo '')
chmod +x $NU_BINS
echo ${{ secrets.DOCKER_REGISTRY }} | docker login docker.pkg.github.com -u ${{ github.actor }} --password-stdin
echo ${DOCKER_PASSWORD} | docker login ${DOCKER_REGISTRY} -u ${DOCKER_USER} --password-stdin
docker-compose --file docker/docker-compose.package.yml build
docker-compose --file docker/docker-compose.package.yml push # exact version
env:
BASE_IMAGE: ${{ matrix.base-image }}
REGISTRY: docker.pkg.github.com/${{ github.repository }}
#region semantics tagging
- name: Retag and push without suffixing version
run: |
- name: Retag and push with suffixed version
run: |-
VERSION=${GITHUB_REF##*/}
docker tag ${REGISTRY,,}/nu:${VERSION}-${{ matrix.tag }} ${REGISTRY,,}/nu:${{ matrix.tag }}
docker tag ${REGISTRY,,}/nu:${VERSION}-${{ matrix.tag }} ${REGISTRY,,}/nu:${VERSION%%.*}-${{ matrix.tag }}
docker tag ${REGISTRY,,}/nu:${VERSION}-${{ matrix.tag }} ${REGISTRY,,}/nu:${VERSION%.*}-${{ matrix.tag }}
docker push ${REGISTRY,,}/nu:${VERSION%.*}-${{ matrix.tag }} # latest patch
docker push ${REGISTRY,,}/nu:${VERSION%%.*}-${{ matrix.tag }} # latest features
docker push ${REGISTRY,,}/nu:${{ matrix.tag }} # latest version
env: { REGISTRY: 'docker.pkg.github.com/${{ github.repository }}' }
latest_version=${VERSION%%%.*}-${{ matrix.tag }}
latest_feature=${VERSION%%.*}-${{ matrix.tag }}
latest_patch=${VERSION%.*}-${{ matrix.tag }}
exact_version=${VERSION}-${{ matrix.tag }}
tags=( ${latest_version} ${latest_feature} ${latest_patch} ${exact_version} )
for tag in ${tags[@]}; do
docker tag ${DOCKER_REGISTRY}/nu:${VERSION}-${{ matrix.tag }} ${DOCKER_REGISTRY}/nu:${tag}
docker push ${DOCKER_REGISTRY}/nu:${tag}
done
# latest version
docker tag ${DOCKER_REGISTRY}/nu:${VERSION}-${{ matrix.tag }} ${DOCKER_REGISTRY}/nu:${{ matrix.tag }}
docker push ${DOCKER_REGISTRY}/nu:${{ matrix.tag }}
- name: Retag and push debian as latest
if: matrix.tag == 'debian'
run: |
run: |-
VERSION=${GITHUB_REF##*/}
docker tag ${REGISTRY,,}/nu:${{ matrix.tag }} ${REGISTRY,,}/nu:latest
docker tag ${REGISTRY,,}/nu:${VERSION}-${{ matrix.tag }} ${REGISTRY,,}/nu:${VERSION%.*}
docker tag ${REGISTRY,,}/nu:${VERSION}-${{ matrix.tag }} ${REGISTRY,,}/nu:${VERSION%%.*}
docker tag ${REGISTRY,,}/nu:${VERSION}-${{ matrix.tag }} ${REGISTRY,,}/nu:${VERSION}
docker push ${REGISTRY,,}/nu:${VERSION} # exact version
docker push ${REGISTRY,,}/nu:${VERSION%%.*} # latest features
docker push ${REGISTRY,,}/nu:${VERSION%.*} # latest patch
docker push ${REGISTRY,,}/nu:latest # latest version
env: { REGISTRY: 'docker.pkg.github.com/${{ github.repository }}' }
# ${latest features} ${latest patch} ${exact version}
tags=( ${VERSION%%.*} ${VERSION%.*} ${VERSION} )
for tag in ${tags[@]}; do
docker tag ${DOCKER_REGISTRY}/nu:${VERSION}-${{ matrix.tag }} ${DOCKER_REGISTRY}/nu:${tag}
docker push ${DOCKER_REGISTRY}/nu:${tag}
done
# latest version
docker tag ${DOCKER_REGISTRY}/nu:${{ matrix.tag }} ${DOCKER_REGISTRY}/nu:latest
docker push ${DOCKER_REGISTRY}/nu:latest
#endregion semantics tagging

View file

@ -1,8 +1,8 @@
image:
file: .gitpod.Dockerfile
tasks:
- init: cargo build
command: cargo run
- init: cargo install nu
command: nu
github:
prebuilds:
# enable for the master/default branch (defaults to true)

1620
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[package]
name = "nu"
version = "0.3.0"
version = "0.5.1"
authors = ["Yehuda Katz <wycats@gmail.com>", "Jonathan Turner <jonathan.d.turner@gmail.com>", "Andrés N. Robalino <andres@androbtech.com>"]
description = "A shell for the GitHub era"
license = "MIT"
@ -14,80 +14,80 @@ documentation = "https://book.nushell.sh"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
rustyline = "5.0.3"
rustyline = "5.0.4"
chrono = { version = "0.4.9", features = ["serde"] }
derive-new = "0.5.8"
prettytable-rs = "0.8.0"
itertools = "0.8.0"
itertools = "0.8.1"
ansi_term = "0.12.1"
nom = "5.0.0"
nom = "5.0.1"
dunce = "1.0.0"
indexmap = { version = "1.2.0", features = ["serde-1"] }
indexmap = { version = "1.3.0", features = ["serde-1"] }
chrono-humanize = "0.0.11"
byte-unit = "3.0.1"
base64 = "0.10.1"
futures-preview = { version = "=0.3.0-alpha.18", features = ["compat", "io-compat"] }
async-stream = "0.1.1"
byte-unit = "3.0.3"
base64 = "0.11"
futures-preview = { version = "=0.3.0-alpha.19", features = ["compat", "io-compat"] }
async-stream = "0.1.2"
futures_codec = "0.2.5"
num-traits = "0.2.8"
term = "0.5.2"
bytes = "0.4.12"
log = "0.4.8"
pretty_env_logger = "0.3.1"
serde = { version = "1.0.100", features = ["derive"] }
serde = { version = "1.0.102", features = ["derive"] }
bson = { version = "0.14.0", features = ["decimal128"] }
serde_json = "1.0.40"
serde_json = "1.0.41"
serde-hjson = "0.9.1"
serde_yaml = "0.8"
serde_bytes = "0.11.2"
getset = "0.0.8"
#language-reporting = "0.3.1"
language-reporting = { git = "https://github.com/wycats/language-reporting" }
getset = "0.0.9"
language-reporting = "0.4.0"
app_dirs = "1.2.1"
csv = "1.1"
toml = "0.5.3"
toml = "0.5.5"
clap = "2.33.0"
git2 = { version = "0.10.1", default_features = false }
dirs = "2.0.2"
glob = "0.3.0"
ctrlc = "3.1.3"
surf = "1.0.2"
surf = "1.0.3"
url = "2.1.0"
roxmltree = "0.7.0"
roxmltree = "0.7.2"
nom_locate = "1.0.0"
enum-utils = "0.1.1"
nom-tracable = "0.4.1"
unicode-xid = "0.2.0"
serde_ini = "0.2.0"
subprocess = "0.1.18"
mime = "0.3.14"
pretty-hex = "0.1.0"
hex = "0.3.2"
pretty-hex = "0.1.1"
hex = "0.4"
tempfile = "3.1.0"
semver = "0.9.0"
which = "2.0.1"
uuid = {version = "0.7.4", features = [ "v4", "serde" ]}
which = "3.1"
textwrap = {version = "0.11.0", features = ["term_size"]}
shellexpand = "1.0.0"
futures-timer = "0.4.0"
futures-timer = "2.0.0"
pin-utils = "0.1.0-alpha.4"
num-bigint = { version = "0.2.3", features = ["serde"] }
bigdecimal = { version = "0.1.0", features = ["serde"] }
natural = "0.3.0"
serde_urlencoded = "0.6.1"
sublime_fuzzy = "0.5"
sublime_fuzzy = "0.6"
trash = "1.0.0"
regex = "1"
cfg-if = "0.1"
neso = { version = "0.5.0", optional = true }
crossterm = { version = "0.10.2", optional = true }
syntect = {version = "3.2.0", optional = true }
onig_sys = {version = "=69.1.0", optional = true }
heim = {version = "0.0.8-alpha.1", optional = true }
heim = {version = "0.0.8", optional = true }
battery = {version = "0.7.4", optional = true }
rawkey = {version = "0.1.2", optional = true }
clipboard = {version = "0.5", optional = true }
ptree = {version = "0.2", optional = true }
ptree = {version = "0.2" }
image = { version = "0.22.2", default_features = false, features = ["png_codec", "jpeg"], optional = true }
starship = { version = "0.22.0", optional = true}
starship = { version = "0.26.4", optional = true}
[features]
default = ["textview", "sys", "ps"]
@ -97,6 +97,7 @@ binaryview = ["image", "crossterm"]
sys = ["heim", "battery"]
ps = ["heim"]
starship-prompt = ["starship"]
# trace = ["nom-tracable/trace"]
[dependencies.rusqlite]
version = "0.20.0"
@ -105,6 +106,10 @@ features = ["bundled", "blob"]
[dev-dependencies]
pretty_assertions = "0.6.1"
[build-dependencies]
toml = "0.5.5"
serde = { version = "1.0.102", features = ["derive"] }
[lib]
name = "nu"
path = "src/lib.rs"
@ -117,18 +122,30 @@ path = "src/plugins/inc.rs"
name = "nu_plugin_sum"
path = "src/plugins/sum.rs"
[[bin]]
name = "nu_plugin_average"
path = "src/plugins/average.rs"
[[bin]]
name = "nu_plugin_embed"
path = "src/plugins/embed.rs"
[[bin]]
name = "nu_plugin_add"
path = "src/plugins/add.rs"
name = "nu_plugin_insert"
path = "src/plugins/insert.rs"
[[bin]]
name = "nu_plugin_edit"
path = "src/plugins/edit.rs"
[[bin]]
name = "nu_plugin_format"
path = "src/plugins/format.rs"
[[bin]]
name = "nu_plugin_parse"
path = "src/plugins/parse.rs"
[[bin]]
name = "nu_plugin_str"
path = "src/plugins/str.rs"

View file

@ -32,9 +32,9 @@ Try it in Gitpod.
## Local
Up-to-date installation instructions can be found in the [installation chapter of the book](https://book.nushell.sh/en/installation).
Up-to-date installation instructions can be found in the [installation chapter of the book](https://book.nushell.sh/en/installation). **Windows users**: please note that Nu works on Windows 10 and does not currently have Windows 7/8.1 support.
To build Nu, you will need to use the **nightly** version of the compiler.
To build Nu, you will need to use the **latest stable (1.39 or later)** version of the compiler.
Required dependencies:
@ -46,16 +46,16 @@ Optional dependencies:
* To use Nu with all possible optional features enabled, you'll also need the following:
* on Linux (on Debian/Ubuntu): `apt install libxcb-composite0-dev libx11-dev`
To install Nu via cargo (make sure you have installed [rustup](https://rustup.rs/) and the nightly compiler via `rustup install nightly`):
To install Nu via cargo (make sure you have installed [rustup](https://rustup.rs/) and the latest stable compiler via `rustup install stable`):
```
cargo +nightly install nu
cargo install nu
```
You can also install Nu with all the bells and whistles (be sure to have installed the [dependencies](https://book.nushell.sh/en/installation#dependencies) for your platform):
```
cargo +nightly install nu --all-features
cargo install nu --all-features
```
## Docker
@ -173,7 +173,7 @@ We can pipeline this into a command that gets the contents of one of the columns
━━━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━━━━┯━━━━━━━━━┯━━━━━━━━━┯━━━━━━┯━━━━━━━━━
authors │ description │ edition │ license │ name │ version
─────────────────┼────────────────────────────┼─────────┼─────────┼──────┼─────────
[table: 3 rows] │ A shell for the GitHub era │ 2018 │ ISC │ nu │ 0.3.0
[table: 3 rows] │ A shell for the GitHub era │ 2018 │ MIT │ nu │ 0.5.0
━━━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━━━━━━━━━━┷━━━━━━━━━┷━━━━━━━━━┷━━━━━━┷━━━━━━━━━
```
@ -181,7 +181,7 @@ 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.3.0
0.5.0
```
Here we use the variable `$it` to refer to the value being piped to the external command.
@ -200,7 +200,7 @@ Nu supports plugins that offer additional functionality to the shell and follow
There are a few examples in the `plugins` directory.
Plugins are binaries that are available in your path and follow a "nu_plugin_*" naming convention. These binaries interact with nu via a simple JSON-RPC protocol where the command identifies itself and passes along its configuration, which then makes it available for use. If the plugin is a filter, data streams to it one element at a time, and it can stream data back in return via stdin/stdout. If the plugin is a sink, it is given the full vector of final data and is given free reign over stdin/stdout to use as it pleases.
Plugins are binaries that are available in your path and follow a `nu_plugin_*` naming convention. These binaries interact with nu via a simple JSON-RPC protocol where the command identifies itself and passes along its configuration, which then makes it available for use. If the plugin is a filter, data streams to it one element at a time, and it can stream data back in return via stdin/stdout. If the plugin is a sink, it is given the full vector of final data and is given free reign over stdin/stdout to use as it pleases.
# Goals
@ -248,20 +248,27 @@ Nu adheres closely to a set of goals that make up its design philosophy. As feat
## Filters on tables (structured data)
| command | description |
| ------------- | ------------- |
| add column-or-column-path value | Add a new column to the table |
| append row-data | Append a row to the end of the table |
| count | Show the total number of rows |
| edit column-or-column-path value | Edit an existing column to have a new value |
| embed column | Creates a new table of one column with the given name, and places the current table inside of it |
| first amount | Show only the first number of rows |
| format pattern | Format table row data as a string following the given pattern |
| get column-or-column-path | Open column and get data from the corresponding cells |
| group-by column | Creates a new table with the data from the table rows grouped by the column given |
| histogram column ...column-names | Creates a new table with a histogram based on the column name passed in, optionally give the frequency column name
| inc (column-or-column-path) | Increment a value or version. Optionally use the column of a table |
| insert column-or-column-path value | Insert a new column to the table |
| last amount | Show only the last number of rows |
| nth row-number | Return only the selected row |
| pick ...columns | Down-select table to only these columns |
| pivot --header-row <headers> | Pivot the tables, making columns into rows and vice versa |
| prepend row-data | Prepend a row to the beginning of the table |
| reject ...columns | Remove the given columns from the table |
| reverse | Reverses the table. |
| skip amount | Skip a number of rows |
| skip-while condition | Skips rows while the condition matches. |
| split-by column | Creates a new table with the data from the inner tables splitted by the column given |
| sort-by ...columns | Sort by the given columns |
| str (column) | Apply string function. Optionally use the column of a table |
| sum | Sum a column of values |
@ -284,12 +291,14 @@ Nu adheres closely to a set of goals that make up its design philosophy. As feat
| from-ini | Parse text as .ini and create table |
| from-json | Parse text as .json and create table |
| from-sqlite | Parse binary data as sqlite .db and create table |
| from-ssv --minimum-spaces <minimum number of spaces to count as a separator> | Parse text as space-separated values and create table |
| from-toml | Parse text as .toml and create table |
| from-tsv | Parse text as .tsv and create table |
| from-url | Parse urlencoded string and create a table |
| from-xml | Parse text as .xml and create a table |
| from-yaml | Parse text as a .yaml/.yml and create a table |
| lines | Split single string into rows, one per line |
| parse pattern | Convert text to a table by matching the given pattern |
| size | Gather word count statistics on the text |
| split-column sep ...column-names | Split row contents across multiple columns via the separator, optionally give the columns names |
| split-row sep | Split row contents over multiple rows via the separator |

39
build.rs Normal file
View file

@ -0,0 +1,39 @@
use serde::Deserialize;
use std::collections::HashMap;
use std::collections::HashSet;
use std::env;
use std::path::Path;
#[derive(Deserialize)]
struct Feature {
#[allow(unused)]
description: String,
enabled: bool,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let input = env::var("CARGO_MANIFEST_DIR").unwrap();
let all_on = env::var("NUSHELL_ENABLE_ALL_FLAGS").is_ok();
let flags: HashSet<String> = env::var("NUSHELL_ENABLE_FLAGS")
.map(|s| s.split(",").map(|s| s.to_string()).collect())
.unwrap_or_else(|_| HashSet::new());
if all_on && !flags.is_empty() {
println!(
"cargo:warning={}",
"Both NUSHELL_ENABLE_ALL_FLAGS and NUSHELL_ENABLE_FLAGS were set. You don't need both."
);
}
let path = Path::new(&input).join("features.toml");
let toml: HashMap<String, Feature> = toml::from_str(&std::fs::read_to_string(path)?)?;
for (key, value) in toml.iter() {
if value.enabled == true || all_on || flags.contains(key) {
println!("cargo:rustc-cfg={}", key);
}
}
Ok(())
}

View file

@ -11,9 +11,9 @@ RUN apt-get update && apt-get install -y libssl-dev \
ARG RELEASE=false
WORKDIR /code
COPY ./rust-toolchain ./rust-toolchain
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --no-modify-path --default-toolchain `cat rust-toolchain`
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --no-modify-path --default-toolchain "stable"
ENV PATH=/root/.cargo/bin:$PATH
RUN rustup update
COPY . /code
RUN echo "##vso[task.prependpath]/root/.cargo/bin" && \
rustc -Vv && \

View file

@ -2,7 +2,7 @@ version: '3'
services:
nushell:
image: ${REGISTRY}/nu:${TAG}
image: ${DOCKER_REGISTRY}/nu:${DOCKER_TAG}
build:
context: ..
dockerfile: docker/Package${PATCH}.Dockerfile

53
docs/commands/append.md Normal file
View file

@ -0,0 +1,53 @@
# append
This command allows you to append the given row to the table.
**Note**:
- `append` does not change a file itself. If you want to save your changes, you need to run the `save` command
- if you want to add something containing a whitespace character, you need to put it in quotation marks
## Examples
Let's add more cities to this table:
```shell
> open cities.txt | lines
━━━┯━━━━━━━━━━━━
# │ <value>
───┼────────────
0 │ Canberra
1 │ London
2 │ Nairobi
3 │ Washington
━━━┷━━━━━━━━━━━━
```
You can add a new row by using `append`:
```shell
> open cities.txt | lines | append Beijing
━━━┯━━━━━━━━━━━━
# │ <value>
───┼────────────
0 │ Canberra
1 │ London
2 │ Nairobi
3 │ Washington
4 │ Beijing
━━━┷━━━━━━━━━━━━
```
It's not possible to add multiple rows at once, so you'll need to call `append` multiple times:
```shell
> open cities.txt | lines | append Beijing | append "Buenos Aires"
━━━┯━━━━━━━━━━━━━━
# │ <value>
───┼──────────────
0 │ Canberra
1 │ London
2 │ Nairobi
3 │ Washington
4 │ Beijing
5 │ Buenos Aires
━━━┷━━━━━━━━━━━━━━
```

45
docs/commands/average.md Normal file
View file

@ -0,0 +1,45 @@
# average
This command allows you to calculate the average of values in a column.
## Examples
To get the average of the file sizes in a directory, simply pipe the size column from the ls command to the average command.
```shell
> ls | get size | average
━━━━━━━━━
<value>
━━━━━━━━━
2282.727272727273
━━━━━━━━━
```
```shell
> pwd | split-row / | size | get chars | average
━━━━━━━━━
<value>
━━━━━━━━━
5.250000000000000
━━━━━━━━━
```
Note that average only works for integer and byte values. If the shell doesn't recognize the values in a column as one of those types, it will return an error.
One way to solve this is to convert each row to an integer when possible and then pipe the result to `average`
```shell
> open tests/fixtures/formats/caco3_plastics.csv | get tariff_item | average
error: Unrecognized type in stream: Primitive(String("2509000000"))
- shell:1:0
1 | open tests/fixtures/formats/caco3_plastics.csv | get tariff_item | average
| ^^^^ source
```
```shell
> open tests/fixtures/formats/caco3_plastics.csv | get tariff_item | str --to-int | average
━━━━━━━━━━━━━━━━━━━
<value>
───────────────────
3239404444.000000
━━━━━━━━━━━━━━━━━━━
```

View file

@ -2,7 +2,7 @@
If you didn't already know, the `cd` command is very simple. It stands for 'change directory' and it does exactly that. It changes the current directory to the one specified. If no directory is specified, it takes you to the home directory. Additionally, using `cd ..` takes you to the parent directory.
## Examples -
## Examples
```shell
/home/username> cd Desktop
@ -21,5 +21,13 @@ If you didn't already know, the `cd` command is very simple. It stands for 'chan
/home/username/Desktop/super/duper/crazy/nested/folders> cd
/home/username> cd ../../usr
/usr> cd
/home/username>
/home/username>
```
Using `cd -` will take you to the previous directory:
```shell
/home/username/Desktop/super/duper/crazy/nested/folders> cd
/home/username> cd -
/home/username/Desktop/super/duper/crazy/nested/folders> cd
```

48
docs/commands/count.md Normal file
View file

@ -0,0 +1,48 @@
# count
This command counts the number of rows in a table.
## Examples -
```shell
> ls
━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┯━━━━━━━━━━━┯━━━━━━━━━━┯━━━━━━━━━┯━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━
# │ name │ type │ readonly │ size │ created │ accessed │ modified
────┼──────────────────────────────┼───────────┼──────────┼─────────┼──────────────┼──────────────┼──────────────
0 │ Desktop │ Directory │ │ 4.1 KB │ 2 months ago │ 2 months ago │ 2 months ago
1 │ aur │ Directory │ │ 4.1 KB │ 4 hours ago │ 4 hours ago │ 4 hours ago
...
75 │ .emulator_console_auth_token │ File │ │ 16 B │ 2 months ago │ 2 months ago │ 2 months ago
76 │ bin │ Directory │ │ 4.1 KB │ 2 months ago │ 2 months ago │ 2 months ago
━━━━┷━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┷━━━━━━━━━━━┷━━━━━━━━━━┷━━━━━━━━━┷━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━
> ls | count
━━━━━━━━━
<value>
─────────
77
━━━━━━━━━
> ls | get name | count
━━━━━━━━━
<value>
─────────
77
━━━━━━━━━
> ls | where type == File | count
━━━━━━━━━
<value>
─────────
29
━━━━━━━━━
> ls | where type == Directory | count
━━━━━━━━━
<value>
─────────
48
━━━━━━━━━
> ls | where size > 2KB | count
━━━━━━━━━
<value>
─────────
57
━━━━━━━━━
```

112
docs/commands/from-csv.md Normal file
View file

@ -0,0 +1,112 @@
# from-csv
Converts csv data into table. Use this when nushell cannot dertermine the input file extension.
## Example
Let's say we have the following file :
```shell
> cat pets.txt
animal, name, age
cat, Tom, 7
dog, Alfred, 10
chameleon, Linda, 1
```
`pets.txt` is actually a .csv file but it has the .txt extension, `open` is not able to convert it into a table :
```shell
> open pets.txt
animal, name, age
cat, Tom, 7
dog, Alfred, 10
chameleon, Linda, 1
```
To get a table from `pets.txt` we need to use the `from-csv` command :
```shell
> open pets.txt | from-csv
━━━┯━━━━━━━━━━━┯━━━━━━━━━┯━━━━━━
# │ animal │ name │ age
───┼───────────┼─────────┼──────
0 │ cat │ Tom │ 7
1 │ dog │ Alfred │ 10
2 │ chameleon │ Linda │ 1
━━━┷━━━━━━━━━━━┷━━━━━━━━━┷━━━━━━
```
To ignore the csv headers use `--headerless` :
```shell
━━━┯━━━━━━━━━━━┯━━━━━━━━━┯━━━━━━━━━
# │ Column1 │ Column2 │ Column3
───┼───────────┼─────────┼─────────
0 │ dog │ Alfred │ 10
1 │ chameleon │ Linda │ 1
━━━┷━━━━━━━━━━━┷━━━━━━━━━┷━━━━━━━━━
```
To split on a character other than ',' use `--separator` :
```shell
> open pets.txt
animal; name; age
cat; Tom; 7
dog; Alfred; 10
chameleon; Linda; 1
```
```shell
> open pets.txt | from-csv --separator ';'
━━━┯━━━━━━━━━━━┯━━━━━━━━━┯━━━━━━
# │ animal │ name │ age
───┼───────────┼─────────┼──────
0 │ cat │ Tom │ 7
1 │ dog │ Alfred │ 10
2 │ chameleon │ Linda │ 1
━━━┷━━━━━━━━━━━┷━━━━━━━━━┷━━━━━━
```
To use this command to open a csv with separators other than a comma, use the `--raw` switch of `open` to open the csv, othewise the csv will enter `from-csv` as a table split on commas rather than raw text.
```shell
> mv pets.txt pets.csv
> open pets.csv | from-csv --separator ';'
error: Expected a string from pipeline
- shell:1:16
1 | open pets.csv | from-csv --separator ';'
| ^^^^^^^^ requires string input
- shell:1:0
1 | open pets.csv | from-csv --separator ';'
| value originates from here
> open pets.csv --raw | from-csv --separator ';'
━━━┯━━━━━━━━━━━┯━━━━━━━━━┯━━━━━━
# │ animal │ name │ age
───┼───────────┼─────────┼──────
0 │ cat │ Tom │ 7
1 │ dog │ Alfred │ 10
2 │ chameleon │ Linda │ 1
━━━┷━━━━━━━━━━━┷━━━━━━━━━┷━━━━━━
```
Note that separators are currently provided as strings and need to be wrapped in quotes.
```shell
> open pets.csv --raw | from-csv --separator ;
- shell:1:43
1 | open pets.csv --raw | from-csv --separator ;
| ^
```
It is also considered an error to use a separator greater than one char :
```shell
> open pets.txt | from-csv --separator '123'
error: Expected a single separator char from --separator
- shell:1:37
1 | open pets.txt | from-csv --separator '123'
| ^^^^^ requires a single character string input
```

View file

@ -0,0 +1,23 @@
# from-toml
Converts toml data into table. Use this when nushell cannot dertermine the input file extension.
## Example
Let's say we have the following Rust .lock file :
```shell
> open Cargo.lock
# This file is automatically @generated by Cargo.
# It is not intended for manual editing. [[package]] name = "adler32" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index"
...
```
The "Cargo.lock" file is actually a .toml file, but the file extension isn't .toml. That's okay, we can use the `from-toml` command :
```shell
> open Cargo.lock | from-toml
━━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━━
metadata │ package
────────────────┼───────────────────
[table: 1 row] │ [table: 154 rows]
━━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━
```

72
docs/commands/group-by.md Normal file
View file

@ -0,0 +1,72 @@
# group-by
This command creates a new table with the data from the table rows grouped by the column given.
## Examples
Let's say we have this table of all countries in the world sorted by their population:
```shell
> open countries_by_population.json | from-json | first 10
━━━┯━━━━━━┯━━━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━┯━━━━━━━━
# │ rank │ country or area │ UN continental region │ UN statistical region │ population 2018 │ population 2019 │ change
───┼──────┼─────────────────┼───────────────────────┼───────────────────────┼─────────────────┼─────────────────┼────────
0 │ 1 │ China │ Asia │ Eastern Asia │ 1,427,647,786 │ 1,433,783,686 │ +0.4%
1 │ 2 │ India │ Asia │ Southern Asia │ 1,352,642,280 │ 1,366,417,754 │ +1.0%
2 │ 3 │ United States │ Americas │ Northern America │ 327,096,265 │ 329,064,917 │ +0.6%
3 │ 4 │ Indonesia │ Asia │ South-eastern Asia │ 267,670,543 │ 270,625,568 │ +1.1%
4 │ 5 │ Pakistan │ Asia │ Southern Asia │ 212,228,286 │ 216,565,318 │ +2.0%
5 │ 6 │ Brazil │ Americas │ South America │ 209,469,323 │ 211,049,527 │ +0.8%
6 │ 7 │ Nigeria │ Africa │ Western Africa │ 195,874,683 │ 200,963,599 │ +2.6%
7 │ 8 │ Bangladesh │ Asia │ Southern Asia │ 161,376,708 │ 163,046,161 │ +1.0%
8 │ 9 │ Russia │ Europe │ Eastern Europe │ 145,734,038 │ 145,872,256 │ +0.1%
9 │ 10 │ Mexico │ Americas │ Central America │ 126,190,788 │ 127,575,529 │ +1.1%
━━━┷━━━━━━┷━━━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━┷━━━━━━━━
```
Here we have listed only the first 10 lines. In total this table has got 233 rows which is to big to get information easily out of it.
We can use the `group-by` command on 'UN statistical region' to create a table per continental region.
```shell
> open countries_by_population.json | from-json | group-by "UN continental region"
━━━━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━
Asia │ Americas │ Africa │ Europe │ Oceania
──────────────────┼──────────────────┼──────────────────┼──────────────────┼──────────────────
[table: 51 rows] │ [table: 53 rows] │ [table: 58 rows] │ [table: 48 rows] │ [table: 23 rows]
━━━━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━
```
Now we can already get some informations like "which continental regions are there" and "how many countries are in each region".
If we want to see only the countries in the continental region of Oceania we can type:
```shell
> open countries_by_population.json | from-json | group-by "UN continental region" | get Oceania
━━━━┯━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━┯━━━━━━━━
# │ rank │ country or area │ UN continental region │ UN statistical region │ population 2018 │ population 2019 │ change
────┼──────┼────────────────────────────────┼───────────────────────┼───────────────────────────┼─────────────────┼─────────────────┼────────
0 │ 55 │ Australia │ Oceania │ Australia and New Zealand │ 24,898,152 │ 25,203,198 │ +1.2%
1 │ 98 │ Papua New Guinea │ Oceania │ Melanesia │ 8,606,323 │ 8,776,109 │ +2.0%
2 │ 125 │ New Zealand │ Oceania │ Australia and New Zealand │ 4,743,131 │ 4,783,063 │ +0.8%
3 │ 161 │ Fiji │ Oceania │ Melanesia │ 883,483 │ 889,953 │ +0.7%
4 │ 166 │ Solomon Islands │ Oceania │ Melanesia │ 652,857 │ 669,823 │ +2.6%
5 │ 181 │ Vanuatu │ Oceania │ Melanesia │ 292,680 │ 299,882 │ +2.5%
6 │ 183 │ New Caledonia │ Oceania │ Melanesia │ 279,993 │ 282,750 │ +1.0%
7 │ 185 │ French Polynesia │ Oceania │ Polynesia │ 277,679 │ 279,287 │ +0.6%
8 │ 188 │ Samoa │ Oceania │ Polynesia │ 196,129 │ 197,097 │ +0.5%
9 │ 191 │ Guam │ Oceania │ Micronesia │ 165,768 │ 167,294 │ +0.9%
10 │ 193 │ Kiribati │ Oceania │ Micronesia │ 115,847 │ 117,606 │ +1.5%
11 │ 194 │ Federated States of Micronesia │ Oceania │ Micronesia │ 112,640 │ 113,815 │ +1.0%
12 │ 196 │ Tonga │ Oceania │ Polynesia │ 110,589 │ 110,940 │ +0.3%
13 │ 207 │ Marshall Islands │ Oceania │ Micronesia │ 58,413 │ 58,791 │ +0.6%
14 │ 209 │ Northern Mariana Islands │ Oceania │ Micronesia │ 56,882 │ 56,188 │ 1.2%
15 │ 210 │ American Samoa │ Oceania │ Polynesia │ 55,465 │ 55,312 │ 0.3%
16 │ 221 │ Palau │ Oceania │ Micronesia │ 17,907 │ 18,008 │ +0.6%
17 │ 222 │ Cook Islands │ Oceania │ Polynesia │ 17,518 │ 17,548 │ +0.2%
18 │ 224 │ Tuvalu │ Oceania │ Polynesia │ 11,508 │ 11,646 │ +1.2%
19 │ 225 │ Wallis and Futuna │ Oceania │ Polynesia │ 11,661 │ 11,432 │ 2.0%
20 │ 226 │ Nauru │ Oceania │ Micronesia │ 10,670 │ 10,756 │ +0.8%
21 │ 231 │ Niue │ Oceania │ Polynesia │ 1,620 │ 1,615 │ 0.3%
22 │ 232 │ Tokelau │ Oceania │ Polynesia │ 1,319 │ 1,340 │ +1.6%
━━━━┷━━━━━━┷━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━┷━━━━━━━━
```

53
docs/commands/pick.md Normal file
View file

@ -0,0 +1,53 @@
# pick
This command displays only the column names passed on to it.
## Examples
```shell
> ls
━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━━━━┯━━━━━━┯━━━━━━━━━━┯━━━━━━━━┯━━━━━━━━━━━━━┯━━━━━━━━━━━━━┯━━━━━━━━━━━━━
# │ name │ type │ readonly │ size │ created │ accessed │ modified
───┼────────────────────────────┼──────┼──────────┼────────┼─────────────┼─────────────┼─────────────
0 │ zeusiscrazy.txt │ File │ │ 556 B │ a month ago │ a month ago │ a month ago
1 │ coww.txt │ File │ │ 24 B │ a month ago │ a month ago │ a month ago
2 │ randomweirdstuff.txt │ File │ │ 197 B │ a month ago │ a month ago │ a month ago
3 │ abaracadabra.txt │ File │ │ 401 B │ a month ago │ a month ago │ a month ago
4 │ youshouldeatmorecereal.txt │ File │ │ 768 B │ a month ago │ a month ago │ a month ago
━━━┷━━━━━━━━━━━━━━━━━━━━━━━━━━━━┷━━━━━━┷━━━━━━━━━━┷━━━━━━━━┷━━━━━━━━━━━━━┷━━━━━━━━━━━━━┷━━━━━━━━━━━━━
> ls | pick name
━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# │ name
───┼────────────────────────────
0 │ zeusiscrazy.txt
1 │ coww.txt
2 │ randomweirdstuff.txt
3 │ abaracadabra.txt
4 │ youshouldeatmorecereal.txt
━━━┷━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```
The order in which you put the column names matters:
```shell
> ls | pick type name size
━━━┯━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━━━━┯━━━━━━━━
# │ type │ name │ size
───┼──────┼────────────────────────────┼────────
0 │ File │ zeusiscrazy.txt │ 556 B
1 │ File │ coww.txt │ 24 B
2 │ File │ randomweirdstuff.txt │ 197 B
3 │ File │ abaracadabra.txt │ 401 B
4 │ File │ youshouldeatmorecereal.txt │ 768 B
━━━┷━━━━━━┷━━━━━━━━━━━━━━━━━━━━━━━━━━━━┷━━━━━━━━
> ls | pick size type name
━━━┯━━━━━━━━┯━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# │ size │ type │ name
───┼────────┼──────┼────────────────────────────
0 │ 556 B │ File │ zeusiscrazy.txt
1 │ 24 B │ File │ coww.txt
2 │ 197 B │ File │ randomweirdstuff.txt
3 │ 401 B │ File │ abaracadabra.txt
4 │ 768 B │ File │ youshouldeatmorecereal.txt
━━━┷━━━━━━━━┷━━━━━━┷━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```

75
docs/commands/pivot.md Normal file
View file

@ -0,0 +1,75 @@
# pivot
Pivots the table contents so rows become columns and columns become rows.
## Examples
```sh
> ls docs
━━━┯━━━━━━━━━━━━━━━━━━━━┯━━━━━━━━━━━┯━━━━━━━━━━┯━━━━━━━━┯━━━━━━━━━━━━━┯━━━━━━━━━━━━━
# │ name │ type │ readonly │ size │ accessed │ modified
───┼────────────────────┼───────────┼──────────┼────────┼─────────────┼─────────────
0 │ docs/commands │ Directory │ │ 4.1 KB │ an hour ago │ an hour ago
1 │ docs/docker.md │ File │ │ 7.0 KB │ an hour ago │ a day ago
2 │ docs/philosophy.md │ File │ │ 896 B │ an hour ago │ a day ago
━━━┷━━━━━━━━━━━━━━━━━━━━┷━━━━━━━━━━━┷━━━━━━━━━━┷━━━━━━━━┷━━━━━━━━━━━━━┷━━━━━━━━━━━━━
> ls docs | pivot
━━━┯━━━━━━━━━━┯━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━
# │ Column0 │ Column1 │ Column2 │ Column3
───┼──────────┼───────────────┼────────────────┼────────────────────
0 │ name │ docs/commands │ docs/docker.md │ docs/philosophy.md
1 │ type │ Directory │ File │ File
2 │ readonly │ │ │
3 │ size │ 4.1 KB │ 7.0 KB │ 896 B
4 │ accessed │ an hour ago │ an hour ago │ an hour ago
5 │ modified │ an hour ago │ a day ago │ a day ago
━━━┷━━━━━━━━━━┷━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━━
```
Use `--header-row` to treat the first row as column names:
```shell
> ls docs | pivot --header-row
━━━┯━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━
# │ docs/commands │ docs/docker.md │ docs/philosophy.md
───┼───────────────┼────────────────┼────────────────────
0 │ Directory │ File │ File
1 │ │ │
2 │ 4.1 KB │ 7.0 KB │ 896 B
3 │ an hour ago │ an hour ago │ an hour ago
4 │ an hour ago │ a day ago │ a day ago
━━━┷━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━━
```
Use `--ignore-titles` to prevent pivoting the column names into values:
```shell
> ls docs | pivot --ignore-titles
━━━┯━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━
# │ Column0 │ Column1 │ Column2
───┼───────────────┼────────────────┼────────────────────
0 │ docs/commands │ docs/docker.md │ docs/philosophy.md
1 │ Directory │ File │ File
2 │ │ │
3 │ 4.1 KB │ 7.0 KB │ 896 B
4 │ an hour ago │ an hour ago │ an hour ago
5 │ an hour ago │ a day ago │ a day ago
━━━┷━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━━
```
Additional arguments are used as column names:
```shell
> ls docs | pivot foo bar baz
━━━┯━━━━━━━━━━┯━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━
# │ foo │ bar │ baz │ Column3
───┼──────────┼───────────────┼────────────────┼────────────────────
0 │ name │ docs/commands │ docs/docker.md │ docs/philosophy.md
1 │ type │ Directory │ File │ File
2 │ readonly │ │ │
3 │ size │ 4.1 KB │ 7.0 KB │ 896 B
4 │ accessed │ 2 hours ago │ 2 hours ago │ 2 hours ago
5 │ modified │ 2 hours ago │ a day ago │ a day ago
━━━┷━━━━━━━━━━┷━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━━
```

56
docs/commands/prepend.md Normal file
View file

@ -0,0 +1,56 @@
# prepend
This command prepends the given row to the front of the table
**Note**:
- `prepend` does not change a file itself. If you want to save your changes, you need to run the `save` command
- if you want to add something containing a whitespace character, you need to put it in quotation marks
## Examples
Let's complete this table with the missing continents:
```shell
> open continents.txt | lines
━━━┯━━━━━━━━━━━━━━━
# │ <value>
───┼───────────────
0 │ Africa
1 │ South America
2 │ Australia
3 │ Europe
4 │ Antarctica
━━━┷━━━━━━━━━━━━━━━
```
You can add a new row at the top by using `prepend`:
```shell
> open continents.txt | lines | prepend Asia
━━━┯━━━━━━━━━━━━━━━
# │ <value>
───┼───────────────
0 │ Asia
1 │ Africa
2 │ South America
3 │ Australia
4 │ Europe
5 │ Antarctica
━━━┷━━━━━━━━━━━━━━━
```
It's not possible to add multiple rows at once, so you'll need to call `prepend` multiple times:
```shell
> open continents.txt | lines | prepend Asia | prepend "North America"
━━━┯━━━━━━━━━━━━━━━
# │ <value>
───┼───────────────
0 │ North America
1 │ Asia
2 │ Africa
3 │ South America
4 │ Australia
5 │ Europe
6 │ Antarctica
━━━┷━━━━━━━━━━━━━━━
```

38
docs/commands/reject.md Normal file
View file

@ -0,0 +1,38 @@
# reject
This column removes or rejects the columns passed to it.
## Examples
```shell
> ls
━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━━━━┯━━━━━━┯━━━━━━━━━━┯━━━━━━━━┯━━━━━━━━━━━━━┯━━━━━━━━━━━━━┯━━━━━━━━━━━━━
# │ name │ type │ readonly │ size │ created │ accessed │ modified
───┼────────────────────────────┼──────┼──────────┼────────┼─────────────┼─────────────┼─────────────
0 │ zeusiscrazy.txt │ File │ │ 556 B │ a month ago │ a month ago │ a month ago
1 │ coww.txt │ File │ │ 24 B │ a month ago │ a month ago │ a month ago
2 │ randomweirdstuff.txt │ File │ │ 197 B │ a month ago │ a month ago │ a month ago
3 │ abaracadabra.txt │ File │ │ 401 B │ a month ago │ a month ago │ a month ago
4 │ youshouldeatmorecereal.txt │ File │ │ 768 B │ a month ago │ a month ago │ a month ago
━━━┷━━━━━━━━━━━━━━━━━━━━━━━━━━━━┷━━━━━━┷━━━━━━━━━━┷━━━━━━━━┷━━━━━━━━━━━━━┷━━━━━━━━━━━━━┷━━━━━━━━━━━━━
> ls | reject readonly
━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━━━━┯━━━━━━┯━━━━━━━━┯━━━━━━━━━━━━━┯━━━━━━━━━━━━━┯━━━━━━━━━━━━━
# │ name │ type │ size │ created │ accessed │ modified
───┼────────────────────────────┼──────┼────────┼─────────────┼─────────────┼─────────────
0 │ zeusiscrazy.txt │ File │ 556 B │ a month ago │ a month ago │ a month ago
1 │ coww.txt │ File │ 24 B │ a month ago │ a month ago │ a month ago
2 │ randomweirdstuff.txt │ File │ 197 B │ a month ago │ a month ago │ a month ago
3 │ abaracadabra.txt │ File │ 401 B │ a month ago │ a month ago │ a month ago
4 │ youshouldeatmorecereal.txt │ File │ 768 B │ a month ago │ a month ago │ a month ago
━━━┷━━━━━━━━━━━━━━━━━━━━━━━━━━━━┷━━━━━━┷━━━━━━━━┷━━━━━━━━━━━━━┷━━━━━━━━━━━━━┷━━━━━━━━━━━━━
> ls | reject readonly accessed
━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━━━━┯━━━━━━┯━━━━━━━━┯━━━━━━━━━━━━━┯━━━━━━━━━━━━━
# │ name │ type │ size │ created │ modified
───┼────────────────────────────┼──────┼────────┼─────────────┼─────────────
0 │ zeusiscrazy.txt │ File │ 556 B │ a month ago │ a month ago
1 │ coww.txt │ File │ 24 B │ a month ago │ a month ago
2 │ randomweirdstuff.txt │ File │ 197 B │ a month ago │ a month ago
3 │ abaracadabra.txt │ File │ 401 B │ a month ago │ a month ago
4 │ youshouldeatmorecereal.txt │ File │ 768 B │ a month ago │ a month ago
━━━┷━━━━━━━━━━━━━━━━━━━━━━━━━━━━┷━━━━━━┷━━━━━━━━┷━━━━━━━━━━━━━┷━━━━━━━━━━━━━
```

20
docs/commands/size.md Normal file
View file

@ -0,0 +1,20 @@
# size
This commands gives word count statistics on any text.
## Examples -
```shell
> open lalala.txt | size
━━━━━━━┯━━━━━━━┯━━━━━━━┯━━━━━━━━━━━━
lines │ words │ chars │ max length
───────┼───────┼───────┼────────────
4 │ 10 │ 72 │ 72
━━━━━━━┷━━━━━━━┷━━━━━━━┷━━━━━━━━━━━━
> open the_mysterious_affair_at_styles.txt | size
━━━━━━━┯━━━━━━━┯━━━━━━━━┯━━━━━━━━━━━━
lines │ words │ chars │ max length
───────┼───────┼────────┼────────────
8935 │ 62352 │ 349459 │ 361771
━━━━━━━┷━━━━━━━┷━━━━━━━━┷━━━━━━━━━━━━
```

56
docs/commands/sort-by.md Normal file
View file

@ -0,0 +1,56 @@
# env
The `sort-by` command sorts the table being displayed in the terminal by a chosen column(s).
`sort-by` takes multiple arguments (being the names of columns) sorting by each argument in order.
## Examples -
```shell
/home/example> ls | sort-by size
━━━┯━━━━━━┯━━━━━━┯━━━━━━━━━━┯━━━━━━━━┯━━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━
# │ name │ type │ readonly │ size │ accessed │ modified
───┼──────┼──────┼──────────┼────────┼────────────────┼────────────────
0 │ az │ File │ │ 18 B │ 4 minutes ago │ 4 minutes ago
1 │ a │ File │ │ 18 B │ 4 minutes ago │ 38 minutes ago
2 │ ad │ File │ │ 18 B │ 4 minutes ago │ 4 minutes ago
3 │ ac │ File │ │ 18 B │ 4 minutes ago │ 4 minutes ago
4 │ ab │ File │ │ 18 B │ 4 minutes ago │ 4 minutes ago
5 │ c │ File │ │ 102 B │ 35 minutes ago │ 35 minutes ago
6 │ d │ File │ │ 189 B │ 35 minutes ago │ 34 minutes ago
7 │ b │ File │ │ 349 B │ 35 minutes ago │ 35 minutes ago
━━━┷━━━━━━┷━━━━━━┷━━━━━━━━━━┷━━━━━━━━┷━━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━
```
```shell
/home/example> ls | sort-by size name
━━━┯━━━━━━┯━━━━━━┯━━━━━━━━━━┯━━━━━━━━┯━━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━
# │ name │ type │ readonly │ size │ accessed │ modified
───┼──────┼──────┼──────────┼────────┼────────────────┼────────────────
0 │ a │ File │ │ 18 B │ 4 minutes ago │ 39 minutes ago
1 │ ab │ File │ │ 18 B │ 4 minutes ago │ 4 minutes ago
2 │ ac │ File │ │ 18 B │ 4 minutes ago │ 4 minutes ago
3 │ ad │ File │ │ 18 B │ 4 minutes ago │ 4 minutes ago
4 │ az │ File │ │ 18 B │ 4 minutes ago │ 4 minutes ago
5 │ c │ File │ │ 102 B │ 36 minutes ago │ 35 minutes ago
6 │ d │ File │ │ 189 B │ 35 minutes ago │ 35 minutes ago
7 │ b │ File │ │ 349 B │ 36 minutes ago │ 36 minutes ago
```
```
/home/example> ls | sort-by accessed
━━━┯━━━━━━┯━━━━━━┯━━━━━━━━━━┯━━━━━━━━┯━━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━
# │ name │ type │ readonly │ size │ accessed │ modified
───┼──────┼──────┼──────────┼────────┼────────────────┼────────────────
0 │ b │ File │ │ 349 B │ 37 minutes ago │ 37 minutes ago
1 │ c │ File │ │ 102 B │ 37 minutes ago │ 37 minutes ago
2 │ d │ File │ │ 189 B │ 37 minutes ago │ 36 minutes ago
3 │ a │ File │ │ 18 B │ 6 minutes ago │ 40 minutes ago
4 │ ab │ File │ │ 18 B │ 6 minutes ago │ 6 minutes ago
5 │ ac │ File │ │ 18 B │ 6 minutes ago │ 6 minutes ago
6 │ ad │ File │ │ 18 B │ 5 minutes ago │ 5 minutes ago
7 │ az │ File │ │ 18 B │ 5 minutes ago │ 5 minutes ago
━━━┷━━━━━━┷━━━━━━┷━━━━━━━━━━┷━━━━━━━━┷━━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━
```

50
docs/commands/str.md Normal file
View file

@ -0,0 +1,50 @@
# str
Consumes either a single value or a table and converts the provided data to a string and optionally applies a change.
## Examples
```shell
> shells
━━━┯━━━┯━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# │ │ name │ path
───┼───┼────────────┼────────────────────────────────
0 │ X │ filesystem │ /home/TUX/stuff/expr/stuff
1 │ │ filesystem │ /
━━━┷━━━┷━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
> shells | str path --upcase
━━━┯━━━┯━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# │ │ name │ path
───┼───┼────────────┼────────────────────────────────
0 │ X │ filesystem │ /HOME/TUX/STUFF/EXPR/STUFF
1 │ │ filesystem │ /
━━━┷━━━┷━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
> shells | str path --downcase
━━━┯━━━┯━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# │ │ name │ path
───┼───┼────────────┼────────────────────────────────
0 │ X │ filesystem │ /home/tux/stuff/expr/stuff
1 │ │ filesystem │ /
━━━┷━━━┷━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
> shells | str # --substring "21, 99"
━━━┯━━━┯━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# │ │ name │ path
───┼───┼────────────┼────────────────────────────────
0 │ X │ filesystem │ stuff
1 │ │ filesystem │
━━━┷━━━┷━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
> shells | str # --substring "6,"
━━━┯━━━┯━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# │ │ name │ path
───┼───┼────────────┼────────────────────────────────
0 │ X │ filesystem │ TUX/stuff/expr/stuff
1 │ │ filesystem │
━━━┷━━━┷━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
> echo "1, 2, 3" | split-row "," | str --to-int | sum
━━━━━━━━━
<value>
─────────
6
━━━━━━━━━
```

44
docs/commands/sum.md Normal file
View file

@ -0,0 +1,44 @@
# sum
This command allows you to calculate the sum of values in a column.
## Examples
To get the sum of the file sizes in a directory, simply pipe the size column from the ls command to the sum command.
```shell
> ls | get size | sum
━━━━━━━━━
value
━━━━━━━━━
51.0 MB
━━━━━━━━━
```
To get the sum of the characters that make up your present working directory.
```shell
> pwd | split-row / | size | get chars | sum
━━━━━━━━━
<value>
━━━━━━━━━
21
━━━━━━━━━
```
Note that sum only works for integer and byte values. If the shell doesn't recognize the values in a column as one of those types, it will return an error.
One way to solve this is to convert each row to an integer when possible and then pipe the result to `sum`
```shell
> open tests/fixtures/formats/caco3_plastics.csv | get tariff_item | sum
error: Unrecognized type in stream: Primitive(String("2509000000"))
- shell:1:0
1 | open tests/fixtures/formats/caco3_plastics.csv | get tariff_item | sum
| ^^^^ source
```
```shell
> open tests/fixtures/formats/caco3_plastics.csv | get tariff_item | str --to-int | sum
━━━━━━━━━━━━━
<value>
─────────────
29154639996
━━━━━━━━━━━━━
```

47
docs/commands/tags.md Normal file
View file

@ -0,0 +1,47 @@
# tags
The tags commands allows users to access the metadata of the previous value in
the pipeline. This command may be run on multiple values of input as well.
As of writing this, the only metadata returned includes:
- `span`: the start and end indices of the previous value's substring location
- `anchor`: the source where data was loaded from; this may not appear if the
previous pipeline value didn't actually have a source (like trying to `open` a
dir, or running `ls` on a dir)
## Examples
```shell
> open README.md | tags
━━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
span │ anchor
────────────────┼──────────────────────────────────────────────────
[table: 1 row] │ /Users/danielh/Projects/github/nushell/README.md
━━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```
```shell
> open README.md | tags | get span
━━━━━━━┯━━━━━
start │ end
───────┼─────
5 │ 14
━━━━━━━┷━━━━━
```
```shell
> ls | tags | first 3 | get span
━━━┯━━━━━━━┯━━━━━
# │ start │ end
───┼───────┼─────
0 │ 0 │ 2
1 │ 0 │ 2
2 │ 0 │ 2
━━━┷━━━━━━━┷━━━━━
```
## Reference
More useful information on the `tags` command can be found by referencing [The
Nu Book's entry on Metadata](https://book.nushell.sh/en/metadata)

21
features.toml Normal file
View file

@ -0,0 +1,21 @@
[hintsv1]
description = "Adding hints based upon error states in the syntax highlighter"
enabled = false
[coloring_in_tokens]
description = "Move coloring into the TokensIterator so they can be atomic with the rest of the iterator"
reason = """
This is laying the groundwork for merging coloring and parsing. It also makes token_nodes.atomic() naturally
work with coloring, which is pretty useful on its own.
"""
enabled = false
[data_processing_primitives]
description = "Groundwork so tables can be data processed"
reason = """
These will allow take tables and be able to transform, process, and explore.
"""
enabled = false

View file

@ -1 +0,0 @@
beta-2019-09-25

View file

@ -1,4 +1,3 @@
use crate::commands::autoview;
use crate::commands::classified::{
ClassifiedCommand, ClassifiedInputStream, ClassifiedPipeline, ExternalCommand, InternalCommand,
StreamNext,
@ -14,18 +13,22 @@ use crate::fuzzysearch::{interactive_fuzzy_search, SelectionResult};
#[cfg(not(feature = "starship-prompt"))]
use crate::git::current_branch;
use crate::parser::registry::Signature;
use crate::parser::{hir, CallNode, Pipeline, PipelineElement, TokenNode};
use crate::parser::{
hir,
hir::syntax_shape::{expand_syntax, ExpandContext, PipelineShape},
hir::{expand_external_tokens::ExternalTokensShape, tokens_iterator::TokensIterator},
TokenNode,
};
use crate::prelude::*;
use log::{debug, trace};
use log::{debug, log_enabled, trace};
use rustyline::error::ReadlineError;
use rustyline::{self, config::Configurer, config::EditMode, ColorMode, Config, Editor};
use std::env;
use std::error::Error;
use std::io::{BufRead, BufReader, Write};
use std::iter::Iterator;
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::atomic::Ordering;
#[derive(Debug)]
pub enum MaybeOwned<'a, T> {
@ -76,7 +79,7 @@ fn load_plugin(path: &std::path::Path, context: &mut Context) -> Result<(), Shel
let name = params.name.clone();
let fname = fname.to_string();
if context.has_command(&name) {
if let Some(_) = context.get_command(&name) {
trace!("plugin {:?} already loaded.", &name);
} else {
if params.is_filter {
@ -95,11 +98,17 @@ fn load_plugin(path: &std::path::Path, context: &mut Context) -> Result<(), Shel
},
Err(e) => {
trace!("incompatible plugin {:?}", input);
Err(ShellError::string(format!("Error: {:?}", e)))
Err(ShellError::untagged_runtime_error(format!(
"Error: {:?}",
e
)))
}
}
}
Err(e) => Err(ShellError::string(format!("Error: {:?}", e))),
Err(e) => Err(ShellError::untagged_runtime_error(format!(
"Error: {:?}",
e
))),
};
let _ = child.wait();
@ -110,13 +119,6 @@ fn load_plugin(path: &std::path::Path, context: &mut Context) -> Result<(), Shel
fn search_paths() -> Vec<std::path::PathBuf> {
let mut search_paths = Vec::new();
match env::var_os("PATH") {
Some(paths) => {
search_paths = env::split_paths(&paths).collect::<Vec<_>>();
}
None => println!("PATH is not defined in the environment."),
}
#[cfg(debug_assertions)]
{
// Use our debug plugins in debug mode
@ -131,6 +133,15 @@ fn search_paths() -> Vec<std::path::PathBuf> {
#[cfg(not(debug_assertions))]
{
use std::env;
match env::var_os("PATH") {
Some(paths) => {
search_paths = env::split_paths(&paths).collect::<Vec<_>>();
}
None => println!("PATH is not defined in the environment."),
}
// Use our release plugins in release mode
let mut path = std::path::PathBuf::from(".");
path.push("target");
@ -154,6 +165,8 @@ fn load_plugins(context: &mut Context) -> Result<(), ShellError> {
require_literal_leading_dot: false,
};
set_env_from_config();
for path in search_paths() {
let mut pattern = path.to_path_buf();
@ -249,13 +262,14 @@ pub async fn cli() -> Result<(), Box<dyn Error>> {
whole_stream_command(Next),
whole_stream_command(Previous),
whole_stream_command(Debug),
whole_stream_command(Lines),
whole_stream_command(Shells),
whole_stream_command(SplitColumn),
whole_stream_command(SplitRow),
whole_stream_command(Lines),
whole_stream_command(Reject),
whole_stream_command(Reverse),
whole_stream_command(Append),
whole_stream_command(Prepend),
whole_stream_command(Trim),
whole_stream_command(ToBSON),
whole_stream_command(ToCSV),
@ -267,12 +281,15 @@ pub async fn cli() -> Result<(), Box<dyn Error>> {
whole_stream_command(ToURL),
whole_stream_command(ToYAML),
whole_stream_command(SortBy),
whole_stream_command(GroupBy),
whole_stream_command(Tags),
whole_stream_command(Count),
whole_stream_command(First),
whole_stream_command(Last),
whole_stream_command(Env),
whole_stream_command(FromCSV),
whole_stream_command(FromTSV),
whole_stream_command(FromSSV),
whole_stream_command(FromINI),
whole_stream_command(FromBSON),
whole_stream_command(FromJSON),
@ -285,6 +302,7 @@ pub async fn cli() -> Result<(), Box<dyn Error>> {
whole_stream_command(FromYML),
whole_stream_command(Pick),
whole_stream_command(Get),
whole_stream_command(Histogram),
per_item_command(Remove),
per_item_command(Fetch),
per_item_command(Open),
@ -295,6 +313,7 @@ pub async fn cli() -> Result<(), Box<dyn Error>> {
whole_stream_command(SkipWhile),
per_item_command(Enter),
per_item_command(Help),
per_item_command(History),
whole_stream_command(Exit),
whole_stream_command(Autoview),
whole_stream_command(Pivot),
@ -303,11 +322,23 @@ pub async fn cli() -> Result<(), Box<dyn Error>> {
per_item_command(Mkdir),
per_item_command(Move),
whole_stream_command(Save),
whole_stream_command(SplitBy),
whole_stream_command(Table),
whole_stream_command(Version),
whole_stream_command(Which),
]);
cfg_if::cfg_if! {
if #[cfg(data_processing_primitives)] {
context.add_commands(vec![
whole_stream_command(ReduceBy),
whole_stream_command(EvaluateBy),
whole_stream_command(TSortBy),
whole_stream_command(MapMaxBy),
]);
}
}
#[cfg(feature = "clipboard")]
{
context.add_commands(vec![whole_stream_command(
@ -315,6 +346,7 @@ pub async fn cli() -> Result<(), Box<dyn Error>> {
)]);
}
}
let _ = load_plugins(&mut context);
let config = Config::builder().color_mode(ColorMode::Forced).build();
@ -328,24 +360,21 @@ pub async fn cli() -> Result<(), Box<dyn Error>> {
// we are ok if history does not exist
let _ = rl.load_history(&History::path());
let ctrl_c = Arc::new(AtomicBool::new(false));
let cc = ctrl_c.clone();
let cc = context.ctrl_c.clone();
ctrlc::set_handler(move || {
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 context.ctrl_c.load(Ordering::SeqCst) {
context.ctrl_c.store(false, Ordering::SeqCst);
continue;
}
let cwd = context.shell_manager.path();
rl.set_helper(Some(crate::shell::Helper::new(
context.shell_manager.clone(),
)));
rl.set_helper(Some(crate::shell::Helper::new(context.clone())));
let edit_mode = config::config(Tag::unknown())?
.get("edit_mode")
@ -416,6 +445,7 @@ pub async fn cli() -> Result<(), Box<dyn Error>> {
match process_line(readline, &mut context).await {
LineResult::Success(line) => {
rl.add_history_entry(line.clone());
let _ = rl.save_history(&History::path());
}
LineResult::CtrlC => {
@ -441,21 +471,12 @@ pub async fn cli() -> Result<(), Box<dyn Error>> {
}
}
LineResult::Error(mut line, err) => {
LineResult::Error(line, err) => {
rl.add_history_entry(line.clone());
let diag = err.to_diagnostic();
let _ = rl.save_history(&History::path());
context.with_host(|host| {
let writer = host.err_termcolor();
line.push_str(" ");
let files = crate::parser::Files::new(line);
let _ = std::panic::catch_unwind(move || {
let _ = language_reporting::emit(
&mut writer.lock(),
&files,
&diag,
&language_reporting::DefaultConfig,
);
});
print_err(err, host, &Text::from(line));
})
}
@ -472,6 +493,78 @@ pub async fn cli() -> Result<(), Box<dyn Error>> {
Ok(())
}
fn chomp_newline(s: &str) -> &str {
if s.ends_with('\n') {
&s[..s.len() - 1]
} else {
s
}
}
fn set_env_from_config() {
let config = crate::data::config::read(Tag::unknown(), &None).unwrap();
if config.contains_key("env") {
// Clear the existing vars, we're about to replace them
for (key, _value) in std::env::vars() {
std::env::remove_var(key);
}
let value = config.get("env");
match value {
Some(Tagged {
item: Value::Row(r),
..
}) => {
for (k, v) in &r.entries {
match v.as_string() {
Ok(value_string) => {
std::env::set_var(k, value_string);
}
_ => {}
}
}
}
_ => {}
}
}
if config.contains_key("path") {
// Override the path with what they give us from config
let value = config.get("path");
match value {
Some(value) => match value {
Tagged {
item: Value::Table(table),
..
} => {
let mut paths = vec![];
for val in table {
let path_str = val.as_string();
match path_str {
Err(_) => {}
Ok(path_str) => {
paths.push(PathBuf::from(path_str));
}
}
}
let path_os_string = std::env::join_paths(&paths);
match path_os_string {
Ok(path_os_string) => {
std::env::set_var("PATH", path_os_string);
}
Err(_) => {}
}
}
_ => {}
},
None => {}
}
}
}
enum LineResult {
Success(String),
Error(String, ShellError),
@ -484,9 +577,11 @@ async fn process_line(readline: Result<String, ReadlineError>, ctx: &mut Context
Ok(line) if line.trim() == "" => LineResult::Success(line.clone()),
Ok(line) => {
let result = match crate::parser::parse(&line, uuid::Uuid::nil()) {
let line = chomp_newline(line);
let result = match crate::parser::parse(&line) {
Err(err) => {
return LineResult::Error(line.clone(), err);
return LineResult::Error(line.to_string(), err);
}
Ok(val) => val,
@ -497,28 +592,32 @@ async fn process_line(readline: Result<String, ReadlineError>, ctx: &mut Context
let mut pipeline = match classify_pipeline(&result, ctx, &Text::from(line)) {
Ok(pipeline) => pipeline,
Err(err) => return LineResult::Error(line.clone(), err),
Err(err) => return LineResult::Error(line.to_string(), err),
};
match pipeline.commands.last() {
Some(ClassifiedCommand::External(_)) => {}
_ => pipeline
.commands
.item
.push(ClassifiedCommand::Internal(InternalCommand {
command: whole_stream_command(autoview::Autoview),
name: "autoview".to_string(),
name_tag: Tag::unknown(),
args: hir::Call::new(
Box::new(hir::Expression::synthetic_string("autoview")),
None,
None,
),
)
.spanned_unknown(),
})),
}
let mut input = ClassifiedInputStream::new();
let mut iter = pipeline.commands.item.into_iter().peekable();
let mut iter = pipeline.commands.into_iter().peekable();
let mut is_first_command = true;
// Check the config to see if we need to update the path
// TODO: make sure config is cached so we don't path this load every call
set_env_from_config();
loop {
let item: Option<ClassifiedCommand> = iter.next();
@ -527,16 +626,24 @@ async fn process_line(readline: Result<String, ReadlineError>, ctx: &mut Context
input = match (item, next) {
(None, _) => break,
(Some(ClassifiedCommand::Dynamic(_)), _)
| (_, Some(ClassifiedCommand::Dynamic(_))) => {
return LineResult::Error(
line.to_string(),
ShellError::unimplemented("Dynamic commands"),
)
}
(Some(ClassifiedCommand::Expr(_)), _) => {
return LineResult::Error(
line.clone(),
line.to_string(),
ShellError::unimplemented("Expression-only commands"),
)
}
(_, Some(ClassifiedCommand::Expr(_))) => {
return LineResult::Error(
line.clone(),
line.to_string(),
ShellError::unimplemented("Expression-only commands"),
)
}
@ -544,31 +651,46 @@ async fn process_line(readline: Result<String, ReadlineError>, ctx: &mut Context
(
Some(ClassifiedCommand::Internal(left)),
Some(ClassifiedCommand::External(_)),
) => match left
.run(ctx, input, Text::from(line), is_first_command)
.await
{
) => match left.run(ctx, input, Text::from(line)) {
Ok(val) => ClassifiedInputStream::from_input_stream(val),
Err(err) => return LineResult::Error(line.clone(), err),
Err(err) => return LineResult::Error(line.to_string(), err),
},
(Some(ClassifiedCommand::Internal(left)), Some(_)) => {
match left
.run(ctx, input, Text::from(line), is_first_command)
.await
{
match left.run(ctx, input, Text::from(line)) {
Ok(val) => ClassifiedInputStream::from_input_stream(val),
Err(err) => return LineResult::Error(line.clone(), err),
Err(err) => return LineResult::Error(line.to_string(), err),
}
}
(Some(ClassifiedCommand::Internal(left)), None) => {
match left
.run(ctx, input, Text::from(line), is_first_command)
.await
{
Ok(val) => ClassifiedInputStream::from_input_stream(val),
Err(err) => return LineResult::Error(line.clone(), err),
match left.run(ctx, input, Text::from(line)) {
Ok(val) => {
use futures::stream::TryStreamExt;
let mut output_stream: OutputStream = val.into();
loop {
match output_stream.try_next().await {
Ok(Some(ReturnSuccess::Value(Tagged {
item: Value::Error(e),
..
}))) => {
return LineResult::Error(line.to_string(), e);
}
Ok(Some(_item)) => {
if ctx.ctrl_c.load(Ordering::SeqCst) {
break;
}
}
_ => {
break;
}
}
}
return LineResult::Success(line.to_string());
}
Err(err) => return LineResult::Error(line.to_string(), err),
}
}
@ -577,28 +699,26 @@ async fn process_line(readline: Result<String, ReadlineError>, ctx: &mut Context
Some(ClassifiedCommand::External(_)),
) => match left.run(ctx, input, StreamNext::External).await {
Ok(val) => val,
Err(err) => return LineResult::Error(line.clone(), err),
Err(err) => return LineResult::Error(line.to_string(), 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),
Err(err) => return LineResult::Error(line.to_string(), err),
}
}
(Some(ClassifiedCommand::External(left)), None) => {
match left.run(ctx, input, StreamNext::Last).await {
Ok(val) => val,
Err(err) => return LineResult::Error(line.clone(), err),
Err(err) => return LineResult::Error(line.to_string(), err),
}
}
};
is_first_command = false;
}
LineResult::Success(line.clone())
LineResult::Success(line.to_string())
}
Err(ReadlineError::Interrupted) => LineResult::CtrlC,
Err(ReadlineError::Eof) => LineResult::Break,
@ -614,95 +734,62 @@ fn classify_pipeline(
context: &Context,
source: &Text,
) -> Result<ClassifiedPipeline, ShellError> {
let pipeline = pipeline.as_pipeline()?;
let mut pipeline_list = vec![pipeline.clone()];
let mut iterator = TokensIterator::all(&mut pipeline_list, pipeline.span());
let Pipeline { parts, .. } = pipeline;
let result = expand_syntax(
&PipelineShape,
&mut iterator,
&context.expand_context(source),
)
.map_err(|err| err.into());
let commands: Result<Vec<_>, ShellError> = parts
.iter()
.map(|item| classify_command(&item, context, &source))
.collect();
Ok(ClassifiedPipeline {
commands: commands?,
})
}
fn classify_command(
command: &PipelineElement,
context: &Context,
source: &Text,
) -> Result<ClassifiedCommand, ShellError> {
let call = command.call();
match call {
// If the command starts with `^`, treat it as an external command no matter what
call if call.head().is_external() => {
let name_tag = call.head().expect_external();
let name = name_tag.slice(source);
Ok(external_command(call, source, name.tagged(name_tag)))
}
// Otherwise, if the command is a bare word, we'll need to triage it
call if call.head().is_bare() => {
let head = call.head();
let name = head.source(source);
match context.has_command(name) {
// if the command is in the registry, it's an internal command
true => {
let command = context.get_command(name);
let config = command.signature();
trace!(target: "nu::build_pipeline", "classifying {:?}", config);
let args: hir::Call = config.parse_args(call, &context, source)?;
trace!(target: "nu::build_pipeline", "args :: {}", args.debug(source));
Ok(ClassifiedCommand::Internal(InternalCommand {
command,
name_tag: head.tag(),
args,
}))
}
// otherwise, it's an external command
false => Ok(external_command(call, source, name.tagged(head.tag()))),
}
}
// If the command is something else (like a number or a variable), that is currently unsupported.
// We might support `$somevar` as a curried command in the future.
call => Err(ShellError::invalid_command(call.head().tag())),
if log_enabled!(target: "nu::expand_syntax", log::Level::Debug) {
println!("");
ptree::print_tree(&iterator.expand_tracer().print(source.clone())).unwrap();
println!("");
}
result
}
// Classify this command as an external command, which doesn't give special meaning
// to nu syntactic constructs, and passes all arguments to the external command as
// strings.
fn external_command(
call: &Tagged<CallNode>,
source: &Text,
pub(crate) fn external_command(
tokens: &mut TokensIterator,
context: &ExpandContext,
name: Tagged<&str>,
) -> ClassifiedCommand {
let arg_list_strings: Vec<Tagged<String>> = match call.children() {
Some(args) => args
.iter()
.filter_map(|i| match i {
TokenNode::Whitespace(_) => None,
other => Some(other.as_external_arg(source).tagged(other.tag())),
})
.collect(),
None => vec![],
};
) -> Result<ClassifiedCommand, ParseError> {
let Spanned { item, span } = expand_syntax(&ExternalTokensShape, tokens, context)?;
let (name, tag) = name.into_parts();
ClassifiedCommand::External(ExternalCommand {
Ok(ClassifiedCommand::External(ExternalCommand {
name: name.to_string(),
name_tag: tag,
args: arg_list_strings,
})
name_tag: name.tag(),
args: item
.iter()
.map(|x| Tagged {
tag: x.span.into(),
item: x.item.clone(),
})
.collect::<Vec<_>>()
.spanned(span),
}))
}
pub fn print_err(err: ShellError, host: &dyn Host, source: &Text) {
let diag = err.to_diagnostic();
let writer = host.err_termcolor();
let mut source = source.to_string();
source.push_str(" ");
let files = crate::parser::Files::new(source);
let _ = std::panic::catch_unwind(move || {
let _ = language_reporting::emit(
&mut writer.lock(),
&files,
&diag,
&language_reporting::DefaultConfig,
);
});
}

View file

@ -1,6 +1,9 @@
#[macro_use]
pub(crate) mod macros;
mod from_structured_data;
pub(crate) mod append;
pub(crate) mod args;
pub(crate) mod autoview;
pub(crate) mod cd;
@ -8,12 +11,15 @@ pub(crate) mod classified;
pub(crate) mod clip;
pub(crate) mod command;
pub(crate) mod config;
pub(crate) mod count;
pub(crate) mod cp;
pub(crate) mod date;
pub(crate) mod debug;
pub(crate) mod echo;
pub(crate) mod enter;
pub(crate) mod env;
#[allow(unused)]
pub(crate) mod evaluate_by;
pub(crate) mod exit;
pub(crate) mod fetch;
pub(crate) mod first;
@ -22,16 +28,22 @@ pub(crate) mod from_csv;
pub(crate) mod from_ini;
pub(crate) mod from_json;
pub(crate) mod from_sqlite;
pub(crate) mod from_ssv;
pub(crate) mod from_toml;
pub(crate) mod from_tsv;
pub(crate) mod from_url;
pub(crate) mod from_xml;
pub(crate) mod from_yaml;
pub(crate) mod get;
pub(crate) mod group_by;
pub(crate) mod help;
pub(crate) mod histogram;
pub(crate) mod history;
pub(crate) mod last;
pub(crate) mod lines;
pub(crate) mod ls;
#[allow(unused)]
pub(crate) mod map_max_by;
pub(crate) mod mkdir;
pub(crate) mod mv;
pub(crate) mod next;
@ -41,8 +53,11 @@ pub(crate) mod pick;
pub(crate) mod pivot;
pub(crate) mod plugin;
pub(crate) mod post;
pub(crate) mod prepend;
pub(crate) mod prev;
pub(crate) mod pwd;
#[allow(unused)]
pub(crate) mod reduce_by;
pub(crate) mod reject;
pub(crate) mod reverse;
pub(crate) mod rm;
@ -51,8 +66,11 @@ pub(crate) mod shells;
pub(crate) mod size;
pub(crate) mod skip_while;
pub(crate) mod sort_by;
pub(crate) mod split_by;
pub(crate) mod split_column;
pub(crate) mod split_row;
#[allow(unused)]
pub(crate) mod t_sort_by;
pub(crate) mod table;
pub(crate) mod tags;
pub(crate) mod to_bson;
@ -75,13 +93,18 @@ pub(crate) use command::{
UnevaluatedCallInfo, WholeStreamCommand,
};
pub(crate) use append::Append;
pub(crate) use classified::ClassifiedCommand;
pub(crate) use config::Config;
pub(crate) use count::Count;
pub(crate) use cp::Cpy;
pub(crate) use date::Date;
pub(crate) use debug::Debug;
pub(crate) use echo::Echo;
pub(crate) use enter::Enter;
pub(crate) use env::Env;
#[allow(unused)]
pub(crate) use evaluate_by::EvaluateBy;
pub(crate) use exit::Exit;
pub(crate) use fetch::Fetch;
pub(crate) use first::First;
@ -91,6 +114,7 @@ pub(crate) use from_ini::FromINI;
pub(crate) use from_json::FromJSON;
pub(crate) use from_sqlite::FromDB;
pub(crate) use from_sqlite::FromSQLite;
pub(crate) use from_ssv::FromSSV;
pub(crate) use from_toml::FromTOML;
pub(crate) use from_tsv::FromTSV;
pub(crate) use from_url::FromURL;
@ -98,10 +122,15 @@ pub(crate) use from_xml::FromXML;
pub(crate) use from_yaml::FromYAML;
pub(crate) use from_yaml::FromYML;
pub(crate) use get::Get;
pub(crate) use group_by::GroupBy;
pub(crate) use help::Help;
pub(crate) use histogram::Histogram;
pub(crate) use history::History;
pub(crate) use last::Last;
pub(crate) use lines::Lines;
pub(crate) use ls::LS;
#[allow(unused)]
pub(crate) use map_max_by::MapMaxBy;
pub(crate) use mkdir::Mkdir;
pub(crate) use mv::Move;
pub(crate) use next::Next;
@ -110,8 +139,11 @@ pub(crate) use open::Open;
pub(crate) use pick::Pick;
pub(crate) use pivot::Pivot;
pub(crate) use post::Post;
pub(crate) use prepend::Prepend;
pub(crate) use prev::Previous;
pub(crate) use pwd::PWD;
#[allow(unused)]
pub(crate) use reduce_by::ReduceBy;
pub(crate) use reject::Reject;
pub(crate) use reverse::Reverse;
pub(crate) use rm::Remove;
@ -120,8 +152,11 @@ pub(crate) use shells::Shells;
pub(crate) use size::Size;
pub(crate) use skip_while::SkipWhile;
pub(crate) use sort_by::SortBy;
pub(crate) use split_by::SplitBy;
pub(crate) use split_column::SplitColumn;
pub(crate) use split_row::SplitRow;
#[allow(unused)]
pub(crate) use t_sort_by::TSortBy;
pub(crate) use table::Table;
pub(crate) use tags::Tags;
pub(crate) use to_bson::ToBSON;

47
src/commands/append.rs Normal file
View file

@ -0,0 +1,47 @@
use crate::commands::WholeStreamCommand;
use crate::errors::ShellError;
use crate::parser::CommandRegistry;
use crate::prelude::*;
#[derive(Deserialize)]
struct AppendArgs {
row: Tagged<Value>,
}
pub struct Append;
impl WholeStreamCommand for Append {
fn name(&self) -> &str {
"append"
}
fn signature(&self) -> Signature {
Signature::build("append").required(
"row value",
SyntaxShape::Any,
"the value of the row to append to the table",
)
}
fn usage(&self) -> &str {
"Append the given row to the table"
}
fn run(
&self,
args: CommandArgs,
registry: &CommandRegistry,
) -> Result<OutputStream, ShellError> {
args.process(registry, append)?.run()
}
}
fn append(
AppendArgs { row }: AppendArgs,
RunnableContext { input, .. }: RunnableContext,
) -> Result<OutputStream, ShellError> {
let mut after: VecDeque<Tagged<Value>> = VecDeque::new();
after.push_back(row);
Ok(OutputStream::from_input(input.values.chain(after)))
}

View file

@ -1,9 +1,14 @@
use crate::commands::{RawCommandArgs, WholeStreamCommand};
use crate::errors::ShellError;
use crate::parser::hir::{Expression, NamedArguments};
use crate::prelude::*;
use futures::stream::TryStreamExt;
use std::sync::atomic::Ordering;
pub struct Autoview;
const STREAM_PAGE_SIZE: u64 = 50;
#[derive(Deserialize)]
pub struct AutoviewArgs {}
@ -31,61 +36,132 @@ impl WholeStreamCommand for Autoview {
pub fn autoview(
AutoviewArgs {}: AutoviewArgs,
mut context: RunnableContext,
context: RunnableContext,
raw: RawCommandArgs,
) -> Result<OutputStream, ShellError> {
Ok(OutputStream::new(async_stream! {
let input = context.input.drain_vec().await;
let binary = context.get_command("binaryview");
let text = context.get_command("textview");
let table = context.get_command("table");
if input.len() > 0 {
if let Tagged {
item: Value::Primitive(Primitive::Binary(_)),
..
} = input[0usize]
{
let binary = context.get_command("binaryview");
if let Some(binary) = binary {
let result = binary.run(raw.with_input(input), &context.commands, false);
result.collect::<Vec<_>>().await;
} else {
for i in input {
match i.item {
Value::Primitive(Primitive::Binary(b)) => {
use pretty_hex::*;
println!("{:?}", b.hex_dump());
Ok(OutputStream::new(async_stream! {
let mut output_stream: OutputStream = context.input.into();
match output_stream.try_next().await {
Ok(Some(x)) => {
match output_stream.try_next().await {
Ok(Some(y)) => {
let ctrl_c = context.ctrl_c.clone();
let stream = async_stream! {
yield Ok(x);
yield Ok(y);
loop {
match output_stream.try_next().await {
Ok(Some(z)) => {
if ctrl_c.load(Ordering::SeqCst) {
break;
}
yield Ok(z);
}
_ => break,
}
}
};
if let Some(table) = table {
let mut new_output_stream: OutputStream = stream.to_output_stream();
let mut finished = false;
let mut current_idx = 0;
loop {
let mut new_input = VecDeque::new();
for _ in 0..STREAM_PAGE_SIZE {
match new_output_stream.try_next().await {
Ok(Some(a)) => {
if let ReturnSuccess::Value(v) = a {
new_input.push_back(v);
}
}
_ => {
finished = true;
break;
}
}
}
let raw = raw.clone();
let mut command_args = raw.with_input(new_input.into());
let mut named_args = NamedArguments::new();
named_args.insert_optional("start_number", Some(Expression::number(current_idx, Tag::unknown())));
command_args.call_info.args.named = Some(named_args);
let result = table.run(command_args, &context.commands);
result.collect::<Vec<_>>().await;
if finished {
break;
} else {
current_idx += STREAM_PAGE_SIZE;
}
}
_ => {}
}
}
};
} else if is_single_anchored_text_value(&input) {
let text = context.get_command("textview");
if let Some(text) = text {
let result = text.run(raw.with_input(input), &context.commands, false);
result.collect::<Vec<_>>().await;
} else {
for i in input {
match i.item {
Value::Primitive(Primitive::String(s)) => {
println!("{}", s);
_ => {
if let ReturnSuccess::Value(x) = x {
match x {
Tagged {
item: Value::Primitive(Primitive::String(ref s)),
tag: Tag { anchor, span },
} if anchor.is_some() => {
if let Some(text) = text {
let mut stream = VecDeque::new();
stream.push_back(Value::string(s).tagged(Tag { anchor, span }));
let result = text.run(raw.with_input(stream.into()), &context.commands);
result.collect::<Vec<_>>().await;
} else {
println!("{}", s);
}
}
Tagged {
item: Value::Primitive(Primitive::String(s)),
..
} => {
println!("{}", s);
}
Tagged { item: Value::Primitive(Primitive::Binary(ref b)), .. } => {
if let Some(binary) = binary {
let mut stream = VecDeque::new();
stream.push_back(x.clone());
let result = binary.run(raw.with_input(stream.into()), &context.commands);
result.collect::<Vec<_>>().await;
} else {
use pretty_hex::*;
println!("{:?}", b.hex_dump());
}
}
Tagged { item: Value::Error(e), .. } => {
yield Err(e);
}
Tagged { item: ref item, .. } => {
if let Some(table) = table {
let mut stream = VecDeque::new();
stream.push_back(x.clone());
let result = table.run(raw.with_input(stream.into()), &context.commands);
result.collect::<Vec<_>>().await;
} else {
println!("{:?}", item);
}
}
}
_ => {}
}
}
}
} else if is_single_text_value(&input) {
for i in input {
match i.item {
Value::Primitive(Primitive::String(s)) => {
println!("{}", s);
}
_ => {}
}
}
} else {
let table = context.expect_command("table");
let result = table.run(raw.with_input(input), &context.commands, false);
result.collect::<Vec<_>>().await;
}
_ => {
//println!("<no results>");
}
}
@ -95,34 +171,3 @@ pub fn autoview(
}
}))
}
fn is_single_text_value(input: &Vec<Tagged<Value>>) -> bool {
if input.len() != 1 {
return false;
}
if let Tagged {
item: Value::Primitive(Primitive::String(_)),
..
} = input[0]
{
true
} else {
false
}
}
fn is_single_anchored_text_value(input: &Vec<Tagged<Value>>) -> bool {
if input.len() != 1 {
return false;
}
if let Tagged {
item: Value::Primitive(Primitive::String(_)),
tag: Tag { anchor, .. },
} = input[0]
{
anchor != uuid::Uuid::nil()
} else {
false
}
}

View file

@ -10,7 +10,11 @@ impl WholeStreamCommand for CD {
}
fn signature(&self) -> Signature {
Signature::build("cd").optional("directory", SyntaxShape::Path)
Signature::build("cd").optional(
"directory",
SyntaxShape::Path,
"the directory to change to",
)
}
fn usage(&self) -> &str {

View file

@ -1,12 +1,13 @@
use crate::commands::Command;
use crate::parser::{hir, TokenNode};
use crate::prelude::*;
use bytes::{BufMut, BytesMut};
use derive_new::new;
use futures::stream::StreamExt;
use futures_codec::{Decoder, Encoder, Framed};
use itertools::Itertools;
use log::{log_enabled, trace};
use std::fmt;
use std::io::{Error, ErrorKind};
use std::sync::Arc;
use subprocess::Exec;
/// A simple `Codec` implementation that splits up data into lines.
@ -53,7 +54,7 @@ pub(crate) struct ClassifiedInputStream {
impl ClassifiedInputStream {
pub(crate) fn new() -> ClassifiedInputStream {
ClassifiedInputStream {
objects: VecDeque::new().into(),
objects: vec![Value::nothing().tagged(Tag::unknown())].into(),
stdin: None,
}
}
@ -73,125 +74,212 @@ impl ClassifiedInputStream {
}
}
#[derive(Debug, Clone)]
pub(crate) struct ClassifiedPipeline {
pub(crate) commands: Vec<ClassifiedCommand>,
pub(crate) commands: Spanned<Vec<ClassifiedCommand>>,
}
impl FormatDebug for ClassifiedPipeline {
fn fmt_debug(&self, f: &mut DebugFormatter, source: &str) -> fmt::Result {
f.say_str(
"classified pipeline",
self.commands.iter().map(|c| c.debug(source)).join(" | "),
)
}
}
impl HasSpan for ClassifiedPipeline {
fn span(&self) -> Span {
self.commands.span
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub(crate) enum ClassifiedCommand {
#[allow(unused)]
Expr(TokenNode),
Internal(InternalCommand),
#[allow(unused)]
Dynamic(Spanned<hir::Call>),
External(ExternalCommand),
}
impl FormatDebug for ClassifiedCommand {
fn fmt_debug(&self, f: &mut DebugFormatter, source: &str) -> fmt::Result {
match self {
ClassifiedCommand::Expr(expr) => expr.fmt_debug(f, source),
ClassifiedCommand::Internal(internal) => internal.fmt_debug(f, source),
ClassifiedCommand::Dynamic(dynamic) => dynamic.fmt_debug(f, source),
ClassifiedCommand::External(external) => external.fmt_debug(f, source),
}
}
}
impl HasSpan for ClassifiedCommand {
fn span(&self) -> Span {
match self {
ClassifiedCommand::Expr(node) => node.span(),
ClassifiedCommand::Internal(command) => command.span(),
ClassifiedCommand::Dynamic(call) => call.span,
ClassifiedCommand::External(command) => command.span(),
}
}
}
#[derive(new, Debug, Clone, Eq, PartialEq)]
pub(crate) struct InternalCommand {
pub(crate) command: Arc<Command>,
pub(crate) name: String,
pub(crate) name_tag: Tag,
pub(crate) args: Spanned<hir::Call>,
}
impl HasSpan for InternalCommand {
fn span(&self) -> Span {
let start = self.name_tag.span;
start.until(self.args.span)
}
}
impl FormatDebug for InternalCommand {
fn fmt_debug(&self, f: &mut DebugFormatter, source: &str) -> fmt::Result {
f.say("internal", self.args.debug(source))
}
}
#[derive(new, Debug, Eq, PartialEq)]
pub(crate) struct DynamicCommand {
pub(crate) args: hir::Call,
}
impl InternalCommand {
pub(crate) async fn run(
pub(crate) fn run(
self,
context: &mut Context,
input: ClassifiedInputStream,
source: Text,
is_first_command: bool,
) -> Result<InputStream, ShellError> {
if log_enabled!(log::Level::Trace) {
trace!(target: "nu::run::internal", "->");
trace!(target: "nu::run::internal", "{}", self.command.name());
trace!(target: "nu::run::internal", "{}", self.name);
trace!(target: "nu::run::internal", "{}", self.args.debug(&source));
}
let objects: InputStream =
trace_stream!(target: "nu::trace_stream::internal", "input" = input.objects);
let result = context.run_command(
self.command,
self.name_tag.clone(),
context.source_map.clone(),
self.args,
&source,
objects,
is_first_command,
);
let command = context.expect_command(&self.name);
let result = {
context.run_command(
command,
self.name_tag.clone(),
self.args.item,
&source,
objects,
)
};
let result = trace_out_stream!(target: "nu::trace_stream::internal", source: &source, "output" = result);
let mut result = result.values;
let mut context = context.clone();
let mut stream = VecDeque::new();
while let Some(item) = result.next().await {
match item? {
ReturnSuccess::Action(action) => match action {
CommandAction::ChangePath(path) => {
context.shell_manager.set_path(path);
}
CommandAction::AddAnchorLocation(uuid, anchor_location) => {
context.add_anchor_location(uuid, anchor_location);
}
CommandAction::Exit => std::process::exit(0), // TODO: save history.txt
CommandAction::EnterHelpShell(value) => {
match value {
Tagged {
item: Value::Primitive(Primitive::String(cmd)),
tag,
} => {
context.shell_manager.insert_at_current(Box::new(
HelpShell::for_command(
Value::string(cmd).tagged(tag),
&context.registry(),
)?,
));
}
_ => {
context.shell_manager.insert_at_current(Box::new(
HelpShell::index(&context.registry())?,
));
let stream = async_stream! {
while let Some(item) = result.next().await {
match item {
Ok(ReturnSuccess::Action(action)) => match action {
CommandAction::ChangePath(path) => {
context.shell_manager.set_path(path);
}
CommandAction::Exit => std::process::exit(0), // TODO: save history.txt
CommandAction::EnterHelpShell(value) => {
match value {
Tagged {
item: Value::Primitive(Primitive::String(cmd)),
tag,
} => {
context.shell_manager.insert_at_current(Box::new(
HelpShell::for_command(
Value::string(cmd).tagged(tag),
&context.registry(),
).unwrap(),
));
}
_ => {
context.shell_manager.insert_at_current(Box::new(
HelpShell::index(&context.registry()).unwrap(),
));
}
}
}
}
CommandAction::EnterValueShell(value) => {
context
.shell_manager
.insert_at_current(Box::new(ValueShell::new(value)));
}
CommandAction::EnterShell(location) => {
context.shell_manager.insert_at_current(Box::new(
FilesystemShell::with_location(location, context.registry().clone())?,
));
}
CommandAction::PreviousShell => {
context.shell_manager.prev();
}
CommandAction::NextShell => {
context.shell_manager.next();
}
CommandAction::LeaveShell => {
context.shell_manager.remove_at_current();
if context.shell_manager.is_empty() {
std::process::exit(0); // TODO: save history.txt
CommandAction::EnterValueShell(value) => {
context
.shell_manager
.insert_at_current(Box::new(ValueShell::new(value)));
}
}
},
CommandAction::EnterShell(location) => {
context.shell_manager.insert_at_current(Box::new(
FilesystemShell::with_location(location, context.registry().clone()).unwrap(),
));
}
CommandAction::PreviousShell => {
context.shell_manager.prev();
}
CommandAction::NextShell => {
context.shell_manager.next();
}
CommandAction::LeaveShell => {
context.shell_manager.remove_at_current();
if context.shell_manager.is_empty() {
std::process::exit(0); // TODO: save history.txt
}
}
},
ReturnSuccess::Value(v) => {
stream.push_back(v);
Ok(ReturnSuccess::Value(v)) => {
yield Ok(v);
}
Err(x) => {
yield Ok(Value::Error(x).tagged_unknown());
break;
}
}
}
}
};
Ok(stream.into())
Ok(stream.to_input_stream())
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub(crate) struct ExternalCommand {
pub(crate) name: String,
pub(crate) name_tag: Tag,
pub(crate) args: Vec<Tagged<String>>,
pub(crate) args: Spanned<Vec<Tagged<String>>>,
}
impl FormatDebug for ExternalCommand {
fn fmt_debug(&self, f: &mut DebugFormatter, source: &str) -> fmt::Result {
write!(f, "{}", self.name)?;
if self.args.item.len() > 0 {
write!(f, " ")?;
write!(f, "{}", self.args.iter().map(|i| i.debug(source)).join(" "))?;
}
Ok(())
}
}
impl HasSpan for ExternalCommand {
fn span(&self) -> Span {
self.name_tag.span.until(self.args.span)
}
}
#[derive(Debug)]
pub(crate) enum StreamNext {
Last,
External,
@ -207,58 +295,57 @@ impl ExternalCommand {
) -> Result<ClassifiedInputStream, ShellError> {
let stdin = input.stdin;
let inputs: Vec<Tagged<Value>> = input.objects.into_vec().await;
let name_tag = self.name_tag.clone();
trace!(target: "nu::run::external", "-> {}", self.name);
trace!(target: "nu::run::external", "inputs = {:?}", inputs);
let mut arg_string = format!("{}", self.name);
for arg in &self.args {
for arg in &self.args.item {
arg_string.push_str(&arg);
}
trace!(target: "nu::run::external", "command = {:?}", self.name);
let mut process;
process = Exec::cmd(&self.name);
if arg_string.contains("$it") {
let mut first = true;
for i in &inputs {
if i.as_string().is_err() {
let mut tag = None;
for arg in &self.args {
if arg.item.contains("$it") {
tag = Some(arg.tag());
let input_strings = inputs
.iter()
.map(|i| {
i.as_string().map_err(|_| {
let arg = self.args.iter().find(|arg| arg.item.contains("$it"));
if let Some(arg) = arg {
ShellError::labeled_error(
"External $it needs string data",
"given row instead of string data",
arg.tag(),
)
} else {
ShellError::labeled_error(
"$it needs string data",
"given something else",
self.name_tag.clone(),
)
}
}
if let Some(tag) = tag {
return Err(ShellError::labeled_error(
"External $it needs string data",
"given row instead of string data",
tag,
));
} else {
return Err(ShellError::string("Error: $it needs string data"));
}
}
if !first {
process = process.arg("&&");
process = process.arg(&self.name);
} else {
first = false;
}
})
})
.collect::<Result<Vec<String>, ShellError>>()?;
for arg in &self.args {
let commands = input_strings.iter().map(|i| {
let args = self.args.iter().filter_map(|arg| {
if arg.chars().all(|c| c.is_whitespace()) {
continue;
None
} else {
Some(arg.replace("$it", &i))
}
});
process = process.arg(&arg.replace("$it", &i.as_string()?));
}
}
format!("{} {}", self.name, itertools::join(args, " "))
});
process = Exec::shell(itertools::join(commands, " && "))
} else {
for arg in &self.args {
process = Exec::cmd(&self.name);
for arg in &self.args.item {
let arg_chars: Vec<_> = arg.chars().collect();
if arg_chars.len() > 1
&& arg_chars[0] == '"'
@ -275,6 +362,8 @@ impl ExternalCommand {
process = process.cwd(context.shell_manager.path());
trace!(target: "nu::run::external", "cwd = {:?}", context.shell_manager.path());
let mut process = match stream_next {
StreamNext::Last => process,
StreamNext::External | StreamNext::Internal => {
@ -282,43 +371,60 @@ impl ExternalCommand {
}
};
trace!(target: "nu::run::external", "set up stdout pipe");
if let Some(stdin) = stdin {
process = process.stdin(stdin);
}
let mut popen = process.popen()?;
trace!(target: "nu::run::external", "set up stdin pipe");
trace!(target: "nu::run::external", "built process {:?}", process);
match stream_next {
StreamNext::Last => {
let _ = popen.detach();
loop {
match popen.poll() {
None => {
let _ = std::thread::sleep(std::time::Duration::new(0, 100000000));
}
_ => {
let _ = popen.terminate();
break;
let popen = process.popen();
trace!(target: "nu::run::external", "next = {:?}", stream_next);
let name_tag = self.name_tag.clone();
if let Ok(mut popen) = popen {
match stream_next {
StreamNext::Last => {
let _ = popen.detach();
loop {
match popen.poll() {
None => {
let _ = std::thread::sleep(std::time::Duration::new(0, 100000000));
}
_ => {
let _ = popen.terminate();
break;
}
}
}
Ok(ClassifiedInputStream::new())
}
StreamNext::External => {
let _ = popen.detach();
let stdout = popen.stdout.take().unwrap();
Ok(ClassifiedInputStream::from_stdout(stdout))
}
StreamNext::Internal => {
let _ = popen.detach();
let stdout = popen.stdout.take().unwrap();
let file = futures::io::AllowStdIo::new(stdout);
let stream = Framed::new(file, LinesCodec {});
let stream =
stream.map(move |line| Value::string(line.unwrap()).tagged(&name_tag));
Ok(ClassifiedInputStream::from_input_stream(
stream.boxed() as BoxStream<'static, Tagged<Value>>
))
}
Ok(ClassifiedInputStream::new())
}
StreamNext::External => {
let _ = popen.detach();
let stdout = popen.stdout.take().unwrap();
Ok(ClassifiedInputStream::from_stdout(stdout))
}
StreamNext::Internal => {
let _ = popen.detach();
let stdout = popen.stdout.take().unwrap();
let file = futures::io::AllowStdIo::new(stdout);
let stream = Framed::new(file, LinesCodec {});
let stream = stream.map(move |line| Value::string(line.unwrap()).tagged(name_tag));
Ok(ClassifiedInputStream::from_input_stream(
stream.boxed() as BoxStream<'static, Tagged<Value>>
))
}
} else {
return Err(ShellError::labeled_error(
"Command not found",
"command not found",
name_tag,
));
}
}
}

View file

@ -1,4 +1,3 @@
use crate::context::{AnchorLocation, SourceMap};
use crate::data::Value;
use crate::errors::ShellError;
use crate::evaluate::Scope;
@ -11,18 +10,17 @@ use serde::{Deserialize, Serialize};
use std::fmt;
use std::ops::Deref;
use std::path::PathBuf;
use uuid::Uuid;
use std::sync::atomic::AtomicBool;
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct UnevaluatedCallInfo {
pub args: hir::Call,
pub source: Text,
pub source_map: SourceMap,
pub name_tag: Tag,
}
impl ToDebug for UnevaluatedCallInfo {
fn fmt_debug(&self, f: &mut fmt::Formatter, source: &str) -> fmt::Result {
impl FormatDebug for UnevaluatedCallInfo {
fn fmt_debug(&self, f: &mut DebugFormatter, source: &str) -> fmt::Result {
self.args.fmt_debug(f, source)
}
}
@ -37,7 +35,6 @@ impl UnevaluatedCallInfo {
Ok(CallInfo {
args,
source_map: self.source_map,
name_tag: self.name_tag,
})
}
@ -46,7 +43,6 @@ impl UnevaluatedCallInfo {
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct CallInfo {
pub args: registry::EvaluatedArgs,
pub source_map: SourceMap,
pub name_tag: Tag,
}
@ -62,7 +58,7 @@ impl CallInfo {
args: T::deserialize(&mut deserializer)?,
context: RunnablePerItemContext {
shell_manager: shell_manager.clone(),
name: self.name_tag,
name: self.name_tag.clone(),
},
callback,
})
@ -73,6 +69,7 @@ impl CallInfo {
#[get = "pub(crate)"]
pub struct CommandArgs {
pub host: Arc<Mutex<dyn Host>>,
pub ctrl_c: Arc<AtomicBool>,
pub shell_manager: ShellManager,
pub call_info: UnevaluatedCallInfo,
pub input: InputStream,
@ -82,6 +79,7 @@ pub struct CommandArgs {
#[get = "pub(crate)"]
pub struct RawCommandArgs {
pub host: Arc<Mutex<dyn Host>>,
pub ctrl_c: Arc<AtomicBool>,
pub shell_manager: ShellManager,
pub call_info: UnevaluatedCallInfo,
}
@ -90,6 +88,7 @@ impl RawCommandArgs {
pub fn with_input(self, input: Vec<Tagged<Value>>) -> CommandArgs {
CommandArgs {
host: self.host,
ctrl_c: self.ctrl_c,
shell_manager: self.shell_manager,
call_info: self.call_info,
input: input.into(),
@ -97,8 +96,14 @@ impl RawCommandArgs {
}
}
impl ToDebug for CommandArgs {
fn fmt_debug(&self, f: &mut fmt::Formatter, source: &str) -> fmt::Result {
impl std::fmt::Debug for CommandArgs {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.call_info.fmt(f)
}
}
impl FormatDebug for CommandArgs {
fn fmt_debug(&self, f: &mut DebugFormatter, source: &str) -> fmt::Result {
self.call_info.fmt_debug(f, source)
}
}
@ -109,12 +114,14 @@ impl CommandArgs {
registry: &registry::CommandRegistry,
) -> Result<EvaluatedWholeStreamCommandArgs, ShellError> {
let host = self.host.clone();
let ctrl_c = self.ctrl_c.clone();
let shell_manager = self.shell_manager.clone();
let input = self.input;
let call_info = self.call_info.evaluate(registry, &Scope::empty())?;
Ok(EvaluatedWholeStreamCommandArgs::new(
host,
ctrl_c,
shell_manager,
call_info,
input,
@ -127,12 +134,13 @@ impl CommandArgs {
callback: fn(T, RunnableContext) -> Result<OutputStream, ShellError>,
) -> Result<RunnableArgs<T>, ShellError> {
let shell_manager = self.shell_manager.clone();
let source_map = self.call_info.source_map.clone();
let host = self.host.clone();
let ctrl_c = self.ctrl_c.clone();
let args = self.evaluate_once(registry)?;
let call_info = args.call_info.clone();
let (input, args) = args.split();
let name_tag = args.call_info.name_tag;
let mut deserializer = ConfigDeserializer::from_call_info(args.call_info);
let mut deserializer = ConfigDeserializer::from_call_info(call_info);
Ok(RunnableArgs {
args: T::deserialize(&mut deserializer)?,
@ -141,8 +149,8 @@ impl CommandArgs {
commands: registry.clone(),
shell_manager,
name: name_tag,
source_map,
host,
ctrl_c,
},
callback,
})
@ -155,17 +163,20 @@ impl CommandArgs {
) -> Result<RunnableRawArgs<T>, ShellError> {
let raw_args = RawCommandArgs {
host: self.host.clone(),
ctrl_c: self.ctrl_c.clone(),
shell_manager: self.shell_manager.clone(),
call_info: self.call_info.clone(),
};
let shell_manager = self.shell_manager.clone();
let source_map = self.call_info.source_map.clone();
let host = self.host.clone();
let ctrl_c = self.ctrl_c.clone();
let args = self.evaluate_once(registry)?;
let call_info = args.call_info.clone();
let (input, args) = args.split();
let name_tag = args.call_info.name_tag;
let mut deserializer = ConfigDeserializer::from_call_info(args.call_info);
let mut deserializer = ConfigDeserializer::from_call_info(call_info.clone());
Ok(RunnableRawArgs {
args: T::deserialize(&mut deserializer)?,
@ -174,8 +185,8 @@ impl CommandArgs {
commands: registry.clone(),
shell_manager,
name: name_tag,
source_map,
host,
ctrl_c,
},
raw_args,
callback,
@ -198,18 +209,12 @@ pub struct RunnableContext {
pub input: InputStream,
pub shell_manager: ShellManager,
pub host: Arc<Mutex<dyn Host>>,
pub ctrl_c: Arc<AtomicBool>,
pub commands: CommandRegistry,
pub source_map: SourceMap,
pub name: Tag,
}
impl RunnableContext {
pub fn expect_command(&self, name: &str) -> Arc<Command> {
self.commands
.get_command(name)
.expect(&format!("Expected command {}", name))
}
pub fn get_command(&self, name: &str) -> Option<Arc<Command>> {
self.commands.get_command(name)
}
@ -270,6 +275,7 @@ impl Deref for EvaluatedWholeStreamCommandArgs {
impl EvaluatedWholeStreamCommandArgs {
pub fn new(
host: Arc<Mutex<dyn Host>>,
ctrl_c: Arc<AtomicBool>,
shell_manager: ShellManager,
call_info: CallInfo,
input: impl Into<InputStream>,
@ -277,6 +283,7 @@ impl EvaluatedWholeStreamCommandArgs {
EvaluatedWholeStreamCommandArgs {
args: EvaluatedCommandArgs {
host,
ctrl_c,
shell_manager,
call_info,
},
@ -285,7 +292,7 @@ impl EvaluatedWholeStreamCommandArgs {
}
pub fn name_tag(&self) -> Tag {
self.args.call_info.name_tag
self.args.call_info.name_tag.clone()
}
pub fn parts(self) -> (InputStream, registry::EvaluatedArgs) {
@ -317,12 +324,14 @@ impl Deref for EvaluatedFilterCommandArgs {
impl EvaluatedFilterCommandArgs {
pub fn new(
host: Arc<Mutex<dyn Host>>,
ctrl_c: Arc<AtomicBool>,
shell_manager: ShellManager,
call_info: CallInfo,
) -> EvaluatedFilterCommandArgs {
EvaluatedFilterCommandArgs {
args: EvaluatedCommandArgs {
host,
ctrl_c,
shell_manager,
call_info,
},
@ -334,6 +343,7 @@ impl EvaluatedFilterCommandArgs {
#[get = "pub(crate)"]
pub struct EvaluatedCommandArgs {
pub host: Arc<Mutex<dyn Host>>,
pub ctrl_c: Arc<AtomicBool>,
pub shell_manager: ShellManager,
pub call_info: CallInfo,
}
@ -373,10 +383,9 @@ impl EvaluatedCommandArgs {
}
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum CommandAction {
ChangePath(String),
AddAnchorLocation(Uuid, AnchorLocation),
Exit,
EnterShell(String),
EnterValueShell(Tagged<Value>),
@ -386,13 +395,10 @@ pub enum CommandAction {
LeaveShell,
}
impl ToDebug for CommandAction {
fn fmt_debug(&self, f: &mut fmt::Formatter, _source: &str) -> fmt::Result {
impl FormatDebug for CommandAction {
fn fmt_debug(&self, f: &mut DebugFormatter, _source: &str) -> fmt::Result {
match self {
CommandAction::ChangePath(s) => write!(f, "action:change-path={}", s),
CommandAction::AddAnchorLocation(u, source) => {
write!(f, "action:add-span-source={}@{:?}", u, source)
}
CommandAction::Exit => write!(f, "action:exit"),
CommandAction::EnterShell(s) => write!(f, "action:enter-shell={}", s),
CommandAction::EnterValueShell(t) => {
@ -408,7 +414,7 @@ impl ToDebug for CommandAction {
}
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ReturnSuccess {
Value(Tagged<Value>),
Action(CommandAction),
@ -416,8 +422,8 @@ pub enum ReturnSuccess {
pub type ReturnValue = Result<ReturnSuccess, ShellError>;
impl ToDebug for ReturnValue {
fn fmt_debug(&self, f: &mut fmt::Formatter, source: &str) -> fmt::Result {
impl FormatDebug for ReturnValue {
fn fmt_debug(&self, f: &mut DebugFormatter, source: &str) -> fmt::Result {
match self {
Err(err) => write!(f, "{}", err.debug(source)),
Ok(ReturnSuccess::Value(v)) => write!(f, "{:?}", v.debug()),
@ -507,6 +513,15 @@ pub enum Command {
PerItem(Arc<dyn PerItemCommand>),
}
impl std::fmt::Debug for Command {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Command::WholeStream(command) => write!(f, "WholeStream({})", command.name()),
Command::PerItem(command) => write!(f, "PerItem({})", command.name()),
}
}
}
impl Command {
pub fn name(&self) -> &str {
match self {
@ -529,20 +544,13 @@ impl Command {
}
}
pub fn run(
&self,
args: CommandArgs,
registry: &registry::CommandRegistry,
is_first_command: bool,
) -> OutputStream {
pub fn run(&self, args: CommandArgs, registry: &registry::CommandRegistry) -> OutputStream {
match self {
Command::WholeStream(command) => match command.run(args, registry) {
Ok(stream) => stream,
Err(err) => OutputStream::one(Err(err)),
},
Command::PerItem(command) => {
self.run_helper(command.clone(), args, registry.clone(), is_first_command)
}
Command::PerItem(command) => self.run_helper(command.clone(), args, registry.clone()),
}
}
@ -551,48 +559,31 @@ impl Command {
command: Arc<dyn PerItemCommand>,
args: CommandArgs,
registry: CommandRegistry,
is_first_command: bool,
) -> OutputStream {
let raw_args = RawCommandArgs {
host: args.host,
ctrl_c: args.ctrl_c,
shell_manager: args.shell_manager,
call_info: args.call_info,
};
if !is_first_command {
let out = args
.input
.values
.map(move |x| {
let call_info = raw_args
.clone()
.call_info
.evaluate(&registry, &Scope::it_value(x.clone()))
.unwrap();
match command.run(&call_info, &registry, &raw_args, x) {
Ok(o) => o,
Err(e) => VecDeque::from(vec![ReturnValue::Err(e)]).to_output_stream(),
}
})
.flatten();
let out = args
.input
.values
.map(move |x| {
let call_info = raw_args
.clone()
.call_info
.evaluate(&registry, &Scope::it_value(x.clone()))
.unwrap();
match command.run(&call_info, &registry, &raw_args, x) {
Ok(o) => o,
Err(e) => VecDeque::from(vec![ReturnValue::Err(e)]).to_output_stream(),
}
})
.flatten();
out.to_output_stream()
} else {
let nothing = Value::nothing().tagged(Tag::unknown());
let call_info = raw_args
.clone()
.call_info
.evaluate(&registry, &Scope::it_value(nothing.clone()))
.unwrap();
match command
.run(&call_info, &registry, &raw_args, nothing)
.into()
{
Ok(o) => o,
Err(e) => OutputStream::one(Err(e)),
}
}
out.to_output_stream()
}
pub fn is_binary(&self) -> bool {
@ -624,6 +615,7 @@ impl WholeStreamCommand for FnFilterCommand {
) -> Result<OutputStream, ShellError> {
let CommandArgs {
host,
ctrl_c,
shell_manager,
call_info,
input,
@ -641,8 +633,12 @@ impl WholeStreamCommand for FnFilterCommand {
Ok(args) => args,
};
let args =
EvaluatedFilterCommandArgs::new(host.clone(), shell_manager.clone(), call_info);
let args = EvaluatedFilterCommandArgs::new(
host.clone(),
ctrl_c.clone(),
shell_manager.clone(),
call_info,
);
match func(args) {
Err(err) => return OutputStream::from(vec![Err(err)]).values,

View file

@ -4,7 +4,6 @@ use crate::errors::ShellError;
use crate::parser::hir::SyntaxShape;
use crate::parser::registry::{self};
use crate::prelude::*;
use std::iter::FromIterator;
use std::path::PathBuf;
pub struct Config;
@ -13,6 +12,7 @@ pub struct Config;
pub struct ConfigArgs {
load: Option<Tagged<PathBuf>>,
set: Option<(Tagged<String>, Tagged<Value>)>,
set_into: Option<Tagged<String>>,
get: Option<Tagged<String>>,
clear: Tagged<bool>,
remove: Option<Tagged<String>>,
@ -26,12 +26,25 @@ impl WholeStreamCommand for Config {
fn signature(&self) -> Signature {
Signature::build("config")
.named("load", SyntaxShape::Path)
.named("set", SyntaxShape::Any)
.named("get", SyntaxShape::Any)
.named("remove", SyntaxShape::Any)
.switch("clear")
.switch("path")
.named(
"load",
SyntaxShape::Path,
"load the config from the path give",
)
.named(
"set",
SyntaxShape::Any,
"set a value in the config, eg) --set [key value]",
)
.named(
"set_into",
SyntaxShape::Member,
"sets a variable from values in the pipeline",
)
.named("get", SyntaxShape::Any, "get a value from the config")
.named("remove", SyntaxShape::Any, "remove a value from the config")
.switch("clear", "clear the config")
.switch("path", "return the path to the config file")
}
fn usage(&self) -> &str {
@ -51,84 +64,110 @@ pub fn config(
ConfigArgs {
load,
set,
set_into,
get,
clear,
remove,
path,
}: ConfigArgs,
RunnableContext { name, .. }: RunnableContext,
RunnableContext { name, input, .. }: RunnableContext,
) -> Result<OutputStream, ShellError> {
let name_span = name;
let name_span = name.clone();
let configuration = if let Some(supplied) = load {
Some(supplied.item().clone())
} else {
None
let stream = async_stream! {
let configuration = if let Some(supplied) = load {
Some(supplied.item().clone())
} else {
None
};
let mut result = crate::data::config::read(name_span, &configuration)?;
if let Some(v) = get {
let key = v.to_string();
let value = result
.get(&key)
.ok_or_else(|| ShellError::labeled_error("Missing key in config", "key", v.tag()))?;
match value {
Tagged {
item: Value::Table(list),
..
} => {
for l in list {
yield ReturnSuccess::value(l.clone());
}
}
x => yield ReturnSuccess::value(x.clone()),
}
}
else if let Some((key, value)) = set {
result.insert(key.to_string(), value.clone());
config::write(&result, &configuration)?;
yield ReturnSuccess::value(Value::Row(result.into()).tagged(value.tag()));
}
else if let Some(v) = set_into {
let rows: Vec<Tagged<Value>> = input.values.collect().await;
let key = v.to_string();
if rows.len() == 0 {
yield Err(ShellError::labeled_error("No values given for set_into", "needs value(s) from pipeline", v.tag()));
} else if rows.len() == 1 {
// A single value
let value = &rows[0];
result.insert(key.to_string(), value.clone());
config::write(&result, &configuration)?;
yield ReturnSuccess::value(Value::Row(result.into()).tagged(name));
} else {
// Take in the pipeline as a table
let value = Value::Table(rows).tagged(name.clone());
result.insert(key.to_string(), value.clone());
config::write(&result, &configuration)?;
yield ReturnSuccess::value(Value::Row(result.into()).tagged(name));
}
}
else if let Tagged { item: true, tag } = clear {
result.clear();
config::write(&result, &configuration)?;
yield ReturnSuccess::value(Value::Row(result.into()).tagged(tag));
return;
}
else if let Tagged { item: true, tag } = path {
let path = config::default_path_for(&configuration)?;
yield ReturnSuccess::value(Value::Primitive(Primitive::Path(path)).tagged(tag));
}
else if let Some(v) = remove {
let key = v.to_string();
if result.contains_key(&key) {
result.swap_remove(&key);
config::write(&result, &configuration).unwrap();
} else {
yield Err(ShellError::labeled_error(
"Key does not exist in config",
"key",
v.tag(),
));
}
yield ReturnSuccess::value(Value::Row(result.into()).tagged(v.tag()));
}
else {
yield ReturnSuccess::value(Value::Row(result.into()).tagged(name));
}
};
let mut result = crate::data::config::read(name_span, &configuration)?;
if let Some(v) = get {
let key = v.to_string();
let value = result
.get(&key)
.ok_or_else(|| ShellError::string(&format!("Missing key {} in config", key)))?;
let mut results = VecDeque::new();
match value {
Tagged {
item: Value::Table(list),
..
} => {
for l in list {
results.push_back(ReturnSuccess::value(l.clone()));
}
}
x => results.push_back(ReturnSuccess::value(x.clone())),
}
return Ok(results.to_output_stream());
}
if let Some((key, value)) = set {
result.insert(key.to_string(), value.clone());
config::write(&result, &configuration)?;
return Ok(stream![Value::Row(result.into()).tagged(value.tag())].from_input_stream());
}
if let Tagged { item: true, tag } = clear {
result.clear();
config::write(&result, &configuration)?;
return Ok(stream![Value::Row(result.into()).tagged(tag)].from_input_stream());
}
if let Tagged { item: true, tag } = path {
let path = config::default_path_for(&configuration)?;
return Ok(stream![Value::Primitive(Primitive::Path(path)).tagged(tag)].from_input_stream());
}
if let Some(v) = remove {
let key = v.to_string();
if result.contains_key(&key) {
result.swap_remove(&key);
config::write(&result, &configuration)?;
} else {
return Err(ShellError::string(&format!(
"{} does not exist in config",
key
)));
}
let obj = VecDeque::from_iter(vec![Value::Row(result.into()).tagged(v.tag())]);
return Ok(obj.from_input_stream());
}
return Ok(vec![Value::Row(result.into()).tagged(name)].into());
Ok(stream.to_output_stream())
}

46
src/commands/count.rs Normal file
View file

@ -0,0 +1,46 @@
use crate::commands::WholeStreamCommand;
use crate::data::Value;
use crate::errors::ShellError;
use crate::parser::CommandRegistry;
use crate::prelude::*;
use futures::stream::StreamExt;
pub struct Count;
#[derive(Deserialize)]
pub struct CountArgs {}
impl WholeStreamCommand for Count {
fn name(&self) -> &str {
"count"
}
fn signature(&self) -> Signature {
Signature::build("count")
}
fn usage(&self) -> &str {
"Show the total number of rows."
}
fn run(
&self,
args: CommandArgs,
registry: &CommandRegistry,
) -> Result<OutputStream, ShellError> {
args.process(registry, count)?.run()
}
}
pub fn count(
CountArgs {}: CountArgs,
RunnableContext { input, name, .. }: RunnableContext,
) -> Result<OutputStream, ShellError> {
let stream = async_stream! {
let rows: Vec<Tagged<Value>> = input.values.collect().await;
yield ReturnSuccess::value(Value::int(rows.len()).tagged(name))
};
Ok(stream.to_output_stream())
}

View file

@ -21,10 +21,9 @@ impl PerItemCommand for Cpy {
fn signature(&self) -> Signature {
Signature::build("cp")
.required("src", SyntaxShape::Pattern)
.required("dst", SyntaxShape::Path)
.named("file", SyntaxShape::Any)
.switch("recursive")
.required("src", SyntaxShape::Pattern, "the place to copy from")
.required("dst", SyntaxShape::Path, "the place to copy to")
.switch("recursive", "copy recursively through subdirectories")
}
fn usage(&self) -> &str {

View file

@ -17,7 +17,9 @@ impl WholeStreamCommand for Date {
}
fn signature(&self) -> Signature {
Signature::build("date").switch("utc").switch("local")
Signature::build("date")
.switch("utc", "use universal time (UTC)")
.switch("local", "use the local time")
}
fn usage(&self) -> &str {
@ -39,27 +41,27 @@ where
{
let mut indexmap = IndexMap::new();
indexmap.insert("year".to_string(), Value::int(dt.year()).tagged(tag));
indexmap.insert("month".to_string(), Value::int(dt.month()).tagged(tag));
indexmap.insert("day".to_string(), Value::int(dt.day()).tagged(tag));
indexmap.insert("hour".to_string(), Value::int(dt.hour()).tagged(tag));
indexmap.insert("minute".to_string(), Value::int(dt.minute()).tagged(tag));
indexmap.insert("second".to_string(), Value::int(dt.second()).tagged(tag));
indexmap.insert("year".to_string(), Value::int(dt.year()).tagged(&tag));
indexmap.insert("month".to_string(), Value::int(dt.month()).tagged(&tag));
indexmap.insert("day".to_string(), Value::int(dt.day()).tagged(&tag));
indexmap.insert("hour".to_string(), Value::int(dt.hour()).tagged(&tag));
indexmap.insert("minute".to_string(), Value::int(dt.minute()).tagged(&tag));
indexmap.insert("second".to_string(), Value::int(dt.second()).tagged(&tag));
let tz = dt.offset();
indexmap.insert(
"timezone".to_string(),
Value::string(format!("{}", tz)).tagged(tag),
Value::string(format!("{}", tz)).tagged(&tag),
);
Value::Row(Dictionary::from(indexmap)).tagged(tag)
Value::Row(Dictionary::from(indexmap)).tagged(&tag)
}
pub fn date(args: CommandArgs, registry: &CommandRegistry) -> Result<OutputStream, ShellError> {
let args = args.evaluate_once(registry)?;
let mut date_out = VecDeque::new();
let tag = args.call_info.name_tag;
let tag = args.call_info.name_tag.clone();
let value = if args.has("utc") {
let utc: DateTime<Utc> = Utc::now();

View file

@ -12,7 +12,7 @@ impl PerItemCommand for Echo {
}
fn signature(&self) -> Signature {
Signature::build("echo").rest(SyntaxShape::Any)
Signature::build("echo").rest(SyntaxShape::Any, "the values to echo")
}
fn usage(&self) -> &str {
@ -35,38 +35,34 @@ fn run(
_registry: &CommandRegistry,
_raw_args: &RawCommandArgs,
) -> Result<OutputStream, ShellError> {
let name = call_info.name_tag;
let mut output = String::new();
let mut first = true;
let mut output = vec![];
if let Some(ref positional) = call_info.args.positional {
for i in positional {
match i.as_string() {
Ok(s) => {
if !first {
output.push_str(" ");
} else {
first = false;
output.push(Ok(ReturnSuccess::Value(
Value::string(s).tagged(i.tag.clone()),
)));
}
_ => match i {
Tagged {
item: Value::Table(table),
..
} => {
for item in table {
output.push(Ok(ReturnSuccess::Value(item.clone())));
}
}
output.push_str(&s);
}
_ => {
return Err(ShellError::labeled_error(
"Expect a string from pipeline",
"not a string-compatible value",
i.tag(),
));
}
_ => {
output.push(Ok(ReturnSuccess::Value(i.clone())));
}
},
}
}
}
let stream = VecDeque::from(vec![Ok(ReturnSuccess::Value(
Value::string(output).tagged(name),
))]);
let stream = VecDeque::from(output);
Ok(stream.to_output_stream())
}

View file

@ -1,7 +1,6 @@
use crate::commands::command::CommandAction;
use crate::commands::PerItemCommand;
use crate::commands::UnevaluatedCallInfo;
use crate::data::meta::Span;
use crate::errors::ShellError;
use crate::parser::registry;
use crate::prelude::*;
@ -15,7 +14,11 @@ impl PerItemCommand for Enter {
}
fn signature(&self) -> registry::Signature {
Signature::build("enter").required("location", SyntaxShape::Block)
Signature::build("enter").required(
"location",
SyntaxShape::Path,
"the location to create a new shell from",
)
}
fn usage(&self) -> &str {
@ -33,14 +36,16 @@ impl PerItemCommand for Enter {
let raw_args = raw_args.clone();
match call_info.args.expect_nth(0)? {
Tagged {
item: Value::Primitive(Primitive::String(location)),
item: Value::Primitive(Primitive::Path(location)),
tag,
..
} => {
let location = location.to_string();
let location_clone = location.to_string();
let location_string = location.display().to_string();
let location_clone = location_string.clone();
let tag_clone = tag.clone();
if location.starts_with("help") {
let spec = location.split(":").collect::<Vec<&str>>();
let spec = location_string.split(":").collect::<Vec<&str>>();
let (_, command) = (spec[0], spec[1]);
@ -67,26 +72,16 @@ impl PerItemCommand for Enter {
let full_path = std::path::PathBuf::from(cwd);
let (file_extension, contents, contents_tag, anchor_location) =
let (file_extension, contents, contents_tag) =
crate::commands::open::fetch(
&full_path,
&location_clone,
Span::unknown(),
)
.await.unwrap();
if contents_tag.anchor != uuid::Uuid::nil() {
// If we have loaded something, track its source
yield ReturnSuccess::action(CommandAction::AddAnchorLocation(
contents_tag.anchor,
anchor_location,
));
}
tag_clone.span,
).await?;
match contents {
Value::Primitive(Primitive::String(_)) => {
let tagged_contents = contents.tagged(contents_tag);
let tagged_contents = contents.tagged(&contents_tag);
if let Some(extension) = file_extension {
let command_name = format!("from-{}", extension);
@ -95,6 +90,7 @@ impl PerItemCommand for Enter {
{
let new_args = RawCommandArgs {
host: raw_args.host,
ctrl_c: raw_args.ctrl_c,
shell_manager: raw_args.shell_manager,
call_info: UnevaluatedCallInfo {
args: crate::parser::hir::Call {
@ -103,14 +99,12 @@ impl PerItemCommand for Enter {
named: None,
},
source: raw_args.call_info.source,
source_map: raw_args.call_info.source_map,
name_tag: raw_args.call_info.name_tag,
},
};
let mut result = converter.run(
new_args.with_input(vec![tagged_contents]),
&registry,
false
);
let result_vec: Vec<Result<ReturnSuccess, ShellError>> =
result.drain_vec().await;
@ -123,7 +117,7 @@ impl PerItemCommand for Enter {
yield Ok(ReturnSuccess::Action(CommandAction::EnterValueShell(
Tagged {
item,
tag: contents_tag,
tag: contents_tag.clone(),
})));
}
x => yield x,

View file

@ -37,22 +37,22 @@ pub fn get_environment(tag: Tag) -> Result<Tagged<Value>, Box<dyn std::error::Er
let mut indexmap = IndexMap::new();
let path = std::env::current_dir()?;
indexmap.insert("cwd".to_string(), Value::path(path).tagged(tag));
indexmap.insert("cwd".to_string(), Value::path(path).tagged(&tag));
if let Some(home) = dirs::home_dir() {
indexmap.insert("home".to_string(), Value::path(home).tagged(tag));
indexmap.insert("home".to_string(), Value::path(home).tagged(&tag));
}
let config = config::default_path()?;
indexmap.insert("config".to_string(), Value::path(config).tagged(tag));
indexmap.insert("config".to_string(), Value::path(config).tagged(&tag));
let history = History::path();
indexmap.insert("history".to_string(), Value::path(history).tagged(tag));
indexmap.insert("history".to_string(), Value::path(history).tagged(&tag));
let temp = std::env::temp_dir();
indexmap.insert("temp".to_string(), Value::path(temp).tagged(tag));
indexmap.insert("temp".to_string(), Value::path(temp).tagged(&tag));
let mut dict = TaggedDictBuilder::new(tag);
let mut dict = TaggedDictBuilder::new(&tag);
for v in std::env::vars() {
dict.insert(v.0, Value::string(v.1));
}
@ -60,14 +60,14 @@ pub fn get_environment(tag: Tag) -> Result<Tagged<Value>, Box<dyn std::error::Er
indexmap.insert("vars".to_string(), dict.into_tagged_value());
}
Ok(Value::Row(Dictionary::from(indexmap)).tagged(tag))
Ok(Value::Row(Dictionary::from(indexmap)).tagged(&tag))
}
pub fn env(args: CommandArgs, registry: &CommandRegistry) -> Result<OutputStream, ShellError> {
let args = args.evaluate_once(registry)?;
let mut env_out = VecDeque::new();
let tag = args.call_info.name_tag;
let tag = args.call_info.name_tag.clone();
let value = get_environment(tag)?;
env_out.push_back(value);

260
src/commands/evaluate_by.rs Normal file
View file

@ -0,0 +1,260 @@
use crate::commands::WholeStreamCommand;
use crate::parser::hir::SyntaxShape;
use crate::prelude::*;
pub struct EvaluateBy;
#[derive(Deserialize)]
pub struct EvaluateByArgs {
evaluate_with: Option<Tagged<String>>,
}
impl WholeStreamCommand for EvaluateBy {
fn name(&self) -> &str {
"evaluate-by"
}
fn signature(&self) -> Signature {
Signature::build("evaluate-by").named(
"evaluate_with",
SyntaxShape::String,
"the name of the column to evaluate by",
)
}
fn usage(&self) -> &str {
"Creates a new table with the data from the tables rows evaluated by the column given."
}
fn run(
&self,
args: CommandArgs,
registry: &CommandRegistry,
) -> Result<OutputStream, ShellError> {
args.process(registry, evaluate_by)?.run()
}
}
pub fn evaluate_by(
EvaluateByArgs { evaluate_with }: EvaluateByArgs,
RunnableContext { input, name, .. }: RunnableContext,
) -> Result<OutputStream, ShellError> {
let stream = async_stream! {
let values: Vec<Tagged<Value>> = input.values.collect().await;
if values.is_empty() {
yield Err(ShellError::labeled_error(
"Expected table from pipeline",
"requires a table input",
name
))
} else {
let evaluate_with = if let Some(evaluator) = evaluate_with {
Some(evaluator.item().clone())
} else {
None
};
match evaluate(&values[0], evaluate_with, name) {
Ok(evaluated) => yield ReturnSuccess::value(evaluated),
Err(err) => yield Err(err)
}
}
};
Ok(stream.to_output_stream())
}
fn fetch(
key: Option<String>,
) -> Box<dyn Fn(Tagged<Value>, Tag) -> Option<Tagged<Value>> + 'static> {
Box::new(move |value: Tagged<Value>, tag| match key {
Some(ref key_given) => {
if let Some(Tagged { item, .. }) = value.get_data_by_key(&key_given) {
Some(item.clone().tagged(tag))
} else {
None
}
}
None => Some(Value::int(1).tagged(tag)),
})
}
pub fn evaluate(
values: &Tagged<Value>,
evaluator: Option<String>,
tag: impl Into<Tag>,
) -> Result<Tagged<Value>, ShellError> {
let tag = tag.into();
let evaluate_with = match evaluator {
Some(keyfn) => fetch(Some(keyfn)),
None => fetch(None),
};
let results: Tagged<Value> = match values {
Tagged {
item: Value::Table(datasets),
..
} => {
let datasets: Vec<_> = datasets
.into_iter()
.map(|subsets| match subsets {
Tagged {
item: Value::Table(subsets),
..
} => {
let subsets: Vec<_> = subsets
.clone()
.into_iter()
.map(|data| match data {
Tagged {
item: Value::Table(data),
..
} => {
let data: Vec<_> = data
.into_iter()
.map(|x| evaluate_with(x.clone(), tag.clone()).unwrap())
.collect();
Value::Table(data).tagged(&tag)
}
_ => Value::Table(vec![]).tagged(&tag),
})
.collect();
Value::Table(subsets).tagged(&tag)
}
_ => Value::Table(vec![]).tagged(&tag),
})
.collect();
Value::Table(datasets.clone()).tagged(&tag)
}
_ => Value::Table(vec![]).tagged(&tag),
};
Ok(results)
}
#[cfg(test)]
mod tests {
use crate::commands::evaluate_by::{evaluate, fetch};
use crate::commands::group_by::group;
use crate::commands::t_sort_by::t_sort;
use crate::data::meta::*;
use crate::prelude::*;
use crate::Value;
use indexmap::IndexMap;
fn int(s: impl Into<BigInt>) -> Tagged<Value> {
Value::int(s).tagged_unknown()
}
fn string(input: impl Into<String>) -> Tagged<Value> {
Value::string(input.into()).tagged_unknown()
}
fn row(entries: IndexMap<String, Tagged<Value>>) -> Tagged<Value> {
Value::row(entries).tagged_unknown()
}
fn table(list: &Vec<Tagged<Value>>) -> Tagged<Value> {
Value::table(list).tagged_unknown()
}
fn nu_releases_sorted_by_date() -> Tagged<Value> {
let key = String::from("date");
t_sort(
Some(key),
None,
&nu_releases_grouped_by_date(),
Tag::unknown(),
)
.unwrap()
}
fn nu_releases_grouped_by_date() -> Tagged<Value> {
let key = String::from("date").tagged_unknown();
group(&key, nu_releases_commiters(), Tag::unknown()).unwrap()
}
fn nu_releases_commiters() -> Vec<Tagged<Value>> {
vec![
row(
indexmap! {"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("August 23-2019")},
),
row(
indexmap! {"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => string("August 23-2019")},
),
row(
indexmap! {"name".into() => string("YK"), "country".into() => string("US"), "date".into() => string("October 10-2019")},
),
row(
indexmap! {"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("September 24-2019")},
),
row(
indexmap! {"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => string("October 10-2019")},
),
row(
indexmap! {"name".into() => string("YK"), "country".into() => string("US"), "date".into() => string("September 24-2019")},
),
row(
indexmap! {"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("October 10-2019")},
),
row(
indexmap! {"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => string("September 24-2019")},
),
row(
indexmap! {"name".into() => string("YK"), "country".into() => string("US"), "date".into() => string("August 23-2019")},
),
]
}
#[test]
fn evaluator_fetches_by_column_if_supplied_a_column_name() {
let subject = row(indexmap! { "name".into() => string("andres") });
let evaluator = fetch(Some(String::from("name")));
assert_eq!(evaluator(subject, Tag::unknown()), Some(string("andres")));
}
#[test]
fn evaluator_returns_1_if_no_column_name_given() {
let subject = row(indexmap! { "name".into() => string("andres") });
let evaluator = fetch(None);
assert_eq!(
evaluator(subject, Tag::unknown()),
Some(Value::int(1).tagged_unknown())
);
}
#[test]
fn evaluates_the_tables() {
assert_eq!(
evaluate(&nu_releases_sorted_by_date(), None, Tag::unknown()).unwrap(),
table(&vec![table(&vec![
table(&vec![int(1), int(1), int(1)]),
table(&vec![int(1), int(1), int(1)]),
table(&vec![int(1), int(1), int(1)]),
]),])
);
}
#[test]
fn evaluates_the_tables_with_custom_evaluator() {
let eval = String::from("name");
assert_eq!(
evaluate(&nu_releases_sorted_by_date(), Some(eval), Tag::unknown()).unwrap(),
table(&vec![table(&vec![
table(&vec![string("AR"), string("JT"), string("YK")]),
table(&vec![string("AR"), string("YK"), string("JT")]),
table(&vec![string("YK"), string("JT"), string("AR")]),
]),])
);
}
}

View file

@ -11,7 +11,7 @@ impl WholeStreamCommand for Exit {
}
fn signature(&self) -> Signature {
Signature::build("exit").switch("now")
Signature::build("exit").switch("now", "exit out of the shell immediately")
}
fn usage(&self) -> &str {

View file

@ -10,7 +10,6 @@ use mime::Mime;
use std::path::PathBuf;
use std::str::FromStr;
use surf::mime;
use uuid::Uuid;
pub struct Fetch;
impl PerItemCommand for Fetch {
@ -20,8 +19,12 @@ impl PerItemCommand for Fetch {
fn signature(&self) -> Signature {
Signature::build(self.name())
.required("path", SyntaxShape::Path)
.switch("raw")
.required(
"path",
SyntaxShape::Path,
"the URL to fetch the contents from",
)
.switch("raw", "fetch contents as text rather than a table")
}
fn usage(&self) -> &str {
@ -44,16 +47,18 @@ fn run(
registry: &CommandRegistry,
raw_args: &RawCommandArgs,
) -> Result<OutputStream, ShellError> {
let path = match call_info
.args
.nth(0)
.ok_or_else(|| ShellError::string(&format!("No file or directory specified")))?
{
let path = match call_info.args.nth(0).ok_or_else(|| {
ShellError::labeled_error(
"No file or directory specified",
"for command",
&call_info.name_tag,
)
})? {
file => file,
};
let path_buf = path.as_path()?;
let path_str = path_buf.display().to_string();
let path_span = path.span();
let path_span = path.tag.span;
let has_raw = call_info.args.has("raw");
let registry = registry.clone();
let raw_args = raw_args.clone();
@ -66,7 +71,7 @@ fn run(
yield Err(e);
return;
}
let (file_extension, contents, contents_tag, anchor_location) = result.unwrap();
let (file_extension, contents, contents_tag) = result.unwrap();
let file_extension = if has_raw {
None
@ -76,21 +81,14 @@ fn run(
file_extension.or(path_str.split('.').last().map(String::from))
};
if contents_tag.anchor != uuid::Uuid::nil() {
// If we have loaded something, track its source
yield ReturnSuccess::action(CommandAction::AddAnchorLocation(
contents_tag.anchor,
anchor_location,
));
}
let tagged_contents = contents.tagged(contents_tag);
let tagged_contents = contents.tagged(&contents_tag);
if let Some(extension) = file_extension {
let command_name = format!("from-{}", extension);
if let Some(converter) = registry.get_command(&command_name) {
let new_args = RawCommandArgs {
host: raw_args.host,
ctrl_c: raw_args.ctrl_c,
shell_manager: raw_args.shell_manager,
call_info: UnevaluatedCallInfo {
args: crate::parser::hir::Call {
@ -99,11 +97,10 @@ fn run(
named: None
},
source: raw_args.call_info.source,
source_map: raw_args.call_info.source_map,
name_tag: raw_args.call_info.name_tag,
}
};
let mut result = converter.run(new_args.with_input(vec![tagged_contents]), &registry, false);
let mut result = converter.run(new_args.with_input(vec![tagged_contents]), &registry);
let result_vec: Vec<Result<ReturnSuccess, ShellError>> = result.drain_vec().await;
for res in result_vec {
match res {
@ -113,7 +110,7 @@ fn run(
}
}
Ok(ReturnSuccess::Value(Tagged { item, .. })) => {
yield Ok(ReturnSuccess::Value(Tagged { item, tag: contents_tag }));
yield Ok(ReturnSuccess::Value(Tagged { item, tag: contents_tag.clone() }));
}
x => yield x,
}
@ -129,10 +126,7 @@ fn run(
Ok(stream.to_output_stream())
}
pub async fn fetch(
location: &str,
span: Span,
) -> Result<(Option<String>, Value, Tag, AnchorLocation), ShellError> {
pub async fn fetch(location: &str, span: Span) -> Result<(Option<String>, Value, Tag), ShellError> {
if let Err(_) = url::Url::parse(location) {
return Err(ShellError::labeled_error(
"Incomplete or incorrect url",
@ -158,9 +152,8 @@ pub async fn fetch(
})?),
Tag {
span,
anchor: Uuid::new_v4(),
anchor: Some(AnchorLocation::Url(location.to_string())),
},
AnchorLocation::Url(location.to_string()),
)),
(mime::APPLICATION, mime::JSON) => Ok((
Some("json".to_string()),
@ -173,9 +166,8 @@ pub async fn fetch(
})?),
Tag {
span,
anchor: Uuid::new_v4(),
anchor: Some(AnchorLocation::Url(location.to_string())),
},
AnchorLocation::Url(location.to_string()),
)),
(mime::APPLICATION, mime::OCTET_STREAM) => {
let buf: Vec<u8> = r.body_bytes().await.map_err(|_| {
@ -190,9 +182,8 @@ pub async fn fetch(
Value::binary(buf),
Tag {
span,
anchor: Uuid::new_v4(),
anchor: Some(AnchorLocation::Url(location.to_string())),
},
AnchorLocation::Url(location.to_string()),
))
}
(mime::IMAGE, mime::SVG) => Ok((
@ -206,9 +197,8 @@ pub async fn fetch(
})?),
Tag {
span,
anchor: Uuid::new_v4(),
anchor: Some(AnchorLocation::Url(location.to_string())),
},
AnchorLocation::Url(location.to_string()),
)),
(mime::IMAGE, image_ty) => {
let buf: Vec<u8> = r.body_bytes().await.map_err(|_| {
@ -223,9 +213,8 @@ pub async fn fetch(
Value::binary(buf),
Tag {
span,
anchor: Uuid::new_v4(),
anchor: Some(AnchorLocation::Url(location.to_string())),
},
AnchorLocation::Url(location.to_string()),
))
}
(mime::TEXT, mime::HTML) => Ok((
@ -239,9 +228,8 @@ pub async fn fetch(
})?),
Tag {
span,
anchor: Uuid::new_v4(),
anchor: Some(AnchorLocation::Url(location.to_string())),
},
AnchorLocation::Url(location.to_string()),
)),
(mime::TEXT, mime::PLAIN) => {
let path_extension = url::Url::parse(location)
@ -266,9 +254,8 @@ pub async fn fetch(
})?),
Tag {
span,
anchor: Uuid::new_v4(),
anchor: Some(AnchorLocation::Url(location.to_string())),
},
AnchorLocation::Url(location.to_string()),
))
}
(ty, sub_ty) => Ok((
@ -276,9 +263,8 @@ pub async fn fetch(
Value::string(format!("Not yet supported MIME type: {} {}", ty, sub_ty)),
Tag {
span,
anchor: Uuid::new_v4(),
anchor: Some(AnchorLocation::Url(location.to_string())),
},
AnchorLocation::Url(location.to_string()),
)),
}
}
@ -287,9 +273,8 @@ pub async fn fetch(
Value::string(format!("No content type found")),
Tag {
span,
anchor: Uuid::new_v4(),
anchor: Some(AnchorLocation::Url(location.to_string())),
},
AnchorLocation::Url(location.to_string()),
)),
},
Err(_) => {

View file

@ -7,7 +7,7 @@ pub struct First;
#[derive(Deserialize)]
pub struct FirstArgs {
amount: Tagged<u64>,
rows: Option<Tagged<u64>>,
}
impl WholeStreamCommand for First {
@ -16,7 +16,11 @@ impl WholeStreamCommand for First {
}
fn signature(&self) -> Signature {
Signature::build("first").required("amount", SyntaxShape::Literal)
Signature::build("first").optional(
"rows",
SyntaxShape::Int,
"starting from the front, the number of rows to return",
)
}
fn usage(&self) -> &str {
@ -33,8 +37,16 @@ impl WholeStreamCommand for First {
}
fn first(
FirstArgs { amount }: FirstArgs,
FirstArgs { rows }: FirstArgs,
context: RunnableContext,
) -> Result<OutputStream, ShellError> {
Ok(OutputStream::from_input(context.input.values.take(*amount)))
let rows_desired = if let Some(quantity) = rows {
*quantity
} else {
1
};
Ok(OutputStream::from_input(
context.input.values.take(rows_desired),
))
}

View file

@ -33,7 +33,7 @@ fn bson_array(input: &Vec<Bson>, tag: Tag) -> Result<Vec<Tagged<Value>>, ShellEr
let mut out = vec![];
for value in input {
out.push(convert_bson_value_to_nu_value(value, tag)?);
out.push(convert_bson_value_to_nu_value(value, &tag)?);
}
Ok(out)
@ -46,100 +46,100 @@ fn convert_bson_value_to_nu_value(
let tag = tag.into();
Ok(match v {
Bson::FloatingPoint(n) => Value::Primitive(Primitive::from(*n)).tagged(tag),
Bson::String(s) => Value::Primitive(Primitive::String(String::from(s))).tagged(tag),
Bson::Array(a) => Value::Table(bson_array(a, tag)?).tagged(tag),
Bson::FloatingPoint(n) => Value::Primitive(Primitive::from(*n)).tagged(&tag),
Bson::String(s) => Value::Primitive(Primitive::String(String::from(s))).tagged(&tag),
Bson::Array(a) => Value::Table(bson_array(a, tag.clone())?).tagged(&tag),
Bson::Document(doc) => {
let mut collected = TaggedDictBuilder::new(tag);
let mut collected = TaggedDictBuilder::new(tag.clone());
for (k, v) in doc.iter() {
collected.insert_tagged(k.clone(), convert_bson_value_to_nu_value(v, tag)?);
collected.insert_tagged(k.clone(), convert_bson_value_to_nu_value(v, &tag)?);
}
collected.into_tagged_value()
}
Bson::Boolean(b) => Value::Primitive(Primitive::Boolean(*b)).tagged(tag),
Bson::Null => Value::Primitive(Primitive::Nothing).tagged(tag),
Bson::Boolean(b) => Value::Primitive(Primitive::Boolean(*b)).tagged(&tag),
Bson::Null => Value::Primitive(Primitive::Nothing).tagged(&tag),
Bson::RegExp(r, opts) => {
let mut collected = TaggedDictBuilder::new(tag);
let mut collected = TaggedDictBuilder::new(tag.clone());
collected.insert_tagged(
"$regex".to_string(),
Value::Primitive(Primitive::String(String::from(r))).tagged(tag),
Value::Primitive(Primitive::String(String::from(r))).tagged(&tag),
);
collected.insert_tagged(
"$options".to_string(),
Value::Primitive(Primitive::String(String::from(opts))).tagged(tag),
Value::Primitive(Primitive::String(String::from(opts))).tagged(&tag),
);
collected.into_tagged_value()
}
Bson::I32(n) => Value::number(n).tagged(tag),
Bson::I64(n) => Value::number(n).tagged(tag),
Bson::I32(n) => Value::number(n).tagged(&tag),
Bson::I64(n) => Value::number(n).tagged(&tag),
Bson::Decimal128(n) => {
// TODO: this really isn't great, and we should update this to do a higher
// fidelity translation
let decimal = BigDecimal::from_str(&format!("{}", n)).map_err(|_| {
ShellError::range_error(
ExpectedRange::BigDecimal,
&n.tagged(tag),
&n.tagged(&tag),
format!("converting BSON Decimal128 to BigDecimal"),
)
})?;
Value::Primitive(Primitive::Decimal(decimal)).tagged(tag)
Value::Primitive(Primitive::Decimal(decimal)).tagged(&tag)
}
Bson::JavaScriptCode(js) => {
let mut collected = TaggedDictBuilder::new(tag);
let mut collected = TaggedDictBuilder::new(tag.clone());
collected.insert_tagged(
"$javascript".to_string(),
Value::Primitive(Primitive::String(String::from(js))).tagged(tag),
Value::Primitive(Primitive::String(String::from(js))).tagged(&tag),
);
collected.into_tagged_value()
}
Bson::JavaScriptCodeWithScope(js, doc) => {
let mut collected = TaggedDictBuilder::new(tag);
let mut collected = TaggedDictBuilder::new(tag.clone());
collected.insert_tagged(
"$javascript".to_string(),
Value::Primitive(Primitive::String(String::from(js))).tagged(tag),
Value::Primitive(Primitive::String(String::from(js))).tagged(&tag),
);
collected.insert_tagged(
"$scope".to_string(),
convert_bson_value_to_nu_value(&Bson::Document(doc.to_owned()), tag)?,
convert_bson_value_to_nu_value(&Bson::Document(doc.to_owned()), tag.clone())?,
);
collected.into_tagged_value()
}
Bson::TimeStamp(ts) => {
let mut collected = TaggedDictBuilder::new(tag);
collected.insert_tagged("$timestamp".to_string(), Value::number(ts).tagged(tag));
let mut collected = TaggedDictBuilder::new(tag.clone());
collected.insert_tagged("$timestamp".to_string(), Value::number(ts).tagged(&tag));
collected.into_tagged_value()
}
Bson::Binary(bst, bytes) => {
let mut collected = TaggedDictBuilder::new(tag);
let mut collected = TaggedDictBuilder::new(tag.clone());
collected.insert_tagged(
"$binary_subtype".to_string(),
match bst {
BinarySubtype::UserDefined(u) => Value::number(u),
_ => Value::Primitive(Primitive::String(binary_subtype_to_string(*bst))),
}
.tagged(tag),
.tagged(&tag),
);
collected.insert_tagged(
"$binary".to_string(),
Value::Primitive(Primitive::Binary(bytes.to_owned())).tagged(tag),
Value::Primitive(Primitive::Binary(bytes.to_owned())).tagged(&tag),
);
collected.into_tagged_value()
}
Bson::ObjectId(obj_id) => {
let mut collected = TaggedDictBuilder::new(tag);
let mut collected = TaggedDictBuilder::new(tag.clone());
collected.insert_tagged(
"$object_id".to_string(),
Value::Primitive(Primitive::String(obj_id.to_hex())).tagged(tag),
Value::Primitive(Primitive::String(obj_id.to_hex())).tagged(&tag),
);
collected.into_tagged_value()
}
Bson::UtcDatetime(dt) => Value::Primitive(Primitive::Date(*dt)).tagged(tag),
Bson::UtcDatetime(dt) => Value::Primitive(Primitive::Date(*dt)).tagged(&tag),
Bson::Symbol(s) => {
let mut collected = TaggedDictBuilder::new(tag);
let mut collected = TaggedDictBuilder::new(tag.clone());
collected.insert_tagged(
"$symbol".to_string(),
Value::Primitive(Primitive::String(String::from(s))).tagged(tag),
Value::Primitive(Primitive::String(String::from(s))).tagged(&tag),
);
collected.into_tagged_value()
}
@ -208,13 +208,13 @@ fn from_bson(args: CommandArgs, registry: &CommandRegistry) -> Result<OutputStre
let value_tag = value.tag();
match value.item {
Value::Primitive(Primitive::Binary(vb)) =>
match from_bson_bytes_to_value(vb, tag) {
match from_bson_bytes_to_value(vb, tag.clone()) {
Ok(x) => yield ReturnSuccess::value(x),
Err(_) => {
yield Err(ShellError::labeled_error_with_secondary(
"Could not parse as BSON",
"input cannot be parsed as BSON",
tag,
tag.clone(),
"value originates from here",
value_tag,
))
@ -223,7 +223,7 @@ fn from_bson(args: CommandArgs, registry: &CommandRegistry) -> Result<OutputStre
_ => yield Err(ShellError::labeled_error_with_secondary(
"Expected a string from pipeline",
"requires string input",
tag,
tag.clone(),
"value originates from here",
value_tag,
)),

View file

@ -1,13 +1,14 @@
use crate::commands::from_structured_data::from_structured_data;
use crate::commands::WholeStreamCommand;
use crate::data::{Primitive, TaggedDictBuilder, Value};
use crate::data::{Primitive, Value};
use crate::prelude::*;
use csv::ReaderBuilder;
pub struct FromCSV;
#[derive(Deserialize)]
pub struct FromCSVArgs {
headerless: bool,
separator: Option<Tagged<Value>>,
}
impl WholeStreamCommand for FromCSV {
@ -16,11 +17,17 @@ impl WholeStreamCommand for FromCSV {
}
fn signature(&self) -> Signature {
Signature::build("from-csv").switch("headerless")
Signature::build("from-csv")
.named(
"separator",
SyntaxShape::String,
"a character to separate columns, defaults to ','",
)
.switch("headerless", "don't treat the first row as column names")
}
fn usage(&self) -> &str {
"Parse text as .csv and create table"
"Parse text as .csv and create table."
}
fn run(
@ -32,107 +39,31 @@ impl WholeStreamCommand for FromCSV {
}
}
pub fn from_csv_string_to_value(
s: String,
headerless: bool,
tag: impl Into<Tag>,
) -> Result<Tagged<Value>, csv::Error> {
let mut reader = ReaderBuilder::new()
.has_headers(false)
.from_reader(s.as_bytes());
let tag = tag.into();
let mut fields: VecDeque<String> = VecDeque::new();
let mut iter = reader.records();
let mut rows = vec![];
if let Some(result) = iter.next() {
let line = result?;
for (idx, item) in line.iter().enumerate() {
if headerless {
fields.push_back(format!("Column{}", idx + 1));
} else {
fields.push_back(item.to_string());
}
}
}
loop {
if let Some(row_values) = iter.next() {
let row_values = row_values?;
let mut row = TaggedDictBuilder::new(tag);
for (idx, entry) in row_values.iter().enumerate() {
row.insert_tagged(
fields.get(idx).unwrap(),
Value::Primitive(Primitive::String(String::from(entry))).tagged(tag),
);
}
rows.push(row.into_tagged_value());
} else {
break;
}
}
Ok(Tagged::from_item(Value::Table(rows), tag))
}
fn from_csv(
FromCSVArgs {
headerless: skip_headers,
headerless,
separator,
}: FromCSVArgs,
RunnableContext { input, name, .. }: RunnableContext,
runnable_context: RunnableContext,
) -> Result<OutputStream, ShellError> {
let name_tag = name;
let stream = async_stream! {
let values: Vec<Tagged<Value>> = input.values.collect().await;
let mut concat_string = String::new();
let mut latest_tag: Option<Tag> = None;
for value in values {
let value_tag = value.tag();
latest_tag = Some(value_tag);
match value.item {
Value::Primitive(Primitive::String(s)) => {
concat_string.push_str(&s);
concat_string.push_str("\n");
}
_ => yield Err(ShellError::labeled_error_with_secondary(
"Expected a string from pipeline",
"requires string input",
name_tag,
"value originates from here",
value_tag,
)),
}
}
match from_csv_string_to_value(concat_string, skip_headers, name_tag) {
Ok(x) => match x {
Tagged { item: Value::Table(list), .. } => {
for l in list {
yield ReturnSuccess::value(l);
}
}
x => yield ReturnSuccess::value(x),
},
Err(_) => if let Some(last_tag) = latest_tag {
yield Err(ShellError::labeled_error_with_secondary(
"Could not parse as CSV",
"input cannot be parsed as CSV",
name_tag,
"value originates from here",
last_tag,
))
} ,
let sep = match separator {
Some(Tagged {
item: Value::Primitive(Primitive::String(s)),
tag,
..
}) => {
let vec_s: Vec<char> = s.chars().collect();
if vec_s.len() != 1 {
return Err(ShellError::labeled_error(
"Expected a single separator char from --separator",
"requires a single character string input",
tag,
));
};
vec_s[0]
}
_ => ',',
};
Ok(stream.to_output_stream())
from_structured_data(headerless, sep, "CSV", runnable_context)
}

View file

@ -45,10 +45,13 @@ fn convert_ini_top_to_nu_value(
tag: impl Into<Tag>,
) -> Tagged<Value> {
let tag = tag.into();
let mut top_level = TaggedDictBuilder::new(tag);
let mut top_level = TaggedDictBuilder::new(tag.clone());
for (key, value) in v.iter() {
top_level.insert_tagged(key.clone(), convert_ini_second_to_nu_value(value, tag));
top_level.insert_tagged(
key.clone(),
convert_ini_second_to_nu_value(value, tag.clone()),
);
}
top_level.into_tagged_value()
@ -75,7 +78,7 @@ fn from_ini(args: CommandArgs, registry: &CommandRegistry) -> Result<OutputStrea
for value in values {
let value_tag = value.tag();
latest_tag = Some(value_tag);
latest_tag = Some(value_tag.clone());
match value.item {
Value::Primitive(Primitive::String(s)) => {
concat_string.push_str(&s);
@ -84,15 +87,15 @@ fn from_ini(args: CommandArgs, registry: &CommandRegistry) -> Result<OutputStrea
_ => yield Err(ShellError::labeled_error_with_secondary(
"Expected a string from pipeline",
"requires string input",
tag,
&tag,
"value originates from here",
value_tag,
&value_tag,
)),
}
}
match from_ini_string_to_value(concat_string, tag) {
match from_ini_string_to_value(concat_string, tag.clone()) {
Ok(x) => match x {
Tagged { item: Value::Table(list), .. } => {
for l in list {
@ -105,7 +108,7 @@ fn from_ini(args: CommandArgs, registry: &CommandRegistry) -> Result<OutputStrea
yield Err(ShellError::labeled_error_with_secondary(
"Could not parse as INI",
"input cannot be parsed as INI",
tag,
&tag,
"value originates from here",
last_tag,
))

View file

@ -15,7 +15,7 @@ impl WholeStreamCommand for FromJSON {
}
fn signature(&self) -> Signature {
Signature::build("from-json").switch("objects")
Signature::build("from-json").switch("objects", "treat each line as a separate value")
}
fn usage(&self) -> &str {
@ -35,24 +35,24 @@ fn convert_json_value_to_nu_value(v: &serde_hjson::Value, tag: impl Into<Tag>) -
let tag = tag.into();
match v {
serde_hjson::Value::Null => Value::Primitive(Primitive::Nothing).tagged(tag),
serde_hjson::Value::Bool(b) => Value::boolean(*b).tagged(tag),
serde_hjson::Value::F64(n) => Value::number(n).tagged(tag),
serde_hjson::Value::U64(n) => Value::number(n).tagged(tag),
serde_hjson::Value::I64(n) => Value::number(n).tagged(tag),
serde_hjson::Value::Null => Value::Primitive(Primitive::Nothing).tagged(&tag),
serde_hjson::Value::Bool(b) => Value::boolean(*b).tagged(&tag),
serde_hjson::Value::F64(n) => Value::number(n).tagged(&tag),
serde_hjson::Value::U64(n) => Value::number(n).tagged(&tag),
serde_hjson::Value::I64(n) => Value::number(n).tagged(&tag),
serde_hjson::Value::String(s) => {
Value::Primitive(Primitive::String(String::from(s))).tagged(tag)
Value::Primitive(Primitive::String(String::from(s))).tagged(&tag)
}
serde_hjson::Value::Array(a) => Value::Table(
a.iter()
.map(|x| convert_json_value_to_nu_value(x, tag))
.map(|x| convert_json_value_to_nu_value(x, &tag))
.collect(),
)
.tagged(tag),
serde_hjson::Value::Object(o) => {
let mut collected = TaggedDictBuilder::new(tag);
let mut collected = TaggedDictBuilder::new(&tag);
for (k, v) in o.iter() {
collected.insert_tagged(k.clone(), convert_json_value_to_nu_value(v, tag));
collected.insert_tagged(k.clone(), convert_json_value_to_nu_value(v, &tag));
}
collected.into_tagged_value()
@ -82,7 +82,7 @@ fn from_json(
for value in values {
let value_tag = value.tag();
latest_tag = Some(value_tag);
latest_tag = Some(value_tag.clone());
match value.item {
Value::Primitive(Primitive::String(s)) => {
concat_string.push_str(&s);
@ -91,9 +91,9 @@ fn from_json(
_ => yield Err(ShellError::labeled_error_with_secondary(
"Expected a string from pipeline",
"requires string input",
name_tag,
&name_tag,
"value originates from here",
value_tag,
&value_tag,
)),
}
@ -106,15 +106,15 @@ fn from_json(
continue;
}
match from_json_string_to_value(json_str.to_string(), name_tag) {
match from_json_string_to_value(json_str.to_string(), &name_tag) {
Ok(x) =>
yield ReturnSuccess::value(x),
Err(_) => {
if let Some(last_tag) = latest_tag {
if let Some(ref last_tag) = latest_tag {
yield Err(ShellError::labeled_error_with_secondary(
"Could nnot parse as JSON",
"input cannot be parsed as JSON",
name_tag,
&name_tag,
"value originates from here",
last_tag))
}
@ -122,7 +122,7 @@ fn from_json(
}
}
} else {
match from_json_string_to_value(concat_string, name_tag) {
match from_json_string_to_value(concat_string, name_tag.clone()) {
Ok(x) =>
match x {
Tagged { item: Value::Table(list), .. } => {

View file

@ -138,7 +138,7 @@ fn from_sqlite(args: CommandArgs, registry: &CommandRegistry) -> Result<OutputSt
let value_tag = value.tag();
match value.item {
Value::Primitive(Primitive::Binary(vb)) =>
match from_sqlite_bytes_to_value(vb, tag) {
match from_sqlite_bytes_to_value(vb, tag.clone()) {
Ok(x) => match x {
Tagged { item: Value::Table(list), .. } => {
for l in list {
@ -151,7 +151,7 @@ fn from_sqlite(args: CommandArgs, registry: &CommandRegistry) -> Result<OutputSt
yield Err(ShellError::labeled_error_with_secondary(
"Could not parse as SQLite",
"input cannot be parsed as SQLite",
tag,
&tag,
"value originates from here",
value_tag,
))
@ -160,7 +160,7 @@ fn from_sqlite(args: CommandArgs, registry: &CommandRegistry) -> Result<OutputSt
_ => yield Err(ShellError::labeled_error_with_secondary(
"Expected a string from pipeline",
"requires string input",
tag,
&tag,
"value originates from here",
value_tag,
)),

504
src/commands/from_ssv.rs Normal file
View file

@ -0,0 +1,504 @@
use crate::commands::WholeStreamCommand;
use crate::data::{Primitive, TaggedDictBuilder, Value};
use crate::prelude::*;
pub struct FromSSV;
#[derive(Deserialize)]
pub struct FromSSVArgs {
headerless: bool,
#[serde(rename(deserialize = "aligned-columns"))]
aligned_columns: bool,
#[serde(rename(deserialize = "minimum-spaces"))]
minimum_spaces: Option<Tagged<usize>>,
}
const STRING_REPRESENTATION: &str = "from-ssv";
const DEFAULT_MINIMUM_SPACES: usize = 2;
impl WholeStreamCommand for FromSSV {
fn name(&self) -> &str {
STRING_REPRESENTATION
}
fn signature(&self) -> Signature {
Signature::build(STRING_REPRESENTATION)
.switch("headerless", "don't treat the first row as column names")
.switch("aligned-columns", "assume columns are aligned")
.named(
"minimum-spaces",
SyntaxShape::Int,
"the mininum spaces to separate columns",
)
}
fn usage(&self) -> &str {
"Parse text as space-separated values and create a table. The default minimum number of spaces counted as a separator is 2."
}
fn run(
&self,
args: CommandArgs,
registry: &CommandRegistry,
) -> Result<OutputStream, ShellError> {
args.process(registry, from_ssv)?.run()
}
}
enum HeaderOptions<'a> {
WithHeaders(&'a str),
WithoutHeaders,
}
fn parse_aligned_columns<'a>(
lines: impl Iterator<Item = &'a str>,
headers: HeaderOptions,
separator: &str,
) -> Vec<Vec<(String, String)>> {
fn construct<'a>(
lines: impl Iterator<Item = &'a str>,
headers: Vec<(String, usize)>,
) -> Vec<Vec<(String, String)>> {
lines
.map(|l| {
headers
.iter()
.enumerate()
.map(|(i, (header_name, start_position))| {
let val = match headers.get(i + 1) {
Some((_, end)) => {
if *end < l.len() {
l.get(*start_position..*end)
} else {
l.get(*start_position..)
}
}
None => l.get(*start_position..),
}
.unwrap_or("")
.trim()
.into();
(header_name.clone(), val)
})
.collect()
})
.collect()
}
let find_indices = |line: &str| {
let values = line
.split(&separator)
.map(str::trim)
.filter(|s| !s.is_empty());
values
.fold(
(0, vec![]),
|(current_pos, mut indices), value| match line[current_pos..].find(value) {
None => (current_pos, indices),
Some(index) => {
let absolute_index = current_pos + index;
indices.push(absolute_index);
(absolute_index + value.len(), indices)
}
},
)
.1
};
let parse_with_headers = |lines, headers_raw: &str| {
let indices = find_indices(headers_raw);
let headers = headers_raw
.split(&separator)
.map(str::trim)
.filter(|s| !s.is_empty())
.map(String::from)
.zip(indices);
let columns = headers.collect::<Vec<(String, usize)>>();
construct(lines, columns)
};
let parse_without_headers = |ls: Vec<&str>| {
let mut indices = ls
.iter()
.flat_map(|s| find_indices(*s))
.collect::<Vec<usize>>();
indices.sort();
indices.dedup();
let headers: Vec<(String, usize)> = indices
.iter()
.enumerate()
.map(|(i, position)| (format!("Column{}", i + 1), *position))
.collect();
construct(ls.iter().map(|s| s.to_owned()), headers)
};
match headers {
HeaderOptions::WithHeaders(headers_raw) => parse_with_headers(lines, headers_raw),
HeaderOptions::WithoutHeaders => parse_without_headers(lines.collect()),
}
}
fn parse_separated_columns<'a>(
lines: impl Iterator<Item = &'a str>,
headers: HeaderOptions,
separator: &str,
) -> Vec<Vec<(String, String)>> {
fn collect<'a>(
headers: Vec<String>,
rows: impl Iterator<Item = &'a str>,
separator: &str,
) -> Vec<Vec<(String, String)>> {
rows.map(|r| {
headers
.iter()
.zip(r.split(separator).map(str::trim).filter(|s| !s.is_empty()))
.map(|(a, b)| (a.to_owned(), b.to_owned()))
.collect()
})
.collect()
}
let parse_with_headers = |lines, headers_raw: &str| {
let headers = headers_raw
.split(&separator)
.map(str::trim)
.map(|s| s.to_owned())
.filter(|s| !s.is_empty())
.collect();
collect(headers, lines, separator)
};
let parse_without_headers = |ls: Vec<&str>| {
let num_columns = ls.iter().map(|r| r.len()).max().unwrap_or(0);
let headers = (1..=num_columns)
.map(|i| format!("Column{}", i))
.collect::<Vec<String>>();
collect(headers, ls.iter().map(|s| s.as_ref()), separator)
};
match headers {
HeaderOptions::WithHeaders(headers_raw) => parse_with_headers(lines, headers_raw),
HeaderOptions::WithoutHeaders => parse_without_headers(lines.collect()),
}
}
fn string_to_table(
s: &str,
headerless: bool,
aligned_columns: bool,
split_at: usize,
) -> Option<Vec<Vec<(String, String)>>> {
let mut lines = s.lines().filter(|l| !l.trim().is_empty());
let separator = " ".repeat(std::cmp::max(split_at, 1));
let (ls, header_options) = if headerless {
(lines, HeaderOptions::WithoutHeaders)
} else {
let headers = lines.next()?;
(lines, HeaderOptions::WithHeaders(headers))
};
let f = if aligned_columns {
parse_aligned_columns
} else {
parse_separated_columns
};
let parsed = f(ls, header_options, &separator);
match parsed.len() {
0 => None,
_ => Some(parsed),
}
}
fn from_ssv_string_to_value(
s: &str,
headerless: bool,
aligned_columns: bool,
split_at: usize,
tag: impl Into<Tag>,
) -> Option<Tagged<Value>> {
let tag = tag.into();
let rows = string_to_table(s, headerless, aligned_columns, split_at)?
.iter()
.map(|row| {
let mut tagged_dict = TaggedDictBuilder::new(&tag);
for (col, entry) in row {
tagged_dict.insert_tagged(
col,
Value::Primitive(Primitive::String(String::from(entry))).tagged(&tag),
)
}
tagged_dict.into_tagged_value()
})
.collect();
Some(Value::Table(rows).tagged(&tag))
}
fn from_ssv(
FromSSVArgs {
headerless,
aligned_columns,
minimum_spaces,
}: FromSSVArgs,
RunnableContext { input, name, .. }: RunnableContext,
) -> Result<OutputStream, ShellError> {
let stream = async_stream! {
let values: Vec<Tagged<Value>> = input.values.collect().await;
let mut concat_string = String::new();
let mut latest_tag: Option<Tag> = None;
let split_at = match minimum_spaces {
Some(number) => number.item,
None => DEFAULT_MINIMUM_SPACES
};
for value in values {
let value_tag = value.tag();
latest_tag = Some(value_tag.clone());
match value.item {
Value::Primitive(Primitive::String(s)) => {
concat_string.push_str(&s);
}
_ => yield Err(ShellError::labeled_error_with_secondary (
"Expected a string from pipeline",
"requires string input",
&name,
"value originates from here",
&value_tag
)),
}
}
match from_ssv_string_to_value(&concat_string, headerless, aligned_columns, split_at, name.clone()) {
Some(x) => match x {
Tagged { item: Value::Table(list), ..} => {
for l in list { yield ReturnSuccess::value(l) }
}
x => yield ReturnSuccess::value(x)
},
None => if let Some(tag) = latest_tag {
yield Err(ShellError::labeled_error_with_secondary(
"Could not parse as SSV",
"input cannot be parsed ssv",
&name,
"value originates from here",
&tag,
))
},
}
};
Ok(stream.to_output_stream())
}
#[cfg(test)]
mod tests {
use super::*;
fn owned(x: &str, y: &str) -> (String, String) {
(String::from(x), String::from(y))
}
#[test]
fn it_trims_empty_and_whitespace_only_lines() {
let input = r#"
a b
1 2
3 4
"#;
let result = string_to_table(input, false, true, 1);
assert_eq!(
result,
Some(vec![
vec![owned("a", "1"), owned("b", "2")],
vec![owned("a", "3"), owned("b", "4")]
])
);
}
#[test]
fn it_deals_with_single_column_input() {
let input = r#"
a
1
2
"#;
let result = string_to_table(input, false, true, 1);
assert_eq!(
result,
Some(vec![vec![owned("a", "1")], vec![owned("a", "2")]])
);
}
#[test]
fn it_uses_first_row_as_data_when_headerless() {
let input = r#"
a b
1 2
3 4
"#;
let result = string_to_table(input, true, true, 1);
assert_eq!(
result,
Some(vec![
vec![owned("Column1", "a"), owned("Column2", "b")],
vec![owned("Column1", "1"), owned("Column2", "2")],
vec![owned("Column1", "3"), owned("Column2", "4")]
])
);
}
#[test]
fn it_returns_none_given_an_empty_string() {
let input = "";
let result = string_to_table(input, true, true, 1);
assert!(result.is_none());
}
#[test]
fn it_allows_a_predefined_number_of_spaces() {
let input = r#"
column a column b
entry 1 entry number 2
3 four
"#;
let result = string_to_table(input, false, true, 3);
assert_eq!(
result,
Some(vec![
vec![
owned("column a", "entry 1"),
owned("column b", "entry number 2")
],
vec![owned("column a", "3"), owned("column b", "four")]
])
);
}
#[test]
fn it_trims_remaining_separator_space() {
let input = r#"
colA colB colC
val1 val2 val3
"#;
let trimmed = |s: &str| s.trim() == s;
let result = string_to_table(input, false, true, 2).unwrap();
assert!(result
.iter()
.all(|row| row.iter().all(|(a, b)| trimmed(a) && trimmed(b))))
}
#[test]
fn it_keeps_empty_columns() {
let input = r#"
colA col B col C
val2 val3
val4 val 5 val 6
val7 val8
"#;
let result = string_to_table(input, false, true, 2).unwrap();
assert_eq!(
result,
vec![
vec![
owned("colA", ""),
owned("col B", "val2"),
owned("col C", "val3")
],
vec![
owned("colA", "val4"),
owned("col B", "val 5"),
owned("col C", "val 6")
],
vec![
owned("colA", "val7"),
owned("col B", ""),
owned("col C", "val8")
],
]
)
}
#[test]
fn it_uses_the_full_final_column() {
let input = r#"
colA col B
val1 val2 trailing value that should be included
"#;
let result = string_to_table(input, false, true, 2).unwrap();
assert_eq!(
result,
vec![vec![
owned("colA", "val1"),
owned("col B", "val2 trailing value that should be included"),
],]
)
}
#[test]
fn it_handles_empty_values_when_headerless_and_aligned_columns() {
let input = r#"
a multi-word value b d
1 3-3 4
last
"#;
let result = string_to_table(input, true, true, 2).unwrap();
assert_eq!(
result,
vec![
vec![
owned("Column1", "a multi-word value"),
owned("Column2", "b"),
owned("Column3", ""),
owned("Column4", "d"),
owned("Column5", "")
],
vec![
owned("Column1", "1"),
owned("Column2", ""),
owned("Column3", "3-3"),
owned("Column4", "4"),
owned("Column5", "")
],
vec![
owned("Column1", ""),
owned("Column2", ""),
owned("Column3", ""),
owned("Column4", ""),
owned("Column5", "last")
],
]
)
}
#[test]
fn input_is_parsed_correctly_if_either_option_works() {
let input = r#"
docker-registry docker-registry=default docker-registry=default 172.30.78.158 5000/TCP
kubernetes component=apiserver,provider=kubernetes <none> 172.30.0.2 443/TCP
kubernetes-ro component=apiserver,provider=kubernetes <none> 172.30.0.1 80/TCP
"#;
let aligned_columns_headerless = string_to_table(input, true, true, 2).unwrap();
let separator_headerless = string_to_table(input, true, false, 2).unwrap();
let aligned_columns_with_headers = string_to_table(input, false, true, 2).unwrap();
let separator_with_headers = string_to_table(input, false, false, 2).unwrap();
assert_eq!(aligned_columns_headerless, separator_headerless);
assert_eq!(aligned_columns_with_headers, separator_with_headers);
}
}

View file

@ -0,0 +1,97 @@
use crate::data::{Primitive, TaggedDictBuilder, Value};
use crate::prelude::*;
use csv::ReaderBuilder;
fn from_stuctured_string_to_value(
s: String,
headerless: bool,
separator: char,
tag: impl Into<Tag>,
) -> Result<Tagged<Value>, csv::Error> {
let mut reader = ReaderBuilder::new()
.has_headers(!headerless)
.delimiter(separator as u8)
.from_reader(s.as_bytes());
let tag = tag.into();
let headers = if headerless {
(1..=reader.headers()?.len())
.map(|i| format!("Column{}", i))
.collect::<Vec<String>>()
} else {
reader.headers()?.iter().map(String::from).collect()
};
let mut rows = vec![];
for row in reader.records() {
let mut tagged_row = TaggedDictBuilder::new(&tag);
for (value, header) in row?.iter().zip(headers.iter()) {
tagged_row.insert_tagged(
header,
Value::Primitive(Primitive::String(String::from(value))).tagged(&tag),
)
}
rows.push(tagged_row.into_tagged_value());
}
Ok(Value::Table(rows).tagged(&tag))
}
pub fn from_structured_data(
headerless: bool,
sep: char,
format_name: &'static str,
RunnableContext { input, name, .. }: RunnableContext,
) -> Result<OutputStream, ShellError> {
let name_tag = name;
let stream = async_stream! {
let values: Vec<Tagged<Value>> = input.values.collect().await;
let mut concat_string = String::new();
let mut latest_tag: Option<Tag> = None;
for value in values {
let value_tag = value.tag();
latest_tag = Some(value_tag.clone());
match value.item {
Value::Primitive(Primitive::String(s)) => {
concat_string.push_str(&s);
concat_string.push_str("\n");
}
_ => yield Err(ShellError::labeled_error_with_secondary(
"Expected a string from pipeline",
"requires string input",
name_tag.clone(),
"value originates from here",
value_tag.clone(),
)),
}
}
match from_stuctured_string_to_value(concat_string, headerless, sep, name_tag.clone()) {
Ok(x) => match x {
Tagged { item: Value::Table(list), .. } => {
for l in list {
yield ReturnSuccess::value(l);
}
}
x => yield ReturnSuccess::value(x),
},
Err(_) => if let Some(last_tag) = latest_tag {
let line_one = format!("Could not parse as {}", format_name);
let line_two = format!("input cannot be parsed as {}", format_name);
yield Err(ShellError::labeled_error_with_secondary(
line_one,
line_two,
name_tag.clone(),
"value originates from here",
last_tag.clone(),
))
} ,
}
};
Ok(stream.to_output_stream())
}

View file

@ -36,7 +36,7 @@ pub fn convert_toml_value_to_nu_value(v: &toml::Value, tag: impl Into<Tag>) -> T
toml::Value::String(s) => Value::Primitive(Primitive::String(String::from(s))).tagged(tag),
toml::Value::Array(a) => Value::Table(
a.iter()
.map(|x| convert_toml_value_to_nu_value(x, tag))
.map(|x| convert_toml_value_to_nu_value(x, &tag))
.collect(),
)
.tagged(tag),
@ -44,10 +44,10 @@ pub fn convert_toml_value_to_nu_value(v: &toml::Value, tag: impl Into<Tag>) -> T
Value::Primitive(Primitive::String(dt.to_string())).tagged(tag)
}
toml::Value::Table(t) => {
let mut collected = TaggedDictBuilder::new(tag);
let mut collected = TaggedDictBuilder::new(&tag);
for (k, v) in t.iter() {
collected.insert_tagged(k.clone(), convert_toml_value_to_nu_value(v, tag));
collected.insert_tagged(k.clone(), convert_toml_value_to_nu_value(v, &tag));
}
collected.into_tagged_value()
@ -79,7 +79,7 @@ pub fn from_toml(
for value in values {
let value_tag = value.tag();
latest_tag = Some(value_tag);
latest_tag = Some(value_tag.clone());
match value.item {
Value::Primitive(Primitive::String(s)) => {
concat_string.push_str(&s);
@ -88,15 +88,15 @@ pub fn from_toml(
_ => yield Err(ShellError::labeled_error_with_secondary(
"Expected a string from pipeline",
"requires string input",
tag,
&tag,
"value originates from here",
value_tag,
&value_tag,
)),
}
}
match from_toml_string_to_value(concat_string, tag) {
match from_toml_string_to_value(concat_string, tag.clone()) {
Ok(x) => match x {
Tagged { item: Value::Table(list), .. } => {
for l in list {
@ -109,7 +109,7 @@ pub fn from_toml(
yield Err(ShellError::labeled_error_with_secondary(
"Could not parse as TOML",
"input cannot be parsed as TOML",
tag,
&tag,
"value originates from here",
last_tag,
))

View file

@ -1,7 +1,6 @@
use crate::commands::from_structured_data::from_structured_data;
use crate::commands::WholeStreamCommand;
use crate::data::{Primitive, TaggedDictBuilder, Value};
use crate::prelude::*;
use csv::ReaderBuilder;
pub struct FromTSV;
@ -16,7 +15,8 @@ impl WholeStreamCommand for FromTSV {
}
fn signature(&self) -> Signature {
Signature::build("from-tsv").switch("headerless")
Signature::build("from-tsv")
.switch("headerless", "don't treat the first row as column names")
}
fn usage(&self) -> &str {
@ -32,108 +32,9 @@ impl WholeStreamCommand for FromTSV {
}
}
pub fn from_tsv_string_to_value(
s: String,
headerless: bool,
tag: impl Into<Tag>,
) -> Result<Tagged<Value>, csv::Error> {
let mut reader = ReaderBuilder::new()
.has_headers(false)
.delimiter(b'\t')
.from_reader(s.as_bytes());
let tag = tag.into();
let mut fields: VecDeque<String> = VecDeque::new();
let mut iter = reader.records();
let mut rows = vec![];
if let Some(result) = iter.next() {
let line = result?;
for (idx, item) in line.iter().enumerate() {
if headerless {
fields.push_back(format!("Column{}", idx + 1));
} else {
fields.push_back(item.to_string());
}
}
}
loop {
if let Some(row_values) = iter.next() {
let row_values = row_values?;
let mut row = TaggedDictBuilder::new(tag);
for (idx, entry) in row_values.iter().enumerate() {
row.insert_tagged(
fields.get(idx).unwrap(),
Value::Primitive(Primitive::String(String::from(entry))).tagged(tag),
);
}
rows.push(row.into_tagged_value());
} else {
break;
}
}
Ok(Tagged::from_item(Value::Table(rows), tag))
}
fn from_tsv(
FromTSVArgs {
headerless: skip_headers,
}: FromTSVArgs,
RunnableContext { input, name, .. }: RunnableContext,
FromTSVArgs { headerless }: FromTSVArgs,
runnable_context: RunnableContext,
) -> Result<OutputStream, ShellError> {
let name_tag = name;
let stream = async_stream! {
let values: Vec<Tagged<Value>> = input.values.collect().await;
let mut concat_string = String::new();
let mut latest_tag: Option<Tag> = None;
for value in values {
let value_tag = value.tag();
latest_tag = Some(value_tag);
match value.item {
Value::Primitive(Primitive::String(s)) => {
concat_string.push_str(&s);
concat_string.push_str("\n");
}
_ => yield Err(ShellError::labeled_error_with_secondary(
"Expected a string from pipeline",
"requires string input",
name_tag,
"value originates from here",
value_tag,
)),
}
}
match from_tsv_string_to_value(concat_string, skip_headers, name_tag) {
Ok(x) => match x {
Tagged { item: Value::Table(list), .. } => {
for l in list {
yield ReturnSuccess::value(l);
}
}
x => yield ReturnSuccess::value(x),
},
Err(_) => if let Some(last_tag) = latest_tag {
yield Err(ShellError::labeled_error_with_secondary(
"Could not parse as TSV",
"input cannot be parsed as TSV",
name_tag,
"value originates from here",
last_tag,
))
} ,
}
};
Ok(stream.to_output_stream())
from_structured_data(headerless, '\t', "TSV", runnable_context)
}

View file

@ -39,7 +39,7 @@ fn from_url(args: CommandArgs, registry: &CommandRegistry) -> Result<OutputStrea
for value in values {
let value_tag = value.tag();
latest_tag = Some(value_tag);
latest_tag = Some(value_tag.clone());
match value.item {
Value::Primitive(Primitive::String(s)) => {
concat_string.push_str(&s);
@ -47,9 +47,9 @@ fn from_url(args: CommandArgs, registry: &CommandRegistry) -> Result<OutputStrea
_ => yield Err(ShellError::labeled_error_with_secondary(
"Expected a string from pipeline",
"requires string input",
tag,
&tag,
"value originates from here",
value_tag,
&value_tag,
)),
}

View file

@ -34,7 +34,7 @@ fn from_node_to_value<'a, 'd>(n: &roxmltree::Node<'a, 'd>, tag: impl Into<Tag>)
let mut children_values = vec![];
for c in n.children() {
children_values.push(from_node_to_value(&c, tag));
children_values.push(from_node_to_value(&c, &tag));
}
let children_values: Vec<Tagged<Value>> = children_values
@ -94,7 +94,7 @@ fn from_xml(args: CommandArgs, registry: &CommandRegistry) -> Result<OutputStrea
for value in values {
let value_tag = value.tag();
latest_tag = Some(value_tag);
latest_tag = Some(value_tag.clone());
match value.item {
Value::Primitive(Primitive::String(s)) => {
concat_string.push_str(&s);
@ -103,15 +103,15 @@ fn from_xml(args: CommandArgs, registry: &CommandRegistry) -> Result<OutputStrea
_ => yield Err(ShellError::labeled_error_with_secondary(
"Expected a string from pipeline",
"requires string input",
tag,
&tag,
"value originates from here",
value_tag,
&value_tag,
)),
}
}
match from_xml_string_to_value(concat_string, tag) {
match from_xml_string_to_value(concat_string, tag.clone()) {
Ok(x) => match x {
Tagged { item: Value::Table(list), .. } => {
for l in list {
@ -124,9 +124,9 @@ fn from_xml(args: CommandArgs, registry: &CommandRegistry) -> Result<OutputStrea
yield Err(ShellError::labeled_error_with_secondary(
"Could not parse as XML",
"input cannot be parsed as XML",
tag,
&tag,
"value originates from here",
last_tag,
&last_tag,
))
} ,
}
@ -134,3 +134,73 @@ fn from_xml(args: CommandArgs, registry: &CommandRegistry) -> Result<OutputStrea
Ok(stream.to_output_stream())
}
#[cfg(test)]
mod tests {
use crate::commands::from_xml;
use crate::data::meta::*;
use crate::Value;
use indexmap::IndexMap;
fn string(input: impl Into<String>) -> Tagged<Value> {
Value::string(input.into()).tagged_unknown()
}
fn row(entries: IndexMap<String, Tagged<Value>>) -> Tagged<Value> {
Value::row(entries).tagged_unknown()
}
fn table(list: &Vec<Tagged<Value>>) -> Tagged<Value> {
Value::table(list).tagged_unknown()
}
fn parse(xml: &str) -> Tagged<Value> {
from_xml::from_xml_string_to_value(xml.to_string(), Tag::unknown()).unwrap()
}
#[test]
fn parses_empty_element() {
let source = "<nu></nu>";
assert_eq!(
parse(source),
row(indexmap! {
"nu".into() => table(&vec![])
})
);
}
#[test]
fn parses_element_with_text() {
let source = "<nu>La era de los tres caballeros</nu>";
assert_eq!(
parse(source),
row(indexmap! {
"nu".into() => table(&vec![string("La era de los tres caballeros")])
})
);
}
#[test]
fn parses_element_with_elements() {
let source = "\
<nu>
<dev>Andrés</dev>
<dev>Jonathan</dev>
<dev>Yehuda</dev>
</nu>";
assert_eq!(
parse(source),
row(indexmap! {
"nu".into() => table(&vec![
row(indexmap! {"dev".into() => table(&vec![string("Andrés")])}),
row(indexmap! {"dev".into() => table(&vec![string("Jonathan")])}),
row(indexmap! {"dev".into() => table(&vec![string("Yehuda")])})
])
})
);
}
}

View file

@ -64,17 +64,17 @@ fn convert_yaml_value_to_nu_value(v: &serde_yaml::Value, tag: impl Into<Tag>) ->
serde_yaml::Value::String(s) => Value::string(s).tagged(tag),
serde_yaml::Value::Sequence(a) => Value::Table(
a.iter()
.map(|x| convert_yaml_value_to_nu_value(x, tag))
.map(|x| convert_yaml_value_to_nu_value(x, &tag))
.collect(),
)
.tagged(tag),
serde_yaml::Value::Mapping(t) => {
let mut collected = TaggedDictBuilder::new(tag);
let mut collected = TaggedDictBuilder::new(&tag);
for (k, v) in t.iter() {
match k {
serde_yaml::Value::String(k) => {
collected.insert_tagged(k.clone(), convert_yaml_value_to_nu_value(v, tag));
collected.insert_tagged(k.clone(), convert_yaml_value_to_nu_value(v, &tag));
}
_ => unimplemented!("Unknown key type"),
}
@ -108,7 +108,7 @@ fn from_yaml(args: CommandArgs, registry: &CommandRegistry) -> Result<OutputStre
for value in values {
let value_tag = value.tag();
latest_tag = Some(value_tag);
latest_tag = Some(value_tag.clone());
match value.item {
Value::Primitive(Primitive::String(s)) => {
concat_string.push_str(&s);
@ -117,15 +117,15 @@ fn from_yaml(args: CommandArgs, registry: &CommandRegistry) -> Result<OutputStre
_ => yield Err(ShellError::labeled_error_with_secondary(
"Expected a string from pipeline",
"requires string input",
tag,
&tag,
"value originates from here",
value_tag,
&value_tag,
)),
}
}
match from_yaml_string_to_value(concat_string, tag) {
match from_yaml_string_to_value(concat_string, tag.clone()) {
Ok(x) => match x {
Tagged { item: Value::Table(list), .. } => {
for l in list {
@ -138,9 +138,9 @@ fn from_yaml(args: CommandArgs, registry: &CommandRegistry) -> Result<OutputStre
yield Err(ShellError::labeled_error_with_secondary(
"Could not parse as YAML",
"input cannot be parsed as YAML",
tag,
&tag,
"value originates from here",
last_tag,
&last_tag,
))
} ,
}

View file

@ -2,13 +2,15 @@ use crate::commands::WholeStreamCommand;
use crate::data::Value;
use crate::errors::ShellError;
use crate::prelude::*;
use crate::utils::did_you_mean;
use log::trace;
pub struct Get;
#[derive(Deserialize)]
pub struct GetArgs {
member: Tagged<String>,
rest: Vec<Tagged<String>>,
member: ColumnPath,
rest: Vec<ColumnPath>,
}
impl WholeStreamCommand for Get {
@ -18,8 +20,15 @@ impl WholeStreamCommand for Get {
fn signature(&self) -> Signature {
Signature::build("get")
.required("member", SyntaxShape::Member)
.rest(SyntaxShape::Member)
.required(
"member",
SyntaxShape::ColumnPath,
"the path to the data to get",
)
.rest(
SyntaxShape::ColumnPath,
"optionally return additional data by path",
)
}
fn usage(&self) -> &str {
@ -35,59 +44,98 @@ impl WholeStreamCommand for Get {
}
}
fn get_member(path: &Tagged<String>, obj: &Tagged<Value>) -> Result<Tagged<Value>, ShellError> {
let mut current = Some(obj);
for p in path.split(".") {
if let Some(obj) = current {
current = match obj.get_data_by_key(p) {
Some(v) => Some(v),
None =>
// Before we give up, see if they gave us a path that matches a field name by itself
{
match obj.get_data_by_key(&path.item) {
Some(v) => return Ok(v.clone()),
None => {
let possibilities = obj.data_descriptors();
pub type ColumnPath = Vec<Tagged<Value>>;
let mut possible_matches: Vec<_> = possibilities
.iter()
.map(|x| {
(natural::distance::levenshtein_distance(x, &path.item), x)
})
.collect();
pub fn get_column_path(
path: &ColumnPath,
obj: &Tagged<Value>,
) -> Result<Tagged<Value>, ShellError> {
let fields = path.clone();
possible_matches.sort();
let value = obj.get_data_by_column_path(
obj.tag(),
path,
Box::new(move |(obj_source, column_path_tried)| {
match obj_source {
Value::Table(rows) => {
let total = rows.len();
let end_tag = match fields.iter().nth_back(if fields.len() > 2 { 1 } else { 0 })
{
Some(last_field) => last_field.tag(),
None => column_path_tried.tag(),
};
if possible_matches.len() > 0 {
return Err(ShellError::labeled_error(
"Unknown column",
format!("did you mean '{}'?", possible_matches[0].1),
path.tag(),
));
return ShellError::labeled_error_with_secondary(
"Row not found",
format!(
"There isn't a row indexed at '{}'",
match &*column_path_tried {
Value::Primitive(primitive) => primitive.format(None),
_ => String::from(""),
}
None
}
}
),
column_path_tried.tag(),
format!("The table only has {} rows (0..{})", total, total - 1),
end_tag,
);
}
_ => {}
}
}
}
match current {
Some(v) => Ok(v.clone()),
None => match obj {
// If its None check for certain values.
Tagged {
item: Value::Primitive(Primitive::String(_)),
..
} => Ok(obj.clone()),
Tagged {
item: Value::Primitive(Primitive::Path(_)),
..
} => Ok(obj.clone()),
_ => Ok(Value::nothing().tagged(obj.tag)),
match &column_path_tried {
Tagged {
item: Value::Primitive(Primitive::Int(index)),
..
} => {
return ShellError::labeled_error(
"No rows available",
format!(
"Not a table. Perhaps you meant to get the column '{}' instead?",
index
),
column_path_tried.tag(),
)
}
_ => match did_you_mean(&obj_source, &column_path_tried) {
Some(suggestions) => {
return ShellError::labeled_error(
"Unknown column",
format!("did you mean '{}'?", suggestions[0].1),
tag_for_tagged_list(fields.iter().map(|p| p.tag())),
)
}
None => {
return ShellError::labeled_error(
"Unknown column",
"row does not contain this column",
tag_for_tagged_list(fields.iter().map(|p| p.tag())),
)
}
},
}
}),
);
let res = match value {
Ok(fetched) => match fetched {
Some(Tagged { item: v, .. }) => Ok((v.clone()).tagged(&obj.tag)),
None => match obj {
// If its None check for certain values.
Tagged {
item: Value::Primitive(Primitive::String(_)),
..
} => Ok(obj.clone()),
Tagged {
item: Value::Primitive(Primitive::Path(_)),
..
} => Ok(obj.clone()),
_ => Ok(Value::nothing().tagged(&obj.tag)),
},
},
}
Err(reason) => Err(reason),
};
res
}
pub fn get(
@ -97,6 +145,8 @@ pub fn get(
}: GetArgs,
RunnableContext { input, .. }: RunnableContext,
) -> Result<OutputStream, ShellError> {
trace!("get {:?} {:?}", member, fields);
let stream = input
.values
.map(move |item| {
@ -104,26 +154,36 @@ pub fn get(
let member = vec![member.clone()];
let fields = vec![&member, &fields]
let column_paths = vec![&member, &fields]
.into_iter()
.flatten()
.collect::<Vec<&Tagged<String>>>();
.collect::<Vec<&ColumnPath>>();
for field in &fields {
match get_member(field, &item) {
Ok(Tagged {
item: Value::Table(l),
..
}) => {
for item in l {
result.push_back(ReturnSuccess::value(item.clone()));
for path in column_paths {
let res = get_column_path(&path, &item);
match res {
Ok(got) => match got {
Tagged {
item: Value::Table(rows),
..
} => {
for row in rows {
result.push_back(ReturnSuccess::value(
Tagged {
item: row.item,
tag: Tag::from(&item.tag),
}
.map_anchored(&item.tag.anchor),
))
}
}
}
Ok(x) => result.push_back(ReturnSuccess::value(x.clone())),
Err(x) => result.push_back(Err(x)),
other => result
.push_back(ReturnSuccess::value((*other).clone().tagged(&item.tag))),
},
Err(reason) => result.push_back(Err(reason)),
}
}
result
})
.flatten();

217
src/commands/group_by.rs Normal file
View file

@ -0,0 +1,217 @@
use crate::commands::WholeStreamCommand;
use crate::data::TaggedDictBuilder;
use crate::errors::ShellError;
use crate::prelude::*;
pub struct GroupBy;
#[derive(Deserialize)]
pub struct GroupByArgs {
column_name: Tagged<String>,
}
impl WholeStreamCommand for GroupBy {
fn name(&self) -> &str {
"group-by"
}
fn signature(&self) -> Signature {
Signature::build("group-by").required(
"column_name",
SyntaxShape::String,
"the name of the column to group by",
)
}
fn usage(&self) -> &str {
"Creates a new table with the data from the table rows grouped by the column given."
}
fn run(
&self,
args: CommandArgs,
registry: &CommandRegistry,
) -> Result<OutputStream, ShellError> {
args.process(registry, group_by)?.run()
}
}
pub fn group_by(
GroupByArgs { column_name }: GroupByArgs,
RunnableContext { input, name, .. }: RunnableContext,
) -> Result<OutputStream, ShellError> {
let stream = async_stream! {
let values: Vec<Tagged<Value>> = input.values.collect().await;
if values.is_empty() {
yield Err(ShellError::labeled_error(
"Expected table from pipeline",
"requires a table input",
column_name.span()
))
} else {
match group(&column_name, values, name) {
Ok(grouped) => yield ReturnSuccess::value(grouped),
Err(err) => yield Err(err)
}
}
};
Ok(stream.to_output_stream())
}
pub fn group(
column_name: &Tagged<String>,
values: Vec<Tagged<Value>>,
tag: impl Into<Tag>,
) -> Result<Tagged<Value>, ShellError> {
let tag = tag.into();
let mut groups = indexmap::IndexMap::new();
for value in values {
let group_key = value.get_data_by_key(column_name);
if group_key.is_none() {
let possibilities = value.data_descriptors();
let mut possible_matches: Vec<_> = possibilities
.iter()
.map(|x| (natural::distance::levenshtein_distance(x, column_name), x))
.collect();
possible_matches.sort();
if possible_matches.len() > 0 {
return Err(ShellError::labeled_error(
"Unknown column",
format!("did you mean '{}'?", possible_matches[0].1),
column_name.tag(),
));
} else {
return Err(ShellError::labeled_error(
"Unknown column",
"row does not contain this column",
column_name.tag(),
));
}
}
let group_key = group_key.unwrap().as_string()?;
let group = groups.entry(group_key).or_insert(vec![]);
group.push(value);
}
let mut out = TaggedDictBuilder::new(&tag);
for (k, v) in groups.iter() {
out.insert(k, Value::table(v));
}
Ok(out.into_tagged_value())
}
#[cfg(test)]
mod tests {
use crate::commands::group_by::group;
use crate::data::meta::*;
use crate::Value;
use indexmap::IndexMap;
fn string(input: impl Into<String>) -> Tagged<Value> {
Value::string(input.into()).tagged_unknown()
}
fn row(entries: IndexMap<String, Tagged<Value>>) -> Tagged<Value> {
Value::row(entries).tagged_unknown()
}
fn table(list: &Vec<Tagged<Value>>) -> Tagged<Value> {
Value::table(list).tagged_unknown()
}
fn nu_releases_commiters() -> Vec<Tagged<Value>> {
vec![
row(
indexmap! {"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("August 23-2019")},
),
row(
indexmap! {"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => string("August 23-2019")},
),
row(
indexmap! {"name".into() => string("YK"), "country".into() => string("US"), "date".into() => string("October 10-2019")},
),
row(
indexmap! {"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("Sept 24-2019")},
),
row(
indexmap! {"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => string("October 10-2019")},
),
row(
indexmap! {"name".into() => string("YK"), "country".into() => string("US"), "date".into() => string("Sept 24-2019")},
),
row(
indexmap! {"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("October 10-2019")},
),
row(
indexmap! {"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => string("Sept 24-2019")},
),
row(
indexmap! {"name".into() => string("YK"), "country".into() => string("US"), "date".into() => string("August 23-2019")},
),
]
}
#[test]
fn groups_table_by_date_column() {
let for_key = String::from("date").tagged_unknown();
assert_eq!(
group(&for_key, nu_releases_commiters(), Tag::unknown()).unwrap(),
row(indexmap! {
"August 23-2019".into() => table(&vec![
row(indexmap!{"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("August 23-2019")}),
row(indexmap!{"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => string("August 23-2019")}),
row(indexmap!{"name".into() => string("YK"), "country".into() => string("US"), "date".into() => string("August 23-2019")})
]),
"October 10-2019".into() => table(&vec![
row(indexmap!{"name".into() => string("YK"), "country".into() => string("US"), "date".into() => string("October 10-2019")}),
row(indexmap!{"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => string("October 10-2019")}),
row(indexmap!{"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("October 10-2019")})
]),
"Sept 24-2019".into() => table(&vec![
row(indexmap!{"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("Sept 24-2019")}),
row(indexmap!{"name".into() => string("YK"), "country".into() => string("US"), "date".into() => string("Sept 24-2019")}),
row(indexmap!{"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => string("Sept 24-2019")})
]),
})
);
}
#[test]
fn groups_table_by_country_column() {
let for_key = String::from("country").tagged_unknown();
assert_eq!(
group(&for_key, nu_releases_commiters(), Tag::unknown()).unwrap(),
row(indexmap! {
"EC".into() => table(&vec![
row(indexmap!{"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("August 23-2019")}),
row(indexmap!{"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("Sept 24-2019")}),
row(indexmap!{"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("October 10-2019")})
]),
"NZ".into() => table(&vec![
row(indexmap!{"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => string("August 23-2019")}),
row(indexmap!{"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => string("October 10-2019")}),
row(indexmap!{"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => string("Sept 24-2019")})
]),
"US".into() => table(&vec![
row(indexmap!{"name".into() => string("YK"), "country".into() => string("US"), "date".into() => string("October 10-2019")}),
row(indexmap!{"name".into() => string("YK"), "country".into() => string("US"), "date".into() => string("Sept 24-2019")}),
row(indexmap!{"name".into() => string("YK"), "country".into() => string("US"), "date".into() => string("August 23-2019")}),
]),
})
);
}
}

View file

@ -12,7 +12,7 @@ impl PerItemCommand for Help {
}
fn signature(&self) -> registry::Signature {
Signature::build("help").rest(SyntaxShape::Any)
Signature::build("help").rest(SyntaxShape::Any, "the name of command(s) to get help on")
}
fn usage(&self) -> &str {
@ -26,7 +26,7 @@ impl PerItemCommand for Help {
_raw_args: &RawCommandArgs,
_input: Tagged<Value>,
) -> Result<OutputStream, ShellError> {
let tag = call_info.name_tag;
let tag = &call_info.name_tag;
match call_info.args.nth(0) {
Some(Tagged {
@ -61,12 +61,9 @@ impl PerItemCommand for Help {
let mut one_liner = String::new();
one_liner.push_str(&signature.name);
one_liner.push_str(" ");
if signature.named.len() > 0 {
one_liner.push_str("{flags} ");
}
for positional in signature.positional {
match positional {
for positional in &signature.positional {
match &positional.0 {
PositionalType::Mandatory(name, _m) => {
one_liner.push_str(&format!("<{}> ", name));
}
@ -77,25 +74,70 @@ impl PerItemCommand for Help {
}
if signature.rest_positional.is_some() {
one_liner.push_str(" ...args");
one_liner.push_str(&format!(" ...args",));
}
if signature.named.len() > 0 {
one_liner.push_str("{flags} ");
}
long_desc.push_str(&format!("\nUsage:\n > {}\n", one_liner));
if signature.positional.len() > 0 || signature.rest_positional.is_some() {
long_desc.push_str("\nparameters:\n");
for positional in signature.positional {
match positional.0 {
PositionalType::Mandatory(name, _m) => {
long_desc
.push_str(&format!(" <{}> {}\n", name, positional.1));
}
PositionalType::Optional(name, _o) => {
long_desc
.push_str(&format!(" ({}) {}\n", name, positional.1));
}
}
}
if signature.rest_positional.is_some() {
long_desc.push_str(&format!(
" ...args{} {}\n",
if signature.rest_positional.is_some() {
":"
} else {
""
},
signature.rest_positional.unwrap().1
));
}
}
if signature.named.len() > 0 {
long_desc.push_str("\nflags:\n");
for (flag, ty) in signature.named {
match ty {
match ty.0 {
NamedType::Switch => {
long_desc.push_str(&format!(" --{}\n", flag));
long_desc.push_str(&format!(
" --{}{} {}\n",
flag,
if ty.1.len() > 0 { ":" } else { "" },
ty.1
));
}
NamedType::Mandatory(m) => {
long_desc.push_str(&format!(
" --{} <{}> (required parameter)\n",
flag, m
" --{} <{}> (required parameter){} {}\n",
flag,
m,
if ty.1.len() > 0 { ":" } else { "" },
ty.1
));
}
NamedType::Optional(o) => {
long_desc.push_str(&format!(" --{} <{}>\n", flag, o));
long_desc.push_str(&format!(
" --{} <{}>{} {}\n",
flag,
o,
if ty.1.len() > 0 { ":" } else { "" },
ty.1
));
}
}
}

163
src/commands/histogram.rs Normal file
View file

@ -0,0 +1,163 @@
use crate::commands::evaluate_by::evaluate;
use crate::commands::group_by::group;
use crate::commands::map_max_by::map_max;
use crate::commands::reduce_by::reduce;
use crate::commands::t_sort_by::columns_sorted;
use crate::commands::t_sort_by::t_sort;
use crate::commands::WholeStreamCommand;
use crate::data::TaggedDictBuilder;
use crate::errors::ShellError;
use crate::prelude::*;
use num_traits::cast::ToPrimitive;
pub struct Histogram;
#[derive(Deserialize)]
pub struct HistogramArgs {
column_name: Tagged<String>,
rest: Vec<Tagged<String>>,
}
impl WholeStreamCommand for Histogram {
fn name(&self) -> &str {
"histogram"
}
fn signature(&self) -> Signature {
Signature::build("histogram")
.required(
"column_name",
SyntaxShape::String,
"the name of the column to graph by",
)
.rest(
SyntaxShape::Member,
"column name to give the histogram's frequency column",
)
}
fn usage(&self) -> &str {
"Creates a new table with a histogram based on the column name passed in."
}
fn run(
&self,
args: CommandArgs,
registry: &CommandRegistry,
) -> Result<OutputStream, ShellError> {
args.process(registry, histogram)?.run()
}
}
pub fn histogram(
HistogramArgs { column_name, rest }: HistogramArgs,
RunnableContext { input, name, .. }: RunnableContext,
) -> Result<OutputStream, ShellError> {
let stream = async_stream! {
let values: Vec<Tagged<Value>> = input.values.collect().await;
let Tagged { item: group_by, .. } = column_name.clone();
let groups = group(&column_name, values, &name)?;
let group_labels = columns_sorted(Some(group_by.clone()), &groups, &name);
let sorted = t_sort(Some(group_by.clone()), None, &groups, &name)?;
let evaled = evaluate(&sorted, None, &name)?;
let reduced = reduce(&evaled, None, &name)?;
let maxima = map_max(&reduced, None, &name)?;
let percents = percentages(&reduced, maxima, &name)?;
match percents {
Tagged {
item: Value::Table(datasets),
..
} => {
let mut idx = 0;
let column_names_supplied: Vec<_> = rest.iter().map(|f| f.item.clone()).collect();
let frequency_column_name = if column_names_supplied.is_empty() {
"frecuency".to_string()
} else {
column_names_supplied[0].clone()
};
let column = (*column_name).clone();
if let Tagged { item: Value::Table(start), .. } = datasets.get(0).unwrap() {
for percentage in start.into_iter() {
let mut fact = TaggedDictBuilder::new(&name);
fact.insert_tagged(&column, group_labels.get(idx).unwrap().clone());
if let Tagged { item: Value::Primitive(Primitive::Int(ref num)), .. } = percentage.clone() {
fact.insert(&frequency_column_name, std::iter::repeat("*").take(num.to_i32().unwrap() as usize).collect::<String>());
}
idx = idx + 1;
yield ReturnSuccess::value(fact.into_tagged_value());
}
}
}
_ => {}
}
};
Ok(stream.to_output_stream())
}
fn percentages(
values: &Tagged<Value>,
max: Tagged<Value>,
tag: impl Into<Tag>,
) -> Result<Tagged<Value>, ShellError> {
let tag = tag.into();
let results: Tagged<Value> = match values {
Tagged {
item: Value::Table(datasets),
..
} => {
let datasets: Vec<_> = datasets
.into_iter()
.map(|subsets| match subsets {
Tagged {
item: Value::Table(data),
..
} => {
let data = data
.into_iter()
.map(|d| match d {
Tagged {
item: Value::Primitive(Primitive::Int(n)),
..
} => {
let max = match max {
Tagged {
item: Value::Primitive(Primitive::Int(ref maxima)),
..
} => maxima.to_i32().unwrap(),
_ => 0,
};
let n = { n.to_i32().unwrap() * 100 / max };
Value::number(n).tagged(&tag)
}
_ => Value::number(0).tagged(&tag),
})
.collect::<Vec<_>>();
Value::Table(data).tagged(&tag)
}
_ => Value::Table(vec![]).tagged(&tag),
})
.collect();
Value::Table(datasets).tagged(&tag)
}
other => other.clone(),
};
Ok(results)
}

49
src/commands/history.rs Normal file
View file

@ -0,0 +1,49 @@
use crate::cli::History as HistoryFile;
use crate::commands::PerItemCommand;
use crate::errors::ShellError;
use crate::parser::registry::{self};
use crate::prelude::*;
use std::fs::File;
use std::io::{BufRead, BufReader};
pub struct History;
impl PerItemCommand for History {
fn name(&self) -> &str {
"history"
}
fn signature(&self) -> registry::Signature {
Signature::build("history")
}
fn usage(&self) -> &str {
"Display command history."
}
fn run(
&self,
call_info: &CallInfo,
_registry: &CommandRegistry,
_raw_args: &RawCommandArgs,
_input: Tagged<Value>,
) -> Result<OutputStream, ShellError> {
let tag = call_info.name_tag.clone();
let stream = async_stream! {
let history_path = HistoryFile::path();
let file = File::open(history_path);
if let Ok(file) = file {
let reader = BufReader::new(file);
for line in reader.lines() {
if let Ok(line) = line {
yield ReturnSuccess::value(Value::string(line).tagged(tag.clone()));
}
}
} else {
yield Err(ShellError::labeled_error("Could not open history", "history file could not be opened", tag.clone()));
}
};
Ok(stream.to_output_stream())
}
}

View file

@ -7,7 +7,7 @@ pub struct Last;
#[derive(Deserialize)]
pub struct LastArgs {
amount: Tagged<u64>,
rows: Option<Tagged<u64>>,
}
impl WholeStreamCommand for Last {
@ -16,7 +16,11 @@ impl WholeStreamCommand for Last {
}
fn signature(&self) -> Signature {
Signature::build("last").required("amount", SyntaxShape::Number)
Signature::build("last").optional(
"rows",
SyntaxShape::Number,
"starting from the back, the number of rows to return",
)
}
fn usage(&self) -> &str {
@ -32,13 +36,17 @@ impl WholeStreamCommand for Last {
}
}
fn last(
LastArgs { amount }: LastArgs,
context: RunnableContext,
) -> Result<OutputStream, ShellError> {
fn last(LastArgs { rows }: LastArgs, context: RunnableContext) -> Result<OutputStream, ShellError> {
let stream = async_stream! {
let v: Vec<_> = context.input.into_vec().await;
let count = (*amount as usize);
let rows_desired = if let Some(quantity) = rows {
*quantity
} else {
1
};
let count = (rows_desired as usize);
if count < v.len() {
let k = v.len() - count;
for x in v[k..].iter() {

View file

@ -58,7 +58,7 @@ fn lines(args: CommandArgs, registry: &CommandRegistry) -> Result<OutputStream,
result.push_back(Err(ShellError::labeled_error_with_secondary(
"Expected a string from pipeline",
"requires string input",
tag,
&tag,
"value originates from here",
v.tag(),
)));

View file

@ -16,7 +16,11 @@ impl WholeStreamCommand for LS {
}
fn signature(&self) -> Signature {
Signature::build("ls").optional("path", SyntaxShape::Pattern)
Signature::build("ls").optional(
"path",
SyntaxShape::Pattern,
"a path to get the directory contents from",
)
}
fn usage(&self) -> &str {
@ -34,5 +38,5 @@ impl WholeStreamCommand for LS {
}
fn ls(LsArgs { path }: LsArgs, context: RunnableContext) -> Result<OutputStream, ShellError> {
context.shell_manager.ls(path, context.name)
context.shell_manager.ls(path, &context)
}

225
src/commands/map_max_by.rs Normal file
View file

@ -0,0 +1,225 @@
use crate::commands::WholeStreamCommand;
use crate::parser::hir::SyntaxShape;
use crate::prelude::*;
use num_traits::cast::ToPrimitive;
pub struct MapMaxBy;
#[derive(Deserialize)]
pub struct MapMaxByArgs {
column_name: Option<Tagged<String>>,
}
impl WholeStreamCommand for MapMaxBy {
fn name(&self) -> &str {
"map-max-by"
}
fn signature(&self) -> Signature {
Signature::build("map-max-by").named(
"column_name",
SyntaxShape::String,
"the name of the column to map-max the table's rows",
)
}
fn usage(&self) -> &str {
"Creates a new table with the data from the tables rows maxed by the column given."
}
fn run(
&self,
args: CommandArgs,
registry: &CommandRegistry,
) -> Result<OutputStream, ShellError> {
args.process(registry, map_max_by)?.run()
}
}
pub fn map_max_by(
MapMaxByArgs { column_name }: MapMaxByArgs,
RunnableContext { input, name, .. }: RunnableContext,
) -> Result<OutputStream, ShellError> {
let stream = async_stream! {
let values: Vec<Tagged<Value>> = input.values.collect().await;
if values.is_empty() {
yield Err(ShellError::labeled_error(
"Expected table from pipeline",
"requires a table input",
name
))
} else {
let map_by_column = if let Some(column_to_map) = column_name {
Some(column_to_map.item().clone())
} else {
None
};
match map_max(&values[0], map_by_column, name) {
Ok(table_maxed) => yield ReturnSuccess::value(table_maxed),
Err(err) => yield Err(err)
}
}
};
Ok(stream.to_output_stream())
}
pub fn map_max(
values: &Tagged<Value>,
_map_by_column_name: Option<String>,
tag: impl Into<Tag>,
) -> Result<Tagged<Value>, ShellError> {
let tag = tag.into();
let results: Tagged<Value> = match values {
Tagged {
item: Value::Table(datasets),
..
} => {
let datasets: Vec<_> = datasets
.into_iter()
.map(|subsets| match subsets {
Tagged {
item: Value::Table(data),
..
} => {
let data = data.into_iter().fold(0, |acc, value| match value {
Tagged {
item: Value::Primitive(Primitive::Int(n)),
..
} => {
if n.to_i32().unwrap() > acc {
n.to_i32().unwrap()
} else {
acc
}
}
_ => acc,
});
Value::number(data).tagged(&tag)
}
_ => Value::number(0).tagged(&tag),
})
.collect();
let datasets = datasets.iter().fold(0, |max, value| match value {
Tagged {
item: Value::Primitive(Primitive::Int(n)),
..
} => {
if n.to_i32().unwrap() > max {
n.to_i32().unwrap()
} else {
max
}
}
_ => max,
});
Value::number(datasets).tagged(&tag)
}
_ => Value::number(-1).tagged(&tag),
};
Ok(results)
}
#[cfg(test)]
mod tests {
use crate::commands::evaluate_by::evaluate;
use crate::commands::group_by::group;
use crate::commands::map_max_by::map_max;
use crate::commands::reduce_by::reduce;
use crate::commands::t_sort_by::t_sort;
use crate::data::meta::*;
use crate::prelude::*;
use crate::Value;
use indexmap::IndexMap;
fn int(s: impl Into<BigInt>) -> Tagged<Value> {
Value::int(s).tagged_unknown()
}
fn string(input: impl Into<String>) -> Tagged<Value> {
Value::string(input.into()).tagged_unknown()
}
fn row(entries: IndexMap<String, Tagged<Value>>) -> Tagged<Value> {
Value::row(entries).tagged_unknown()
}
fn nu_releases_evaluated_by_default_one() -> Tagged<Value> {
evaluate(&nu_releases_sorted_by_date(), None, Tag::unknown()).unwrap()
}
fn nu_releases_reduced_by_sum() -> Tagged<Value> {
reduce(
&nu_releases_evaluated_by_default_one(),
Some(String::from("sum")),
Tag::unknown(),
)
.unwrap()
}
fn nu_releases_sorted_by_date() -> Tagged<Value> {
let key = String::from("date");
t_sort(
Some(key),
None,
&nu_releases_grouped_by_date(),
Tag::unknown(),
)
.unwrap()
}
fn nu_releases_grouped_by_date() -> Tagged<Value> {
let key = String::from("date").tagged_unknown();
group(&key, nu_releases_commiters(), Tag::unknown()).unwrap()
}
fn nu_releases_commiters() -> Vec<Tagged<Value>> {
vec![
row(
indexmap! {"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("August 23-2019")},
),
row(
indexmap! {"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => string("August 23-2019")},
),
row(
indexmap! {"name".into() => string("YK"), "country".into() => string("US"), "date".into() => string("October 10-2019")},
),
row(
indexmap! {"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("September 24-2019")},
),
row(
indexmap! {"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => string("October 10-2019")},
),
row(
indexmap! {"name".into() => string("YK"), "country".into() => string("US"), "date".into() => string("September 24-2019")},
),
row(
indexmap! {"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("October 10-2019")},
),
row(
indexmap! {"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => string("September 24-2019")},
),
row(
indexmap! {"name".into() => string("YK"), "country".into() => string("US"), "date".into() => string("August 23-2019")},
),
row(
indexmap! {"name".into() => string("JK"), "country".into() => string("US"), "date".into() => string("August 23-2019")},
),
]
}
#[test]
fn maps_and_gets_max_value() {
assert_eq!(
map_max(&nu_releases_reduced_by_sum(), None, Tag::unknown()).unwrap(),
int(4)
);
}
}

View file

@ -17,7 +17,7 @@ impl PerItemCommand for Mkdir {
}
fn signature(&self) -> Signature {
Signature::build("mkdir").rest(SyntaxShape::Path)
Signature::build("mkdir").rest(SyntaxShape::Path, "the name(s) of the path(s) to create")
}
fn usage(&self) -> &str {

View file

@ -20,9 +20,16 @@ impl PerItemCommand for Move {
fn signature(&self) -> Signature {
Signature::build("mv")
.required("source", SyntaxShape::Pattern)
.required("destination", SyntaxShape::Path)
.named("file", SyntaxShape::Any)
.required(
"source",
SyntaxShape::Pattern,
"the location to move files/directories from",
)
.required(
"destination",
SyntaxShape::Path,
"the location to move files/directories to",
)
}
fn usage(&self) -> &str {

View file

@ -16,7 +16,11 @@ impl WholeStreamCommand for Nth {
}
fn signature(&self) -> Signature {
Signature::build("nth").required("row number", SyntaxShape::Any)
Signature::build("nth").required(
"row number",
SyntaxShape::Any,
"the number of the row to return",
)
}
fn usage(&self) -> &str {

View file

@ -7,7 +7,6 @@ use crate::parser::hir::SyntaxShape;
use crate::parser::registry::Signature;
use crate::prelude::*;
use std::path::{Path, PathBuf};
use uuid::Uuid;
pub struct Open;
impl PerItemCommand for Open {
@ -17,8 +16,12 @@ impl PerItemCommand for Open {
fn signature(&self) -> Signature {
Signature::build(self.name())
.required("path", SyntaxShape::Path)
.switch("raw")
.required(
"path",
SyntaxShape::Path,
"the file path to load values from",
)
.switch("raw", "load content as a string insead of a table")
}
fn usage(&self) -> &str {
@ -45,16 +48,18 @@ fn run(
let cwd = PathBuf::from(shell_manager.path());
let full_path = PathBuf::from(cwd);
let path = match call_info
.args
.nth(0)
.ok_or_else(|| ShellError::string(&format!("No file or directory specified")))?
{
let path = match call_info.args.nth(0).ok_or_else(|| {
ShellError::labeled_error(
"No file or directory specified",
"for command",
&call_info.name_tag,
)
})? {
file => file,
};
let path_buf = path.as_path()?;
let path_str = path_buf.display().to_string();
let path_span = path.span();
let path_span = path.tag.span;
let has_raw = call_info.args.has("raw");
let registry = registry.clone();
let raw_args = raw_args.clone();
@ -67,7 +72,7 @@ fn run(
yield Err(e);
return;
}
let (file_extension, contents, contents_tag, anchor_location) = result.unwrap();
let (file_extension, contents, contents_tag) = result.unwrap();
let file_extension = if has_raw {
None
@ -77,21 +82,14 @@ fn run(
file_extension.or(path_str.split('.').last().map(String::from))
};
if contents_tag.anchor != uuid::Uuid::nil() {
// If we have loaded something, track its source
yield ReturnSuccess::action(CommandAction::AddAnchorLocation(
contents_tag.anchor,
anchor_location,
));
}
let tagged_contents = contents.tagged(contents_tag);
let tagged_contents = contents.tagged(&contents_tag);
if let Some(extension) = file_extension {
let command_name = format!("from-{}", extension);
if let Some(converter) = registry.get_command(&command_name) {
let new_args = RawCommandArgs {
host: raw_args.host,
ctrl_c: raw_args.ctrl_c,
shell_manager: raw_args.shell_manager,
call_info: UnevaluatedCallInfo {
args: crate::parser::hir::Call {
@ -100,11 +98,10 @@ fn run(
named: None
},
source: raw_args.call_info.source,
source_map: raw_args.call_info.source_map,
name_tag: raw_args.call_info.name_tag,
}
};
let mut result = converter.run(new_args.with_input(vec![tagged_contents]), &registry, false);
let mut result = converter.run(new_args.with_input(vec![tagged_contents]), &registry);
let result_vec: Vec<Result<ReturnSuccess, ShellError>> = result.drain_vec().await;
for res in result_vec {
match res {
@ -114,7 +111,7 @@ fn run(
}
}
Ok(ReturnSuccess::Value(Tagged { item, .. })) => {
yield Ok(ReturnSuccess::Value(Tagged { item, tag: contents_tag }));
yield Ok(ReturnSuccess::Value(Tagged { item, tag: contents_tag.clone() }));
}
x => yield x,
}
@ -134,7 +131,7 @@ pub async fn fetch(
cwd: &PathBuf,
location: &str,
span: Span,
) -> Result<(Option<String>, Value, Tag, AnchorLocation), ShellError> {
) -> Result<(Option<String>, Value, Tag), ShellError> {
let mut cwd = cwd.clone();
cwd.push(Path::new(location));
@ -147,9 +144,8 @@ pub async fn fetch(
Value::string(s),
Tag {
span,
anchor: Uuid::new_v4(),
anchor: Some(AnchorLocation::File(cwd.to_string_lossy().to_string())),
},
AnchorLocation::File(cwd.to_string_lossy().to_string()),
)),
Err(_) => {
//Non utf8 data.
@ -166,18 +162,20 @@ pub async fn fetch(
Value::string(s),
Tag {
span,
anchor: Uuid::new_v4(),
anchor: Some(AnchorLocation::File(
cwd.to_string_lossy().to_string(),
)),
},
AnchorLocation::File(cwd.to_string_lossy().to_string()),
)),
Err(_) => Ok((
None,
Value::binary(bytes),
Tag {
span,
anchor: Uuid::new_v4(),
anchor: Some(AnchorLocation::File(
cwd.to_string_lossy().to_string(),
)),
},
AnchorLocation::File(cwd.to_string_lossy().to_string()),
)),
}
} else {
@ -186,9 +184,10 @@ pub async fn fetch(
Value::binary(bytes),
Tag {
span,
anchor: Uuid::new_v4(),
anchor: Some(AnchorLocation::File(
cwd.to_string_lossy().to_string(),
)),
},
AnchorLocation::File(cwd.to_string_lossy().to_string()),
))
}
}
@ -204,18 +203,20 @@ pub async fn fetch(
Value::string(s),
Tag {
span,
anchor: Uuid::new_v4(),
anchor: Some(AnchorLocation::File(
cwd.to_string_lossy().to_string(),
)),
},
AnchorLocation::File(cwd.to_string_lossy().to_string()),
)),
Err(_) => Ok((
None,
Value::binary(bytes),
Tag {
span,
anchor: Uuid::new_v4(),
anchor: Some(AnchorLocation::File(
cwd.to_string_lossy().to_string(),
)),
},
AnchorLocation::File(cwd.to_string_lossy().to_string()),
)),
}
} else {
@ -224,9 +225,10 @@ pub async fn fetch(
Value::binary(bytes),
Tag {
span,
anchor: Uuid::new_v4(),
anchor: Some(AnchorLocation::File(
cwd.to_string_lossy().to_string(),
)),
},
AnchorLocation::File(cwd.to_string_lossy().to_string()),
))
}
}
@ -235,9 +237,10 @@ pub async fn fetch(
Value::binary(bytes),
Tag {
span,
anchor: Uuid::new_v4(),
anchor: Some(AnchorLocation::File(
cwd.to_string_lossy().to_string(),
)),
},
AnchorLocation::File(cwd.to_string_lossy().to_string()),
)),
}
}

View file

@ -17,7 +17,7 @@ impl WholeStreamCommand for Pick {
}
fn signature(&self) -> Signature {
Signature::build("pick").rest(SyntaxShape::Any)
Signature::build("pick").rest(SyntaxShape::Any, "the columns to select from the table")
}
fn usage(&self) -> &str {

View file

@ -21,9 +21,12 @@ impl WholeStreamCommand for Pivot {
fn signature(&self) -> Signature {
Signature::build("pivot")
.switch("header-row")
.switch("ignore-titles")
.rest(SyntaxShape::String)
.switch("header-row", "treat the first row as column names")
.switch("ignore-titles", "don't pivot the column names into values")
.rest(
SyntaxShape::String,
"the names to give columns once pivoted",
)
}
fn usage(&self) -> &str {
@ -104,7 +107,7 @@ pub fn pivot(args: PivotArgs, context: RunnableContext) -> Result<OutputStream,
for desc in descs {
let mut column_num: usize = 0;
let mut dict = TaggedDictBuilder::new(context.name);
let mut dict = TaggedDictBuilder::new(&context.name);
if !args.ignore_titles && !args.header_row {
dict.insert(headers[column_num].clone(), Value::string(desc.clone()));

View file

@ -128,7 +128,7 @@ pub fn filter_plugin(
},
Err(e) => {
let mut result = VecDeque::new();
result.push_back(Err(ShellError::string(format!(
result.push_back(Err(ShellError::untagged_runtime_error(format!(
"Error while processing begin_filter response: {:?} {}",
e, input
))));
@ -138,7 +138,7 @@ pub fn filter_plugin(
}
Err(e) => {
let mut result = VecDeque::new();
result.push_back(Err(ShellError::string(format!(
result.push_back(Err(ShellError::untagged_runtime_error(format!(
"Error while reading begin_filter response: {:?}",
e
))));
@ -189,7 +189,7 @@ pub fn filter_plugin(
},
Err(e) => {
let mut result = VecDeque::new();
result.push_back(Err(ShellError::string(format!(
result.push_back(Err(ShellError::untagged_runtime_error(format!(
"Error while processing end_filter response: {:?} {}",
e, input
))));
@ -199,7 +199,7 @@ pub fn filter_plugin(
}
Err(e) => {
let mut result = VecDeque::new();
result.push_back(Err(ShellError::string(format!(
result.push_back(Err(ShellError::untagged_runtime_error(format!(
"Error while reading end_filter: {:?}",
e
))));
@ -236,7 +236,7 @@ pub fn filter_plugin(
},
Err(e) => {
let mut result = VecDeque::new();
result.push_back(Err(ShellError::string(format!(
result.push_back(Err(ShellError::untagged_runtime_error(format!(
"Error while processing filter response: {:?} {}",
e, input
))));
@ -246,7 +246,7 @@ pub fn filter_plugin(
}
Err(e) => {
let mut result = VecDeque::new();
result.push_back(Err(ShellError::string(format!(
result.push_back(Err(ShellError::untagged_runtime_error(format!(
"Error while reading filter response: {:?}",
e
))));

View file

@ -25,13 +25,25 @@ impl PerItemCommand for Post {
fn signature(&self) -> Signature {
Signature::build(self.name())
.required("path", SyntaxShape::Any)
.required("body", SyntaxShape::Any)
.named("user", SyntaxShape::Any)
.named("password", SyntaxShape::Any)
.named("content-type", SyntaxShape::Any)
.named("content-length", SyntaxShape::Any)
.switch("raw")
.required("path", SyntaxShape::Any, "the URL to post to")
.required("body", SyntaxShape::Any, "the contents of the post body")
.named("user", SyntaxShape::Any, "the username when authenticating")
.named(
"password",
SyntaxShape::Any,
"the password when authenticating",
)
.named(
"content-type",
SyntaxShape::Any,
"the MIME type of content to post",
)
.named(
"content-length",
SyntaxShape::Any,
"the length of the content being posted",
)
.switch("raw", "return values as a string instead of a table")
}
fn usage(&self) -> &str {
@ -54,21 +66,20 @@ fn run(
registry: &CommandRegistry,
raw_args: &RawCommandArgs,
) -> Result<OutputStream, ShellError> {
let name_tag = call_info.name_tag.clone();
let call_info = call_info.clone();
let path = match call_info
.args
.nth(0)
.ok_or_else(|| ShellError::string(&format!("No url specified")))?
{
file => file.clone(),
};
let body = match call_info
.args
.nth(1)
.ok_or_else(|| ShellError::string(&format!("No body specified")))?
{
file => file.clone(),
};
let path =
match call_info.args.nth(0).ok_or_else(|| {
ShellError::labeled_error("No url specified", "for command", &name_tag)
})? {
file => file.clone(),
};
let body =
match call_info.args.nth(1).ok_or_else(|| {
ShellError::labeled_error("No body specified", "for command", &name_tag)
})? {
file => file.clone(),
};
let path_str = path.as_string()?;
let path_span = path.tag();
let has_raw = call_info.args.has("raw");
@ -83,7 +94,7 @@ fn run(
let headers = get_headers(&call_info)?;
let stream = async_stream! {
let (file_extension, contents, contents_tag, anchor_location) =
let (file_extension, contents, contents_tag) =
post(&path_str, &body, user, password, &headers, path_span, &registry, &raw_args).await.unwrap();
let file_extension = if has_raw {
@ -94,21 +105,14 @@ fn run(
file_extension.or(path_str.split('.').last().map(String::from))
};
if contents_tag.anchor != uuid::Uuid::nil() {
// If we have loaded something, track its source
yield ReturnSuccess::action(CommandAction::AddAnchorLocation(
contents_tag.anchor,
anchor_location,
));
}
let tagged_contents = contents.tagged(contents_tag);
let tagged_contents = contents.tagged(&contents_tag);
if let Some(extension) = file_extension {
let command_name = format!("from-{}", extension);
if let Some(converter) = registry.get_command(&command_name) {
let new_args = RawCommandArgs {
host: raw_args.host,
ctrl_c: raw_args.ctrl_c,
shell_manager: raw_args.shell_manager,
call_info: UnevaluatedCallInfo {
args: crate::parser::hir::Call {
@ -117,11 +121,10 @@ fn run(
named: None
},
source: raw_args.call_info.source,
source_map: raw_args.call_info.source_map,
name_tag: raw_args.call_info.name_tag,
}
};
let mut result = converter.run(new_args.with_input(vec![tagged_contents]), &registry, false);
let mut result = converter.run(new_args.with_input(vec![tagged_contents]), &registry);
let result_vec: Vec<Result<ReturnSuccess, ShellError>> = result.drain_vec().await;
for res in result_vec {
match res {
@ -131,7 +134,7 @@ fn run(
}
}
Ok(ReturnSuccess::Value(Tagged { item, .. })) => {
yield Ok(ReturnSuccess::Value(Tagged { item, tag: contents_tag }));
yield Ok(ReturnSuccess::Value(Tagged { item, tag: contents_tag.clone() }));
}
x => yield x,
}
@ -211,7 +214,7 @@ pub async fn post(
tag: Tag,
registry: &CommandRegistry,
raw_args: &RawCommandArgs,
) -> Result<(Option<String>, Value, Tag, AnchorLocation), ShellError> {
) -> Result<(Option<String>, Value, Tag), ShellError> {
let registry = registry.clone();
let raw_args = raw_args.clone();
if location.starts_with("http:") || location.starts_with("https:") {
@ -252,6 +255,7 @@ pub async fn post(
if let Some(converter) = registry.get_command("to-json") {
let new_args = RawCommandArgs {
host: raw_args.host,
ctrl_c: raw_args.ctrl_c,
shell_manager: raw_args.shell_manager,
call_info: UnevaluatedCallInfo {
args: crate::parser::hir::Call {
@ -260,14 +264,12 @@ pub async fn post(
named: None,
},
source: raw_args.call_info.source,
source_map: raw_args.call_info.source_map,
name_tag: raw_args.call_info.name_tag,
},
};
let mut result = converter.run(
new_args.with_input(vec![item.clone().tagged(tag.clone())]),
&registry,
false,
);
let result_vec: Vec<Result<ReturnSuccess, ShellError>> =
result.drain_vec().await;
@ -284,7 +286,7 @@ pub async fn post(
return Err(ShellError::labeled_error(
"Save could not successfully save",
"unexpected data during save",
*tag,
tag,
));
}
}
@ -300,7 +302,7 @@ pub async fn post(
return Err(ShellError::labeled_error(
"Could not automatically convert table",
"needs manual conversion",
*tag,
tag,
));
}
}
@ -316,11 +318,13 @@ pub async fn post(
ShellError::labeled_error(
"Could not load text from remote url",
"could not load",
tag,
&tag,
)
})?),
tag,
AnchorLocation::Url(location.to_string()),
Tag {
anchor: Some(AnchorLocation::Url(location.to_string())),
span: tag.span,
},
)),
(mime::APPLICATION, mime::JSON) => Ok((
Some("json".to_string()),
@ -328,25 +332,29 @@ pub async fn post(
ShellError::labeled_error(
"Could not load text from remote url",
"could not load",
tag,
&tag,
)
})?),
tag,
AnchorLocation::Url(location.to_string()),
Tag {
anchor: Some(AnchorLocation::Url(location.to_string())),
span: tag.span,
},
)),
(mime::APPLICATION, mime::OCTET_STREAM) => {
let buf: Vec<u8> = r.body_bytes().await.map_err(|_| {
ShellError::labeled_error(
"Could not load binary file",
"could not load",
tag,
&tag,
)
})?;
Ok((
None,
Value::binary(buf),
tag,
AnchorLocation::Url(location.to_string()),
Tag {
anchor: Some(AnchorLocation::Url(location.to_string())),
span: tag.span,
},
))
}
(mime::IMAGE, image_ty) => {
@ -354,14 +362,16 @@ pub async fn post(
ShellError::labeled_error(
"Could not load image file",
"could not load",
tag,
&tag,
)
})?;
Ok((
Some(image_ty.to_string()),
Value::binary(buf),
tag,
AnchorLocation::Url(location.to_string()),
Tag {
anchor: Some(AnchorLocation::Url(location.to_string())),
span: tag.span,
},
))
}
(mime::TEXT, mime::HTML) => Ok((
@ -370,11 +380,13 @@ pub async fn post(
ShellError::labeled_error(
"Could not load text from remote url",
"could not load",
tag,
&tag,
)
})?),
tag,
AnchorLocation::Url(location.to_string()),
Tag {
anchor: Some(AnchorLocation::Url(location.to_string())),
span: tag.span,
},
)),
(mime::TEXT, mime::PLAIN) => {
let path_extension = url::Url::parse(location)
@ -394,11 +406,13 @@ pub async fn post(
ShellError::labeled_error(
"Could not load text from remote url",
"could not load",
tag,
&tag,
)
})?),
tag,
AnchorLocation::Url(location.to_string()),
Tag {
anchor: Some(AnchorLocation::Url(location.to_string())),
span: tag.span,
},
))
}
(ty, sub_ty) => Ok((
@ -407,16 +421,20 @@ pub async fn post(
"Not yet supported MIME type: {} {}",
ty, sub_ty
)),
tag,
AnchorLocation::Url(location.to_string()),
Tag {
anchor: Some(AnchorLocation::Url(location.to_string())),
span: tag.span,
},
)),
}
}
None => Ok((
None,
Value::string(format!("No content type found")),
tag,
AnchorLocation::Url(location.to_string()),
Tag {
anchor: Some(AnchorLocation::Url(location.to_string())),
span: tag.span,
},
)),
},
Err(_) => {

47
src/commands/prepend.rs Normal file
View file

@ -0,0 +1,47 @@
use crate::commands::WholeStreamCommand;
use crate::errors::ShellError;
use crate::parser::CommandRegistry;
use crate::prelude::*;
#[derive(Deserialize)]
struct PrependArgs {
row: Tagged<Value>,
}
pub struct Prepend;
impl WholeStreamCommand for Prepend {
fn name(&self) -> &str {
"prepend"
}
fn signature(&self) -> Signature {
Signature::build("prepend").required(
"row value",
SyntaxShape::Any,
"the value of the row to prepend to the table",
)
}
fn usage(&self) -> &str {
"Prepend the given row to the front of the table"
}
fn run(
&self,
args: CommandArgs,
registry: &CommandRegistry,
) -> Result<OutputStream, ShellError> {
args.process(registry, prepend)?.run()
}
}
fn prepend(
PrependArgs { row }: PrependArgs,
RunnableContext { input, .. }: RunnableContext,
) -> Result<OutputStream, ShellError> {
let mut prepend: VecDeque<Tagged<Value>> = VecDeque::new();
prepend.push_back(row);
Ok(OutputStream::from_input(prepend.chain(input.values)))
}

257
src/commands/reduce_by.rs Normal file
View file

@ -0,0 +1,257 @@
use crate::commands::WholeStreamCommand;
use crate::parser::hir::SyntaxShape;
use crate::prelude::*;
use num_traits::cast::ToPrimitive;
pub struct ReduceBy;
#[derive(Deserialize)]
pub struct ReduceByArgs {
reduce_with: Option<Tagged<String>>,
}
impl WholeStreamCommand for ReduceBy {
fn name(&self) -> &str {
"reduce-by"
}
fn signature(&self) -> Signature {
Signature::build("reduce-by").named(
"reduce_with",
SyntaxShape::String,
"the command to reduce by with",
)
}
fn usage(&self) -> &str {
"Creates a new table with the data from the tables rows reduced by the command given."
}
fn run(
&self,
args: CommandArgs,
registry: &CommandRegistry,
) -> Result<OutputStream, ShellError> {
args.process(registry, reduce_by)?.run()
}
}
pub fn reduce_by(
ReduceByArgs { reduce_with }: ReduceByArgs,
RunnableContext { input, name, .. }: RunnableContext,
) -> Result<OutputStream, ShellError> {
let stream = async_stream! {
let values: Vec<Tagged<Value>> = input.values.collect().await;
if values.is_empty() {
yield Err(ShellError::labeled_error(
"Expected table from pipeline",
"requires a table input",
name
))
} else {
let reduce_with = if let Some(reducer) = reduce_with {
Some(reducer.item().clone())
} else {
None
};
match reduce(&values[0], reduce_with, name) {
Ok(reduced) => yield ReturnSuccess::value(reduced),
Err(err) => yield Err(err)
}
}
};
Ok(stream.to_output_stream())
}
fn sum(data: Vec<Tagged<Value>>) -> i32 {
data.into_iter().fold(0, |acc, value| match value {
Tagged {
item: Value::Primitive(Primitive::Int(n)),
..
} => acc + n.to_i32().unwrap(),
_ => acc,
})
}
fn formula(
acc_begin: i32,
calculator: Box<dyn Fn(Vec<Tagged<Value>>) -> i32 + 'static>,
) -> Box<dyn Fn(i32, Vec<Tagged<Value>>) -> i32 + 'static> {
Box::new(move |acc, datax| -> i32 {
let result = acc * acc_begin;
result + calculator(datax)
})
}
fn reducer_for(command: Reduce) -> Box<dyn Fn(i32, Vec<Tagged<Value>>) -> i32 + 'static> {
match command {
Reduce::Sum | Reduce::Default => Box::new(formula(0, Box::new(sum))),
}
}
pub enum Reduce {
Sum,
Default,
}
pub fn reduce(
values: &Tagged<Value>,
reducer: Option<String>,
tag: impl Into<Tag>,
) -> Result<Tagged<Value>, ShellError> {
let tag = tag.into();
let reduce_with = match reducer {
Some(cmd) if cmd == "sum" => reducer_for(Reduce::Sum),
Some(_) | None => reducer_for(Reduce::Default),
};
let results: Tagged<Value> = match values {
Tagged {
item: Value::Table(datasets),
..
} => {
let datasets: Vec<_> = datasets
.into_iter()
.map(|subsets| {
let mut acc = 0;
match subsets {
Tagged {
item: Value::Table(data),
..
} => {
let data = data
.into_iter()
.map(|d| {
if let Tagged {
item: Value::Table(x),
..
} = d
{
acc = reduce_with(acc, x.clone());
Value::number(acc).tagged(&tag)
} else {
Value::number(0).tagged(&tag)
}
})
.collect::<Vec<_>>();
Value::Table(data).tagged(&tag)
}
_ => Value::Table(vec![]).tagged(&tag),
}
})
.collect();
Value::Table(datasets).tagged(&tag)
}
_ => Value::Table(vec![]).tagged(&tag),
};
Ok(results)
}
#[cfg(test)]
mod tests {
use crate::commands::evaluate_by::evaluate;
use crate::commands::group_by::group;
use crate::commands::reduce_by::{reduce, reducer_for, Reduce};
use crate::commands::t_sort_by::t_sort;
use crate::data::meta::*;
use crate::prelude::*;
use crate::Value;
use indexmap::IndexMap;
fn int(s: impl Into<BigInt>) -> Tagged<Value> {
Value::int(s).tagged_unknown()
}
fn string(input: impl Into<String>) -> Tagged<Value> {
Value::string(input.into()).tagged_unknown()
}
fn row(entries: IndexMap<String, Tagged<Value>>) -> Tagged<Value> {
Value::row(entries).tagged_unknown()
}
fn table(list: &Vec<Tagged<Value>>) -> Tagged<Value> {
Value::table(list).tagged_unknown()
}
fn nu_releases_sorted_by_date() -> Tagged<Value> {
let key = String::from("date");
t_sort(
Some(key),
None,
&nu_releases_grouped_by_date(),
Tag::unknown(),
)
.unwrap()
}
fn nu_releases_evaluated_by_default_one() -> Tagged<Value> {
evaluate(&nu_releases_sorted_by_date(), None, Tag::unknown()).unwrap()
}
fn nu_releases_grouped_by_date() -> Tagged<Value> {
let key = String::from("date").tagged_unknown();
group(&key, nu_releases_commiters(), Tag::unknown()).unwrap()
}
fn nu_releases_commiters() -> Vec<Tagged<Value>> {
vec![
row(
indexmap! {"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("August 23-2019")},
),
row(
indexmap! {"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => string("August 23-2019")},
),
row(
indexmap! {"name".into() => string("YK"), "country".into() => string("US"), "date".into() => string("October 10-2019")},
),
row(
indexmap! {"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("September 24-2019")},
),
row(
indexmap! {"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => string("October 10-2019")},
),
row(
indexmap! {"name".into() => string("YK"), "country".into() => string("US"), "date".into() => string("September 24-2019")},
),
row(
indexmap! {"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("October 10-2019")},
),
row(
indexmap! {"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => string("September 24-2019")},
),
row(
indexmap! {"name".into() => string("YK"), "country".into() => string("US"), "date".into() => string("August 23-2019")},
),
]
}
#[test]
fn reducer_computes_given_a_sum_command() {
let subject = vec![int(1), int(1), int(1)];
let action = reducer_for(Reduce::Sum);
assert_eq!(action(0, subject), 3);
}
#[test]
fn reducer_computes() {
assert_eq!(
reduce(
&nu_releases_evaluated_by_default_one(),
Some(String::from("sum")),
Tag::unknown()
),
Ok(table(&vec![table(&vec![int(3), int(3), int(3)])]))
);
}
}

View file

@ -16,7 +16,7 @@ impl WholeStreamCommand for Reject {
}
fn signature(&self) -> Signature {
Signature::build("reject").rest(SyntaxShape::Member)
Signature::build("reject").rest(SyntaxShape::Member, "the names of columns to remove")
}
fn usage(&self) -> &str {

View file

@ -11,6 +11,7 @@ pub struct Remove;
pub struct RemoveArgs {
pub target: Tagged<PathBuf>,
pub recursive: Tagged<bool>,
pub trash: Tagged<bool>,
}
impl PerItemCommand for Remove {
@ -20,12 +21,16 @@ impl PerItemCommand for Remove {
fn signature(&self) -> Signature {
Signature::build("rm")
.required("path", SyntaxShape::Pattern)
.switch("recursive")
.required("path", SyntaxShape::Pattern, "the file path to remove")
.switch(
"trash",
"use the platform's recycle bin instead of permanently deleting",
)
.switch("recursive", "delete subdirectories recursively")
}
fn usage(&self) -> &str {
"Remove a file, (for removing directory append '--recursive')"
"Remove a file"
}
fn run(

View file

@ -93,8 +93,11 @@ impl WholeStreamCommand for Save {
fn signature(&self) -> Signature {
Signature::build("save")
.optional("path", SyntaxShape::Path)
.switch("raw")
.optional("path", SyntaxShape::Path, "the path to save contents to")
.switch(
"raw",
"treat values as-is rather than auto-converting based on file extension",
)
}
fn usage(&self) -> &str {
@ -119,49 +122,48 @@ fn save(
input,
name,
shell_manager,
source_map,
host,
ctrl_c,
commands: registry,
..
}: RunnableContext,
raw_args: RawCommandArgs,
) -> Result<OutputStream, ShellError> {
let mut full_path = PathBuf::from(shell_manager.path());
let name_tag = name;
let name_tag = name.clone();
let source_map = source_map.clone();
let stream = async_stream! {
let input: Vec<Tagged<Value>> = input.values.collect().await;
if path.is_none() {
// If there is no filename, check the metadata for the anchor filename
if input.len() > 0 {
let anchor = input[0].anchor();
match source_map.get(&anchor) {
match anchor {
Some(path) => match path {
AnchorLocation::File(file) => {
full_path.push(Path::new(file));
full_path.push(Path::new(&file));
}
_ => {
yield Err(ShellError::labeled_error(
"Save requires a filepath",
"Save requires a filepath (1)",
"needs path",
name_tag,
name_tag.clone(),
));
}
},
None => {
yield Err(ShellError::labeled_error(
"Save requires a filepath",
"Save requires a filepath (2)",
"needs path",
name_tag,
name_tag.clone(),
));
}
}
} else {
yield Err(ShellError::labeled_error(
"Save requires a filepath",
"Save requires a filepath (3)",
"needs path",
name_tag,
name_tag.clone(),
));
}
} else {
@ -179,6 +181,7 @@ fn save(
if let Some(converter) = registry.get_command(&command_name) {
let new_args = RawCommandArgs {
host,
ctrl_c,
shell_manager,
call_info: UnevaluatedCallInfo {
args: crate::parser::hir::Call {
@ -187,11 +190,10 @@ fn save(
named: None
},
source: raw_args.call_info.source,
source_map: raw_args.call_info.source_map,
name_tag: raw_args.call_info.name_tag,
}
};
let mut result = converter.run(new_args.with_input(input), &registry, false);
let mut result = converter.run(new_args.with_input(input), &registry);
let result_vec: Vec<Result<ReturnSuccess, ShellError>> = result.drain_vec().await;
if converter.is_binary() {
process_binary_return_success!('scope, result_vec, name_tag)
@ -212,9 +214,9 @@ fn save(
match content {
Ok(save_data) => match std::fs::write(full_path, save_data) {
Ok(o) => o,
Err(e) => yield Err(ShellError::string(e.to_string())),
Err(e) => yield Err(ShellError::labeled_error(e.to_string(), "for command", name)),
},
Err(e) => yield Err(ShellError::string(e.to_string())),
Err(e) => yield Err(ShellError::labeled_error(e.to_string(), "for command", name)),
}
};

View file

@ -2,6 +2,7 @@ use crate::commands::WholeStreamCommand;
use crate::data::TaggedDictBuilder;
use crate::errors::ShellError;
use crate::prelude::*;
use std::sync::atomic::Ordering;
pub struct Shells;
@ -32,14 +33,14 @@ fn shells(args: CommandArgs, _registry: &CommandRegistry) -> Result<OutputStream
let tag = args.call_info.name_tag;
for (index, shell) in args.shell_manager.shells.lock().unwrap().iter().enumerate() {
let mut dict = TaggedDictBuilder::new(tag);
let mut dict = TaggedDictBuilder::new(&tag);
if index == args.shell_manager.current_shell {
if index == (*args.shell_manager.current_shell).load(Ordering::SeqCst) {
dict.insert(" ", "X".to_string());
} else {
dict.insert(" ", " ".to_string());
}
dict.insert("name", shell.name(&args.call_info.source_map));
dict.insert("name", shell.name());
dict.insert("path", shell.path());
shells_out.push_back(dict.into_tagged_value());

View file

@ -37,7 +37,7 @@ fn size(args: CommandArgs, _registry: &CommandRegistry) -> Result<OutputStream,
_ => Err(ShellError::labeled_error_with_secondary(
"Expected a string from pipeline",
"requires string input",
tag,
&tag,
"value originates from here",
v.tag(),
)),

View file

@ -1,6 +1,7 @@
use crate::commands::WholeStreamCommand;
use crate::errors::ShellError;
use crate::prelude::*;
use log::trace;
pub struct SkipWhile;
@ -16,7 +17,11 @@ impl WholeStreamCommand for SkipWhile {
fn signature(&self) -> Signature {
Signature::build("skip-while")
.required("condition", SyntaxShape::Block)
.required(
"condition",
SyntaxShape::Block,
"the condition that must be met to continue skipping",
)
.filter()
}
@ -38,7 +43,9 @@ pub fn skip_while(
RunnableContext { input, .. }: RunnableContext,
) -> Result<OutputStream, ShellError> {
let objects = input.values.skip_while(move |item| {
trace!("ITEM = {:?}", item);
let result = condition.invoke(&item);
trace!("RESULT = {:?}", result);
let return_value = match result {
Ok(ref v) if v.is_true() => true,

View file

@ -15,7 +15,7 @@ impl WholeStreamCommand for SortBy {
}
fn signature(&self) -> Signature {
Signature::build("sort-by").rest(SyntaxShape::String)
Signature::build("sort-by").rest(SyntaxShape::String, "the column(s) to sort by")
}
fn usage(&self) -> &str {

270
src/commands/split_by.rs Normal file
View file

@ -0,0 +1,270 @@
use crate::commands::WholeStreamCommand;
use crate::data::TaggedDictBuilder;
use crate::errors::ShellError;
use crate::prelude::*;
pub struct SplitBy;
#[derive(Deserialize)]
pub struct SplitByArgs {
column_name: Tagged<String>,
}
impl WholeStreamCommand for SplitBy {
fn name(&self) -> &str {
"split-by"
}
fn signature(&self) -> Signature {
Signature::build("split-by").required(
"column_name",
SyntaxShape::String,
"the name of the column within the nested table to split by",
)
}
fn usage(&self) -> &str {
"Creates a new table with the data from the inner tables splitted by the column given."
}
fn run(
&self,
args: CommandArgs,
registry: &CommandRegistry,
) -> Result<OutputStream, ShellError> {
args.process(registry, split_by)?.run()
}
}
pub fn split_by(
SplitByArgs { column_name }: SplitByArgs,
RunnableContext { input, name, .. }: RunnableContext,
) -> Result<OutputStream, ShellError> {
let stream = async_stream! {
let values: Vec<Tagged<Value>> = input.values.collect().await;
if values.len() > 1 || values.is_empty() {
yield Err(ShellError::labeled_error(
"Expected table from pipeline",
"requires a table input",
column_name.span()
))
} else {
match split(&column_name, &values[0], name) {
Ok(split) => yield ReturnSuccess::value(split),
Err(err) => yield Err(err),
}
}
};
Ok(stream.to_output_stream())
}
pub fn split(
column_name: &Tagged<String>,
value: &Tagged<Value>,
tag: impl Into<Tag>,
) -> Result<Tagged<Value>, ShellError> {
let origin_tag = tag.into();
let mut splits = indexmap::IndexMap::new();
match value {
Tagged {
item: Value::Row(group_sets),
..
} => {
for (group_key, group_value) in group_sets.entries.iter() {
match *group_value {
Tagged {
item: Value::Table(ref dataset),
..
} => {
let group = crate::commands::group_by::group(
&column_name,
dataset.to_vec(),
&origin_tag,
)?;
match group {
Tagged {
item: Value::Row(o),
..
} => {
for (split_label, subset) in o.entries.into_iter() {
match subset {
Tagged {
item: Value::Table(subset),
tag,
} => {
let s = splits
.entry(split_label.clone())
.or_insert(indexmap::IndexMap::new());
s.insert(
group_key.clone(),
Value::table(&subset).tagged(tag),
);
}
other => {
return Err(ShellError::type_error(
"a table value",
other.tagged_type_name(),
))
}
}
}
}
_ => {
return Err(ShellError::type_error(
"a table value",
group.tagged_type_name(),
))
}
}
}
ref other => {
return Err(ShellError::type_error(
"a table value",
other.tagged_type_name(),
))
}
}
}
}
_ => {
return Err(ShellError::type_error(
"a table value",
value.tagged_type_name(),
))
}
}
let mut out = TaggedDictBuilder::new(&origin_tag);
for (k, v) in splits.into_iter() {
out.insert(k, Value::row(v));
}
Ok(out.into_tagged_value())
}
#[cfg(test)]
mod tests {
use crate::commands::group_by::group;
use crate::commands::split_by::split;
use crate::data::meta::*;
use crate::Value;
use indexmap::IndexMap;
fn string(input: impl Into<String>) -> Tagged<Value> {
Value::string(input.into()).tagged_unknown()
}
fn row(entries: IndexMap<String, Tagged<Value>>) -> Tagged<Value> {
Value::row(entries).tagged_unknown()
}
fn table(list: &Vec<Tagged<Value>>) -> Tagged<Value> {
Value::table(list).tagged_unknown()
}
fn nu_releases_grouped_by_date() -> Tagged<Value> {
let key = String::from("date").tagged_unknown();
group(&key, nu_releases_commiters(), Tag::unknown()).unwrap()
}
fn nu_releases_commiters() -> Vec<Tagged<Value>> {
vec![
row(
indexmap! {"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("August 23-2019")},
),
row(
indexmap! {"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => string("August 23-2019")},
),
row(
indexmap! {"name".into() => string("YK"), "country".into() => string("US"), "date".into() => string("October 10-2019")},
),
row(
indexmap! {"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("Sept 24-2019")},
),
row(
indexmap! {"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => string("October 10-2019")},
),
row(
indexmap! {"name".into() => string("YK"), "country".into() => string("US"), "date".into() => string("Sept 24-2019")},
),
row(
indexmap! {"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("October 10-2019")},
),
row(
indexmap! {"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => string("Sept 24-2019")},
),
row(
indexmap! {"name".into() => string("YK"), "country".into() => string("US"), "date".into() => string("August 23-2019")},
),
]
}
#[test]
fn splits_inner_tables_by_key() {
let for_key = String::from("country").tagged_unknown();
assert_eq!(
split(&for_key, &nu_releases_grouped_by_date(), Tag::unknown()).unwrap(),
Value::row(indexmap! {
"EC".into() => row(indexmap! {
"August 23-2019".into() => table(&vec![
row(indexmap!{"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("August 23-2019")})
]),
"Sept 24-2019".into() => table(&vec![
row(indexmap!{"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("Sept 24-2019")})
]),
"October 10-2019".into() => table(&vec![
row(indexmap!{"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("October 10-2019")})
])
}),
"NZ".into() => row(indexmap! {
"August 23-2019".into() => table(&vec![
row(indexmap!{"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => string("August 23-2019")})
]),
"Sept 24-2019".into() => table(&vec![
row(indexmap!{"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => string("Sept 24-2019")})
]),
"October 10-2019".into() => table(&vec![
row(indexmap!{"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => string("October 10-2019")})
])
}),
"US".into() => row(indexmap! {
"August 23-2019".into() => table(&vec![
row(indexmap!{"name".into() => string("YK"), "country".into() => string("US"), "date".into() => string("August 23-2019")})
]),
"Sept 24-2019".into() => table(&vec![
row(indexmap!{"name".into() => string("YK"), "country".into() => string("US"), "date".into() => string("Sept 24-2019")})
]),
"October 10-2019".into() => table(&vec![
row(indexmap!{"name".into() => string("YK"), "country".into() => string("US"), "date".into() => string("October 10-2019")})
])
})
}).tagged_unknown()
);
}
#[test]
fn errors_if_key_within_some_inner_table_is_missing() {
let for_key = String::from("country").tagged_unknown();
let nu_releases = row(indexmap! {
"August 23-2019".into() => table(&vec![
row(indexmap!{"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("August 23-2019")})
]),
"Sept 24-2019".into() => table(&vec![
row(indexmap!{"name".into() => Value::string("JT").tagged(Tag::from(Span::new(5,10))), "date".into() => string("Sept 24-2019")})
]),
"October 10-2019".into() => table(&vec![
row(indexmap!{"name".into() => string("YK"), "country".into() => string("US"), "date".into() => string("October 10-2019")})
])
});
assert!(split(&for_key, &nu_releases, Tag::from(Span::new(5, 10))).is_err());
}
}

View file

@ -21,9 +21,13 @@ impl WholeStreamCommand for SplitColumn {
fn signature(&self) -> Signature {
Signature::build("split-column")
.required("separator", SyntaxShape::Any)
.switch("collapse-empty")
.rest(SyntaxShape::Member)
.required(
"separator",
SyntaxShape::Any,
"the character that denotes what separates columns",
)
.switch("collapse-empty", "remove empty columns")
.rest(SyntaxShape::Member, "column names to give the new columns")
}
fn usage(&self) -> &str {
@ -94,7 +98,7 @@ fn split_column(
_ => Err(ShellError::labeled_error_with_secondary(
"Expected a string from pipeline",
"requires string input",
name,
&name,
"value originates from here",
v.tag(),
)),

View file

@ -17,7 +17,11 @@ impl WholeStreamCommand for SplitRow {
}
fn signature(&self) -> Signature {
Signature::build("split-row").required("separator", SyntaxShape::Any)
Signature::build("split-row").required(
"separator",
SyntaxShape::Any,
"the character that denotes what separates rows",
)
}
fn usage(&self) -> &str {
@ -60,7 +64,7 @@ fn split_row(
result.push_back(Err(ShellError::labeled_error_with_secondary(
"Expected a string from pipeline",
"requires string input",
name,
&name,
"value originates from here",
v.tag(),
)));

358
src/commands/t_sort_by.rs Normal file
View file

@ -0,0 +1,358 @@
use crate::commands::WholeStreamCommand;
use crate::data::{TaggedDictBuilder, TaggedListBuilder};
use crate::errors::ShellError;
use crate::prelude::*;
use chrono::{DateTime, NaiveDate, Utc};
pub struct TSortBy;
#[derive(Deserialize)]
pub struct TSortByArgs {
#[serde(rename(deserialize = "show-columns"))]
show_columns: bool,
group_by: Option<Tagged<String>>,
#[allow(unused)]
split_by: Option<String>,
}
impl WholeStreamCommand for TSortBy {
fn name(&self) -> &str {
"t-sort-by"
}
fn signature(&self) -> Signature {
Signature::build("t-sort-by")
.switch("show-columns", "Displays the column names sorted")
.named(
"group_by",
SyntaxShape::String,
"the name of the column to group by",
)
.named(
"split_by",
SyntaxShape::String,
"the name of the column within the grouped by table to split by",
)
}
fn usage(&self) -> &str {
"Sort by the given columns."
}
fn run(
&self,
args: CommandArgs,
registry: &CommandRegistry,
) -> Result<OutputStream, ShellError> {
args.process(registry, t_sort_by)?.run()
}
}
fn t_sort_by(
TSortByArgs {
show_columns,
group_by,
..
}: TSortByArgs,
RunnableContext { input, name, .. }: RunnableContext,
) -> Result<OutputStream, ShellError> {
Ok(OutputStream::new(async_stream! {
let values: Vec<Tagged<Value>> = input.values.collect().await;
let column_grouped_by_name = if let Some(grouped_by) = group_by {
Some(grouped_by.item().clone())
} else {
None
};
if show_columns {
for label in columns_sorted(column_grouped_by_name, &values[0], &name).iter() {
yield ReturnSuccess::value(label.clone());
}
} else {
match t_sort(column_grouped_by_name, None, &values[0], name) {
Ok(sorted) => yield ReturnSuccess::value(sorted),
Err(err) => yield Err(err)
}
}
}))
}
pub fn columns_sorted(
_group_by_name: Option<String>,
value: &Tagged<Value>,
tag: impl Into<Tag>,
) -> Vec<Tagged<Value>> {
let origin_tag = tag.into();
match value {
Tagged {
item: Value::Row(rows),
..
} => {
let mut keys: Vec<Tagged<Value>> =
rows.entries
.keys()
.map(|s| s.as_ref())
.map(|k: &str| {
let date = NaiveDate::parse_from_str(k, "%B %d-%Y");
let date = match date {
Ok(parsed) => Value::Primitive(Primitive::Date(
DateTime::<Utc>::from_utc(parsed.and_hms(12, 34, 56), Utc),
)),
Err(_) => Value::string(k),
};
date.tagged_unknown()
})
.collect();
keys.sort();
let keys: Vec<Value> = keys
.into_iter()
.map(|k| {
Value::string(match k {
Tagged {
item: Value::Primitive(Primitive::Date(d)),
..
} => format!("{}", d.format("%B %d-%Y")),
_ => k.as_string().unwrap(),
})
})
.collect();
keys.into_iter().map(|k| k.tagged(&origin_tag)).collect()
}
_ => vec![Value::string("default").tagged(&origin_tag)],
}
}
pub fn t_sort(
group_by_name: Option<String>,
split_by_name: Option<String>,
value: &Tagged<Value>,
tag: impl Into<Tag>,
) -> Result<Tagged<Value>, ShellError> {
let origin_tag = tag.into();
match group_by_name {
Some(column_name) => {
let sorted_labels = columns_sorted(Some(column_name), value, &origin_tag);
match split_by_name {
None => {
let mut dataset = TaggedDictBuilder::new(&origin_tag);
dataset.insert_tagged("default", value.clone());
let dataset = dataset.into_tagged_value();
let split_labels = match &dataset {
Tagged {
item: Value::Row(rows),
..
} => {
let mut keys: Vec<Tagged<Value>> = rows
.entries
.keys()
.map(|s| s.as_ref())
.map(|k: &str| {
let date = NaiveDate::parse_from_str(k, "%B %d-%Y");
let date = match date {
Ok(parsed) => Value::Primitive(Primitive::Date(
DateTime::<Utc>::from_utc(
parsed.and_hms(12, 34, 56),
Utc,
),
)),
Err(_) => Value::string(k),
};
date.tagged_unknown()
})
.collect();
keys.sort();
let keys: Vec<Value> = keys
.into_iter()
.map(|k| {
Value::string(match k {
Tagged {
item: Value::Primitive(Primitive::Date(d)),
..
} => format!("{}", d.format("%B %d-%Y")),
_ => k.as_string().unwrap(),
})
})
.collect();
keys.into_iter().map(|k| k.tagged(&origin_tag)).collect()
}
_ => vec![],
};
let results: Vec<Vec<Tagged<Value>>> = split_labels
.into_iter()
.map(|split| {
let groups = dataset.get_data_by_key(&split.as_string().unwrap());
sorted_labels
.clone()
.into_iter()
.map(|label| {
let label = label.as_string().unwrap();
match groups {
Some(Tagged {
item: Value::Row(dict),
..
}) => dict.get_data_by_key(&label).unwrap().clone(),
_ => Value::Table(vec![]).tagged(&origin_tag),
}
})
.collect()
})
.collect();
let mut outer = TaggedListBuilder::new(&origin_tag);
for i in results {
outer.insert_tagged(Value::Table(i).tagged(&origin_tag));
}
return Ok(Value::Table(outer.list).tagged(&origin_tag));
}
Some(_) => return Ok(Value::nothing().tagged(&origin_tag)),
}
}
None => return Ok(Value::nothing().tagged(&origin_tag)),
}
}
#[cfg(test)]
mod tests {
use crate::commands::group_by::group;
use crate::commands::t_sort_by::{columns_sorted, t_sort};
use crate::data::meta::*;
use crate::Value;
use indexmap::IndexMap;
fn string(input: impl Into<String>) -> Tagged<Value> {
Value::string(input.into()).tagged_unknown()
}
fn row(entries: IndexMap<String, Tagged<Value>>) -> Tagged<Value> {
Value::row(entries).tagged_unknown()
}
fn table(list: &Vec<Tagged<Value>>) -> Tagged<Value> {
Value::table(list).tagged_unknown()
}
fn nu_releases_grouped_by_date() -> Tagged<Value> {
let key = String::from("date").tagged_unknown();
group(&key, nu_releases_commiters(), Tag::unknown()).unwrap()
}
fn nu_releases_commiters() -> Vec<Tagged<Value>> {
vec![
row(
indexmap! {"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("August 23-2019")},
),
row(
indexmap! {"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => string("August 23-2019")},
),
row(
indexmap! {"name".into() => string("YK"), "country".into() => string("US"), "date".into() => string("October 10-2019")},
),
row(
indexmap! {"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("September 24-2019")},
),
row(
indexmap! {"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => string("October 10-2019")},
),
row(
indexmap! {"name".into() => string("YK"), "country".into() => string("US"), "date".into() => string("September 24-2019")},
),
row(
indexmap! {"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("October 10-2019")},
),
row(
indexmap! {"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => string("September 24-2019")},
),
row(
indexmap! {"name".into() => string("YK"), "country".into() => string("US"), "date".into() => string("August 23-2019")},
),
]
}
#[test]
fn show_columns_sorted_given_a_column_to_sort_by() {
let by_column = String::from("date");
assert_eq!(
columns_sorted(
Some(by_column),
&nu_releases_grouped_by_date(),
Tag::unknown()
),
vec![
string("August 23-2019"),
string("September 24-2019"),
string("October 10-2019")
]
)
}
#[test]
fn sorts_the_tables() {
let group_by = String::from("date");
assert_eq!(
t_sort(
Some(group_by),
None,
&nu_releases_grouped_by_date(),
Tag::unknown()
)
.unwrap(),
table(&vec![table(&vec![
table(&vec![
row(
indexmap! {"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("August 23-2019")}
),
row(
indexmap! {"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => string("August 23-2019")}
),
row(
indexmap! {"name".into() => string("YK"), "country".into() => string("US"), "date".into() => string("August 23-2019")}
)
]),
table(&vec![
row(
indexmap! {"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("September 24-2019")}
),
row(
indexmap! {"name".into() => string("YK"), "country".into() => string("US"), "date".into() => string("September 24-2019")}
),
row(
indexmap! {"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => string("September 24-2019")}
)
]),
table(&vec![
row(
indexmap! {"name".into() => string("YK"), "country".into() => string("US"), "date".into() => string("October 10-2019")}
),
row(
indexmap! {"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => string("October 10-2019")}
),
row(
indexmap! {"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("October 10-2019")}
)
]),
]),])
);
}
}

View file

@ -5,16 +5,17 @@ use crate::prelude::*;
pub struct Table;
#[derive(Deserialize)]
pub struct TableArgs {}
impl WholeStreamCommand for Table {
fn name(&self) -> &str {
"table"
}
fn signature(&self) -> Signature {
Signature::build("table")
Signature::build("table").named(
"start_number",
SyntaxShape::Number,
"row number to start viewing from",
)
}
fn usage(&self) -> &str {
@ -26,16 +27,29 @@ impl WholeStreamCommand for Table {
args: CommandArgs,
registry: &CommandRegistry,
) -> Result<OutputStream, ShellError> {
args.process(registry, table)?.run()
table(args, registry)
}
}
pub fn table(_args: TableArgs, context: RunnableContext) -> Result<OutputStream, ShellError> {
fn table(args: CommandArgs, registry: &CommandRegistry) -> Result<OutputStream, ShellError> {
let args = args.evaluate_once(registry)?;
let stream = async_stream! {
let input: Vec<Tagged<Value>> = context.input.into_vec().await;
let host = args.host.clone();
let start_number = match args.get("start_number") {
Some(Tagged { item: Value::Primitive(Primitive::Int(i)), .. }) => {
i.to_usize().unwrap()
}
_ => {
0
}
};
let input: Vec<Tagged<Value>> = args.input.into_vec().await;
if input.len() > 0 {
let mut host = context.host.lock().unwrap();
let view = TableView::from_list(&input);
let mut host = host.lock().unwrap();
let view = TableView::from_list(&input, start_number);
if let Some(view) = view {
handle_unexpected(&mut *host, |host| crate::format::print_view(&view, host));
}

View file

@ -28,7 +28,6 @@ impl WholeStreamCommand for Tags {
}
fn tags(args: CommandArgs, _registry: &CommandRegistry) -> Result<OutputStream, ShellError> {
let source_map = args.call_info.source_map.clone();
Ok(args
.input
.values
@ -38,11 +37,11 @@ fn tags(args: CommandArgs, _registry: &CommandRegistry) -> Result<OutputStream,
let anchor = v.anchor();
let span = v.tag().span;
let mut dict = TaggedDictBuilder::new(v.tag());
dict.insert("start", Value::int(span.start as i64));
dict.insert("end", Value::int(span.end as i64));
dict.insert("start", Value::int(span.start() as i64));
dict.insert("end", Value::int(span.end() as i64));
tags.insert_tagged("span", dict.into_tagged_value());
match source_map.get(&anchor) {
match anchor {
Some(AnchorLocation::File(source)) => {
tags.insert("anchor", Value::string(source));
}

View file

@ -46,7 +46,7 @@ pub fn value_to_bson_value(v: &Tagged<Value>) -> Result<Bson, ShellError> {
Value::Primitive(Primitive::BeginningOfStream) => Bson::Null,
Value::Primitive(Primitive::Decimal(d)) => Bson::FloatingPoint(d.to_f64().unwrap()),
Value::Primitive(Primitive::Int(i)) => {
Bson::I64(i.tagged(v.tag).coerce_into("converting to BSON")?)
Bson::I64(i.tagged(&v.tag).coerce_into("converting to BSON")?)
}
Value::Primitive(Primitive::Nothing) => Bson::Null,
Value::Primitive(Primitive::String(s)) => Bson::String(s.clone()),
@ -58,6 +58,7 @@ pub fn value_to_bson_value(v: &Tagged<Value>) -> Result<Bson, ShellError> {
.collect::<Result<_, _>>()?,
),
Value::Block(_) => Bson::Null,
Value::Error(e) => return Err(e.clone()),
Value::Primitive(Primitive::Binary(b)) => Bson::Binary(BinarySubtype::Generic, b.clone()),
Value::Row(o) => object_value_to_bson(o)?,
})
@ -170,7 +171,7 @@ fn get_binary_subtype<'a>(tagged_value: &'a Tagged<Value>) -> Result<BinarySubty
_ => unreachable!(),
}),
Value::Primitive(Primitive::Int(i)) => Ok(BinarySubtype::UserDefined(
i.tagged(tagged_value.tag)
i.tagged(&tagged_value.tag)
.coerce_into("converting to BSON binary subtype")?,
)),
_ => Err(ShellError::type_error(
@ -207,12 +208,12 @@ fn bson_value_to_bytes(bson: Bson, tag: Tag) -> Result<Vec<u8>, ShellError> {
Bson::Array(a) => {
for v in a.into_iter() {
match v {
Bson::Document(d) => shell_encode_document(&mut out, d, tag)?,
Bson::Document(d) => shell_encode_document(&mut out, d, tag.clone())?,
_ => {
return Err(ShellError::labeled_error(
format!("All top level values must be Documents, got {:?}", v),
"requires BSON-compatible document",
tag,
&tag,
))
}
}
@ -237,7 +238,7 @@ fn to_bson(args: CommandArgs, registry: &CommandRegistry) -> Result<OutputStream
let input: Vec<Tagged<Value>> = args.input.values.collect().await;
let to_process_input = if input.len() > 1 {
let tag = input[0].tag;
let tag = input[0].tag.clone();
vec![Tagged { item: Value::Table(input), tag } ]
} else if input.len() == 1 {
input
@ -248,14 +249,14 @@ fn to_bson(args: CommandArgs, registry: &CommandRegistry) -> Result<OutputStream
for value in to_process_input {
match value_to_bson_value(&value) {
Ok(bson_value) => {
match bson_value_to_bytes(bson_value, name_tag) {
match bson_value_to_bytes(bson_value, name_tag.clone()) {
Ok(x) => yield ReturnSuccess::value(
Value::binary(x).tagged(name_tag),
Value::binary(x).tagged(&name_tag),
),
_ => yield Err(ShellError::labeled_error_with_secondary(
"Expected a table with BSON-compatible structure.tag() from pipeline",
"requires BSON-compatible input",
name_tag,
&name_tag,
"originates from here".to_string(),
value.tag(),
)),
@ -264,7 +265,7 @@ fn to_bson(args: CommandArgs, registry: &CommandRegistry) -> Result<OutputStream
_ => yield Err(ShellError::labeled_error(
"Expected a table with BSON-compatible structure from pipeline",
"requires BSON-compatible input",
name_tag))
&name_tag))
}
}
};

View file

@ -16,7 +16,10 @@ impl WholeStreamCommand for ToCSV {
}
fn signature(&self) -> Signature {
Signature::build("to-csv").switch("headerless")
Signature::build("to-csv").switch(
"headerless",
"do not output the columns names as the first row",
)
}
fn usage(&self) -> &str {
@ -32,8 +35,8 @@ impl WholeStreamCommand for ToCSV {
}
}
pub fn value_to_csv_value(v: &Value) -> Value {
match v {
pub fn value_to_csv_value(v: &Tagged<Value>) -> Tagged<Value> {
match &v.item {
Value::Primitive(Primitive::String(s)) => Value::Primitive(Primitive::String(s.clone())),
Value::Primitive(Primitive::Nothing) => Value::Primitive(Primitive::Nothing),
Value::Primitive(Primitive::Boolean(b)) => Value::Primitive(Primitive::Boolean(b.clone())),
@ -47,10 +50,11 @@ pub fn value_to_csv_value(v: &Value) -> Value {
Value::Block(_) => Value::Primitive(Primitive::Nothing),
_ => Value::Primitive(Primitive::Nothing),
}
.tagged(v.tag.clone())
}
fn to_string_helper(v: &Value) -> Result<String, ShellError> {
match v {
fn to_string_helper(v: &Tagged<Value>) -> Result<String, ShellError> {
match &v.item {
Value::Primitive(Primitive::Date(d)) => Ok(d.to_string()),
Value::Primitive(Primitive::Bytes(b)) => Ok(format!("{}", b)),
Value::Primitive(Primitive::Boolean(_)) => Ok(v.as_string()?),
@ -60,7 +64,13 @@ fn to_string_helper(v: &Value) -> Result<String, ShellError> {
Value::Table(_) => return Ok(String::from("[Table]")),
Value::Row(_) => return Ok(String::from("[Row]")),
Value::Primitive(Primitive::String(s)) => return Ok(s.to_string()),
_ => return Err(ShellError::string("Unexpected value")),
_ => {
return Err(ShellError::labeled_error(
"Unexpected value",
"",
v.tag.clone(),
))
}
}
}
@ -76,7 +86,9 @@ fn merge_descriptors(values: &[Tagged<Value>]) -> Vec<String> {
ret
}
pub fn to_string(v: &Value) -> Result<String, ShellError> {
pub fn to_string(tagged_value: &Tagged<Value>) -> Result<String, ShellError> {
let v = &tagged_value.item;
match v {
Value::Row(o) => {
let mut wtr = WriterBuilder::new().from_writer(vec![]);
@ -92,11 +104,20 @@ pub fn to_string(v: &Value) -> Result<String, ShellError> {
wtr.write_record(fields).expect("can not write.");
wtr.write_record(values).expect("can not write.");
return Ok(String::from_utf8(
wtr.into_inner()
.map_err(|_| ShellError::string("Could not convert record"))?,
)
.map_err(|_| ShellError::string("Could not convert record"))?);
return Ok(String::from_utf8(wtr.into_inner().map_err(|_| {
ShellError::labeled_error(
"Could not convert record",
"original value",
&tagged_value.tag,
)
})?)
.map_err(|_| {
ShellError::labeled_error(
"Could not convert record",
"original value",
&tagged_value.tag,
)
})?);
}
Value::Table(list) => {
let mut wtr = WriterBuilder::new().from_writer(vec![]);
@ -120,13 +141,22 @@ pub fn to_string(v: &Value) -> Result<String, ShellError> {
wtr.write_record(&row).expect("can not write");
}
return Ok(String::from_utf8(
wtr.into_inner()
.map_err(|_| ShellError::string("Could not convert record"))?,
)
.map_err(|_| ShellError::string("Could not convert record"))?);
return Ok(String::from_utf8(wtr.into_inner().map_err(|_| {
ShellError::labeled_error(
"Could not convert record",
"original value",
&tagged_value.tag,
)
})?)
.map_err(|_| {
ShellError::labeled_error(
"Could not convert record",
"original value",
&tagged_value.tag,
)
})?);
}
_ => return to_string_helper(&v),
_ => return to_string_helper(tagged_value),
}
}
@ -139,7 +169,7 @@ fn to_csv(
let input: Vec<Tagged<Value>> = input.values.collect().await;
let to_process_input = if input.len() > 1 {
let tag = input[0].tag;
let tag = input[0].tag.clone();
vec![Tagged { item: Value::Table(input), tag } ]
} else if input.len() == 1 {
input
@ -148,20 +178,20 @@ fn to_csv(
};
for value in to_process_input {
match to_string(&value_to_csv_value(&value.item)) {
match to_string(&value_to_csv_value(&value)) {
Ok(x) => {
let converted = if headerless {
x.lines().skip(1).collect()
} else {
x
};
yield ReturnSuccess::value(Value::Primitive(Primitive::String(converted)).tagged(name_tag))
yield ReturnSuccess::value(Value::Primitive(Primitive::String(converted)).tagged(&name_tag))
}
_ => {
yield Err(ShellError::labeled_error_with_secondary(
"Expected a table with CSV-compatible structure.tag() from pipeline",
"requires CSV-compatible input",
name_tag,
&name_tag,
"originates from here".to_string(),
value.tag(),
))

View file

@ -42,7 +42,7 @@ pub fn value_to_json_value(v: &Tagged<Value>) -> Result<serde_json::Value, Shell
.unwrap(),
),
Value::Primitive(Primitive::Int(i)) => serde_json::Value::Number(serde_json::Number::from(
CoerceInto::<i64>::coerce_into(i.tagged(v.tag), "converting to JSON number")?,
CoerceInto::<i64>::coerce_into(i.tagged(&v.tag), "converting to JSON number")?,
)),
Value::Primitive(Primitive::Nothing) => serde_json::Value::Null,
Value::Primitive(Primitive::Pattern(s)) => serde_json::Value::String(s.clone()),
@ -50,6 +50,7 @@ pub fn value_to_json_value(v: &Tagged<Value>) -> Result<serde_json::Value, Shell
Value::Primitive(Primitive::Path(s)) => serde_json::Value::String(s.display().to_string()),
Value::Table(l) => serde_json::Value::Array(json_list(l)?),
Value::Error(e) => return Err(e.clone()),
Value::Block(_) => serde_json::Value::Null,
Value::Primitive(Primitive::Binary(b)) => serde_json::Value::Array(
b.iter()
@ -85,7 +86,7 @@ fn to_json(args: CommandArgs, registry: &CommandRegistry) -> Result<OutputStream
let input: Vec<Tagged<Value>> = args.input.values.collect().await;
let to_process_input = if input.len() > 1 {
let tag = input[0].tag;
let tag = input[0].tag.clone();
vec![Tagged { item: Value::Table(input), tag } ]
} else if input.len() == 1 {
input
@ -98,12 +99,12 @@ fn to_json(args: CommandArgs, registry: &CommandRegistry) -> Result<OutputStream
Ok(json_value) => {
match serde_json::to_string(&json_value) {
Ok(x) => yield ReturnSuccess::value(
Value::Primitive(Primitive::String(x)).tagged(name_tag),
Value::Primitive(Primitive::String(x)).tagged(&name_tag),
),
_ => yield Err(ShellError::labeled_error_with_secondary(
"Expected a table with JSON-compatible structure.tag() from pipeline",
"requires JSON-compatible input",
name_tag,
&name_tag,
"originates from here".to_string(),
value.tag(),
)),
@ -112,7 +113,7 @@ fn to_json(args: CommandArgs, registry: &CommandRegistry) -> Result<OutputStream
_ => yield Err(ShellError::labeled_error(
"Expected a table with JSON-compatible structure from pipeline",
"requires JSON-compatible input",
name_tag))
&name_tag))
}
}
};

View file

@ -38,10 +38,10 @@ pub fn value_to_toml_value(v: &Tagged<Value>) -> Result<toml::Value, ShellError>
toml::Value::String("<Beginning of Stream>".to_string())
}
Value::Primitive(Primitive::Decimal(f)) => {
toml::Value::Float(f.tagged(v.tag).coerce_into("converting to TOML float")?)
toml::Value::Float(f.tagged(&v.tag).coerce_into("converting to TOML float")?)
}
Value::Primitive(Primitive::Int(i)) => {
toml::Value::Integer(i.tagged(v.tag).coerce_into("converting to TOML integer")?)
toml::Value::Integer(i.tagged(&v.tag).coerce_into("converting to TOML integer")?)
}
Value::Primitive(Primitive::Nothing) => toml::Value::String("<Nothing>".to_string()),
Value::Primitive(Primitive::Pattern(s)) => toml::Value::String(s.clone()),
@ -49,6 +49,7 @@ pub fn value_to_toml_value(v: &Tagged<Value>) -> Result<toml::Value, ShellError>
Value::Primitive(Primitive::Path(s)) => toml::Value::String(s.display().to_string()),
Value::Table(l) => toml::Value::Array(collect_values(l)?),
Value::Error(e) => return Err(e.clone()),
Value::Block(_) => toml::Value::String("<Block>".to_string()),
Value::Primitive(Primitive::Binary(b)) => {
toml::Value::Array(b.iter().map(|x| toml::Value::Integer(*x as i64)).collect())
@ -80,7 +81,7 @@ fn to_toml(args: CommandArgs, registry: &CommandRegistry) -> Result<OutputStream
let input: Vec<Tagged<Value>> = args.input.values.collect().await;
let to_process_input = if input.len() > 1 {
let tag = input[0].tag;
let tag = input[0].tag.clone();
vec![Tagged { item: Value::Table(input), tag } ]
} else if input.len() == 1 {
input
@ -93,12 +94,12 @@ fn to_toml(args: CommandArgs, registry: &CommandRegistry) -> Result<OutputStream
Ok(toml_value) => {
match toml::to_string(&toml_value) {
Ok(x) => yield ReturnSuccess::value(
Value::Primitive(Primitive::String(x)).tagged(name_tag),
Value::Primitive(Primitive::String(x)).tagged(&name_tag),
),
_ => yield Err(ShellError::labeled_error_with_secondary(
"Expected a table with TOML-compatible structure.tag() from pipeline",
"requires TOML-compatible input",
name_tag,
&name_tag,
"originates from here".to_string(),
value.tag(),
)),
@ -107,7 +108,7 @@ fn to_toml(args: CommandArgs, registry: &CommandRegistry) -> Result<OutputStream
_ => yield Err(ShellError::labeled_error(
"Expected a table with TOML-compatible structure from pipeline",
"requires TOML-compatible input",
name_tag))
&name_tag))
}
}
};

View file

@ -16,7 +16,10 @@ impl WholeStreamCommand for ToTSV {
}
fn signature(&self) -> Signature {
Signature::build("to-tsv").switch("headerless")
Signature::build("to-tsv").switch(
"headerless",
"do not output the column names as the first row",
)
}
fn usage(&self) -> &str {
@ -32,7 +35,9 @@ impl WholeStreamCommand for ToTSV {
}
}
pub fn value_to_tsv_value(v: &Value) -> Value {
pub fn value_to_tsv_value(tagged_value: &Tagged<Value>) -> Tagged<Value> {
let v = &tagged_value.item;
match v {
Value::Primitive(Primitive::String(s)) => Value::Primitive(Primitive::String(s.clone())),
Value::Primitive(Primitive::Nothing) => Value::Primitive(Primitive::Nothing),
@ -47,20 +52,28 @@ pub fn value_to_tsv_value(v: &Value) -> Value {
Value::Block(_) => Value::Primitive(Primitive::Nothing),
_ => Value::Primitive(Primitive::Nothing),
}
.tagged(&tagged_value.tag)
}
fn to_string_helper(v: &Value) -> Result<String, ShellError> {
fn to_string_helper(tagged_value: &Tagged<Value>) -> Result<String, ShellError> {
let v = &tagged_value.item;
match v {
Value::Primitive(Primitive::Date(d)) => Ok(d.to_string()),
Value::Primitive(Primitive::Bytes(b)) => Ok(format!("{}", b)),
Value::Primitive(Primitive::Boolean(_)) => Ok(v.as_string()?),
Value::Primitive(Primitive::Decimal(_)) => Ok(v.as_string()?),
Value::Primitive(Primitive::Int(_)) => Ok(v.as_string()?),
Value::Primitive(Primitive::Path(_)) => Ok(v.as_string()?),
Value::Primitive(Primitive::Boolean(_)) => Ok(tagged_value.as_string()?),
Value::Primitive(Primitive::Decimal(_)) => Ok(tagged_value.as_string()?),
Value::Primitive(Primitive::Int(_)) => Ok(tagged_value.as_string()?),
Value::Primitive(Primitive::Path(_)) => Ok(tagged_value.as_string()?),
Value::Table(_) => return Ok(String::from("[table]")),
Value::Row(_) => return Ok(String::from("[row]")),
Value::Primitive(Primitive::String(s)) => return Ok(s.to_string()),
_ => return Err(ShellError::string("Unexpected value")),
_ => {
return Err(ShellError::labeled_error(
"Unexpected value",
"original value",
&tagged_value.tag,
))
}
}
}
@ -76,7 +89,9 @@ fn merge_descriptors(values: &[Tagged<Value>]) -> Vec<String> {
ret
}
pub fn to_string(v: &Value) -> Result<String, ShellError> {
pub fn to_string(tagged_value: &Tagged<Value>) -> Result<String, ShellError> {
let v = &tagged_value.item;
match v {
Value::Row(o) => {
let mut wtr = WriterBuilder::new().delimiter(b'\t').from_writer(vec![]);
@ -91,11 +106,20 @@ pub fn to_string(v: &Value) -> Result<String, ShellError> {
wtr.write_record(fields).expect("can not write.");
wtr.write_record(values).expect("can not write.");
return Ok(String::from_utf8(
wtr.into_inner()
.map_err(|_| ShellError::string("Could not convert record"))?,
)
.map_err(|_| ShellError::string("Could not convert record"))?);
return Ok(String::from_utf8(wtr.into_inner().map_err(|_| {
ShellError::labeled_error(
"Could not convert record",
"original value",
&tagged_value.tag,
)
})?)
.map_err(|_| {
ShellError::labeled_error(
"Could not convert record",
"original value",
&tagged_value.tag,
)
})?);
}
Value::Table(list) => {
let mut wtr = WriterBuilder::new().delimiter(b'\t').from_writer(vec![]);
@ -119,13 +143,22 @@ pub fn to_string(v: &Value) -> Result<String, ShellError> {
wtr.write_record(&row).expect("can not write");
}
return Ok(String::from_utf8(
wtr.into_inner()
.map_err(|_| ShellError::string("Could not convert record"))?,
)
.map_err(|_| ShellError::string("Could not convert record"))?);
return Ok(String::from_utf8(wtr.into_inner().map_err(|_| {
ShellError::labeled_error(
"Could not convert record",
"original value",
&tagged_value.tag,
)
})?)
.map_err(|_| {
ShellError::labeled_error(
"Could not convert record",
"original value",
&tagged_value.tag,
)
})?);
}
_ => return to_string_helper(&v),
_ => return to_string_helper(tagged_value),
}
}
@ -138,7 +171,7 @@ fn to_tsv(
let input: Vec<Tagged<Value>> = input.values.collect().await;
let to_process_input = if input.len() > 1 {
let tag = input[0].tag;
let tag = input[0].tag.clone();
vec![Tagged { item: Value::Table(input), tag } ]
} else if input.len() == 1 {
input
@ -147,20 +180,20 @@ fn to_tsv(
};
for value in to_process_input {
match to_string(&value_to_tsv_value(&value.item)) {
match to_string(&value_to_tsv_value(&value)) {
Ok(x) => {
let converted = if headerless {
x.lines().skip(1).collect()
} else {
x
};
yield ReturnSuccess::value(Value::Primitive(Primitive::String(converted)).tagged(name_tag))
yield ReturnSuccess::value(Value::Primitive(Primitive::String(converted)).tagged(&name_tag))
}
_ => {
yield Err(ShellError::labeled_error_with_secondary(
"Expected a table with TSV-compatible structure.tag() from pipeline",
"requires TSV-compatible input",
name_tag,
&name_tag,
"originates from here".to_string(),
value.tag(),
))

View file

@ -47,7 +47,7 @@ fn to_url(args: CommandArgs, registry: &CommandRegistry) -> Result<OutputStream,
yield Err(ShellError::labeled_error_with_secondary(
"Expected table with string values",
"requires table with strings",
tag,
&tag,
"value originates from here",
v.tag,
))
@ -57,13 +57,13 @@ fn to_url(args: CommandArgs, registry: &CommandRegistry) -> Result<OutputStream,
match serde_urlencoded::to_string(row_vec) {
Ok(s) => {
yield ReturnSuccess::value(Value::string(s).tagged(tag));
yield ReturnSuccess::value(Value::string(s).tagged(&tag));
}
_ => {
yield Err(ShellError::labeled_error(
"Failed to convert to url-encoded",
"cannot url-encode",
tag,
&tag,
))
}
}
@ -72,7 +72,7 @@ fn to_url(args: CommandArgs, registry: &CommandRegistry) -> Result<OutputStream,
yield Err(ShellError::labeled_error_with_secondary(
"Expected a table from pipeline",
"requires table input",
tag,
&tag,
"value originates from here",
value_tag,
))

View file

@ -39,7 +39,7 @@ pub fn value_to_yaml_value(v: &Tagged<Value>) -> Result<serde_yaml::Value, Shell
serde_yaml::Value::Number(serde_yaml::Number::from(f.to_f64().unwrap()))
}
Value::Primitive(Primitive::Int(i)) => serde_yaml::Value::Number(serde_yaml::Number::from(
CoerceInto::<i64>::coerce_into(i.tagged(v.tag), "converting to YAML number")?,
CoerceInto::<i64>::coerce_into(i.tagged(&v.tag), "converting to YAML number")?,
)),
Value::Primitive(Primitive::Nothing) => serde_yaml::Value::Null,
Value::Primitive(Primitive::Pattern(s)) => serde_yaml::Value::String(s.clone()),
@ -55,6 +55,7 @@ pub fn value_to_yaml_value(v: &Tagged<Value>) -> Result<serde_yaml::Value, Shell
serde_yaml::Value::Sequence(out)
}
Value::Error(e) => return Err(e.clone()),
Value::Block(_) => serde_yaml::Value::Null,
Value::Primitive(Primitive::Binary(b)) => serde_yaml::Value::Sequence(
b.iter()
@ -81,7 +82,7 @@ fn to_yaml(args: CommandArgs, registry: &CommandRegistry) -> Result<OutputStream
let input: Vec<Tagged<Value>> = args.input.values.collect().await;
let to_process_input = if input.len() > 1 {
let tag = input[0].tag;
let tag = input[0].tag.clone();
vec![Tagged { item: Value::Table(input), tag } ]
} else if input.len() == 1 {
input
@ -94,12 +95,12 @@ fn to_yaml(args: CommandArgs, registry: &CommandRegistry) -> Result<OutputStream
Ok(yaml_value) => {
match serde_yaml::to_string(&yaml_value) {
Ok(x) => yield ReturnSuccess::value(
Value::Primitive(Primitive::String(x)).tagged(name_tag),
Value::Primitive(Primitive::String(x)).tagged(&name_tag),
),
_ => yield Err(ShellError::labeled_error_with_secondary(
"Expected a table with YAML-compatible structure.tag() from pipeline",
"requires YAML-compatible input",
name_tag,
&name_tag,
"originates from here".to_string(),
value.tag(),
)),
@ -108,7 +109,7 @@ fn to_yaml(args: CommandArgs, registry: &CommandRegistry) -> Result<OutputStream
_ => yield Err(ShellError::labeled_error(
"Expected a table with YAML-compatible structure from pipeline",
"requires YAML-compatible input",
name_tag))
&name_tag))
}
}
};

View file

@ -31,14 +31,14 @@ impl WholeStreamCommand for Version {
pub fn date(args: CommandArgs, registry: &CommandRegistry) -> Result<OutputStream, ShellError> {
let args = args.evaluate_once(registry)?;
let tag = args.call_info.name_tag;
let tag = args.call_info.name_tag.clone();
let mut indexmap = IndexMap::new();
indexmap.insert(
"version".to_string(),
Value::string(clap::crate_version!()).tagged(tag),
Value::string(clap::crate_version!()).tagged(&tag),
);
let value = Value::Row(Dictionary::from(indexmap)).tagged(tag);
let value = Value::Row(Dictionary::from(indexmap)).tagged(&tag);
Ok(OutputStream::one(value))
}

Some files were not shown because too many files have changed in this diff Show more