Compare commits

...

139 Commits

Author SHA1 Message Date
3783c19d02 bump miette to 4.4.0 (#5167)
This fixes an issue where docsrs error links were not working.

Ref: https://github.com/zkat/miette/issues/147
2022-04-13 08:38:15 +12:00
1f6e0255c0 Fix typo in link (#5168)
skip-checks:true
2022-04-12 22:13:40 +02:00
JT
b61af9a26a Update README.md 2022-04-13 06:24:46 +12:00
JT
6e2f6c71fe Update README.md 2022-04-13 06:15:34 +12:00
JT
f51d5789a3 Update README.md 2022-04-13 06:15:09 +12:00
JT
933cee27ae Update README.md 2022-04-13 06:12:33 +12:00
JT
4566c904d0 Bump 0.61 (#5166) 2022-04-13 05:42:26 +12:00
9b020c056b Pin reedline version for 0.61 release (#5164) 2022-04-13 04:38:36 +12:00
JT
60b5863058 Remove the im crate dependency (#5161) 2022-04-12 07:01:05 +12:00
836f914163 Clean REPL code, hide Hints without ANSI coloring (#5157)
- With a change to reedline hints can now be hidden. This is useful when
no ANSI coloring is available as hints become indistinguishable from the
actual buffer
- remove commented out code
- order the logging calls according to the implementation
2022-04-12 06:19:42 +12:00
594006cfa0 Fix failing unit tests on Windows (#5142) (#5143)
* Fix failing unit tests on Windows (#5142)

Fix let_env_expressions failing on Windows:
The env expression uses PATH, but on windows Path is used.

Fix correctly_escape_external_arguments, execute_binary_in_string
failing on Windows:
Using cococo now to make sure testresults are platform independent

* Update macros.rs

Co-authored-by: JT <547158+jntrnr@users.noreply.github.com>
2022-04-12 06:18:46 +12:00
57761149f4 Update incorrect crate descriptions (#5159) 2022-04-12 06:17:06 +12:00
521e28dcdc fix #5131 (#5153)
I don't personally agree with this; I'd prefer less magic,
and not expanding _anything_ except `~` as an initial path element

Co-authored-by: nicole mazzuca <mazzucan@outlook.com>
2022-04-11 20:05:39 +12:00
a30930324d Support binary literals with binary format (#5149)
* 4924 Support binary literals with binary format

* 4924 Support automatic padding for binary literals
2022-04-11 19:58:57 +12:00
625e807a35 Support unbinding a particular key event (#5152)
To remove a default keybinding for a particular edit mode, set the `event: null`:

e.g. to disable screen clearing with Ctrl-L

```
let $config = {keybindings: [{
        modifier: control
        keycode: char_l
        mode: [emacs, vi_normal, vi_insert]
        event: null
      } ]}

```
2022-04-10 23:54:09 +02:00
d18f34daa4 Allow overriding of menu keybindings (#5148)
Keybindings that were attached to menus like `Ctrl-x` or `Ctrl-q` could not be replaced with custom bindings
2022-04-10 22:48:55 +02:00
JT
4fd73ef54a Allows aliases in use lists (#5150) 2022-04-11 07:37:22 +12:00
58f395989a Remove unused dependencies (#5145)
* Remove unused packages from base Cargo.toml

* Remove unused crossterm_winapi from nu-cli

* Remove unused dependencies from nu-system

* Remove unused dependencies from nu-test-support
2022-04-10 09:14:55 +12:00
791e8a0e59 enable ls to output datetime in local time vs utc (#5141)
* enable `ls` to output datetime in local time vs utc

* clippy
2022-04-09 11:39:41 -05:00
JT
14066ccc30 Fix known externals, fix operator spans (#5140) 2022-04-09 17:17:48 +12:00
683b912263 Track call arguments in a single list (#5125)
* Initial implementation of ordered call args

* Run cargo fmt

* Fix some clippy lints

* Add positional len and nth

* Cargo fmt

* Remove more old nth calls

* Good ole rustfmt

* Add named len

Co-authored-by: Hristo Filaretov <h.filaretov@protonmail.com>
2022-04-09 14:55:02 +12:00
3bac480ca0 rename menu/fix type-o (#5139) 2022-04-08 20:22:33 -05:00
JT
97eb8492a3 Improve $in handling (#5137)
* Simplify in logic

* Add tests

* more tests, and fixes
2022-04-09 09:41:05 +12:00
JT
0892a16a3d Let 'each' also send input to block (#5136) 2022-04-09 07:57:43 +12:00
JT
0b85938415 Soften the block arity checking (#5135) 2022-04-09 07:57:27 +12:00
aaec840b91 doc change from engine-q to nushell (#5134) 2022-04-08 10:29:21 -07:00
74d0f19291 added ability to opt in to normal string replacement in replace cmd (#5133)
* added ability to opt in to normal string replacement in `replace` cmd

* type-o
2022-04-08 12:23:16 -05:00
JT
7ce570e52c Update LICENSE
this isn't our crate originally, we adapted it
2022-04-08 21:53:29 +12:00
JT
3a0eded0b8 Delete LICENSE
this is dual-licensed, there can't be just one LICENSE file
2022-04-08 21:51:25 +12:00
JT
5afd45414e Revert "nu-cli/completions: cache layer for fetching (#5114)" (#5132)
This reverts commit e86c1b118e.
2022-04-08 21:48:27 +12:00
6ed033737d Include license text in all crates (#5094)
* Include license text in all crates

Three crates already have license texts, so I'm keeping them, but
symlinking the `LICENSE` from the top level to the rest of the crate
directories. This works as long as `cargo publish` is done on a Unix-y
system and not Windows.

Also bump the copyright year to end in 2022.

Signed-off-by: Michel Alexandre Salim <salimma@fedoraproject.org>

* Replace symlinks

Co-authored-by: sholderbach <sholderbach@users.noreply.github.com>
2022-04-08 10:47:13 +02:00
d38a3a8b4e Fix command descriptions+examples (#5129)
* Fix exit usage

* Move dfr as-date* format examples to extra_usage

* Update command usage and examples

* More docs on `str trim`

Co-authored-by: sholderbach <sholderbach@users.noreply.github.com>
2022-04-08 10:30:49 +02:00
6b4cb8b0e0 short descriptions (#5130) 2022-04-08 07:57:39 +01:00
48fa25fd42 nu-cli/completions: removed default filter for command (#5126) 2022-04-07 18:45:04 -05:00
bdfad6b1de add keep deprecated commands (#5124) 2022-04-08 10:10:46 +12:00
JT
4f974efeba Move 'keep' to 'take' (#5123) 2022-04-08 08:49:28 +12:00
e86c1b118e nu-cli/completions: cache layer for fetching (#5114) 2022-04-08 07:36:16 +12:00
5e177fe8e7 nu-cli/completions: fix file completions filtering (#5122) 2022-04-08 07:31:56 +12:00
4129f15eb9 update str find-replace to str replace (#5120) 2022-04-07 08:41:09 -05:00
690ec9abfa Implement rest of touch flags (#5119)
* Add timestamp flag to `touch` command

* Add modify flag to `touch` command

* Add date flag to `touch` command

* Remove unnecessary `touch` test and fix tests setups

* Change `touch` flags descriptions

* Update `touch` example

* Add reference flag to `touch` command

* Add access flag to `touch` command

* Add no-create flag to `touch` command

* Replace `unwrap` with `expect`
2022-04-07 06:44:05 -05:00
b2c52b51b7 Change string contains operators to regex (#5117) 2022-04-07 18:23:14 +12:00
JT
888369022f Add datetime to math-like (#5118)
* Add datetime to math-like

* add test
2022-04-07 18:02:28 +12:00
JT
4409185e1b Improve describe to be more accurate (#5116) 2022-04-07 16:34:09 +12:00
JT
ef1934a7ee Remove external name exceptions (#5115) 2022-04-07 14:01:31 +12:00
JT
591fb4bd36 Add unary not (#5111) 2022-04-07 07:10:25 +12:00
12d3e4e424 Add env.nu file for environment config (#5099)
* Add env.nu file for environment config

* Add missing flag

* Add $nu.env-path variable

Prints `env.nu` path

* Add example of adding entries to PATH
2022-04-07 05:11:51 +12:00
c3bed1352a nu-cli/completions: prioritize non hidden folders (#5108) 2022-04-06 16:56:43 +01:00
3ceb39c82c use arc to avoid cloning entire engine for menus (#5104)
* use arc to avoid cloning entire engine for menus

* remove complete import path

* remove stack clone

* reference in completer
2022-04-06 13:25:02 +01:00
13869e7d52 nu-cli: refactor completions (#5102) 2022-04-06 19:58:55 +12:00
JT
121a4f06fb Load plugins for scripts and commands, too (#5105) 2022-04-06 19:45:26 +12:00
d0e636ae7a Trim newline from input results (#5097) 2022-04-05 12:52:09 -05:00
6e7e2dbb97 enables find to search records with regex (#5100)
* enables find to search records with regex

* clippy
2022-04-05 12:26:11 -05:00
d64cf1687e Fix Format for non-basic data types (#5095) 2022-04-05 07:45:38 -05:00
657b631fdc Add search terms to many commands (#5096) 2022-04-05 07:01:21 -05:00
fa6ed7a40b allow record as text style (#5092) 2022-04-04 22:36:48 +01:00
80f21d37e0 Update reedline to mut Completer API 2022-04-04 23:35:31 +02:00
e2cf4cc7d6 new glob command (#5087) 2022-04-05 08:45:01 +12:00
JT
abe028f930 Add raw strings, use raw strings for env (#5090) 2022-04-05 08:42:26 +12:00
ef1cf7e634 feature: Add some context to completions (#5078)
* send current line and position

* copy current line

* fix error

* deleted test completion
2022-04-05 06:31:40 +12:00
1e4b33b9c6 Add quiet and feedback to mv command (#5073)
* Add quiet and feedback to mv command

* replaced filter and map with filter_map
2022-04-05 06:30:51 +12:00
4654f03d31 update shadow-rs, update git2, remove indexmap from version (#5086) 2022-04-04 09:59:59 -05:00
608b6f3634 Generic menus (#5085)
* updated to reedline generic menus

* help menu with examples

* generic menus in the engine

* description menu template

* list of menus in config

* default value for menu

* menu from block

* generic menus examples

* change to reedline git path

* cargo fmt

* menu name typo

* remove commas from default file

* added error message
2022-04-04 15:54:48 +01:00
a86e6ce89b Set LAST_EXIT_CODE on parse error (#5084) 2022-04-04 06:11:27 -05:00
c4cfbaec2d Fix Python plugin (missing search terms) (#5083)
* Add search_terms to Python plugin

* Clean up Python plugin comments
2022-04-03 20:00:53 -05:00
d40109f210 Tweak append+prepend help (#5080) 2022-04-03 17:50:22 -05:00
20be8a4987 Fix for search terms in help --find (#5081)
* Fix search terms in help --find

* Update help --find description
2022-04-03 17:37:22 -05:00
d6dd4078b1 Remove enqine-q from issue template (#5075)
The notice is not necessary anymore since engine-q is merged now
2022-04-03 06:52:42 -05:00
JT
6649da3f5d Add support for single value row conditions (#5072) 2022-04-03 10:41:36 +12:00
62901573d0 update the readme in the docs folder (#5065)
* update docs/Readme

* tweak readme
2022-04-01 13:48:09 -07:00
JT
80c9888f82 Add command descriptions to completions (#5063) 2022-04-02 08:18:11 +13:00
19c3570cf9 Allow open to work with 'from ...' block commands (#5049)
* Remove panic from BlockCommands run function

Instead of panicing, the run method now returns an error to prevent
nushell from unexpected termination.

* Add ability to open command to run with blocks

The open command tries to parse the content of the file
if there is a command called 'from (file ending)'.  This works
fine if the command was 'built in' because the run method doesn't
fail in this case.  It did fail on a BlockCommand, though.

This change will first probe if the command contains a block and
evaluate it, if this is the case.  If there is no block, it will run
the command the same way as before.

* Add test open files with BlockCommands

* Update open.rs

* Adjust file type on open with BlockCommand parser

Co-authored-by: JT <547158+jntrnr@users.noreply.github.com>
2022-04-02 07:52:32 +13:00
2cb815b7b4 Add starts with operator (#5061)
* add starts_with operator

* added a test
2022-04-01 13:35:46 -05:00
a088081695 update reedline (#5062) 2022-04-01 19:22:40 +01:00
JT
4bb95a880f let a simple last be a single value (#5060) 2022-04-01 23:12:31 +13:00
JT
9beecff736 Add 'date to-record' (#5058) 2022-04-01 21:09:30 +13:00
JT
fa0400c3f2 Fix sort signature (#5055) 2022-04-01 13:03:07 +13:00
JT
1d2d31580b Allow strings for prompt env vars (#5052) 2022-04-01 12:00:50 +13:00
JT
834f993547 Sort command (#5054) 2022-04-01 11:43:16 +13:00
2193910579 Update reedline to new constructor API (#5051) 2022-04-01 11:16:28 +13:00
f692da487e Fix load-env unsupported input error message (#5039) 2022-04-01 10:59:04 +13:00
0986c61a5d Lift line editor construction out of loop (#5041)
Enables the use of some features on reedline

- Keeping the line when clearing the screen with `Ctrl-L`
- Using the internal cut buffer between lines
- Submitting external commands via keybinding and keeping the line

Additional effect:

Keep the history around and do basic syncs (performance improvement
minimal as session changes have to be read and written)

Additional change:

Give the option to defer writing/rereading the history file to the
closing of the session ($config.sync_history_on_enter)
2022-03-31 23:25:48 +02:00
msp
6a6471b04b feat: add --suppress-output (-s) to input command (#5017)
* feat: add --suppress-output (-s) to input command

* Don't handle cursor position since existing impl doesn't

* Handle all raw mode outcomes
2022-03-31 14:20:31 -05:00
05f7d7d38b finish hooking up completion descriptions (#5047) 2022-03-31 11:13:16 -05:00
d89ad4fafd Add record, list, and table to signature types (#5040) 2022-03-31 11:11:03 +03:00
385bc40627 evaluate indicators as commands (#5026)
* evaluate indicators are commands

* default strings in config

* default multiline

* removed build string command
2022-03-31 06:22:55 +01:00
e2d24c5956 Fix which-support feature (#5038) 2022-03-30 13:37:31 -05:00
82633e2df7 update nu-glob README (#5037) 2022-03-30 10:44:23 -07:00
31a4fc41eb Fix env var shorthand when value contains = (#5022) 2022-03-30 09:56:55 +13:00
79182db587 Clean up which/which-support Cargo feature (#5019)
* Rename "which" feature to "which-support"

* Ignore currently broken environment tests
2022-03-29 06:10:43 -05:00
a2872b4ccc Strip '+ ' decoration in git branch list (#5016)
- '+' is the prefix for the current branch in some worktree

Closes #5014
2022-03-28 16:07:55 -05:00
JT
0afa18ac4a Use real stack during custom completion (#5010) 2022-03-29 06:49:41 +13:00
1aef3a730a fix: pass metadata to more filter commands for ls_colors (#5009)
* fix(filters): pass metadata for select

* fix(filters): pass metadata for group, window

* fix(filters): pass metadata for each, every

* fix(filters): pass metadata for collect, compact, flatten

* fix(filters): pass metadata for get

* fix: get rid of necessary `.clone()``

* style: rename closure call to be consistent w/ others

* fix(filters): pass metadata for par-each, prepend

* fix(filters): pass metadata for range

* fix(filters): pass metadata for reject

* fix(filters): pass metadata for more commands

* style: run cargo fmt
2022-03-28 06:43:09 -05:00
e934062542 corrects menu selection (#5004) 2022-03-28 20:43:27 +13:00
JT
2e3b74f1b2 Fix for loop ctrlc not terminating (#5003) 2022-03-28 19:13:43 +13:00
JT
a87f53072a See if levenshtein sorting feels goofor completions (#5001) 2022-03-28 13:31:31 +13:00
047081fa72 Add a README about changing capnp schema (#4998)
* Add a README about changing schema

* Add example PRs
2022-03-27 19:02:19 -05:00
5586d4a0a0 did_you_mean: case-insensitive edit distance, tests (#4999) 2022-03-27 17:11:56 -05:00
JT
911fba8a8a Help menu improvements (#4997)
* Help menu improvements

* default config
2022-03-27 15:21:40 -05:00
2873e943b3 Add search terms to Command and Signature (#4980)
* Add search terms to command

* Rename Signature desc to usage

To be named uniformly with extra_usage

* Throw in foldl search term for reduce

* Add missing usage to post

* Add search terms to signature

* Try to add capnp Signature serialization
2022-03-27 22:25:30 +03:00
0c9dd6a29a Update README.md for Rust 1.59 (#4995)
Since `Cargo.toml` now specifies `strip = ...`, and `strip` is only available in cargo >=1.59, specify that at least this version is required.
2022-03-28 07:28:04 +13:00
a4410fef40 Help menu (#4992)
* nu-completer with suggestions

* help menu with scrolling

* updates description rows based on space

* configuration for help menu

* update nu-ansi-term

* corrected test for update cells

* changed keybinding
2022-03-27 14:01:04 +01:00
0011f4df56 Check same-string-different-case in did_you_mean (#4991) 2022-03-27 19:21:39 +13:00
ee5064abed Nu ansi term update (#4988)
* WIP: Testing 0.45.1 nu-ansi-term with the new Default colors

* point reedline to git in cargo.toml
2022-03-27 16:57:31 +13:00
JT
82e3bb0f38 Bump nushell to 0.60.1 (#4987) 2022-03-27 16:18:47 +13:00
319930a1b9 Add streaming support to save for ExternalStream data (#4985)
* Add streaming support to save for ExternalStream data

Prior to this change, save would collect data from an ExternalStream (data
originating from externals) consuming memory for the full amount of data piped
to it,

This change adds streaming support for ExternalStream allowing saving of
arbitrarily large files and bounding memory usage.

* Remove broken save test

This test passes but not for the right reasons, since this test was
written filename has become a required parameter.  The parser outputs
an error but the test still passes as is checking the original un-mutated
file assuming save has re-written the contents.

This change removes the test.

```
running 1 test
=== stderr
Error: nu::parser::missing_positional (https://docs.rs/nu-parser/0.60.0/nu-parser/enum.ParseError.html#variant.MissingPositional)

  × Missing required positional argument.
   ╭─[source:1:1]
 1 │ open save_test_1/cargo_sample.toml | save
   ·                                          ▲
   ·                                          ╰── missing filename
   ╰────
  help: Usage: save {flags} <filename>

test commands::save::figures_out_intelligently_where_to_write_out_with_metadata ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 515 filtered out; finished in 0.10s
```
2022-03-27 15:39:27 +13:00
a64e0956cd Support binary data to stdin of run-external (#4984)
* Add test for passing binary data through externals

This change adds an ignored test to confirm that binary data is passed
correctly between externals to be enabled in a later commit along with
the fix.

To assist in platform agnostic testing of binary data a couple of
additional testbins were added to allow testing on `Value::Binary` inside
`ExternalStream`.

* Support binary data to stdin of run-external

Prior to this change, any pipeline producing binary data (not detected
as string) then feed into an external would be ignored due to
run-external only supporting `Value::String` on stdin.

This change adds binary stdin support for externals allowing something
like this for example:

  〉^cat /dev/urandom | ^head -c 1MiB | ^pv -b | ignore
  1.00MiB

This would previously output `0.00 B [0.00 B/s]` due to the data not
being pushed to stdin at each stage.
2022-03-27 15:35:59 +13:00
91e17d2f9f Limit mem usage + back-pressure via bounded channels (#4986)
Prior to this change, a pipeline of externals would result in high memory
usage if any of the producers in the chain, produced data faster than
the consumers.

For example a pipeline:

  > fast-producer | slow-consumer

Would cause a build up of `Value::{String,Binary}`'s in the mpsc channels
between each command as values are added to the channels faster than they
are consumed, eventually OOM'ing depnding on system resources, the volume
of data and speed diff. between fast v's slow.

This change replaces the unbounded channels with bounded channels
to limit the number of values that can build up and providing
back-pressure to limit ram usage.
2022-03-27 15:34:34 +13:00
56a546e73d fix ls when file is a socket on mac (#4983) 2022-03-26 21:26:39 -05:00
JT
cf88c8eef3 Improve escaping in string interpolation (#4982) 2022-03-27 12:52:09 +13:00
3484e0defd Add parser keyword note to help and $nu.scope (#4978) 2022-03-26 21:22:45 +02:00
79e4d35f01 Remove is_private from $nu.scope.commands (#4979) 2022-03-26 21:22:35 +02:00
71dd857926 Termux/Android target support for v0.60.0 (#4956)
* Add android as target os for procfs-based ps

* Turn off code for dealing with trash on platforms which are known to not support a standard trash protocol

* Update lib.rs

* Update lib.rs

Co-authored-by: JT <547158+jntrnr@users.noreply.github.com>
2022-03-27 07:21:19 +13:00
7a789d68a2 Don't include trailing separator when expanding tilde (#4974)
* Fix path when expanding tilde

Expanding tilde with no other relative paths would result in:
`$HOME/` instead of `$HOME`. This occurs when users run `cd` with
no extra arguments. In that case, the user's PWD would include the
trailing separator. This does not happen when explicitly passing
a value, such as `cd ~`, because in that case, the path would be
canonicalized.

This happens because std::path::PathBuf::push always adds a separator,
even if adding an empty path, which is what happens when `cd` is
invoked.

* Add test

* Fix test on Windows

Co-authored-by: Hristo Filaretov <h.filaretov@protonmail.com>
2022-03-27 06:28:31 +13:00
8a9cc33aac Fix alias import (#4968)
* Fix alias import

Alias importing was registering the alias id as a decl instead of alias.
This caused issues when hiding and then reimporting the alias.

* Clippy format

Co-authored-by: Hristo Filaretov <h.filaretov@protonmail.com>
2022-03-25 17:56:40 -05:00
JT
66087b01e6 Improve the 'use' and 'source' errors (#4966)
* Improve the 'use' and 'source' errors

* Add register
2022-03-26 10:43:46 +13:00
JT
19fa41b114 Fix single quote environment values (#4960)
* Fix single quote external values

* Try to fix windows

* fix test

* fix test
2022-03-26 09:14:48 +13:00
JT
91cd1717e9 Add escapes to 'to nuon' (#4964) 2022-03-26 08:35:37 +13:00
JT
12b85beecc Fix path join on streams (#4959) 2022-03-26 07:46:48 +13:00
2252833917 bump cargo crate sys-locale to the latest version (#4957) 2022-03-25 10:00:35 -07:00
JT
4e9c1067fb Fix 4946 (#4951)
* Fix reject

* test

* clippy
2022-03-25 20:48:01 +13:00
e505e57a7a align all of the serde_json crates to the same version (#4949) 2022-03-25 18:54:49 +13:00
JT
d122827a30 Fix operator precedence parser (#4947) 2022-03-25 16:23:08 +13:00
b007290a4e Fix #4942, and add a table sorting example for sort-by command (#4948)
* Fix #4942, and add a table sorting example for `sort-by` command

* ci skip
2022-03-25 16:22:57 +13:00
7c92791eed add .mailmap to .gitignore 2022-03-24 20:36:52 -05:00
80769b7197 Set the minimum Rust version to 1.59 (#4940)
nushell uses the strip option in two of its profiles in Cargo.toml.
This option is new in Rust 1.59[0], so this commit adjusts Cargo.toml to
mark 1.59 as the minimum supported Rust version[1].

[0] https://blog.rust-lang.org/2022/02/24/Rust-1.59.0.html
[1] https://doc.rust-lang.org/cargo/reference/manifest.html#the-rust-version-field

Signed-off-by: Randy Barlow <randy@electronsweatshop.com>
2022-03-25 08:08:34 +13:00
9b5dff828d bump csv crate to the latest 1.1.6 (#4939) 2022-03-24 12:59:27 -05:00
90013295aa Fix parse_string_strict() to detect unbalanced quotes properly (#4928) 2022-03-25 05:57:03 +13:00
ea7c8c237e CantConvert improvements (#4926)
* CantConvert improvements

* cargo fmt
2022-03-24 07:04:31 -05:00
JT
5d5b02d8dc Don't assume external ls (#4925) 2022-03-24 16:42:41 +13:00
00b67d338d added missing metadata for drop and uniq #4763 (#4908)
* added missing metadata for drop and uniq #4763

* added missing metadata for keep #4763

* added missing metadata for append #4763

* added missing metadata for shuffle #4763
2022-03-24 07:27:01 +13:00
d32e878868 rename export def to export alias (#4912)
copy-n-paste error
2022-03-23 07:53:10 -05:00
41af2e4b30 update link (#4915) 2022-03-23 07:52:49 -05:00
e9f9aab79f chore: Update default register examples (#4904) 2022-03-23 20:41:58 +13:00
e826540037 Pass /D flag to cmd.exe to disable AutoRun (#4903)
* Pass `/D` flag to `cmd.exe` to disable AutoRun

* Pass `/D` flag before `/c`

This avoids running the command '/D <&self.name.item>' in cmd
2022-03-23 19:05:06 +13:00
a435a9924c add ability to execute on demand (#4896) 2022-03-22 21:09:58 -05:00
JT
02ed15b932 Update Cargo.toml 2022-03-23 09:44:24 +13:00
JT
81e269c483 Update Cargo.toml 2022-03-23 09:44:03 +13:00
JT
eceae26b0a Update Cargo.toml 2022-03-23 09:39:03 +13:00
JT
ec5fd62f9f Add licenses (#4893)
* Add licenses

* Add licenses
2022-03-23 09:25:38 +13:00
317 changed files with 8688 additions and 3005 deletions

View File

@ -5,7 +5,7 @@ body:
id: description
attributes:
label: Describe the bug
description: Thank you for your bug report. We are working diligently with our community to integrate our latest code base that we call [engine-q](https://github.com/nushell/engine-q). We would like your help with this by checking to see if this bug report is still needed in engine-q. Thank you for your patience while we ready the next version of nushell.
description: Thank you for your bug report.
validations:
required: true
- type: textarea

View File

@ -5,7 +5,7 @@ body:
id: problem
attributes:
label: Related problem
description: Thank you for your feature request. We are working diligently with our community to integrate our latest code base that we call [engine-q](https://github.com/nushell/engine-q). We would like your help with this by checking to see if this feature request is still needed in engine-q. Thank you for your patience while we ready the next version of nushell.
description: Thank you for your feature request.
placeholder: |
A clear and concise description of what the problem is.
Example: I am trying to do [...] but [...]

View File

@ -1,6 +1,7 @@
name: Submit Nushell package to Windows Package Manager Community Repository
on:
workflow_dispatch:
release:
types: [published]

1
.gitignore vendored
View File

@ -4,6 +4,7 @@
history.txt
tests/fixtures/nuplayground
crates/*/target
.mailmap
# Debian/Ubuntu
debian/.debhelper/

View File

@ -71,5 +71,5 @@ cargo build
- To view verbose logs when developing, enable the `trace` log level.
```shell
cargo build --release --features=extra && cargo run --release --features=extra -- --loglevel trace
cargo build --release --features=extra && cargo run --release --features=extra -- --log-level trace
```

447
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -10,7 +10,8 @@ license = "MIT"
name = "nu"
readme = "README.md"
repository = "https://github.com/nushell/nushell"
version = "0.60.0"
rust-version = "1.59"
version = "0.61.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -33,32 +34,30 @@ members = [
[dependencies]
chrono = "0.4.19"
crossterm = "0.23.0"
crossterm_winapi = "0.9.0"
ctrlc = "3.2.1"
log = "0.4"
miette = "4.1.0"
nu-ansi-term = "0.45.0"
nu-cli = { path="./crates/nu-cli", version = "0.60.0" }
nu-color-config = { path = "./crates/nu-color-config", version = "0.60.0" }
nu-command = { path="./crates/nu-command", version = "0.60.0" }
nu-engine = { path="./crates/nu-engine", version = "0.60.0" }
nu-json = { path="./crates/nu-json", version = "0.60.0" }
nu-parser = { path="./crates/nu-parser", version = "0.60.0" }
nu-path = { path="./crates/nu-path", version = "0.60.0" }
nu-plugin = { path = "./crates/nu-plugin", optional = true, version = "0.60.0" }
nu-pretty-hex = { path = "./crates/nu-pretty-hex", version = "0.60.0" }
nu-protocol = { path = "./crates/nu-protocol", version = "0.60.0" }
nu-system = { path = "./crates/nu-system", version = "0.60.0" }
nu-table = { path = "./crates/nu-table", version = "0.60.0" }
nu-term-grid = { path = "./crates/nu-term-grid", version = "0.60.0" }
nu-utils = { path = "./crates/nu-utils", version = "0.60.0" }
nu-ansi-term = "0.45.1"
nu-cli = { path="./crates/nu-cli", version = "0.61.0" }
nu-color-config = { path = "./crates/nu-color-config", version = "0.61.0" }
nu-command = { path="./crates/nu-command", version = "0.61.0" }
nu-engine = { path="./crates/nu-engine", version = "0.61.0" }
nu-json = { path="./crates/nu-json", version = "0.61.0" }
nu-parser = { path="./crates/nu-parser", version = "0.61.0" }
nu-path = { path="./crates/nu-path", version = "0.61.0" }
nu-plugin = { path = "./crates/nu-plugin", optional = true, version = "0.61.0" }
nu-pretty-hex = { path = "./crates/nu-pretty-hex", version = "0.61.0" }
nu-protocol = { path = "./crates/nu-protocol", version = "0.61.0" }
nu-system = { path = "./crates/nu-system", version = "0.61.0" }
nu-table = { path = "./crates/nu-table", version = "0.61.0" }
nu-term-grid = { path = "./crates/nu-term-grid", version = "0.61.0" }
pretty_env_logger = "0.4.0"
rayon = "1.5.1"
reedline = "0.3.0"
reedline = { version = "0.4.0", features = ["bashisms"]}
is_executable = "1.0.1"
[dev-dependencies]
nu-test-support = { path="./crates/nu-test-support" }
nu-test-support = { path="./crates/nu-test-support", version = "0.61.0" }
tempfile = "3.2.0"
assert_cmd = "2.0.2"
pretty_assertions = "1.0.0"
@ -72,13 +71,13 @@ embed-resource = "1"
[features]
plugin = ["nu-plugin", "nu-cli/plugin", "nu-parser/plugin", "nu-command/plugin", "nu-protocol/plugin", "nu-engine/plugin"]
default = ["plugin", "which", "zip-support", "trash-support"]
default = ["plugin", "which-support", "zip-support", "trash-support"]
stable = ["default"]
extra = ["default", "dataframe"]
wasi = []
# Stable (Default)
which = ["nu-command/which"]
which-support = ["nu-command/which-support"]
zip-support = ["nu-command/zip"]
trash-support = ["nu-command/trash-support"]

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2019 - 2021 The Nushell Project Developers
Copyright (c) 2019 - 2022 The Nushell Project Developers
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@ -43,7 +43,7 @@ You can also find information on more specific topics in our [cookbook](https://
Up-to-date installation instructions can be found in the [installation chapter of the book](https://www.nushell.sh/book/installation.html). **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 **latest stable (1.51 or later)** version of the compiler.
To build Nu, you will need to use the **latest stable (1.59 or later)** version of the compiler.
Required dependencies:
@ -273,6 +273,8 @@ Please submit an issue or PR to be added to this list.
### Integrations
- [zoxide](https://github.com/ajeetdsouza/zoxide)
- [starship](https://github.com/starship/starship)
- [oh-my-posh](https://ohmyposh.dev)
- [Couchbase Shell](https://couchbase.sh)
### Mentions
- [The Python Launcher for Unix](https://github.com/brettcannon/python-launcher#how-do-i-get-a-table-of-python-executables-in-nushell)

View File

@ -1,23 +1,24 @@
[package]
name = "nu-cli"
version = "0.60.0"
authors = ["The Nushell Project Developers"]
description = "CLI-related functionality for Nushell"
edition = "2021"
license = "MIT"
name = "nu-cli"
version = "0.61.0"
[dependencies]
nu-engine = { path = "../nu-engine", version = "0.60.0" }
nu-path = { path = "../nu-path", version = "0.60.0" }
nu-parser = { path = "../nu-parser", version = "0.60.0" }
nu-protocol = { path = "../nu-protocol", version = "0.60.0" }
nu-utils = { path = "../nu-utils", version = "0.60.0" }
nu-ansi-term = "0.45.0"
nu-color-config = { path = "../nu-color-config" }
nu-engine = { path = "../nu-engine", version = "0.61.0" }
nu-path = { path = "../nu-path", version = "0.61.0" }
nu-parser = { path = "../nu-parser", version = "0.61.0" }
nu-protocol = { path = "../nu-protocol", version = "0.61.0" }
nu-utils = { path = "../nu-utils", version = "0.61.0" }
nu-ansi-term = "0.45.1"
nu-color-config = { path = "../nu-color-config", version = "0.61.0" }
crossterm = "0.23.0"
crossterm_winapi = "0.9.0"
miette = { version = "4.1.0", features = ["fancy"] }
miette = { version = "4.4.0", features = ["fancy"] }
thiserror = "1.0.29"
reedline = "0.3.0"
reedline = { version = "0.4.0", features = ["bashisms"]}
log = "0.4"
is_executable = "1.0.1"

21
crates/nu-cli/LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 - 2022 The Nushell Project Developers
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -56,10 +56,6 @@ pub fn evaluate_commands(
}
};
// Make a note of the exceptions we see for externals that look like math expressions
let exceptions = crate::util::external_exceptions(engine_state, stack);
engine_state.external_exceptions = exceptions;
// Merge the delta in case env vars changed in the config
match nu_engine::env::current_dir(engine_state, stack) {
Ok(cwd) => {

View File

@ -1,636 +0,0 @@
use nu_engine::eval_call;
use nu_parser::{flatten_expression, parse, trim_quotes, FlatShape};
use nu_protocol::{
ast::{Call, Expr},
engine::{EngineState, Stack, StateWorkingSet},
PipelineData, Span, Value, CONFIG_VARIABLE_ID,
};
use reedline::Completer;
const SEP: char = std::path::MAIN_SEPARATOR;
pub struct CompletionOptions {
case_sensitive: bool,
positional: bool,
sort: bool,
}
impl Default for CompletionOptions {
fn default() -> Self {
Self {
case_sensitive: true,
positional: true,
sort: true,
}
}
}
#[derive(Clone)]
pub struct NuCompleter {
engine_state: EngineState,
config: Option<Value>,
}
impl NuCompleter {
pub fn new(engine_state: EngineState, config: Option<Value>) -> Self {
Self {
engine_state,
config,
}
}
fn external_command_completion(&self, prefix: &str) -> Vec<String> {
let mut executables = vec![];
let paths = self.engine_state.env_vars.get("PATH");
if let Some(paths) = paths {
if let Ok(paths) = paths.as_list() {
for path in paths {
let path = path.as_string().unwrap_or_default();
if let Ok(mut contents) = std::fs::read_dir(path) {
while let Some(Ok(item)) = contents.next() {
if !executables.contains(
&item
.path()
.file_name()
.map(|x| x.to_string_lossy().to_string())
.unwrap_or_default(),
) && matches!(
item.path()
.file_name()
.map(|x| x.to_string_lossy().starts_with(prefix)),
Some(true)
) && is_executable::is_executable(&item.path())
{
if let Ok(name) = item.file_name().into_string() {
executables.push(name);
}
}
}
}
}
}
}
executables
}
fn complete_variables(
&self,
working_set: &StateWorkingSet,
prefix: &[u8],
span: Span,
offset: usize,
) -> Vec<(reedline::Span, String)> {
let mut output = vec![];
let builtins = ["$nu", "$in", "$config", "$env", "$nothing"];
for builtin in builtins {
if builtin.as_bytes().starts_with(prefix) {
output.push((
reedline::Span {
start: span.start - offset,
end: span.end - offset,
},
builtin.to_string(),
));
}
}
for scope in &working_set.delta.scope {
for v in &scope.vars {
if v.0.starts_with(prefix) {
output.push((
reedline::Span {
start: span.start - offset,
end: span.end - offset,
},
String::from_utf8_lossy(v.0).to_string(),
));
}
}
}
for scope in &self.engine_state.scope {
for v in &scope.vars {
if v.0.starts_with(prefix) {
output.push((
reedline::Span {
start: span.start - offset,
end: span.end - offset,
},
String::from_utf8_lossy(v.0).to_string(),
));
}
}
}
output.dedup();
output
}
fn complete_commands(
&self,
working_set: &StateWorkingSet,
span: Span,
offset: usize,
find_externals: bool,
) -> Vec<(reedline::Span, String)> {
let prefix = working_set.get_span_contents(span);
let results = working_set
.find_commands_by_prefix(prefix)
.into_iter()
.map(move |x| {
(
reedline::Span {
start: span.start - offset,
end: span.end - offset,
},
String::from_utf8_lossy(&x).to_string(),
)
});
let results_aliases =
working_set
.find_aliases_by_prefix(prefix)
.into_iter()
.map(move |x| {
(
reedline::Span {
start: span.start - offset,
end: span.end - offset,
},
String::from_utf8_lossy(&x).to_string(),
)
});
let mut results = results.chain(results_aliases).collect::<Vec<_>>();
let prefix = working_set.get_span_contents(span);
let prefix = String::from_utf8_lossy(prefix).to_string();
if find_externals {
let results_external =
self.external_command_completion(&prefix)
.into_iter()
.map(move |x| {
(
reedline::Span {
start: span.start - offset,
end: span.end - offset,
},
x,
)
});
for external in results_external {
if results.contains(&external) {
results.push((external.0, format!("^{}", external.1)))
} else {
results.push(external)
}
}
results
} else {
results
}
}
fn completion_helper(&self, line: &str, pos: usize) -> Vec<(reedline::Span, String)> {
let mut working_set = StateWorkingSet::new(&self.engine_state);
let offset = working_set.next_span_start();
let mut line = line.to_string();
line.insert(pos, 'a');
let pos = offset + pos;
let (output, _err) = parse(
&mut working_set,
Some("completer"),
line.as_bytes(),
false,
&[],
);
for pipeline in output.pipelines.into_iter() {
for expr in pipeline.expressions {
let flattened: Vec<_> = flatten_expression(&working_set, &expr);
for (flat_idx, flat) in flattened.iter().enumerate() {
if pos >= flat.0.start && pos < flat.0.end {
let new_span = Span {
start: flat.0.start,
end: flat.0.end - 1,
};
let mut prefix = working_set.get_span_contents(flat.0).to_vec();
prefix.remove(pos - flat.0.start);
if prefix.starts_with(b"$") {
let mut output =
self.complete_variables(&working_set, &prefix, new_span, offset);
output.sort_by(|a, b| a.1.cmp(&b.1));
return output;
}
if prefix.starts_with(b"-") {
// this might be a flag, let's see
if let Expr::Call(call) = &expr.expr {
let decl = working_set.get_decl(call.decl_id);
let sig = decl.signature();
let mut output = vec![];
for named in &sig.named {
if let Some(short) = named.short {
let mut named = vec![0; short.len_utf8()];
short.encode_utf8(&mut named);
named.insert(0, b'-');
if named.starts_with(&prefix) {
output.push((
reedline::Span {
start: new_span.start - offset,
end: new_span.end - offset,
},
String::from_utf8_lossy(&named).to_string(),
));
}
}
if named.long.is_empty() {
continue;
}
let mut named = named.long.as_bytes().to_vec();
named.insert(0, b'-');
named.insert(0, b'-');
if named.starts_with(&prefix) {
output.push((
reedline::Span {
start: new_span.start - offset,
end: new_span.end - offset,
},
String::from_utf8_lossy(&named).to_string(),
));
}
}
output.sort_by(|a, b| a.1.cmp(&b.1));
return output;
}
}
match &flat.1 {
FlatShape::Custom(decl_id) => {
//let prefix = working_set.get_span_contents(flat.0).to_vec();
let mut stack = Stack::new();
// Set up our initial config to start from
if let Some(conf) = &self.config {
stack.vars.insert(CONFIG_VARIABLE_ID, conf.clone());
} else {
stack.vars.insert(
CONFIG_VARIABLE_ID,
Value::Record {
cols: vec![],
vals: vec![],
span: Span { start: 0, end: 0 },
},
);
}
let result = eval_call(
&self.engine_state,
&mut stack,
&Call {
decl_id: *decl_id,
head: new_span,
positional: vec![],
named: vec![],
redirect_stdout: true,
redirect_stderr: true,
},
PipelineData::new(new_span),
);
fn map_completions<'a>(
list: impl Iterator<Item = &'a Value>,
new_span: Span,
offset: usize,
) -> Vec<(reedline::Span, String)> {
list.filter_map(move |x| {
let s = x.as_string();
match s {
Ok(s) => Some((
reedline::Span {
start: new_span.start - offset,
end: new_span.end - offset,
},
s,
)),
Err(_) => None,
}
})
.collect()
}
let (completions, options) = match result {
Ok(pd) => {
let value = pd.into_value(new_span);
match &value {
Value::Record { .. } => {
let completions = value
.get_data_by_key("completions")
.and_then(|val| {
val.as_list().ok().map(|it| {
map_completions(
it.iter(),
new_span,
offset,
)
})
})
.unwrap_or_default();
let options = value.get_data_by_key("options");
let options =
if let Some(Value::Record { .. }) = &options {
let options = options.unwrap_or_default();
CompletionOptions {
case_sensitive: options
.get_data_by_key("case_sensitive")
.and_then(|val| val.as_bool().ok())
.unwrap_or(true),
positional: options
.get_data_by_key("positional")
.and_then(|val| val.as_bool().ok())
.unwrap_or(true),
sort: options
.get_data_by_key("sort")
.and_then(|val| val.as_bool().ok())
.unwrap_or(true),
}
} else {
CompletionOptions::default()
};
(completions, options)
}
Value::List { vals, .. } => {
let completions =
map_completions(vals.iter(), new_span, offset);
(completions, CompletionOptions::default())
}
_ => (vec![], CompletionOptions::default()),
}
}
_ => (vec![], CompletionOptions::default()),
};
let mut completions: Vec<(reedline::Span, String)> = completions
.into_iter()
.filter(|it| {
// Minimise clones for new functionality
match (options.case_sensitive, options.positional) {
(true, true) => it.1.as_bytes().starts_with(&prefix),
(true, false) => it.1.contains(
std::str::from_utf8(&prefix).unwrap_or(""),
),
(false, positional) => {
let value = it.1.to_lowercase();
let prefix = std::str::from_utf8(&prefix)
.unwrap_or("")
.to_lowercase();
if positional {
value.starts_with(&prefix)
} else {
value.contains(&prefix)
}
}
}
})
.collect();
if options.sort {
completions.sort_by(|a, b| a.1.cmp(&b.1));
}
return completions;
}
FlatShape::Filepath | FlatShape::GlobPattern => {
let cwd = if let Some(d) = self.engine_state.env_vars.get("PWD") {
match d.as_string() {
Ok(s) => s,
Err(_) => "".to_string(),
}
} else {
"".to_string()
};
let prefix = String::from_utf8_lossy(&prefix).to_string();
let mut output: Vec<_> =
file_path_completion(new_span, &prefix, &cwd)
.into_iter()
.map(move |x| {
(
reedline::Span {
start: x.0.start - offset,
end: x.0.end - offset,
},
x.1,
)
})
.collect();
output.sort_by(|a, b| a.1.cmp(&b.1));
return output;
}
flat_shape => {
let last = flattened
.iter()
.rev()
.skip_while(|x| x.0.end > pos)
.take_while(|x| {
matches!(
x.1,
FlatShape::InternalCall
| FlatShape::External
| FlatShape::ExternalArg
| FlatShape::Literal
| FlatShape::String
)
})
.last();
// The last item here would be the earliest shape that could possible by part of this subcommand
let subcommands = if let Some(last) = last {
self.complete_commands(
&working_set,
Span {
start: last.0.start,
end: pos,
},
offset,
false,
)
} else {
vec![]
};
if !subcommands.is_empty() {
return subcommands;
}
let commands =
if matches!(flat_shape, nu_parser::FlatShape::External)
|| matches!(flat_shape, nu_parser::FlatShape::InternalCall)
|| ((new_span.end - new_span.start) == 0)
{
// we're in a gap or at a command
self.complete_commands(&working_set, new_span, offset, true)
} else {
vec![]
};
let cwd = if let Some(d) = self.engine_state.env_vars.get("PWD") {
match d.as_string() {
Ok(s) => s,
Err(_) => "".to_string(),
}
} else {
"".to_string()
};
let preceding_byte = if new_span.start > offset {
working_set
.get_span_contents(Span {
start: new_span.start - 1,
end: new_span.start,
})
.to_vec()
} else {
vec![]
};
// let prefix = working_set.get_span_contents(flat.0);
let prefix = String::from_utf8_lossy(&prefix).to_string();
let mut output = file_path_completion(new_span, &prefix, &cwd)
.into_iter()
.map(move |x| {
if flat_idx == 0 {
// We're in the command position
if x.1.starts_with('"')
&& !matches!(preceding_byte.get(0), Some(b'^'))
{
let trimmed = trim_quotes(x.1.as_bytes());
let trimmed =
String::from_utf8_lossy(trimmed).to_string();
let expanded =
nu_path::canonicalize_with(trimmed, &cwd);
if let Ok(expanded) = expanded {
if is_executable::is_executable(expanded) {
(x.0, format!("^{}", x.1))
} else {
(x.0, x.1)
}
} else {
(x.0, x.1)
}
} else {
(x.0, x.1)
}
} else {
(x.0, x.1)
}
})
.map(move |x| {
(
reedline::Span {
start: x.0.start - offset,
end: x.0.end - offset,
},
x.1,
)
})
.chain(subcommands.into_iter())
.chain(commands.into_iter())
.collect::<Vec<_>>();
//output.dedup_by(|a, b| a.1 == b.1);
output.sort_by(|a, b| a.1.cmp(&b.1));
return output;
}
}
}
}
}
}
vec![]
}
}
impl Completer for NuCompleter {
fn complete(&self, line: &str, pos: usize) -> Vec<(reedline::Span, String)> {
self.completion_helper(line, pos)
}
}
fn file_path_completion(
span: nu_protocol::Span,
partial: &str,
cwd: &str,
) -> Vec<(nu_protocol::Span, String)> {
use std::path::{is_separator, Path};
let partial = partial.replace('\'', "");
let (base_dir_name, partial) = {
// If partial is only a word we want to search in the current dir
let (base, rest) = partial.rsplit_once(is_separator).unwrap_or((".", &partial));
// On windows, this standardizes paths to use \
let mut base = base.replace(is_separator, &SEP.to_string());
// rsplit_once removes the separator
base.push(SEP);
(base, rest)
};
let base_dir = nu_path::expand_path_with(&base_dir_name, cwd);
// This check is here as base_dir.read_dir() with base_dir == "" will open the current dir
// which we don't want in this case (if we did, base_dir would already be ".")
if base_dir == Path::new("") {
return Vec::new();
}
if let Ok(result) = base_dir.read_dir() {
result
.filter_map(|entry| {
entry.ok().and_then(|entry| {
let mut file_name = entry.file_name().to_string_lossy().into_owned();
if matches(partial, &file_name) {
let mut path = format!("{}{}", base_dir_name, file_name);
if entry.path().is_dir() {
path.push(SEP);
file_name.push(SEP);
}
if path.contains(' ') {
path = format!("\'{}\'", path);
}
Some((span, path))
} else {
None
}
})
})
.collect()
} else {
Vec::new()
}
}
fn matches(partial: &str, from: &str) -> bool {
from.to_ascii_lowercase()
.starts_with(&partial.to_ascii_lowercase())
}

View File

@ -0,0 +1,74 @@
use crate::completions::{CompletionOptions, SortBy};
use nu_protocol::{engine::StateWorkingSet, levenshtein_distance, Span};
use reedline::Suggestion;
// Completer trait represents the three stages of the completion
// fetch, filter and sort
pub trait Completer {
fn fetch(
&mut self,
working_set: &StateWorkingSet,
prefix: Vec<u8>,
span: Span,
offset: usize,
pos: usize,
) -> (Vec<Suggestion>, CompletionOptions);
// Filter results using the completion options
fn filter(
&self,
prefix: Vec<u8>,
items: Vec<Suggestion>,
options: CompletionOptions,
) -> Vec<Suggestion> {
items
.into_iter()
.filter(|it| {
// Minimise clones for new functionality
match (options.case_sensitive, options.positional) {
(true, true) => it.value.as_bytes().starts_with(&prefix),
(true, false) => it
.value
.contains(std::str::from_utf8(&prefix).unwrap_or("")),
(false, positional) => {
let value = it.value.to_lowercase();
let prefix = std::str::from_utf8(&prefix).unwrap_or("").to_lowercase();
if positional {
value.starts_with(&prefix)
} else {
value.contains(&prefix)
}
}
}
})
.collect()
}
// Sort results using the completion options
fn sort(
&self,
items: Vec<Suggestion>,
prefix: Vec<u8>,
options: CompletionOptions,
) -> Vec<Suggestion> {
let prefix_str = String::from_utf8_lossy(&prefix).to_string();
let mut filtered_items = items;
// Sort items
match options.sort_by {
SortBy::LevenshteinDistance => {
filtered_items.sort_by(|a, b| {
let a_distance = levenshtein_distance(&prefix_str, &a.value);
let b_distance = levenshtein_distance(&prefix_str, &b.value);
a_distance.cmp(&b_distance)
});
}
SortBy::Ascending => {
filtered_items.sort_by(|a, b| a.value.cmp(&b.value));
}
SortBy::None => {}
};
filtered_items
}
}

View File

@ -0,0 +1,274 @@
use crate::completions::{
file_completions::file_path_completion, Completer, CompletionOptions, SortBy,
};
use nu_parser::{trim_quotes, FlatShape};
use nu_protocol::{
engine::{EngineState, StateWorkingSet},
Span,
};
use reedline::Suggestion;
use std::sync::Arc;
pub struct CommandCompletion {
engine_state: Arc<EngineState>,
flattened: Vec<(Span, FlatShape)>,
flat_idx: usize,
flat_shape: FlatShape,
}
impl CommandCompletion {
pub fn new(
engine_state: Arc<EngineState>,
_: &StateWorkingSet,
flattened: Vec<(Span, FlatShape)>,
flat_idx: usize,
flat_shape: FlatShape,
) -> Self {
Self {
engine_state,
flattened,
flat_idx,
flat_shape,
}
}
fn external_command_completion(&self, prefix: &str) -> Vec<String> {
let mut executables = vec![];
let paths = self.engine_state.env_vars.get("PATH");
if let Some(paths) = paths {
if let Ok(paths) = paths.as_list() {
for path in paths {
let path = path.as_string().unwrap_or_default();
if let Ok(mut contents) = std::fs::read_dir(path) {
while let Some(Ok(item)) = contents.next() {
if !executables.contains(
&item
.path()
.file_name()
.map(|x| x.to_string_lossy().to_string())
.unwrap_or_default(),
) && matches!(
item.path()
.file_name()
.map(|x| x.to_string_lossy().starts_with(prefix)),
Some(true)
) && is_executable::is_executable(&item.path())
{
if let Ok(name) = item.file_name().into_string() {
executables.push(name);
}
}
}
}
}
}
}
executables
}
fn complete_commands(
&self,
working_set: &StateWorkingSet,
span: Span,
offset: usize,
find_externals: bool,
) -> Vec<Suggestion> {
let prefix = working_set.get_span_contents(span);
let results = working_set
.find_commands_by_prefix(prefix)
.into_iter()
.map(move |x| Suggestion {
value: String::from_utf8_lossy(&x.0).to_string(),
description: x.1,
extra: None,
span: reedline::Span {
start: span.start - offset,
end: span.end - offset,
},
});
let results_aliases =
working_set
.find_aliases_by_prefix(prefix)
.into_iter()
.map(move |x| Suggestion {
value: String::from_utf8_lossy(&x).to_string(),
description: None,
extra: None,
span: reedline::Span {
start: span.start - offset,
end: span.end - offset,
},
});
let mut results = results.chain(results_aliases).collect::<Vec<_>>();
let prefix = working_set.get_span_contents(span);
let prefix = String::from_utf8_lossy(prefix).to_string();
let results = if find_externals {
let results_external =
self.external_command_completion(&prefix)
.into_iter()
.map(move |x| Suggestion {
value: x,
description: None,
extra: None,
span: reedline::Span {
start: span.start - offset,
end: span.end - offset,
},
});
for external in results_external {
if results.contains(&external) {
results.push(Suggestion {
value: format!("^{}", external.value),
description: None,
extra: None,
span: external.span,
})
} else {
results.push(external)
}
}
results
} else {
results
};
results
}
}
impl Completer for CommandCompletion {
fn fetch(
&mut self,
working_set: &StateWorkingSet,
prefix: Vec<u8>,
span: Span,
offset: usize,
pos: usize,
) -> (Vec<Suggestion>, CompletionOptions) {
let last = self
.flattened
.iter()
.rev()
.skip_while(|x| x.0.end > pos)
.take_while(|x| {
matches!(
x.1,
FlatShape::InternalCall
| FlatShape::External
| FlatShape::ExternalArg
| FlatShape::Literal
| FlatShape::String
)
})
.last();
// Options
let options = CompletionOptions::new(true, true, SortBy::LevenshteinDistance);
// The last item here would be the earliest shape that could possible by part of this subcommand
let subcommands = if let Some(last) = last {
self.complete_commands(
working_set,
Span {
start: last.0.start,
end: pos,
},
offset,
false,
)
} else {
vec![]
};
if !subcommands.is_empty() {
return (subcommands, options);
}
let commands = if matches!(self.flat_shape, nu_parser::FlatShape::External)
|| matches!(self.flat_shape, nu_parser::FlatShape::InternalCall)
|| ((span.end - span.start) == 0)
{
// we're in a gap or at a command
self.complete_commands(working_set, span, offset, true)
} else {
vec![]
};
let cwd = if let Some(d) = self.engine_state.env_vars.get("PWD") {
match d.as_string() {
Ok(s) => s,
Err(_) => "".to_string(),
}
} else {
"".to_string()
};
let preceding_byte = if span.start > offset {
working_set
.get_span_contents(Span {
start: span.start - 1,
end: span.start,
})
.to_vec()
} else {
vec![]
};
// let prefix = working_set.get_span_contents(flat.0);
let prefix = String::from_utf8_lossy(&prefix).to_string();
let output = file_path_completion(span, &prefix, &cwd)
.into_iter()
.map(move |x| {
if self.flat_idx == 0 {
// We're in the command position
if x.1.starts_with('"') && !matches!(preceding_byte.get(0), Some(b'^')) {
let trimmed = trim_quotes(x.1.as_bytes());
let trimmed = String::from_utf8_lossy(trimmed).to_string();
let expanded = nu_path::canonicalize_with(trimmed, &cwd);
if let Ok(expanded) = expanded {
if is_executable::is_executable(expanded) {
(x.0, format!("^{}", x.1))
} else {
(x.0, x.1)
}
} else {
(x.0, x.1)
}
} else {
(x.0, x.1)
}
} else {
(x.0, x.1)
}
})
.map(move |x| Suggestion {
value: x.1,
description: None,
extra: None,
span: reedline::Span {
start: x.0.start - offset,
end: x.0.end - offset,
},
})
.chain(subcommands.into_iter())
.chain(commands.into_iter())
.collect::<Vec<_>>();
(output, options)
}
// Replace base filter with no filter once all the results are already based in the current path
fn filter(&self, _: Vec<u8>, items: Vec<Suggestion>, _: CompletionOptions) -> Vec<Suggestion> {
items
}
}

View File

@ -0,0 +1,174 @@
use crate::completions::{
CommandCompletion, Completer, CustomCompletion, FileCompletion, FlagCompletion,
VariableCompletion,
};
use nu_parser::{flatten_expression, parse, FlatShape};
use nu_protocol::{
engine::{EngineState, Stack, StateWorkingSet},
Span, Value,
};
use reedline::{Completer as ReedlineCompleter, Suggestion};
use std::sync::Arc;
#[derive(Clone)]
pub struct NuCompleter {
engine_state: Arc<EngineState>,
stack: Stack,
config: Option<Value>,
}
impl NuCompleter {
pub fn new(engine_state: Arc<EngineState>, stack: Stack, config: Option<Value>) -> Self {
Self {
engine_state,
stack,
config,
}
}
// Process the completion for a given completer
fn process_completion<T: Completer>(
&self,
completer: &mut T,
working_set: &StateWorkingSet,
prefix: Vec<u8>,
new_span: Span,
offset: usize,
pos: usize,
) -> Vec<Suggestion> {
// Fetch
let (mut suggestions, options) =
completer.fetch(working_set, prefix.clone(), new_span, offset, pos);
// Filter
suggestions = completer.filter(prefix.clone(), suggestions, options.clone());
// Sort
suggestions = completer.sort(suggestions, prefix, options);
suggestions
}
fn completion_helper(&mut self, line: &str, pos: usize) -> Vec<Suggestion> {
let mut working_set = StateWorkingSet::new(&self.engine_state);
let offset = working_set.next_span_start();
let mut line = line.to_string();
line.insert(pos, 'a');
let pos = offset + pos;
let (output, _err) = parse(
&mut working_set,
Some("completer"),
line.as_bytes(),
false,
&[],
);
for pipeline in output.pipelines.into_iter() {
for expr in pipeline.expressions {
let flattened: Vec<_> = flatten_expression(&working_set, &expr);
for (flat_idx, flat) in flattened.iter().enumerate() {
if pos >= flat.0.start && pos < flat.0.end {
// Create a new span
let new_span = Span {
start: flat.0.start,
end: flat.0.end - 1,
};
// Parses the prefix
let mut prefix = working_set.get_span_contents(flat.0).to_vec();
prefix.remove(pos - flat.0.start);
// Variables completion
if prefix.starts_with(b"$") {
let mut completer = VariableCompletion::new(self.engine_state.clone());
return self.process_completion(
&mut completer,
&working_set,
prefix,
new_span,
offset,
pos,
);
}
// Flags completion
if prefix.starts_with(b"-") {
let mut completer = FlagCompletion::new(expr);
return self.process_completion(
&mut completer,
&working_set,
prefix,
new_span,
offset,
pos,
);
}
// Match other types
match &flat.1 {
FlatShape::Custom(decl_id) => {
let mut completer = CustomCompletion::new(
self.engine_state.clone(),
self.stack.clone(),
self.config.clone(),
*decl_id,
line,
);
return self.process_completion(
&mut completer,
&working_set,
prefix,
new_span,
offset,
pos,
);
}
FlatShape::Filepath | FlatShape::GlobPattern => {
let mut completer = FileCompletion::new(self.engine_state.clone());
return self.process_completion(
&mut completer,
&working_set,
prefix,
new_span,
offset,
pos,
);
}
flat_shape => {
let mut completer = CommandCompletion::new(
self.engine_state.clone(),
&working_set,
flattened.clone(),
flat_idx,
flat_shape.clone(),
);
return self.process_completion(
&mut completer,
&working_set,
prefix,
new_span,
offset,
pos,
);
}
};
}
}
}
}
return vec![];
}
}
impl ReedlineCompleter for NuCompleter {
fn complete(&mut self, line: &str, pos: usize) -> Vec<Suggestion> {
self.completion_helper(line, pos)
}
}

View File

@ -0,0 +1,33 @@
#[derive(Clone)]
pub enum SortBy {
LevenshteinDistance,
Ascending,
None,
}
#[derive(Clone)]
pub struct CompletionOptions {
pub case_sensitive: bool,
pub positional: bool,
pub sort_by: SortBy,
}
impl CompletionOptions {
pub fn new(case_sensitive: bool, positional: bool, sort_by: SortBy) -> Self {
Self {
case_sensitive,
positional,
sort_by,
}
}
}
impl Default for CompletionOptions {
fn default() -> Self {
Self {
case_sensitive: true,
positional: true,
sort_by: SortBy::Ascending,
}
}
}

View File

@ -0,0 +1,171 @@
use crate::completions::{Completer, CompletionOptions, SortBy};
use nu_engine::eval_call;
use nu_protocol::{
ast::{Argument, Call, Expr, Expression},
engine::{EngineState, Stack, StateWorkingSet},
PipelineData, Span, Type, Value, CONFIG_VARIABLE_ID,
};
use reedline::Suggestion;
use std::sync::Arc;
pub struct CustomCompletion {
engine_state: Arc<EngineState>,
stack: Stack,
config: Option<Value>,
decl_id: usize,
line: String,
}
impl CustomCompletion {
pub fn new(
engine_state: Arc<EngineState>,
stack: Stack,
config: Option<Value>,
decl_id: usize,
line: String,
) -> Self {
Self {
engine_state,
stack,
config,
decl_id,
line,
}
}
fn map_completions<'a>(
&self,
list: impl Iterator<Item = &'a Value>,
span: Span,
offset: usize,
) -> Vec<Suggestion> {
list.filter_map(move |x| {
let s = x.as_string();
match s {
Ok(s) => Some(Suggestion {
value: s,
description: None,
extra: None,
span: reedline::Span {
start: span.start - offset,
end: span.end - offset,
},
}),
Err(_) => None,
}
})
.collect()
}
}
impl Completer for CustomCompletion {
fn fetch(
&mut self,
_: &StateWorkingSet,
_: Vec<u8>,
span: Span,
offset: usize,
pos: usize,
) -> (Vec<Suggestion>, CompletionOptions) {
// Line position
let line_pos = pos - offset;
// Set up our initial config to start from
if let Some(conf) = &self.config {
self.stack.vars.insert(CONFIG_VARIABLE_ID, conf.clone());
} else {
self.stack.vars.insert(
CONFIG_VARIABLE_ID,
Value::Record {
cols: vec![],
vals: vec![],
span: Span { start: 0, end: 0 },
},
);
}
// Call custom declaration
let result = eval_call(
&self.engine_state,
&mut self.stack,
&Call {
decl_id: self.decl_id,
head: span,
arguments: vec![
Argument::Positional(Expression {
span: Span { start: 0, end: 0 },
ty: Type::String,
expr: Expr::String(self.line.clone()),
custom_completion: None,
}),
Argument::Positional(Expression {
span: Span { start: 0, end: 0 },
ty: Type::Int,
expr: Expr::Int(line_pos as i64),
custom_completion: None,
}),
],
redirect_stdout: true,
redirect_stderr: true,
},
PipelineData::new(span),
);
// Parse result
let (suggestions, options) = match result {
Ok(pd) => {
let value = pd.into_value(span);
match &value {
Value::Record { .. } => {
let completions = value
.get_data_by_key("completions")
.and_then(|val| {
val.as_list()
.ok()
.map(|it| self.map_completions(it.iter(), span, offset))
})
.unwrap_or_default();
let options = value.get_data_by_key("options");
let options = if let Some(Value::Record { .. }) = &options {
let options = options.unwrap_or_default();
let should_sort = options
.get_data_by_key("sort")
.and_then(|val| val.as_bool().ok())
.unwrap_or(false);
CompletionOptions {
case_sensitive: options
.get_data_by_key("case_sensitive")
.and_then(|val| val.as_bool().ok())
.unwrap_or(true),
positional: options
.get_data_by_key("positional")
.and_then(|val| val.as_bool().ok())
.unwrap_or(true),
sort_by: if should_sort {
SortBy::Ascending
} else {
SortBy::None
},
}
} else {
CompletionOptions::default()
};
(completions, options)
}
Value::List { vals, .. } => {
let completions = self.map_completions(vals.iter(), span, offset);
(completions, CompletionOptions::default())
}
_ => (vec![], CompletionOptions::default()),
}
}
_ => (vec![], CompletionOptions::default()),
};
(suggestions, options)
}
}

View File

@ -0,0 +1,163 @@
use crate::completions::{Completer, CompletionOptions};
use nu_protocol::{
engine::{EngineState, StateWorkingSet},
levenshtein_distance, Span,
};
use reedline::Suggestion;
use std::path::{is_separator, Path};
use std::sync::Arc;
const SEP: char = std::path::MAIN_SEPARATOR;
#[derive(Clone)]
pub struct FileCompletion {
engine_state: Arc<EngineState>,
}
impl FileCompletion {
pub fn new(engine_state: Arc<EngineState>) -> Self {
Self { engine_state }
}
}
impl Completer for FileCompletion {
fn fetch(
&mut self,
_: &StateWorkingSet,
prefix: Vec<u8>,
span: Span,
offset: usize,
_: usize,
) -> (Vec<Suggestion>, CompletionOptions) {
let cwd = if let Some(d) = self.engine_state.env_vars.get("PWD") {
match d.as_string() {
Ok(s) => s,
Err(_) => "".to_string(),
}
} else {
"".to_string()
};
let prefix = String::from_utf8_lossy(&prefix).to_string();
let output: Vec<_> = file_path_completion(span, &prefix, &cwd)
.into_iter()
.map(move |x| Suggestion {
value: x.1,
description: None,
extra: None,
span: reedline::Span {
start: x.0.start - offset,
end: x.0.end - offset,
},
})
.collect();
// Options
let options = CompletionOptions::default();
(output, options)
}
// Sort results prioritizing the non hidden folders
fn sort(
&self,
items: Vec<Suggestion>,
prefix: Vec<u8>,
_: CompletionOptions, // Ignore the given options, once it's a custom sorting
) -> Vec<Suggestion> {
let prefix_str = String::from_utf8_lossy(&prefix).to_string();
// Sort items
let mut sorted_items = items;
sorted_items.sort_by(|a, b| {
let a_distance = levenshtein_distance(&prefix_str, &a.value);
let b_distance = levenshtein_distance(&prefix_str, &b.value);
a_distance.cmp(&b_distance)
});
// Separate the results between hidden and non hidden
let mut hidden: Vec<Suggestion> = vec![];
let mut non_hidden: Vec<Suggestion> = vec![];
for item in sorted_items.into_iter() {
let item_path = Path::new(&item.value);
if let Some(value) = item_path.file_name() {
if let Some(value) = value.to_str() {
if value.starts_with('.') {
hidden.push(item);
} else {
non_hidden.push(item);
}
}
}
}
// Append the hidden folders to the non hidden vec to avoid creating a new vec
non_hidden.append(&mut hidden);
non_hidden
}
// Replace base filter with no filter once all the results are already based in the current path
fn filter(&self, _: Vec<u8>, items: Vec<Suggestion>, _: CompletionOptions) -> Vec<Suggestion> {
items
}
}
pub fn file_path_completion(
span: nu_protocol::Span,
partial: &str,
cwd: &str,
) -> Vec<(nu_protocol::Span, String)> {
let partial = partial.replace('\'', "");
let (base_dir_name, partial) = {
// If partial is only a word we want to search in the current dir
let (base, rest) = partial.rsplit_once(is_separator).unwrap_or((".", &partial));
// On windows, this standardizes paths to use \
let mut base = base.replace(is_separator, &SEP.to_string());
// rsplit_once removes the separator
base.push(SEP);
(base, rest)
};
let base_dir = nu_path::expand_path_with(&base_dir_name, cwd);
// This check is here as base_dir.read_dir() with base_dir == "" will open the current dir
// which we don't want in this case (if we did, base_dir would already be ".")
if base_dir == Path::new("") {
return Vec::new();
}
if let Ok(result) = base_dir.read_dir() {
return result
.filter_map(|entry| {
entry.ok().and_then(|entry| {
let mut file_name = entry.file_name().to_string_lossy().into_owned();
if matches(partial, &file_name) {
let mut path = format!("{}{}", base_dir_name, file_name);
if entry.path().is_dir() {
path.push(SEP);
file_name.push(SEP);
}
if path.contains(' ') {
path = format!("\'{}\'", path);
}
Some((span, path))
} else {
None
}
})
})
.collect();
}
Vec::new()
}
pub fn matches(partial: &str, from: &str) -> bool {
from.to_ascii_lowercase()
.starts_with(&partial.to_ascii_lowercase())
}

View File

@ -0,0 +1,81 @@
use crate::completions::{Completer, CompletionOptions};
use nu_protocol::{
ast::{Expr, Expression},
engine::StateWorkingSet,
Span,
};
use reedline::Suggestion;
#[derive(Clone)]
pub struct FlagCompletion {
expression: Expression,
}
impl FlagCompletion {
pub fn new(expression: Expression) -> Self {
Self { expression }
}
}
impl Completer for FlagCompletion {
fn fetch(
&mut self,
working_set: &StateWorkingSet,
prefix: Vec<u8>,
span: Span,
offset: usize,
_: usize,
) -> (Vec<Suggestion>, CompletionOptions) {
// Check if it's a flag
if let Expr::Call(call) = &self.expression.expr {
let decl = working_set.get_decl(call.decl_id);
let sig = decl.signature();
let mut output = vec![];
for named in &sig.named {
let flag_desc = &named.desc;
if let Some(short) = named.short {
let mut named = vec![0; short.len_utf8()];
short.encode_utf8(&mut named);
named.insert(0, b'-');
if named.starts_with(&prefix) {
output.push(Suggestion {
value: String::from_utf8_lossy(&named).to_string(),
description: Some(flag_desc.to_string()),
extra: None,
span: reedline::Span {
start: span.start - offset,
end: span.end - offset,
},
});
}
}
if named.long.is_empty() {
continue;
}
let mut named = named.long.as_bytes().to_vec();
named.insert(0, b'-');
named.insert(0, b'-');
if named.starts_with(&prefix) {
output.push(Suggestion {
value: String::from_utf8_lossy(&named).to_string(),
description: Some(flag_desc.to_string()),
extra: None,
span: reedline::Span {
start: span.start - offset,
end: span.end - offset,
},
});
}
}
return (output, CompletionOptions::default());
}
(vec![], CompletionOptions::default())
}
}

View File

@ -0,0 +1,17 @@
mod base;
mod command_completions;
mod completer;
mod completion_options;
mod custom_completions;
mod file_completions;
mod flag_completions;
mod variable_completions;
pub use base::Completer;
pub use command_completions::CommandCompletion;
pub use completer::NuCompleter;
pub use completion_options::{CompletionOptions, SortBy};
pub use custom_completions::CustomCompletion;
pub use file_completions::{file_path_completion, FileCompletion};
pub use flag_completions::FlagCompletion;
pub use variable_completions::VariableCompletion;

View File

@ -0,0 +1,82 @@
use crate::completions::{Completer, CompletionOptions};
use nu_protocol::{
engine::{EngineState, StateWorkingSet},
Span,
};
use reedline::Suggestion;
use std::sync::Arc;
#[derive(Clone)]
pub struct VariableCompletion {
engine_state: Arc<EngineState>,
}
impl VariableCompletion {
pub fn new(engine_state: Arc<EngineState>) -> Self {
Self { engine_state }
}
}
impl Completer for VariableCompletion {
fn fetch(
&mut self,
working_set: &StateWorkingSet,
prefix: Vec<u8>,
span: Span,
offset: usize,
_: usize,
) -> (Vec<Suggestion>, CompletionOptions) {
let mut output = vec![];
let builtins = ["$nu", "$in", "$config", "$env", "$nothing"];
for builtin in builtins {
if builtin.as_bytes().starts_with(&prefix) {
output.push(Suggestion {
value: builtin.to_string(),
description: None,
extra: None,
span: reedline::Span {
start: span.start - offset,
end: span.end - offset,
},
});
}
}
for scope in &working_set.delta.scope {
for v in &scope.vars {
if v.0.starts_with(&prefix) {
output.push(Suggestion {
value: String::from_utf8_lossy(v.0).to_string(),
description: None,
extra: None,
span: reedline::Span {
start: span.start - offset,
end: span.end - offset,
},
});
}
}
}
for scope in &self.engine_state.scope {
for v in &scope.vars {
if v.0.starts_with(&prefix) {
output.push(Suggestion {
value: String::from_utf8_lossy(v.0).to_string(),
description: None,
extra: None,
span: reedline::Span {
start: span.start - offset,
end: span.end - offset,
},
});
}
}
}
output.dedup();
(output, CompletionOptions::default())
}
}

View File

@ -27,10 +27,6 @@ pub fn evaluate_file(
std::process::exit(1);
}
// Make a note of the exceptions we see for externals that look like math expressions
let exceptions = crate::util::external_exceptions(engine_state, stack);
engine_state.external_exceptions = exceptions;
let file = std::fs::read(&path).into_diagnostic()?;
let mut working_set = StateWorkingSet::new(engine_state);

View File

@ -3,6 +3,7 @@ mod completions;
mod config_files;
mod errors;
mod eval_file;
mod menus;
mod nu_highlight;
mod print;
mod prompt;
@ -18,6 +19,7 @@ pub use completions::NuCompleter;
pub use config_files::eval_config_contents;
pub use errors::CliError;
pub use eval_file::evaluate_file;
pub use menus::{DescriptionMenu, NuHelpCompleter};
pub use nu_highlight::NuHighlight;
pub use print::Print;
pub use prompt::NushellPrompt;

View File

@ -0,0 +1,724 @@
use {
nu_ansi_term::{ansi::RESET, Style},
reedline::{
menu_functions::string_difference, Completer, LineBuffer, Menu, MenuEvent, MenuTextStyle,
Painter, Suggestion,
},
};
/// Default values used as reference for the menu. These values are set during
/// the initial declaration of the menu and are always kept as reference for the
/// changeable [`WorkingDetails`]
struct DefaultMenuDetails {
/// Number of columns that the menu will have
pub columns: u16,
/// Column width
pub col_width: Option<usize>,
/// Column padding
pub col_padding: usize,
/// Number of rows for commands
pub selection_rows: u16,
/// Number of rows allowed to display the description
pub description_rows: usize,
}
impl Default for DefaultMenuDetails {
fn default() -> Self {
Self {
columns: 4,
col_width: None,
col_padding: 2,
selection_rows: 4,
description_rows: 10,
}
}
}
/// Represents the actual column conditions of the menu. These conditions change
/// since they need to accommodate possible different line sizes for the column values
#[derive(Default)]
struct WorkingDetails {
/// Number of columns that the menu will have
pub columns: u16,
/// Column width
pub col_width: usize,
/// Number of rows for description
pub description_rows: usize,
}
/// Completion menu definition
pub struct DescriptionMenu {
/// Menu name
name: String,
/// Menu status
active: bool,
/// Menu coloring
color: MenuTextStyle,
/// Default column details that are set when creating the menu
/// These values are the reference for the working details
default_details: DefaultMenuDetails,
/// Number of minimum rows that are displayed when
/// the required lines is larger than the available lines
min_rows: u16,
/// Working column details keep changing based on the collected values
working_details: WorkingDetails,
/// Menu cached values
values: Vec<Suggestion>,
/// column position of the cursor. Starts from 0
col_pos: u16,
/// row position in the menu. Starts from 0
row_pos: u16,
/// Menu marker when active
marker: String,
/// Event sent to the menu
event: Option<MenuEvent>,
/// String collected after the menu is activated
input: Option<String>,
/// Examples to select
examples: Vec<String>,
/// Example index
example_index: Option<usize>,
/// Examples may not be shown if there is not enough space in the screen
show_examples: bool,
/// Skipped description rows
skipped_rows: usize,
/// Calls the completer using only the line buffer difference difference
/// after the menu was activated
only_buffer_difference: bool,
}
impl Default for DescriptionMenu {
fn default() -> Self {
Self {
name: "description_menu".to_string(),
active: false,
color: MenuTextStyle::default(),
default_details: DefaultMenuDetails::default(),
min_rows: 3,
working_details: WorkingDetails::default(),
values: Vec::new(),
col_pos: 0,
row_pos: 0,
marker: "? ".to_string(),
event: None,
input: None,
examples: Vec::new(),
example_index: None,
show_examples: true,
skipped_rows: 0,
only_buffer_difference: true,
}
}
}
// Menu configuration
impl DescriptionMenu {
/// Menu builder with new name
pub fn with_name(mut self, name: &str) -> Self {
self.name = name.into();
self
}
/// Menu builder with new value for text style
pub fn with_text_style(mut self, text_style: Style) -> Self {
self.color.text_style = text_style;
self
}
/// Menu builder with new value for text style
pub fn with_selected_text_style(mut self, selected_text_style: Style) -> Self {
self.color.selected_text_style = selected_text_style;
self
}
/// Menu builder with new value for text style
pub fn with_description_text_style(mut self, description_text_style: Style) -> Self {
self.color.description_style = description_text_style;
self
}
/// Menu builder with new columns value
pub fn with_columns(mut self, columns: u16) -> Self {
self.default_details.columns = columns;
self
}
/// Menu builder with new column width value
pub fn with_column_width(mut self, col_width: Option<usize>) -> Self {
self.default_details.col_width = col_width;
self
}
/// Menu builder with new column width value
pub fn with_column_padding(mut self, col_padding: usize) -> Self {
self.default_details.col_padding = col_padding;
self
}
/// Menu builder with new selection rows value
pub fn with_selection_rows(mut self, selection_rows: u16) -> Self {
self.default_details.selection_rows = selection_rows;
self
}
/// Menu builder with new description rows value
pub fn with_description_rows(mut self, description_rows: usize) -> Self {
self.default_details.description_rows = description_rows;
self
}
/// Menu builder with marker
pub fn with_marker(mut self, marker: String) -> Self {
self.marker = marker;
self
}
/// Menu builder with new only buffer difference
pub fn with_only_buffer_difference(mut self, only_buffer_difference: bool) -> Self {
self.only_buffer_difference = only_buffer_difference;
self
}
}
// Menu functionality
impl DescriptionMenu {
/// Move menu cursor to the next element
fn move_next(&mut self) {
let mut new_col = self.col_pos + 1;
let mut new_row = self.row_pos;
if new_col >= self.get_cols() {
new_row += 1;
new_col = 0;
}
if new_row >= self.get_rows() {
new_row = 0;
new_col = 0;
}
let position = new_row * self.get_cols() + new_col;
if position >= self.get_values().len() as u16 {
self.reset_position();
} else {
self.col_pos = new_col;
self.row_pos = new_row;
}
}
/// Move menu cursor to the previous element
fn move_previous(&mut self) {
let new_col = self.col_pos.checked_sub(1);
let (new_col, new_row) = match new_col {
Some(col) => (col, self.row_pos),
None => match self.row_pos.checked_sub(1) {
Some(row) => (self.get_cols().saturating_sub(1), row),
None => (
self.get_cols().saturating_sub(1),
self.get_rows().saturating_sub(1),
),
},
};
let position = new_row * self.get_cols() + new_col;
if position >= self.get_values().len() as u16 {
self.col_pos = (self.get_values().len() as u16 % self.get_cols()).saturating_sub(1);
self.row_pos = self.get_rows().saturating_sub(1);
} else {
self.col_pos = new_col;
self.row_pos = new_row;
}
}
/// Menu index based on column and row position
fn index(&self) -> usize {
let index = self.row_pos * self.get_cols() + self.col_pos;
index as usize
}
/// Get selected value from the menu
fn get_value(&self) -> Option<Suggestion> {
self.get_values().get(self.index()).cloned()
}
/// Calculates how many rows the Menu will use
fn get_rows(&self) -> u16 {
let values = self.get_values().len() as u16;
if values == 0 {
// When the values are empty the no_records_msg is shown, taking 1 line
return 1;
}
let rows = values / self.get_cols();
if values % self.get_cols() != 0 {
rows + 1
} else {
rows
}
}
/// Returns working details col width
fn get_width(&self) -> usize {
self.working_details.col_width
}
/// Reset menu position
fn reset_position(&mut self) {
self.col_pos = 0;
self.row_pos = 0;
self.skipped_rows = 0;
}
fn no_records_msg(&self, use_ansi_coloring: bool) -> String {
let msg = "TYPE TO START SEACH";
if use_ansi_coloring {
format!(
"{}{}{}",
self.color.selected_text_style.prefix(),
msg,
RESET
)
} else {
msg.to_string()
}
}
/// Returns working details columns
fn get_cols(&self) -> u16 {
self.working_details.columns.max(1)
}
/// End of line for menu
fn end_of_line(&self, column: u16, index: usize) -> &str {
let is_last = index == self.values.len().saturating_sub(1);
if column == self.get_cols().saturating_sub(1) || is_last {
"\r\n"
} else {
""
}
}
/// Update list of examples from the actual value
fn update_examples(&mut self) {
self.examples = self
.get_value()
.and_then(|suggestion| suggestion.extra)
.unwrap_or_default();
self.example_index = None;
}
/// Creates default string that represents one suggestion from the menu
fn create_entry_string(
&self,
suggestion: &Suggestion,
index: usize,
column: u16,
empty_space: usize,
use_ansi_coloring: bool,
) -> String {
if use_ansi_coloring {
if index == self.index() {
format!(
"{}{}{}{:>empty$}{}",
self.color.selected_text_style.prefix(),
&suggestion.value,
RESET,
"",
self.end_of_line(column, index),
empty = empty_space,
)
} else {
format!(
"{}{}{}{:>empty$}{}",
self.color.text_style.prefix(),
&suggestion.value,
RESET,
"",
self.end_of_line(column, index),
empty = empty_space,
)
}
} else {
// If no ansi coloring is found, then the selection word is
// the line in uppercase
let (marker, empty_space) = if index == self.index() {
(">", empty_space.saturating_sub(1))
} else {
("", empty_space)
};
let line = format!(
"{}{}{:>empty$}{}",
marker,
&suggestion.value,
"",
self.end_of_line(column, index),
empty = empty_space,
);
if index == self.index() {
line.to_uppercase()
} else {
line
}
}
}
/// Description string with color
fn create_description_string(&self, use_ansi_coloring: bool) -> String {
let description = self
.get_value()
.and_then(|suggestion| suggestion.description)
.unwrap_or_else(|| "".to_string())
.lines()
.skip(self.skipped_rows)
.take(self.working_details.description_rows)
.collect::<Vec<&str>>()
.join("\r\n");
if use_ansi_coloring && !description.is_empty() {
format!(
"{}{}{}",
self.color.description_style.prefix(),
description,
RESET,
)
} else {
description
}
}
/// Selectable list of examples from the actual value
fn create_example_string(&self, use_ansi_coloring: bool) -> String {
if !self.show_examples {
return "".into();
}
let examples: String = self
.examples
.iter()
.enumerate()
.map(|(index, example)| {
if let Some(example_index) = self.example_index {
if index == example_index {
format!(
" {}{}{}\r\n",
self.color.selected_text_style.prefix(),
example,
RESET
)
} else {
format!(" {}\r\n", example)
}
} else {
format!(" {}\r\n", example)
}
})
.collect();
if examples.is_empty() {
"".into()
} else if use_ansi_coloring {
format!(
"{}\r\n\r\nExamples:\r\n{}{}",
self.color.description_style.prefix(),
RESET,
examples,
)
} else {
format!("\r\n\r\nExamples:\r\n{}", examples,)
}
}
}
impl Menu for DescriptionMenu {
/// Menu name
fn name(&self) -> &str {
self.name.as_str()
}
/// Menu indicator
fn indicator(&self) -> &str {
self.marker.as_str()
}
/// Deactivates context menu
fn is_active(&self) -> bool {
self.active
}
/// The menu stays active even with one record
fn can_quick_complete(&self) -> bool {
false
}
/// The menu does not need to partially complete
fn can_partially_complete(
&mut self,
_values_updated: bool,
_line_buffer: &mut LineBuffer,
_completer: &mut dyn Completer,
) -> bool {
false
}
/// Selects what type of event happened with the menu
fn menu_event(&mut self, event: MenuEvent) {
match &event {
MenuEvent::Activate(_) => self.active = true,
MenuEvent::Deactivate => {
self.active = false;
self.input = None;
self.values = Vec::new();
}
_ => {}
};
self.event = Some(event);
}
/// Updates menu values
fn update_values(&mut self, line_buffer: &mut LineBuffer, completer: &mut dyn Completer) {
if self.only_buffer_difference {
if let Some(old_string) = &self.input {
let (start, input) = string_difference(line_buffer.get_buffer(), old_string);
if !input.is_empty() {
self.reset_position();
self.values = completer.complete(input, start);
}
}
} else {
let trimmed_buffer = line_buffer.get_buffer().replace('\n', " ");
self.values =
completer.complete(trimmed_buffer.as_str(), line_buffer.insertion_point());
self.reset_position();
}
}
/// The working details for the menu changes based on the size of the lines
/// collected from the completer
fn update_working_details(
&mut self,
line_buffer: &mut LineBuffer,
completer: &mut dyn Completer,
painter: &Painter,
) {
if let Some(event) = self.event.take() {
// Updating all working parameters from the menu before executing any of the
// possible event
let max_width = self.get_values().iter().fold(0, |acc, suggestion| {
let str_len = suggestion.value.len() + self.default_details.col_padding;
if str_len > acc {
str_len
} else {
acc
}
});
// If no default width is found, then the total screen width is used to estimate
// the column width based on the default number of columns
let default_width = if let Some(col_width) = self.default_details.col_width {
col_width
} else {
let col_width = painter.screen_width() / self.default_details.columns;
col_width as usize
};
// Adjusting the working width of the column based the max line width found
// in the menu values
if max_width > default_width {
self.working_details.col_width = max_width;
} else {
self.working_details.col_width = default_width;
};
// The working columns is adjusted based on possible number of columns
// that could be fitted in the screen with the calculated column width
let possible_cols = painter.screen_width() / self.working_details.col_width as u16;
if possible_cols > self.default_details.columns {
self.working_details.columns = self.default_details.columns.max(1);
} else {
self.working_details.columns = possible_cols;
}
// Updating the working rows to display the description
if self.menu_required_lines(painter.screen_width()) <= painter.remaining_lines() {
self.working_details.description_rows = self.default_details.description_rows;
self.show_examples = true;
} else {
self.working_details.description_rows = painter
.remaining_lines()
.saturating_sub(self.default_details.selection_rows + 1)
as usize;
self.show_examples = false;
}
match event {
MenuEvent::Activate(_) => {
self.reset_position();
self.input = Some(line_buffer.get_buffer().to_string());
self.update_values(line_buffer, completer);
}
MenuEvent::Deactivate => self.active = false,
MenuEvent::Edit(_) => {
self.reset_position();
self.update_values(line_buffer, completer);
self.update_examples()
}
MenuEvent::NextElement => {
self.skipped_rows = 0;
self.move_next();
self.update_examples();
}
MenuEvent::PreviousElement => {
self.skipped_rows = 0;
self.move_previous();
self.update_examples();
}
MenuEvent::MoveUp => {
if let Some(example_index) = self.example_index {
if let Some(index) = example_index.checked_sub(1) {
self.example_index = Some(index);
} else {
self.example_index = Some(self.examples.len().saturating_sub(1));
}
} else {
self.example_index = Some(0);
}
}
MenuEvent::MoveDown => {
if let Some(example_index) = self.example_index {
let index = example_index + 1;
if index < self.examples.len() {
self.example_index = Some(index);
} else {
self.example_index = Some(0);
}
} else {
self.example_index = Some(0);
}
}
MenuEvent::MoveLeft => self.skipped_rows = self.skipped_rows.saturating_sub(1),
MenuEvent::MoveRight => {
let skipped = self.skipped_rows + 1;
let description_rows = self
.get_value()
.and_then(|suggestion| suggestion.description)
.unwrap_or_else(|| "".to_string())
.lines()
.count();
let allowed_skips =
description_rows.saturating_sub(self.working_details.description_rows);
if skipped < allowed_skips {
self.skipped_rows = skipped;
} else {
self.skipped_rows = allowed_skips;
}
}
MenuEvent::PreviousPage | MenuEvent::NextPage => {}
}
}
}
/// The buffer gets replaced in the Span location
fn replace_in_buffer(&self, line_buffer: &mut LineBuffer) {
if let Some(Suggestion { value, span, .. }) = self.get_value() {
let start = span.start.min(line_buffer.len());
let end = span.end.min(line_buffer.len());
let string_len = if let Some(example_index) = self.example_index {
let example = self
.examples
.get(example_index)
.expect("the example index is always checked");
line_buffer.replace(start..end, example);
example.len()
} else {
line_buffer.replace(start..end, &value);
value.len()
};
let mut offset = line_buffer.insertion_point();
offset += string_len.saturating_sub(end.saturating_sub(start));
line_buffer.set_insertion_point(offset);
}
}
/// Minimum rows that should be displayed by the menu
fn min_rows(&self) -> u16 {
self.get_rows().min(self.min_rows)
}
/// Gets values from filler that will be displayed in the menu
fn get_values(&self) -> &[Suggestion] {
&self.values
}
fn menu_required_lines(&self, _terminal_columns: u16) -> u16 {
let example_lines = self
.examples
.iter()
.fold(0, |acc, example| example.lines().count() + acc);
self.default_details.selection_rows
+ self.default_details.description_rows as u16
+ example_lines as u16
+ 3
}
fn menu_string(&self, _available_lines: u16, use_ansi_coloring: bool) -> String {
if self.get_values().is_empty() {
self.no_records_msg(use_ansi_coloring)
} else {
// The skip values represent the number of lines that should be skipped
// while printing the menu
let available_lines = self.default_details.selection_rows;
let skip_values = if self.row_pos >= available_lines {
let skip_lines = self.row_pos.saturating_sub(available_lines) + 1;
(skip_lines * self.get_cols()) as usize
} else {
0
};
// It seems that crossterm prefers to have a complete string ready to be printed
// rather than looping through the values and printing multiple things
// This reduces the flickering when printing the menu
let available_values = (available_lines * self.get_cols()) as usize;
let selection_values: String = self
.get_values()
.iter()
.skip(skip_values)
.take(available_values)
.enumerate()
.map(|(index, suggestion)| {
// Correcting the enumerate index based on the number of skipped values
let index = index + skip_values;
let column = index as u16 % self.get_cols();
let empty_space = self.get_width().saturating_sub(suggestion.value.len());
self.create_entry_string(
suggestion,
index,
column,
empty_space,
use_ansi_coloring,
)
})
.collect();
format!(
"{}{}{}",
selection_values,
self.create_description_string(use_ansi_coloring),
self.create_example_string(use_ansi_coloring)
)
}
}
}

View File

@ -0,0 +1,105 @@
use nu_engine::documentation::get_flags_section;
use nu_protocol::{engine::EngineState, levenshtein_distance};
use reedline::{Completer, Suggestion};
use std::sync::Arc;
pub struct NuHelpCompleter(Arc<EngineState>);
impl NuHelpCompleter {
pub fn new(engine_state: Arc<EngineState>) -> Self {
Self(engine_state)
}
fn completion_helper(&self, line: &str, pos: usize) -> Vec<Suggestion> {
let full_commands = self.0.get_signatures_with_examples(false);
//Vec<(Signature, Vec<Example>, bool, bool)> {
let mut commands = full_commands
.iter()
.filter(|(sig, _, _, _)| {
sig.name.to_lowercase().contains(&line.to_lowercase())
|| sig.usage.to_lowercase().contains(&line.to_lowercase())
|| sig
.extra_usage
.to_lowercase()
.contains(&line.to_lowercase())
})
.collect::<Vec<_>>();
commands.sort_by(|(a, _, _, _), (b, _, _, _)| {
let a_distance = levenshtein_distance(line, &a.name);
let b_distance = levenshtein_distance(line, &b.name);
a_distance.cmp(&b_distance)
});
commands
.into_iter()
.map(|(sig, examples, _, _)| {
let mut long_desc = String::new();
let usage = &sig.usage;
if !usage.is_empty() {
long_desc.push_str(usage);
long_desc.push_str("\r\n\r\n");
}
let extra_usage = &sig.extra_usage;
if !extra_usage.is_empty() {
long_desc.push_str(extra_usage);
long_desc.push_str("\r\n\r\n");
}
long_desc.push_str(&format!("Usage:\r\n > {}\r\n", sig.call_signature()));
if !sig.named.is_empty() {
long_desc.push_str(&get_flags_section(sig))
}
if !sig.required_positional.is_empty()
|| !sig.optional_positional.is_empty()
|| sig.rest_positional.is_some()
{
long_desc.push_str("\r\nParameters:\r\n");
for positional in &sig.required_positional {
long_desc
.push_str(&format!(" {}: {}\r\n", positional.name, positional.desc));
}
for positional in &sig.optional_positional {
long_desc.push_str(&format!(
" (optional) {}: {}\r\n",
positional.name, positional.desc
));
}
if let Some(rest_positional) = &sig.rest_positional {
long_desc.push_str(&format!(
" ...{}: {}\r\n",
rest_positional.name, rest_positional.desc
));
}
}
let extra: Vec<String> = examples
.iter()
.map(|example| example.example.to_string())
.collect();
Suggestion {
value: sig.name.clone(),
description: Some(long_desc),
extra: Some(extra),
span: reedline::Span {
start: pos,
end: pos + line.len(),
},
}
})
.collect()
}
}
impl Completer for NuHelpCompleter {
fn complete(&mut self, line: &str, pos: usize) -> Vec<Suggestion> {
self.completion_helper(line, pos)
}
}

View File

@ -0,0 +1,174 @@
use nu_engine::eval_block;
use nu_protocol::{
engine::{EngineState, Stack},
IntoPipelineData, Span, Value,
};
use reedline::{menu_functions::parse_selection_char, Completer, Suggestion};
use std::sync::Arc;
const SELECTION_CHAR: char = '!';
pub struct NuMenuCompleter {
block_id: usize,
span: Span,
stack: Stack,
engine_state: Arc<EngineState>,
only_buffer_difference: bool,
}
impl NuMenuCompleter {
pub fn new(
block_id: usize,
span: Span,
stack: Stack,
engine_state: Arc<EngineState>,
only_buffer_difference: bool,
) -> Self {
Self {
block_id,
span,
stack,
engine_state,
only_buffer_difference,
}
}
}
impl Completer for NuMenuCompleter {
fn complete(&mut self, line: &str, pos: usize) -> Vec<Suggestion> {
let parsed = parse_selection_char(line, SELECTION_CHAR);
let block = self.engine_state.get_block(self.block_id);
if let Some(buffer) = block.signature.get_positional(0) {
if let Some(buffer_id) = &buffer.var_id {
let line_buffer = Value::String {
val: parsed.remainder.to_string(),
span: self.span,
};
self.stack.add_var(*buffer_id, line_buffer);
}
}
if let Some(position) = block.signature.get_positional(1) {
if let Some(position_id) = &position.var_id {
let line_buffer = Value::Int {
val: pos as i64,
span: self.span,
};
self.stack.add_var(*position_id, line_buffer);
}
}
let input = Value::nothing(self.span).into_pipeline_data();
let res = eval_block(
&self.engine_state,
&mut self.stack,
block,
input,
false,
false,
);
if let Ok(values) = res {
let values = values.into_value(self.span);
convert_to_suggestions(values, line, pos, self.only_buffer_difference)
} else {
Vec::new()
}
}
}
fn convert_to_suggestions(
value: Value,
line: &str,
pos: usize,
only_buffer_difference: bool,
) -> Vec<Suggestion> {
match value {
Value::Record { .. } => {
let text = match value
.get_data_by_key("value")
.and_then(|val| val.as_string().ok())
{
Some(val) => val,
None => "No value key".to_string(),
};
let description = value
.get_data_by_key("description")
.and_then(|val| val.as_string().ok());
let span = match value.get_data_by_key("span") {
Some(span @ Value::Record { .. }) => {
let start = span
.get_data_by_key("start")
.and_then(|val| val.as_integer().ok());
let end = span
.get_data_by_key("end")
.and_then(|val| val.as_integer().ok());
match (start, end) {
(Some(start), Some(end)) => {
let start = start.min(end);
reedline::Span {
start: start as usize,
end: end as usize,
}
}
_ => reedline::Span {
start: if only_buffer_difference { pos } else { 0 },
end: if only_buffer_difference {
pos + line.len()
} else {
line.len()
},
},
}
}
_ => reedline::Span {
start: if only_buffer_difference { pos } else { 0 },
end: if only_buffer_difference {
pos + line.len()
} else {
line.len()
},
},
};
let extra = match value.get_data_by_key("extra") {
Some(Value::List { vals, .. }) => {
let extra: Vec<String> = vals
.into_iter()
.filter_map(|extra| match extra {
Value::String { val, .. } => Some(val),
_ => None,
})
.collect();
Some(extra)
}
_ => None,
};
vec![Suggestion {
value: text,
description,
extra,
span,
}]
}
Value::List { vals, .. } => vals
.into_iter()
.flat_map(|val| convert_to_suggestions(val, line, pos, only_buffer_difference))
.collect(),
_ => vec![Suggestion {
value: format!("Not a record: {:?}", value),
description: None,
extra: None,
span: reedline::Span {
start: 0,
end: line.len(),
},
}],
}
}

View File

@ -0,0 +1,7 @@
mod description_menu;
mod help_completions;
mod menu_completions;
pub use description_menu::DescriptionMenu;
pub use help_completions::NuHelpCompleter;
pub use menu_completions::NuMenuCompleter;

View File

@ -12,10 +12,10 @@ use {
pub struct NushellPrompt {
left_prompt_string: Option<String>,
right_prompt_string: Option<String>,
default_prompt_indicator: String,
default_vi_insert_prompt_indicator: String,
default_vi_normal_prompt_indicator: String,
default_multiline_indicator: String,
default_prompt_indicator: Option<String>,
default_vi_insert_prompt_indicator: Option<String>,
default_vi_normal_prompt_indicator: Option<String>,
default_multiline_indicator: Option<String>,
}
impl Default for NushellPrompt {
@ -29,10 +29,10 @@ impl NushellPrompt {
NushellPrompt {
left_prompt_string: None,
right_prompt_string: None,
default_prompt_indicator: "".to_string(),
default_vi_insert_prompt_indicator: ": ".to_string(),
default_vi_normal_prompt_indicator: "".to_string(),
default_multiline_indicator: "::: ".to_string(),
default_prompt_indicator: None,
default_vi_insert_prompt_indicator: None,
default_vi_normal_prompt_indicator: None,
default_multiline_indicator: None,
}
}
@ -44,19 +44,19 @@ impl NushellPrompt {
self.right_prompt_string = prompt_string;
}
pub fn update_prompt_indicator(&mut self, prompt_indicator_string: String) {
pub fn update_prompt_indicator(&mut self, prompt_indicator_string: Option<String>) {
self.default_prompt_indicator = prompt_indicator_string;
}
pub fn update_prompt_vi_insert(&mut self, prompt_vi_insert_string: String) {
pub fn update_prompt_vi_insert(&mut self, prompt_vi_insert_string: Option<String>) {
self.default_vi_insert_prompt_indicator = prompt_vi_insert_string;
}
pub fn update_prompt_vi_normal(&mut self, prompt_vi_normal_string: String) {
pub fn update_prompt_vi_normal(&mut self, prompt_vi_normal_string: Option<String>) {
self.default_vi_normal_prompt_indicator = prompt_vi_normal_string;
}
pub fn update_prompt_multiline(&mut self, prompt_multiline_indicator_string: String) {
pub fn update_prompt_multiline(&mut self, prompt_multiline_indicator_string: Option<String>) {
self.default_multiline_indicator = prompt_multiline_indicator_string;
}
@ -64,18 +64,19 @@ impl NushellPrompt {
&mut self,
left_prompt_string: Option<String>,
right_prompt_string: Option<String>,
prompt_indicator_string: String,
prompt_multiline_indicator_string: String,
prompt_vi: (String, String),
prompt_indicator_string: Option<String>,
prompt_multiline_indicator_string: Option<String>,
prompt_vi: (Option<String>, Option<String>),
) {
let (prompt_vi_insert_string, prompt_vi_normal_string) = prompt_vi;
self.left_prompt_string = left_prompt_string;
self.right_prompt_string = right_prompt_string;
self.default_prompt_indicator = prompt_indicator_string;
self.default_multiline_indicator = prompt_multiline_indicator_string;
self.default_vi_insert_prompt_indicator = prompt_vi_insert_string;
self.default_vi_normal_prompt_indicator = prompt_vi_normal_string;
self.default_multiline_indicator = prompt_multiline_indicator_string;
}
fn default_wrapped_custom_string(&self, str: String) -> String {
@ -112,18 +113,33 @@ impl Prompt for NushellPrompt {
fn render_prompt_indicator(&self, edit_mode: PromptEditMode) -> Cow<str> {
match edit_mode {
PromptEditMode::Default => self.default_prompt_indicator.as_str().into(),
PromptEditMode::Emacs => self.default_prompt_indicator.as_str().into(),
PromptEditMode::Default => match &self.default_prompt_indicator {
Some(indicator) => indicator.as_str().into(),
None => "".into(),
},
PromptEditMode::Emacs => match &self.default_prompt_indicator {
Some(indicator) => indicator.as_str().into(),
None => "".into(),
},
PromptEditMode::Vi(vi_mode) => match vi_mode {
PromptViMode::Normal => self.default_vi_normal_prompt_indicator.as_str().into(),
PromptViMode::Insert => self.default_vi_insert_prompt_indicator.as_str().into(),
PromptViMode::Normal => match &self.default_vi_normal_prompt_indicator {
Some(indicator) => indicator.as_str().into(),
None => ": ".into(),
},
PromptViMode::Insert => match &self.default_vi_insert_prompt_indicator {
Some(indicator) => indicator.as_str().into(),
None => "".into(),
},
},
PromptEditMode::Custom(str) => self.default_wrapped_custom_string(str).into(),
}
}
fn render_prompt_multiline_indicator(&self) -> Cow<str> {
Cow::Borrowed(self.default_multiline_indicator.as_str())
match &self.default_multiline_indicator {
Some(indicator) => indicator.as_str().into(),
None => "::: ".into(),
}
}
fn render_prompt_history_search_indicator(

View File

@ -2,7 +2,6 @@ use crate::util::report_error;
use crate::NushellPrompt;
use log::info;
use nu_engine::eval_subexpression;
use nu_parser::parse;
use nu_protocol::{
engine::{EngineState, Stack, StateWorkingSet},
Config, PipelineData, Span, Value,
@ -17,49 +16,6 @@ pub(crate) const PROMPT_INDICATOR_VI_INSERT: &str = "PROMPT_INDICATOR_VI_INSERT"
pub(crate) const PROMPT_INDICATOR_VI_NORMAL: &str = "PROMPT_INDICATOR_VI_NORMAL";
pub(crate) const PROMPT_MULTILINE_INDICATOR: &str = "PROMPT_MULTILINE_INDICATOR";
pub(crate) fn get_prompt_indicators(
config: &Config,
engine_state: &EngineState,
stack: &Stack,
is_perf_true: bool,
) -> (String, String, String, String) {
let prompt_indicator = match stack.get_env_var(engine_state, PROMPT_INDICATOR) {
Some(pi) => pi.into_string("", config),
None => "".to_string(),
};
let prompt_vi_insert = match stack.get_env_var(engine_state, PROMPT_INDICATOR_VI_INSERT) {
Some(pvii) => pvii.into_string("", config),
None => ": ".to_string(),
};
let prompt_vi_normal = match stack.get_env_var(engine_state, PROMPT_INDICATOR_VI_NORMAL) {
Some(pviv) => pviv.into_string("", config),
None => "".to_string(),
};
let prompt_multiline = match stack.get_env_var(engine_state, PROMPT_MULTILINE_INDICATOR) {
Some(pm) => pm.into_string("", config),
None => "::: ".to_string(),
};
if is_perf_true {
info!(
"get_prompt_indicators {}:{}:{}",
file!(),
line!(),
column!()
);
}
(
prompt_indicator,
prompt_vi_insert,
prompt_vi_normal,
prompt_multiline,
)
}
fn get_prompt_string(
prompt: &str,
config: &Config,
@ -102,28 +58,7 @@ fn get_prompt_string(
}
}
}
Value::String { val: source, .. } => {
let mut working_set = StateWorkingSet::new(engine_state);
let (block, _) = parse(&mut working_set, None, source.as_bytes(), true, &[]);
// Use eval_subexpression to force a redirection of output, so we can use everything in prompt
let ret_val = eval_subexpression(
engine_state,
stack,
&block,
PipelineData::new(Span::new(0, 0)), // Don't try this at home, 0 span is ignored
)
.ok();
if is_perf_true {
info!(
"get_prompt_string (string) {}:{}:{}",
file!(),
line!(),
column!()
);
}
ret_val
}
Value::String { .. } => Some(PipelineData::Value(v.clone(), None)),
_ => None,
})
.and_then(|pipeline_data| {
@ -153,32 +88,60 @@ pub(crate) fn update_prompt<'prompt>(
nu_prompt: &'prompt mut NushellPrompt,
is_perf_true: bool,
) -> &'prompt dyn Prompt {
// get the other indicators
let (
prompt_indicator_string,
prompt_vi_insert_string,
prompt_vi_normal_string,
prompt_multiline_string,
) = get_prompt_indicators(config, engine_state, stack, is_perf_true);
let mut stack = stack.clone();
let left_prompt_string = get_prompt_string(
PROMPT_COMMAND,
config,
engine_state,
&mut stack,
is_perf_true,
);
let right_prompt_string = get_prompt_string(
PROMPT_COMMAND_RIGHT,
config,
engine_state,
&mut stack,
is_perf_true,
);
let prompt_indicator_string = get_prompt_string(
PROMPT_INDICATOR,
config,
engine_state,
&mut stack,
is_perf_true,
);
let prompt_multiline_string = get_prompt_string(
PROMPT_MULTILINE_INDICATOR,
config,
engine_state,
&mut stack,
is_perf_true,
);
let prompt_vi_insert_string = get_prompt_string(
PROMPT_INDICATOR_VI_INSERT,
config,
engine_state,
&mut stack,
is_perf_true,
);
let prompt_vi_normal_string = get_prompt_string(
PROMPT_INDICATOR_VI_NORMAL,
config,
engine_state,
&mut stack,
is_perf_true,
);
// apply the other indicators
nu_prompt.update_all_prompt_strings(
get_prompt_string(
PROMPT_COMMAND,
config,
engine_state,
&mut stack,
is_perf_true,
),
get_prompt_string(
PROMPT_COMMAND_RIGHT,
config,
engine_state,
&mut stack,
is_perf_true,
),
left_prompt_string,
right_prompt_string,
prompt_indicator_string,
prompt_multiline_string,
(prompt_vi_insert_string, prompt_vi_normal_string),

View File

@ -1,129 +1,507 @@
use super::DescriptionMenu;
use crate::{menus::NuMenuCompleter, NuHelpCompleter};
use crossterm::event::{KeyCode, KeyModifiers};
use nu_color_config::lookup_ansi_color_style;
use nu_protocol::{extract_value, Config, ParsedKeybinding, ShellError, Span, Value};
use nu_engine::eval_block;
use nu_parser::parse;
use nu_protocol::{
color_value_string, create_menus,
engine::{EngineState, Stack, StateWorkingSet},
extract_value, Config, IntoPipelineData, ParsedKeybinding, ParsedMenu, PipelineData,
ShellError, Span, Value,
};
use reedline::{
default_emacs_keybindings, default_vi_insert_keybindings, default_vi_normal_keybindings,
CompletionMenu, EditCommand, HistoryMenu, Keybindings, Reedline, ReedlineEvent,
ColumnarMenu, EditCommand, Keybindings, ListMenu, Reedline, ReedlineEvent, ReedlineMenu,
};
use std::sync::Arc;
// Creates an input object for the completion menu based on the dictionary
// stored in the config variable
pub(crate) fn add_completion_menu(line_editor: Reedline, config: &Config) -> Reedline {
let mut completion_menu = CompletionMenu::default();
const DEFAULT_COMPLETION_MENU: &str = r#"
{
name: completion_menu
only_buffer_difference: false
marker: "| "
type: {
layout: columnar
columns: 4
col_width: 20
col_padding: 2
}
style: {
text: green,
selected_text: green_reverse
description_text: yellow
}
}"#;
completion_menu = match config
.menu_config
.get("columns")
.and_then(|value| value.as_integer().ok())
{
Some(value) => completion_menu.with_columns(value as u16),
None => completion_menu,
};
const DEFAULT_HISTORY_MENU: &str = r#"
{
name: history_menu
only_buffer_difference: true
marker: "? "
type: {
layout: list
page_size: 10
}
style: {
text: green,
selected_text: green_reverse
description_text: yellow
}
}"#;
completion_menu = completion_menu.with_column_width(
config
.menu_config
.get("col_width")
.and_then(|value| value.as_integer().ok())
.map(|value| value as usize),
);
const DEFAULT_HELP_MENU: &str = r#"
{
name: help_menu
only_buffer_difference: true
marker: "? "
type: {
layout: description
columns: 4
col_width: 20
col_padding: 2
selection_rows: 4
description_rows: 10
}
style: {
text: green,
selected_text: green_reverse
description_text: yellow
}
}"#;
completion_menu = match config
.menu_config
.get("col_padding")
.and_then(|value| value.as_integer().ok())
{
Some(value) => completion_menu.with_column_padding(value as usize),
None => completion_menu,
};
// Adds all menus to line editor
pub(crate) fn add_menus(
mut line_editor: Reedline,
engine_state: Arc<EngineState>,
stack: &Stack,
config: &Config,
) -> Result<Reedline, ShellError> {
line_editor = line_editor.clear_menus();
completion_menu = match config
.menu_config
.get("text_style")
.and_then(|value| value.as_string().ok())
{
Some(value) => completion_menu.with_text_style(lookup_ansi_color_style(&value)),
None => completion_menu,
};
for menu in &config.menus {
line_editor = add_menu(line_editor, menu, engine_state.clone(), stack, config)?
}
completion_menu = match config
.menu_config
.get("selected_text_style")
.and_then(|value| value.as_string().ok())
{
Some(value) => completion_menu.with_selected_text_style(lookup_ansi_color_style(&value)),
None => completion_menu,
};
// Checking if the default menus have been added from the config file
let default_menus = vec![
("completion_menu", DEFAULT_COMPLETION_MENU),
("history_menu", DEFAULT_HISTORY_MENU),
("help_menu", DEFAULT_HELP_MENU),
];
completion_menu = match config
.menu_config
.get("marker")
.and_then(|value| value.as_string().ok())
{
Some(value) => completion_menu.with_marker(value),
None => completion_menu,
};
for (name, definition) in default_menus {
if !config
.menus
.iter()
.any(|menu| menu.name.into_string("", config) == name)
{
let (block, _) = {
let mut working_set = StateWorkingSet::new(&engine_state);
let (output, _) = parse(
&mut working_set,
Some(name), // format!("entry #{}", entry_num)
definition.as_bytes(),
true,
&[],
);
line_editor.with_menu(Box::new(completion_menu))
(output, working_set.render())
};
let mut temp_stack = Stack::new();
let input = Value::nothing(Span::test_data()).into_pipeline_data();
let res = eval_block(&engine_state, &mut temp_stack, &block, input, false, false)?;
if let PipelineData::Value(value, None) = res {
for menu in create_menus(&value, config)? {
line_editor =
add_menu(line_editor, &menu, engine_state.clone(), stack, config)?;
}
}
}
}
Ok(line_editor)
}
// Creates an input object for the history menu based on the dictionary
// stored in the config variable
pub(crate) fn add_history_menu(line_editor: Reedline, config: &Config) -> Reedline {
let mut history_menu = HistoryMenu::default();
fn add_menu(
line_editor: Reedline,
menu: &ParsedMenu,
engine_state: Arc<EngineState>,
stack: &Stack,
config: &Config,
) -> Result<Reedline, ShellError> {
if let Value::Record { cols, vals, span } = &menu.menu_type {
let layout = extract_value("layout", cols, vals, span)?.into_string("", config);
history_menu = match config
.history_config
.get("page_size")
.and_then(|value| value.as_integer().ok())
{
Some(value) => history_menu.with_page_size(value as usize),
None => history_menu,
};
history_menu = match config
.history_config
.get("selector")
.and_then(|value| value.as_string().ok())
{
Some(value) => {
let char = value.chars().next().unwrap_or('!');
history_menu.with_selection_char(char)
match layout.as_str() {
"columnar" => add_columnar_menu(line_editor, menu, engine_state, stack, config),
"list" => add_list_menu(line_editor, menu, engine_state, stack, config),
"description" => add_description_menu(line_editor, menu, engine_state, stack, config),
_ => Err(ShellError::UnsupportedConfigValue(
"columnar, list or description".to_string(),
menu.menu_type.into_abbreviated_string(config),
menu.menu_type.span()?,
)),
}
None => history_menu,
};
} else {
Err(ShellError::UnsupportedConfigValue(
"only record type".to_string(),
menu.menu_type.into_abbreviated_string(config),
menu.menu_type.span()?,
))
}
}
history_menu = match config
.history_config
.get("text_style")
.and_then(|value| value.as_string().ok())
{
Some(value) => history_menu.with_text_style(lookup_ansi_color_style(&value)),
None => history_menu,
macro_rules! add_style {
// first arm match add!(1,2), add!(2,3) etc
($name:expr, $cols: expr, $vals:expr, $span:expr, $config: expr, $menu:expr, $f:expr) => {
$menu = match extract_value($name, $cols, $vals, $span) {
Ok(text) => {
let text = match text {
Value::String { val, .. } => val.clone(),
Value::Record { cols, vals, span } => {
color_value_string(span, cols, vals, $config).into_string("", $config)
}
_ => "green".to_string(),
};
let style = lookup_ansi_color_style(&text);
$f($menu, style)
}
Err(_) => $menu,
};
};
}
history_menu = match config
.history_config
.get("selected_text_style")
.and_then(|value| value.as_string().ok())
{
Some(value) => history_menu.with_selected_text_style(lookup_ansi_color_style(&value)),
None => history_menu,
};
// Adds a columnar menu to the editor engine
pub(crate) fn add_columnar_menu(
line_editor: Reedline,
menu: &ParsedMenu,
engine_state: Arc<EngineState>,
stack: &Stack,
config: &Config,
) -> Result<Reedline, ShellError> {
let name = menu.name.into_string("", config);
let mut columnar_menu = ColumnarMenu::default().with_name(&name);
history_menu = match config
.history_config
.get("marker")
.and_then(|value| value.as_string().ok())
{
Some(value) => history_menu.with_marker(value),
None => history_menu,
};
if let Value::Record { cols, vals, span } = &menu.menu_type {
columnar_menu = match extract_value("columns", cols, vals, span) {
Ok(columns) => {
let columns = columns.as_integer()?;
columnar_menu.with_columns(columns as u16)
}
Err(_) => columnar_menu,
};
line_editor.with_menu(Box::new(history_menu))
columnar_menu = match extract_value("col_width", cols, vals, span) {
Ok(col_width) => {
let col_width = col_width.as_integer()?;
columnar_menu.with_column_width(Some(col_width as usize))
}
Err(_) => columnar_menu.with_column_width(None),
};
columnar_menu = match extract_value("col_padding", cols, vals, span) {
Ok(col_padding) => {
let col_padding = col_padding.as_integer()?;
columnar_menu.with_column_padding(col_padding as usize)
}
Err(_) => columnar_menu,
};
}
if let Value::Record { cols, vals, span } = &menu.style {
add_style!(
"text",
cols,
vals,
span,
config,
columnar_menu,
ColumnarMenu::with_text_style
);
add_style!(
"selected_text",
cols,
vals,
span,
config,
columnar_menu,
ColumnarMenu::with_selected_text_style
);
add_style!(
"description_text",
cols,
vals,
span,
config,
columnar_menu,
ColumnarMenu::with_description_text_style
);
}
let marker = menu.marker.into_string("", config);
columnar_menu = columnar_menu.with_marker(marker);
let only_buffer_difference = menu.only_buffer_difference.as_bool()?;
columnar_menu = columnar_menu.with_only_buffer_difference(only_buffer_difference);
match &menu.source {
Value::Nothing { .. } => {
Ok(line_editor.with_menu(ReedlineMenu::EngineCompleter(Box::new(columnar_menu))))
}
Value::Block {
val,
captures,
span,
} => {
let menu_completer = NuMenuCompleter::new(
*val,
*span,
stack.captures_to_stack(captures),
engine_state,
only_buffer_difference,
);
Ok(line_editor.with_menu(ReedlineMenu::WithCompleter {
menu: Box::new(columnar_menu),
completer: Box::new(menu_completer),
}))
}
_ => Err(ShellError::UnsupportedConfigValue(
"block or omitted value".to_string(),
menu.source.into_abbreviated_string(config),
menu.source.span()?,
)),
}
}
// Adds a search menu to the line editor
pub(crate) fn add_list_menu(
line_editor: Reedline,
menu: &ParsedMenu,
engine_state: Arc<EngineState>,
stack: &Stack,
config: &Config,
) -> Result<Reedline, ShellError> {
let name = menu.name.into_string("", config);
let mut list_menu = ListMenu::default().with_name(&name);
if let Value::Record { cols, vals, span } = &menu.menu_type {
list_menu = match extract_value("page_size", cols, vals, span) {
Ok(page_size) => {
let page_size = page_size.as_integer()?;
list_menu.with_page_size(page_size as usize)
}
Err(_) => list_menu,
};
}
if let Value::Record { cols, vals, span } = &menu.style {
add_style!(
"text",
cols,
vals,
span,
config,
list_menu,
ListMenu::with_text_style
);
add_style!(
"selected_text",
cols,
vals,
span,
config,
list_menu,
ListMenu::with_selected_text_style
);
add_style!(
"description_text",
cols,
vals,
span,
config,
list_menu,
ListMenu::with_description_text_style
);
}
let marker = menu.marker.into_string("", config);
list_menu = list_menu.with_marker(marker);
let only_buffer_difference = menu.only_buffer_difference.as_bool()?;
list_menu = list_menu.with_only_buffer_difference(only_buffer_difference);
match &menu.source {
Value::Nothing { .. } => {
Ok(line_editor.with_menu(ReedlineMenu::HistoryMenu(Box::new(list_menu))))
}
Value::Block {
val,
captures,
span,
} => {
let menu_completer = NuMenuCompleter::new(
*val,
*span,
stack.captures_to_stack(captures),
engine_state,
only_buffer_difference,
);
Ok(line_editor.with_menu(ReedlineMenu::WithCompleter {
menu: Box::new(list_menu),
completer: Box::new(menu_completer),
}))
}
_ => Err(ShellError::UnsupportedConfigValue(
"block or omitted value".to_string(),
menu.source.into_abbreviated_string(config),
menu.source.span()?,
)),
}
}
// Adds a description menu to the line editor
pub(crate) fn add_description_menu(
line_editor: Reedline,
menu: &ParsedMenu,
engine_state: Arc<EngineState>,
stack: &Stack,
config: &Config,
) -> Result<Reedline, ShellError> {
let name = menu.name.into_string("", config);
let mut description_menu = DescriptionMenu::default().with_name(&name);
if let Value::Record { cols, vals, span } = &menu.menu_type {
description_menu = match extract_value("columns", cols, vals, span) {
Ok(columns) => {
let columns = columns.as_integer()?;
description_menu.with_columns(columns as u16)
}
Err(_) => description_menu,
};
description_menu = match extract_value("col_width", cols, vals, span) {
Ok(col_width) => {
let col_width = col_width.as_integer()?;
description_menu.with_column_width(Some(col_width as usize))
}
Err(_) => description_menu.with_column_width(None),
};
description_menu = match extract_value("col_padding", cols, vals, span) {
Ok(col_padding) => {
let col_padding = col_padding.as_integer()?;
description_menu.with_column_padding(col_padding as usize)
}
Err(_) => description_menu,
};
description_menu = match extract_value("selection_rows", cols, vals, span) {
Ok(selection_rows) => {
let selection_rows = selection_rows.as_integer()?;
description_menu.with_selection_rows(selection_rows as u16)
}
Err(_) => description_menu,
};
description_menu = match extract_value("description_rows", cols, vals, span) {
Ok(description_rows) => {
let description_rows = description_rows.as_integer()?;
description_menu.with_description_rows(description_rows as usize)
}
Err(_) => description_menu,
};
}
if let Value::Record { cols, vals, span } = &menu.style {
add_style!(
"text",
cols,
vals,
span,
config,
description_menu,
DescriptionMenu::with_text_style
);
add_style!(
"selected_text",
cols,
vals,
span,
config,
description_menu,
DescriptionMenu::with_selected_text_style
);
add_style!(
"description_text",
cols,
vals,
span,
config,
description_menu,
DescriptionMenu::with_description_text_style
);
}
let marker = menu.marker.into_string("", config);
description_menu = description_menu.with_marker(marker);
let only_buffer_difference = menu.only_buffer_difference.as_bool()?;
description_menu = description_menu.with_only_buffer_difference(only_buffer_difference);
match &menu.source {
Value::Nothing { .. } => {
let completer = Box::new(NuHelpCompleter::new(engine_state));
Ok(line_editor.with_menu(ReedlineMenu::WithCompleter {
menu: Box::new(description_menu),
completer,
}))
}
Value::Block {
val,
captures,
span,
} => {
let menu_completer = NuMenuCompleter::new(
*val,
*span,
stack.captures_to_stack(captures),
engine_state,
only_buffer_difference,
);
Ok(line_editor.with_menu(ReedlineMenu::WithCompleter {
menu: Box::new(description_menu),
completer: Box::new(menu_completer),
}))
}
_ => Err(ShellError::UnsupportedConfigValue(
"block or omitted value".to_string(),
menu.source.into_abbreviated_string(config),
menu.source.span()?,
)),
}
}
fn add_menu_keybindings(keybindings: &mut Keybindings) {
// Completer menu keybindings
keybindings.add_binding(
KeyModifiers::NONE,
KeyCode::Tab,
ReedlineEvent::UntilFound(vec![
ReedlineEvent::Menu("completion_menu".to_string()),
ReedlineEvent::MenuNext,
]),
);
keybindings.add_binding(
KeyModifiers::SHIFT,
KeyCode::BackTab,
ReedlineEvent::MenuPrevious,
);
// History menu keybinding
keybindings.add_binding(
KeyModifiers::CONTROL,
KeyCode::Char('x'),
@ -142,19 +520,11 @@ fn add_menu_keybindings(keybindings: &mut Keybindings) {
]),
);
// Help menu keybinding
keybindings.add_binding(
KeyModifiers::NONE,
KeyCode::Tab,
ReedlineEvent::UntilFound(vec![
ReedlineEvent::Menu("completion_menu".to_string()),
ReedlineEvent::MenuNext,
]),
);
keybindings.add_binding(
KeyModifiers::SHIFT,
KeyCode::BackTab,
ReedlineEvent::MenuPrevious,
KeyModifiers::CONTROL,
KeyCode::Char('q'),
ReedlineEvent::Menu("help_menu".to_string()),
);
}
@ -173,6 +543,15 @@ pub(crate) fn create_keybindings(config: &Config) -> Result<KeybindingsMode, She
let mut insert_keybindings = default_vi_insert_keybindings();
let mut normal_keybindings = default_vi_normal_keybindings();
match config.edit_mode.as_str() {
"emacs" => {
add_menu_keybindings(&mut emacs_keybindings);
}
_ => {
add_menu_keybindings(&mut insert_keybindings);
add_menu_keybindings(&mut normal_keybindings);
}
}
for keybinding in parsed_keybindings {
add_keybinding(
&keybinding.mode,
@ -185,20 +564,11 @@ pub(crate) fn create_keybindings(config: &Config) -> Result<KeybindingsMode, She
}
match config.edit_mode.as_str() {
"emacs" => {
add_menu_keybindings(&mut emacs_keybindings);
Ok(KeybindingsMode::Emacs(emacs_keybindings))
}
_ => {
add_menu_keybindings(&mut insert_keybindings);
add_menu_keybindings(&mut normal_keybindings);
Ok(KeybindingsMode::Vi {
insert_keybindings,
normal_keybindings,
})
}
"emacs" => Ok(KeybindingsMode::Emacs(emacs_keybindings)),
_ => Ok(KeybindingsMode::Vi {
insert_keybindings,
normal_keybindings,
}),
}
}
@ -328,10 +698,11 @@ fn add_parsed_keybinding(
))
}
};
let event = parse_event(&keybinding.event, config)?;
keybindings.add_binding(modifier, keycode, event);
if let Some(event) = parse_event(&keybinding.event, config)? {
keybindings.add_binding(modifier, keycode, event);
} else {
keybindings.remove_binding(modifier, keycode);
}
Ok(())
}
@ -356,7 +727,7 @@ impl<'config> EventType<'config> {
}
}
fn parse_event(value: &Value, config: &Config) -> Result<ReedlineEvent, ShellError> {
fn parse_event(value: &Value, config: &Config) -> Result<Option<ReedlineEvent>, ShellError> {
match value {
Value::Record { cols, vals, span } => {
match EventType::try_from_columns(cols, vals, span)? {
@ -366,7 +737,8 @@ fn parse_event(value: &Value, config: &Config) -> Result<ReedlineEvent, ShellErr
vals,
config,
span,
),
)
.map(Some),
EventType::Edit(value) => {
let edit = edit_from_record(
value.into_string("", config).to_lowercase().as_str(),
@ -375,16 +747,26 @@ fn parse_event(value: &Value, config: &Config) -> Result<ReedlineEvent, ShellErr
config,
span,
)?;
Ok(ReedlineEvent::Edit(vec![edit]))
Ok(Some(ReedlineEvent::Edit(vec![edit])))
}
EventType::Until(value) => match value {
Value::List { vals, .. } => {
let events = vals
.iter()
.map(|value| parse_event(value, config))
.map(|value| match parse_event(value, config) {
Ok(inner) => match inner {
None => Err(ShellError::UnsupportedConfigValue(
"List containing valid events".to_string(),
"Nothing value (null)".to_string(),
value.span()?,
)),
Some(event) => Ok(event),
},
Err(e) => Err(e),
})
.collect::<Result<Vec<ReedlineEvent>, ShellError>>()?;
Ok(ReedlineEvent::UntilFound(events))
Ok(Some(ReedlineEvent::UntilFound(events)))
}
v => Err(ShellError::UnsupportedConfigValue(
"list of events".to_string(),
@ -397,13 +779,24 @@ fn parse_event(value: &Value, config: &Config) -> Result<ReedlineEvent, ShellErr
Value::List { vals, .. } => {
let events = vals
.iter()
.map(|value| parse_event(value, config))
.map(|value| match parse_event(value, config) {
Ok(inner) => match inner {
None => Err(ShellError::UnsupportedConfigValue(
"List containing valid events".to_string(),
"Nothing value (null)".to_string(),
value.span()?,
)),
Some(event) => Ok(event),
},
Err(e) => Err(e),
})
.collect::<Result<Vec<ReedlineEvent>, ShellError>>()?;
Ok(ReedlineEvent::Multiple(events))
Ok(Some(ReedlineEvent::Multiple(events)))
}
Value::Nothing { .. } => Ok(None),
v => Err(ShellError::UnsupportedConfigValue(
"record or list of records".to_string(),
"record or list of records, null to unbind key".to_string(),
v.into_abbreviated_string(config),
v.span()?,
)),
@ -595,7 +988,7 @@ mod test {
let config = Config::default();
let parsed_event = parse_event(&event, &config).unwrap();
assert_eq!(parsed_event, ReedlineEvent::Enter);
assert_eq!(parsed_event, Some(ReedlineEvent::Enter));
}
#[test]
@ -618,7 +1011,10 @@ mod test {
let config = Config::default();
let parsed_event = parse_event(&event, &config).unwrap();
assert_eq!(parsed_event, ReedlineEvent::Edit(vec![EditCommand::Clear]));
assert_eq!(
parsed_event,
Some(ReedlineEvent::Edit(vec![EditCommand::Clear]))
);
}
#[test]
@ -649,7 +1045,7 @@ mod test {
let parsed_event = parse_event(&event, &config).unwrap();
assert_eq!(
parsed_event,
ReedlineEvent::Menu("history_menu".to_string())
Some(ReedlineEvent::Menu("history_menu".to_string()))
);
}
@ -708,10 +1104,10 @@ mod test {
let parsed_event = parse_event(&event, &config).unwrap();
assert_eq!(
parsed_event,
ReedlineEvent::UntilFound(vec![
Some(ReedlineEvent::UntilFound(vec![
ReedlineEvent::Menu("history_menu".to_string()),
ReedlineEvent::Enter,
])
]))
);
}
@ -759,10 +1155,10 @@ mod test {
let parsed_event = parse_event(&event, &config).unwrap();
assert_eq!(
parsed_event,
ReedlineEvent::Multiple(vec![
Some(ReedlineEvent::Multiple(vec![
ReedlineEvent::Menu("history_menu".to_string()),
ReedlineEvent::Enter,
])
]))
);
}

View File

@ -1,10 +1,10 @@
use crate::reedline_config::{add_completion_menu, add_history_menu};
use crate::reedline_config::add_menus;
use crate::{completions::NuCompleter, NuHighlighter, NuValidator, NushellPrompt};
use crate::{prompt_update, reedline_config};
use crate::{
reedline_config::KeybindingsMode,
util::{eval_source, report_error},
};
use crate::{NuCompleter, NuHighlighter, NuValidator, NushellPrompt};
use log::info;
use log::trace;
use miette::{IntoDiagnostic, Result};
@ -27,48 +27,11 @@ pub fn evaluate_repl(
history_path: Option<PathBuf>,
is_perf_true: bool,
) -> Result<()> {
// use crate::logger::{configure, logger};
use reedline::{FileBackedHistory, Reedline, Signal};
let mut entry_num = 0;
let mut nu_prompt = NushellPrompt::new();
// let mut stack = nu_protocol::engine::Stack::new();
// First, set up env vars as strings only
// gather_parent_env_vars(engine_state);
// Set up our initial config to start from
// stack.vars.insert(
// CONFIG_VARIABLE_ID,
// Value::Record {
// cols: vec![],
// vals: vec![],
// span: Span::new(0, 0),
// },
// );
if is_perf_true {
info!("read_plugin_file {}:{}:{}", file!(), line!(), column!());
}
// #[cfg(feature = "plugin")]
// config_files::read_plugin_file(engine_state, &mut stack, is_perf_true);
//
// if is_perf_true {
// info!("read_config_file {}:{}:{}", file!(), line!(), column!());
// }
//
// config_files::read_config_file(engine_state, &mut stack, config_file, is_perf_true);
// let history_path = config_files::create_history_path();
// logger(|builder| {
// configure(&config.log_level, builder)?;
// // trace_filters(self, builder)?;
// // debug_filters(self, builder)?;
// Ok(())
// })?;
if is_perf_true {
info!(
@ -85,10 +48,6 @@ pub fn evaluate_repl(
report_error(&working_set, &e);
}
// Make a note of the exceptions we see for externals that look like math expressions
let exceptions = crate::util::external_exceptions(engine_state, stack);
engine_state.external_exceptions = exceptions;
// seed env vars
stack.add_env_var(
"CMD_DURATION_MS".into(),
@ -106,6 +65,45 @@ pub fn evaluate_repl(
},
);
if is_perf_true {
info!(
"load config initially {}:{}:{}",
file!(),
line!(),
column!()
);
}
// Get the config once for the history `max_history_size`
// Updating that will not be possible in one session
let mut config = match stack.get_config() {
Ok(config) => config,
Err(e) => {
let working_set = StateWorkingSet::new(engine_state);
report_error(&working_set, &e);
Config::default()
}
};
if is_perf_true {
info!("setup reedline {}:{}:{}", file!(), line!(), column!());
}
let mut line_editor = Reedline::create();
if let Some(history_path) = history_path.as_deref() {
if is_perf_true {
info!("setup history {}:{}:{}", file!(), line!(), column!());
}
let history = Box::new(
FileBackedHistory::with_file(
config.max_history_size as usize,
history_path.to_path_buf(),
)
.into_diagnostic()?,
);
line_editor = line_editor.with_history(history);
};
loop {
if is_perf_true {
info!(
@ -116,7 +114,7 @@ pub fn evaluate_repl(
);
}
let config = match stack.get_config() {
config = match stack.get_config() {
Ok(config) => config,
Err(e) => {
let working_set = StateWorkingSet::new(engine_state);
@ -126,17 +124,22 @@ pub fn evaluate_repl(
}
};
if is_perf_true {
info!("setup colors {}:{}:{}", file!(), line!(), column!());
}
let color_hm = get_color_config(&config);
//Reset the ctrl-c handler
if let Some(ctrlc) = &mut engine_state.ctrlc {
ctrlc.store(false, Ordering::SeqCst);
}
if is_perf_true {
info!("setup line editor {}:{}:{}", file!(), line!(), column!());
info!("update reedline {}:{}:{}", file!(), line!(), column!());
}
let mut line_editor = Reedline::create()
.into_diagnostic()?
let engine_reference = std::sync::Arc::new(engine_state.clone());
line_editor = line_editor
.with_highlighter(Box::new(NuHighlighter {
engine_state: engine_state.clone(),
config: config.clone(),
@ -146,59 +149,38 @@ pub fn evaluate_repl(
engine_state: engine_state.clone(),
}))
.with_completer(Box::new(NuCompleter::new(
engine_state.clone(),
engine_reference.clone(),
stack.clone(),
stack.vars.get(&CONFIG_VARIABLE_ID).cloned(),
)))
.with_quick_completions(config.quick_completions)
.with_partial_completions(config.partial_completions)
.with_ansi_colors(config.use_ansi_coloring);
if is_perf_true {
info!("setup reedline {}:{}:{}", file!(), line!(), column!());
}
line_editor = add_completion_menu(line_editor, &config);
line_editor = add_history_menu(line_editor, &config);
if is_perf_true {
info!("setup colors {}:{}:{}", file!(), line!(), column!());
}
//FIXME: if config.use_ansi_coloring is false then we should
// turn off the hinter but I don't see any way to do that yet.
let color_hm = get_color_config(&config);
if is_perf_true {
info!(
"setup history and hinter {}:{}:{}",
file!(),
line!(),
column!()
);
}
line_editor = if let Some(history_path) = history_path.clone() {
let history = std::fs::read_to_string(&history_path);
if history.is_ok() {
line_editor
.with_hinter(Box::new(
DefaultHinter::default().with_style(color_hm["hints"]),
))
.with_history(Box::new(
FileBackedHistory::with_file(
config.max_history_size as usize,
history_path.clone(),
)
.into_diagnostic()?,
))
.into_diagnostic()?
} else {
line_editor
}
line_editor = if config.use_ansi_coloring {
line_editor.with_hinter(Box::new(
DefaultHinter::default().with_style(color_hm["hints"]),
))
} else {
line_editor
line_editor.disable_hints()
};
line_editor = match add_menus(line_editor, engine_reference, stack, &config) {
Ok(line_editor) => line_editor,
Err(e) => {
let working_set = StateWorkingSet::new(engine_state);
report_error(&working_set, &e);
Reedline::create()
}
};
if config.sync_history_on_enter {
if is_perf_true {
info!("sync history {}:{}:{}", file!(), line!(), column!());
}
line_editor.sync_history().into_diagnostic()?;
}
if is_perf_true {
info!("setup keybindings {}:{}:{}", file!(), line!(), column!());
}
@ -338,10 +320,6 @@ pub fn evaluate_repl(
let _ = std::env::set_current_dir(path);
engine_state.env_vars.insert("PWD".into(), cwd);
}
// Make a note of the exceptions we see for externals that look like math expressions
let exceptions = crate::util::external_exceptions(engine_state, stack);
engine_state.external_exceptions = exceptions;
}
Ok(Signal::CtrlC) => {
// `Reedline` clears the line content. New prompt is shown

View File

@ -98,19 +98,6 @@ pub fn print_pipeline_data(
// env vars into it (in a "NAME"="value" format, quite similar to the output of the Unix 'env'
// tool), then uses the file to get the spans. The file stays in memory, no filesystem IO is done.
pub fn gather_parent_env_vars(engine_state: &mut EngineState) {
// Some helper functions
fn get_surround_char(s: &str) -> Option<char> {
if s.contains('"') {
if s.contains('\'') {
None
} else {
Some('\'')
}
} else {
Some('\'')
}
}
fn report_capture_error(engine_state: &EngineState, env_str: &str, msg: &str) {
let working_set = StateWorkingSet::new(engine_state);
report_error(
@ -122,32 +109,14 @@ pub fn gather_parent_env_vars(engine_state: &mut EngineState) {
);
}
fn put_env_to_fake_file(
name: &str,
val: &str,
fake_env_file: &mut String,
engine_state: &EngineState,
) {
let (c_name, c_val) =
if let (Some(cn), Some(cv)) = (get_surround_char(name), get_surround_char(val)) {
(cn, cv)
} else {
// environment variable with its name or value containing both ' and " is ignored
report_capture_error(
engine_state,
&format!("{}={}", name, val),
"Name or value should not contain both ' and \" at the same time.",
);
return;
};
fake_env_file.push(c_name);
fn put_env_to_fake_file(name: &str, val: &str, fake_env_file: &mut String) {
fake_env_file.push('`');
fake_env_file.push_str(name);
fake_env_file.push(c_name);
fake_env_file.push('`');
fake_env_file.push('=');
fake_env_file.push(c_val);
fake_env_file.push('`');
fake_env_file.push_str(val);
fake_env_file.push(c_val);
fake_env_file.push('`');
fake_env_file.push('\n');
}
@ -157,12 +126,7 @@ pub fn gather_parent_env_vars(engine_state: &mut EngineState) {
if std::env::var("PWD").is_err() {
match std::env::current_dir() {
Ok(cwd) => {
put_env_to_fake_file(
"PWD",
&cwd.to_string_lossy(),
&mut fake_env_file,
engine_state,
);
put_env_to_fake_file("PWD", &cwd.to_string_lossy(), &mut fake_env_file);
}
Err(e) => {
// Could not capture current working directory
@ -180,7 +144,7 @@ pub fn gather_parent_env_vars(engine_state: &mut EngineState) {
// Write all the env vars into a fake file
for (name, val) in std::env::vars() {
put_env_to_fake_file(&name, &val, &mut fake_env_file, engine_state);
put_env_to_fake_file(&name, &val, &mut fake_env_file);
}
// Lex the fake file, assign spans to all environment variables and add them
@ -290,6 +254,7 @@ pub fn eval_source(
&[],
);
if let Some(err) = err {
set_last_exit_code(stack, 1);
report_error(&working_set, &err);
return false;
}
@ -317,22 +282,10 @@ pub fn eval_source(
if let Some(exit_code) = exit_code.take().and_then(|it| it.last()) {
stack.add_env_var("LAST_EXIT_CODE".to_string(), exit_code);
} else {
stack.add_env_var(
"LAST_EXIT_CODE".to_string(),
Value::Int {
val: 0,
span: Span { start: 0, end: 0 },
},
);
set_last_exit_code(stack, 0);
}
} else {
stack.add_env_var(
"LAST_EXIT_CODE".to_string(),
Value::Int {
val: 0,
span: Span { start: 0, end: 0 },
},
);
set_last_exit_code(stack, 0);
}
if let Err(err) = print_pipeline_data(pipeline_data, engine_state, stack) {
@ -350,13 +303,7 @@ pub fn eval_source(
}
}
Err(err) => {
stack.add_env_var(
"LAST_EXIT_CODE".to_string(),
Value::Int {
val: 1,
span: Span { start: 0, end: 0 },
},
);
set_last_exit_code(stack, 1);
let working_set = StateWorkingSet::new(engine_state);
@ -369,96 +316,14 @@ pub fn eval_source(
true
}
fn seems_like_number(bytes: &[u8]) -> bool {
if bytes.is_empty() {
false
} else {
let b = bytes[0];
b == b'0'
|| b == b'1'
|| b == b'2'
|| b == b'3'
|| b == b'4'
|| b == b'5'
|| b == b'6'
|| b == b'7'
|| b == b'8'
|| b == b'9'
|| b == b'('
|| b == b'{'
|| b == b'['
|| b == b'$'
|| b == b'"'
|| b == b'\''
|| b == b'-'
}
}
/// Finds externals that have names that look like math expressions
pub fn external_exceptions(engine_state: &EngineState, stack: &Stack) -> Vec<Vec<u8>> {
let mut executables = vec![];
if let Some(path) = stack.get_env_var(engine_state, "PATH") {
match path {
Value::List { vals, .. } => {
for val in vals {
let path = val.as_string();
if let Ok(path) = path {
if let Ok(mut contents) = std::fs::read_dir(path) {
while let Some(Ok(item)) = contents.next() {
if is_executable::is_executable(&item.path()) {
if let Ok(name) = item.file_name().into_string() {
if seems_like_number(name.as_bytes()) {
let name = name.as_bytes().to_vec();
executables.push(name);
}
}
if let Some(name) = item.path().file_stem() {
let name = name.to_string_lossy();
if seems_like_number(name.as_bytes()) {
let name = name.as_bytes().to_vec();
executables.push(name);
}
}
}
}
}
}
}
}
Value::String { val, .. } => {
for path in std::env::split_paths(&val) {
let path = path.to_string_lossy().to_string();
if let Ok(mut contents) = std::fs::read_dir(path) {
while let Some(Ok(item)) = contents.next() {
if is_executable::is_executable(&item.path()) {
if let Ok(name) = item.file_name().into_string() {
if seems_like_number(name.as_bytes()) {
let name = name.as_bytes().to_vec();
executables.push(name);
}
}
if let Some(name) = item.path().file_stem() {
let name = name.to_string_lossy();
if seems_like_number(name.as_bytes()) {
let name = name.as_bytes().to_vec();
executables.push(name);
}
}
}
}
}
}
}
_ => {}
}
}
executables
fn set_last_exit_code(stack: &mut Stack, exit_code: i64) {
stack.add_env_var(
"LAST_EXIT_CODE".to_string(),
Value::Int {
val: exit_code,
span: Span { start: 0, end: 0 },
},
);
}
pub fn report_error(

View File

@ -1,13 +1,14 @@
[package]
name = "nu-color-config"
version = "0.60.0"
authors = ["The Nushell Project Developers"]
description = "Color configuration code used by Nushell"
edition = "2021"
license = "MIT"
name = "nu-color-config"
version = "0.61.0"
[dependencies]
nu-protocol = { path = "../nu-protocol", version = "0.60.0" }
nu-ansi-term = "0.45.0"
nu-json = { path = "../nu-json", version = "0.60.0" }
nu-table = { path = "../nu-table", version = "0.60.0" }
nu-protocol = { path = "../nu-protocol", version = "0.61.0" }
nu-ansi-term = "0.45.1"
nu-json = { path = "../nu-json", version = "0.61.0" }
nu-table = { path = "../nu-table", version = "0.61.0" }
serde = { version="1.0.123", features=["derive"] }

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 - 2022 The Nushell Project Developers
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,27 +1,29 @@
[package]
name = "nu-command"
version = "0.60.0"
authors = ["The Nushell Project Developers"]
description = "Nushell's built-in commands"
edition = "2021"
license = "MIT"
name = "nu-command"
version = "0.61.0"
build = "build.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
nu-color-config = { path = "../nu-color-config", version = "0.60.0" }
nu-engine = { path = "../nu-engine", version = "0.60.0" }
nu-glob = { path = "../nu-glob", version = "0.60.0" }
nu-json = { path = "../nu-json", version = "0.60.0" }
nu-parser = { path = "../nu-parser", version = "0.60.0" }
nu-path = { path = "../nu-path", version = "0.60.0" }
nu-pretty-hex = { path = "../nu-pretty-hex", version = "0.60.0" }
nu-protocol = { path = "../nu-protocol", version = "0.60.0" }
nu-system = { path = "../nu-system", version = "0.60.0" }
nu-table = { path = "../nu-table", version = "0.60.0" }
nu-term-grid = { path = "../nu-term-grid", version = "0.60.0" }
nu-test-support = { path = "../nu-test-support", version = "0.60.0" }
nu-utils = { path = "../nu-utils", version = "0.60.0" }
nu-ansi-term = "0.45.0"
nu-color-config = { path = "../nu-color-config", version = "0.61.0" }
nu-engine = { path = "../nu-engine", version = "0.61.0" }
nu-glob = { path = "../nu-glob", version = "0.61.0" }
nu-json = { path = "../nu-json", version = "0.61.0" }
nu-parser = { path = "../nu-parser", version = "0.61.0" }
nu-path = { path = "../nu-path", version = "0.61.0" }
nu-pretty-hex = { path = "../nu-pretty-hex", version = "0.61.0" }
nu-protocol = { path = "../nu-protocol", version = "0.61.0" }
nu-system = { path = "../nu-system", version = "0.61.0" }
nu-table = { path = "../nu-table", version = "0.61.0" }
nu-term-grid = { path = "../nu-term-grid", version = "0.61.0" }
nu-test-support = { path = "../nu-test-support", version = "0.61.0" }
nu-utils = { path = "../nu-utils", version = "0.61.0" }
nu-ansi-term = "0.45.1"
# Potential dependencies for extras
base64 = "0.13.0"
@ -31,13 +33,14 @@ chrono = { version = "0.4.19", features = ["serde"] }
chrono-humanize = "0.2.1"
chrono-tz = "0.6.0"
crossterm = "0.23.0"
csv = "1.1.3"
csv = "1.1.6"
dialoguer = "0.9.0"
digest = "0.10.0"
dtparse = "1.2.0"
eml-parser = "0.1.0"
encoding_rs = "0.8.30"
filesize = "0.2.0"
filetime = "0.2.15"
fs_extra = "1.2.0"
htmlescape = "0.3.1"
ical = "0.7.0"
@ -64,25 +67,29 @@ serde_ini = "0.2.0"
serde_urlencoded = "0.7.0"
serde_yaml = "0.8.16"
sha2 = "0.10.0"
shadow-rs = "0.8.1"
shadow-rs = "0.11.0"
strip-ansi-escapes = "0.1.1"
sysinfo = "0.23.5"
terminal_size = "0.1.17"
thiserror = "1.0.29"
titlecase = "1.1.0"
toml = "0.5.8"
trash = { version = "2.0.2", optional = true }
unicode-segmentation = "1.8.0"
url = "2.2.1"
uuid = { version = "0.8.2", features = ["v4"] }
which = { version = "4.2.2", optional = true }
reedline = "0.3.0"
reedline = { version = "0.4.0", features = ["bashisms"]}
wax = { version = "0.4.0", features = ["diagnostics"] }
zip = { version="0.5.9", optional = true }
[target.'cfg(unix)'.dependencies]
umask = "1.0.0"
users = "0.11.0"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies.trash]
version = "2.0.2"
optional = true
[dependencies.polars]
version = "0.20.0"
optional = true
@ -95,11 +102,12 @@ features = [
[features]
trash-support = ["trash"]
which-support = ["which"]
plugin = ["nu-parser/plugin"]
dataframe = ["polars", "num"]
[build-dependencies]
shadow-rs = "0.8.1"
shadow-rs = "0.11.0"
[dev-dependencies]
hamcrest2 = "0.3.0"

21
crates/nu-command/LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 - 2022 The Nushell Project Developers
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -14,7 +14,7 @@ impl Command for Fmt {
}
fn usage(&self) -> &str {
"format numbers"
"Format a number"
}
fn signature(&self) -> nu_protocol::Signature {
@ -23,7 +23,7 @@ impl Command for Fmt {
fn examples(&self) -> Vec<Example> {
vec![Example {
description: "format numbers",
description: "Get a record containing multiple formats for the number 42",
example: "42 | fmt",
result: Some(Value::Record {
cols: vec![

View File

@ -136,10 +136,11 @@ fn string_to_boolean(s: &str, span: Span) -> Result<bool, ShellError> {
let val = o.parse::<f64>();
match val {
Ok(f) => Ok(f.abs() >= f64::EPSILON),
Err(_) => Err(ShellError::CantConvert(
Err(_) => Err(ShellError::CantConvertWithHelp(
"boolean".to_string(),
"string".to_string(),
span,
r#"the strings "true" and "false" can be converted into a bool"#.to_string(),
)),
}
}

View File

@ -18,7 +18,7 @@ impl Command for Into {
}
fn usage(&self) -> &str {
"Apply into function."
"Commands to convert data from one type to another."
}
fn run(

View File

@ -101,7 +101,7 @@ impl Command for SubCommand {
}
fn usage(&self) -> &str {
"converts text into datetime"
"Convert text into a datetime"
}
fn examples(&self) -> Vec<Example> {
@ -260,10 +260,11 @@ fn action(
Ok(d) => Value::Date { val: d, span: head },
Err(reason) => {
return Value::Error {
error: ShellError::CantConvert(
error: ShellError::CantConvertWithHelp(
format!("could not parse as datetime using format '{}'", dt.0),
reason.to_string(),
head,
"you can use `into datetime` without a format string to enable flexible parsing".to_string()
),
}
}

View File

@ -22,7 +22,7 @@ impl Command for SubCommand {
}
fn usage(&self) -> &str {
"converts text into decimal"
"Convert text into a decimal"
}
fn run(

View File

@ -151,10 +151,11 @@ fn string_to_duration(s: &str, span: Span) -> Result<i64, ShellError> {
}
}
Err(ShellError::CantConvert(
Err(ShellError::CantConvertWithHelp(
"duration".to_string(),
"string".to_string(),
span,
"supported units are ns, us, ms, sec, min, hr, day, and wk".to_string(),
))
}

View File

@ -206,8 +206,8 @@ fn convert_int(input: &Value, head: Span, radix: u32) -> Value {
};
match i64::from_str_radix(&i, radix) {
Ok(n) => Value::Int { val: n, span: head },
Err(reason) => Value::Error {
error: ShellError::CantConvert("".to_string(), reason.to_string(), head),
Err(_reason) => Value::Error {
error: ShellError::CantConvert("int".to_string(), "string".to_string(), head),
},
}
}
@ -218,11 +218,12 @@ fn int_from_string(a_string: &str, span: Span) -> Result<i64, ShellError> {
b if b.starts_with("0b") => {
let num = match i64::from_str_radix(b.trim_start_matches("0b"), 2) {
Ok(n) => n,
Err(reason) => {
return Err(ShellError::CantConvert(
"could not parse as integer".to_string(),
reason.to_string(),
Err(_reason) => {
return Err(ShellError::CantConvertWithHelp(
"int".to_string(),
"string".to_string(),
span,
r#"digits following "0b" can only be 0 or 1"#.to_string(),
))
}
};
@ -231,11 +232,13 @@ fn int_from_string(a_string: &str, span: Span) -> Result<i64, ShellError> {
h if h.starts_with("0x") => {
let num = match i64::from_str_radix(h.trim_start_matches("0x"), 16) {
Ok(n) => n,
Err(reason) => {
return Err(ShellError::CantConvert(
"could not parse as int".to_string(),
reason.to_string(),
Err(_reason) => {
return Err(ShellError::CantConvertWithHelp(
"int".to_string(),
"string".to_string(),
span,
r#"hexadecimal digits following "0x" should be in 0-9, a-f, or A-F"#
.to_string(),
))
}
};
@ -246,7 +249,7 @@ fn int_from_string(a_string: &str, span: Span) -> Result<i64, ShellError> {
Err(_) => match a_string.parse::<f64>() {
Ok(f) => Ok(f as i64),
_ => Err(ShellError::CantConvert(
"into int".to_string(),
"int".to_string(),
"string".to_string(),
span,
)),

View File

@ -257,6 +257,14 @@ pub fn action(
span,
),
},
Value::Binary { .. } => Value::Error {
error: ShellError::CantConvertWithHelp(
"string".into(),
"binary".into(),
span,
"try using the `decode` command".into(),
),
},
x => Value::Error {
error: ShellError::CantConvert(String::from("string"), x.get_type().to_string(), span),
},

View File

@ -25,6 +25,15 @@ impl Command for Alias {
.category(Category::Core)
}
fn extra_usage(&self) -> &str {
r#"This command is a parser keyword. For details, check
https://www.nushell.sh/book/thinking_in_nushell.html#parsing-and-evaluation-are-different-stages"#
}
fn is_parser_keyword(&self) -> bool {
true
}
fn run(
&self,
_engine_state: &EngineState,

View File

@ -26,6 +26,15 @@ impl Command for Def {
.category(Category::Core)
}
fn extra_usage(&self) -> &str {
r#"This command is a parser keyword. For details, check
https://www.nushell.sh/book/thinking_in_nushell.html#parsing-and-evaluation-are-different-stages"#
}
fn is_parser_keyword(&self) -> bool {
true
}
fn run(
&self,
_engine_state: &EngineState,

View File

@ -26,6 +26,15 @@ impl Command for DefEnv {
.category(Category::Core)
}
fn extra_usage(&self) -> &str {
r#"This command is a parser keyword. For details, check
https://www.nushell.sh/book/thinking_in_nushell.html#parsing-and-evaluation-are-different-stages"#
}
fn is_parser_keyword(&self) -> bool {
true
}
fn run(
&self,
_engine_state: &EngineState,

View File

@ -17,7 +17,6 @@ impl Command for Do {
fn signature(&self) -> nu_protocol::Signature {
Signature::build("do")
.desc(self.usage())
.required("block", SyntaxShape::Any, "the block to run")
.switch(
"ignore-errors",

View File

@ -21,6 +21,15 @@ impl Command for ExportCommand {
"Export custom commands or environment variables from a module."
}
fn extra_usage(&self) -> &str {
r#"This command is a parser keyword. For details, check
https://www.nushell.sh/book/thinking_in_nushell.html#parsing-and-evaluation-are-different-stages"#
}
fn is_parser_keyword(&self) -> bool {
true
}
fn run(
&self,
engine_state: &EngineState,

View File

@ -15,7 +15,7 @@ impl Command for ExportAlias {
}
fn signature(&self) -> nu_protocol::Signature {
Signature::build("export def")
Signature::build("export alias")
.required("name", SyntaxShape::String, "name of the alias")
.required(
"initial_value",
@ -25,6 +25,15 @@ impl Command for ExportAlias {
.category(Category::Core)
}
fn extra_usage(&self) -> &str {
r#"This command is a parser keyword. For details, check
https://www.nushell.sh/book/thinking_in_nushell.html#parsing-and-evaluation-are-different-stages"#
}
fn is_parser_keyword(&self) -> bool {
true
}
fn run(
&self,
_engine_state: &EngineState,

View File

@ -26,6 +26,15 @@ impl Command for ExportDef {
.category(Category::Core)
}
fn extra_usage(&self) -> &str {
r#"This command is a parser keyword. For details, check
https://www.nushell.sh/book/thinking_in_nushell.html#parsing-and-evaluation-are-different-stages"#
}
fn is_parser_keyword(&self) -> bool {
true
}
fn run(
&self,
_engine_state: &EngineState,

View File

@ -26,6 +26,15 @@ impl Command for ExportDefEnv {
.category(Category::Core)
}
fn extra_usage(&self) -> &str {
r#"This command is a parser keyword. For details, check
https://www.nushell.sh/book/thinking_in_nushell.html#parsing-and-evaluation-are-different-stages"#
}
fn is_parser_keyword(&self) -> bool {
true
}
fn run(
&self,
_engine_state: &EngineState,

View File

@ -29,6 +29,15 @@ impl Command for ExportEnv {
.category(Category::Core)
}
fn extra_usage(&self) -> &str {
r#"This command is a parser keyword. For details, check
https://www.nushell.sh/book/thinking_in_nushell.html#parsing-and-evaluation-are-different-stages"#
}
fn is_parser_keyword(&self) -> bool {
true
}
fn run(
&self,
_engine_state: &EngineState,

View File

@ -21,6 +21,15 @@ impl Command for ExportExtern {
.category(Category::Core)
}
fn extra_usage(&self) -> &str {
r#"This command is a parser keyword. For details, check
https://www.nushell.sh/book/thinking_in_nushell.html#parsing-and-evaluation-are-different-stages"#
}
fn is_parser_keyword(&self) -> bool {
true
}
fn run(
&self,
_engine_state: &EngineState,

View File

@ -21,6 +21,15 @@ impl Command for Extern {
.category(Category::Core)
}
fn extra_usage(&self) -> &str {
r#"This command is a parser keyword. For details, check
https://www.nushell.sh/book/thinking_in_nushell.html#parsing-and-evaluation-are-different-stages"#
}
fn is_parser_keyword(&self) -> bool {
true
}
fn run(
&self,
_engine_state: &EngineState,

View File

@ -44,6 +44,15 @@ impl Command for For {
.category(Category::Core)
}
fn extra_usage(&self) -> &str {
r#"This command is a parser keyword. For details, check
https://www.nushell.sh/book/thinking_in_nushell.html#parsing-and-evaluation-are-different-stages"#
}
fn is_parser_keyword(&self) -> bool {
true
}
fn run(
&self,
engine_state: &EngineState,
@ -52,11 +61,15 @@ impl Command for For {
_input: PipelineData,
) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
let head = call.head;
let var_id = call.positional[0]
let var_id = call
.positional_nth(0)
.expect("checked through parser")
.as_var()
.expect("internal error: missing variable");
let keyword_expr = call.positional[1]
let keyword_expr = call
.positional_nth(1)
.expect("checked through parser")
.as_keyword()
.expect("internal error: missing keyword");
let values = eval_expression(engine_state, stack, keyword_expr)?;
@ -116,7 +129,7 @@ impl Command for For {
.filter(|x| !x.is_nothing())
.into_pipeline_data(ctrlc)),
Value::Range { val, .. } => Ok(val
.into_range_iter()?
.into_range_iter(ctrlc.clone())?
.enumerate()
.map(move |(idx, x)| {
stack.with_env(&orig_env_vars, &orig_env_hidden);

View File

@ -7,6 +7,8 @@ use nu_protocol::{
use nu_engine::{get_full_help, CallExt};
use std::borrow::Borrow;
#[derive(Clone)]
pub struct Help;
@ -25,7 +27,7 @@ impl Command for Help {
.named(
"find",
SyntaxShape::String,
"string to find in command usage",
"string to find in command names, usage, and search terms",
Some('f'),
)
.category(Category::Core)
@ -68,7 +70,7 @@ impl Command for Help {
result: None,
},
Example {
description: "search for string in command usage",
description: "search for string in command names, usage and search terms",
example: "help --find char",
result: None,
},
@ -85,22 +87,33 @@ fn help(
let find: Option<Spanned<String>> = call.get_flag(engine_state, stack, "find")?;
let rest: Vec<Spanned<String>> = call.rest(engine_state, stack, 0)?;
let full_commands = engine_state.get_signatures_with_examples(false);
let commands = engine_state.get_decl_ids_sorted(false);
if let Some(f) = find {
let search_string = f.item.to_lowercase();
let mut found_cmds_vec = Vec::new();
for (sig, _, is_plugin, is_custom) in full_commands {
for decl_id in commands {
let mut cols = vec![];
let mut vals = vec![];
let key = sig.name.clone();
let c = sig.usage.clone();
let e = sig.extra_usage.clone();
let decl = engine_state.get_decl(decl_id);
let sig = decl.signature().update_from_command(decl.borrow());
let key = sig.name;
let usage = sig.usage;
let search_terms = sig.search_terms;
let matches_term = if !search_terms.is_empty() {
search_terms
.iter()
.any(|term| term.to_lowercase().contains(&search_string))
} else {
false
};
if key.to_lowercase().contains(&search_string)
|| c.to_lowercase().contains(&search_string)
|| e.to_lowercase().contains(&search_string)
|| usage.to_lowercase().contains(&search_string)
|| matches_term
{
cols.push("name".into());
vals.push(Value::String {
@ -116,21 +129,37 @@ fn help(
cols.push("is_plugin".into());
vals.push(Value::Bool {
val: is_plugin,
val: decl.is_plugin().is_some(),
span: head,
});
cols.push("is_custom".into());
vals.push(Value::Bool {
val: is_custom,
val: decl.get_block_id().is_some(),
span: head,
});
cols.push("is_keyword".into());
vals.push(Value::Bool {
val: decl.is_parser_keyword(),
span: head,
});
cols.push("usage".into());
vals.push(Value::String { val: c, span: head });
vals.push(Value::String {
val: usage,
span: head,
});
cols.push("extra_usage".into());
vals.push(Value::String { val: e, span: head });
cols.push("search_terms".into());
vals.push(if search_terms.is_empty() {
Value::nothing(head)
} else {
Value::String {
val: search_terms.join(", "),
span: head,
}
});
found_cmds_vec.push(Value::Record {
cols,
@ -149,13 +178,16 @@ fn help(
let mut found_cmds_vec = Vec::new();
if rest[0].item == "commands" {
for (sig, _, is_plugin, is_custom) in full_commands {
for decl_id in commands {
let mut cols = vec![];
let mut vals = vec![];
let key = sig.name.clone();
let c = sig.usage.clone();
let e = sig.extra_usage.clone();
let decl = engine_state.get_decl(decl_id);
let sig = decl.signature().update_from_command(decl.borrow());
let key = sig.name;
let usage = sig.usage;
let search_terms = sig.search_terms;
cols.push("name".into());
vals.push(Value::String {
@ -171,21 +203,37 @@ fn help(
cols.push("is_plugin".into());
vals.push(Value::Bool {
val: is_plugin,
val: decl.is_plugin().is_some(),
span: head,
});
cols.push("is_custom".into());
vals.push(Value::Bool {
val: is_custom,
val: decl.get_block_id().is_some(),
span: head,
});
cols.push("is_keyword".into());
vals.push(Value::Bool {
val: decl.is_parser_keyword(),
span: head,
});
cols.push("usage".into());
vals.push(Value::String { val: c, span: head });
vals.push(Value::String {
val: usage,
span: head,
});
cols.push("extra_usage".into());
vals.push(Value::String { val: e, span: head });
cols.push("search_terms".into());
vals.push(if search_terms.is_empty() {
Value::nothing(head)
} else {
Value::String {
val: search_terms.join(", "),
span: head,
}
});
found_cmds_vec.push(Value::Record {
cols,
@ -207,7 +255,8 @@ fn help(
name.push_str(&r.item);
}
let output = full_commands
let output = engine_state
.get_signatures_with_examples(false)
.iter()
.filter(|(signature, _, _, _)| signature.name == name)
.map(|(signature, examples, _, _)| {

View File

@ -23,7 +23,15 @@ impl Command for Hide {
}
fn extra_usage(&self) -> &str {
"Symbols are hidden by priority: First aliases, then custom commands, then environment variables."
r#"Symbols are hidden by priority: First aliases, then custom commands, then environment variables.
This command is a parser keyword. For details, check
https://www.nushell.sh/book/thinking_in_nushell.html#parsing-and-evaluation-are-different-stages
"#
}
fn is_parser_keyword(&self) -> bool {
true
}
fn run(
@ -36,7 +44,7 @@ impl Command for Hide {
let import_pattern = if let Some(Expression {
expr: Expr::ImportPattern(pat),
..
}) = call.positional.get(0)
}) = call.positional_nth(0)
{
pat
} else {
@ -107,7 +115,11 @@ impl Command for Hide {
};
if stack.remove_env_var(engine_state, &name).is_none() {
return Err(ShellError::NotFound(call.positional[0].span));
return Err(ShellError::NotFound(
call.positional_nth(0)
.expect("already checked for present positional")
.span,
));
}
}
} else if !import_pattern.hidden.contains(&import_pattern.head.name)

View File

@ -34,6 +34,15 @@ impl Command for If {
.category(Category::Core)
}
fn extra_usage(&self) -> &str {
r#"This command is a parser keyword. For details, check
https://www.nushell.sh/book/thinking_in_nushell.html#parsing-and-evaluation-are-different-stages"#
}
fn is_parser_keyword(&self) -> bool {
true
}
fn run(
&self,
engine_state: &EngineState,
@ -41,9 +50,9 @@ impl Command for If {
call: &Call,
input: PipelineData,
) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
let cond = &call.positional[0];
let cond = call.positional_nth(0).expect("checked through parser");
let then_block: CaptureBlock = call.req(engine_state, stack, 1)?;
let else_case = call.positional.get(2);
let else_case = call.positional_nth(2);
let result = eval_expression(engine_state, stack, cond)?;
match &result {

View File

@ -26,6 +26,15 @@ impl Command for Let {
.category(Category::Core)
}
fn extra_usage(&self) -> &str {
r#"This command is a parser keyword. For details, check
https://www.nushell.sh/book/thinking_in_nushell.html#parsing-and-evaluation-are-different-stages"#
}
fn is_parser_keyword(&self) -> bool {
true
}
fn run(
&self,
engine_state: &EngineState,
@ -33,11 +42,15 @@ impl Command for Let {
call: &Call,
input: PipelineData,
) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
let var_id = call.positional[0]
let var_id = call
.positional_nth(0)
.expect("checked through parser")
.as_var()
.expect("internal error: missing variable");
let keyword_expr = call.positional[1]
let keyword_expr = call
.positional_nth(1)
.expect("checked through parser")
.as_keyword()
.expect("internal error: missing keyword");

View File

@ -35,7 +35,7 @@ impl Command for Metadata {
call: &Call,
input: PipelineData,
) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
let arg = call.positional.get(0);
let arg = call.positional_nth(0);
let head = call.head;
match arg {

View File

@ -25,6 +25,15 @@ impl Command for Module {
.category(Category::Core)
}
fn extra_usage(&self) -> &str {
r#"This command is a parser keyword. For details, check
https://www.nushell.sh/book/thinking_in_nushell.html#parsing-and-evaluation-are-different-stages"#
}
fn is_parser_keyword(&self) -> bool {
true
}
fn run(
&self,
_engine_state: &EngineState,

View File

@ -41,6 +41,15 @@ impl Command for Register {
.category(Category::Core)
}
fn extra_usage(&self) -> &str {
r#"This command is a parser keyword. For details, check
https://www.nushell.sh/book/thinking_in_nushell.html#parsing-and-evaluation-are-different-stages"#
}
fn is_parser_keyword(&self) -> bool {
true
}
fn run(
&self,
_engine_state: &EngineState,
@ -55,12 +64,12 @@ impl Command for Register {
vec![
Example {
description: "Register `nu_plugin_query` plugin from ~/.cargo/bin/ dir",
example: r#"register -e capnp ~/.cargo/bin/nu_plugin_query"#,
example: r#"register -e json ~/.cargo/bin/nu_plugin_query"#,
result: None,
},
Example {
description: "Register `nu_plugin_query` plugin from `nu -c`(plugin will be available in that nu session only)",
example: r#"let plugin = ((which nu).path.0 | path dirname | path join 'nu_plugin_query'); nu -c $'register -e capnp ($plugin); version'"#,
example: r#"let plugin = ((which nu).path.0 | path dirname | path join 'nu_plugin_query'); nu -c $'register -e json ($plugin); version'"#,
result: None,
},
]

View File

@ -26,6 +26,15 @@ impl Command for Source {
"Runs a script file in the current context."
}
fn extra_usage(&self) -> &str {
r#"This command is a parser keyword. For details, check
https://www.nushell.sh/book/thinking_in_nushell.html#parsing-and-evaluation-are-different-stages"#
}
fn is_parser_keyword(&self) -> bool {
true
}
fn run(
&self,
engine_state: &EngineState,

View File

@ -35,6 +35,10 @@ impl Command for Tutor {
"Run the tutorial. To begin, run: tutor"
}
fn search_terms(&self) -> Vec<&str> {
vec!["help", "learn", "tutorial"]
}
fn run(
&self,
engine_state: &EngineState,
@ -351,7 +355,7 @@ ls | each {|x| $x.name}
```
The above will create a list of the filenames in the directory.
```
if true { echo "it's true" } { echo "it's not true" }
if true { echo "it's true" } else { echo "it's not true" }
```
This `if` call will run the first block if the expression is true, or the
second block if the expression is false.

View File

@ -23,6 +23,15 @@ impl Command for Use {
.category(Category::Core)
}
fn extra_usage(&self) -> &str {
r#"This command is a parser keyword. For details, check
https://www.nushell.sh/book/thinking_in_nushell.html#parsing-and-evaluation-are-different-stages"#
}
fn is_parser_keyword(&self) -> bool {
true
}
fn run(
&self,
engine_state: &EngineState,
@ -33,7 +42,7 @@ impl Command for Use {
let import_pattern = if let Some(Expression {
expr: Expr::ImportPattern(pat),
..
}) = call.positional.get(0)
}) = call.positional_nth(0)
{
pat
} else {
@ -72,7 +81,7 @@ impl Command for Use {
for (name, span) in names {
if let Some(id) = overlay.get_env_var_id(name) {
output.push((name.clone(), id));
} else if !overlay.has_decl(name) {
} else if !overlay.has_decl(name) && !overlay.has_alias(name) {
return Err(ShellError::EnvVarNotFoundAtRuntime(
String::from_utf8_lossy(name).into(),
*span,

View File

@ -1,4 +1,3 @@
use indexmap::IndexMap;
use nu_protocol::ast::Call;
use nu_protocol::engine::{Command, EngineState, Stack};
use nu_protocol::{Example, IntoPipelineData, PipelineData, ShellError, Signature, Value};
@ -49,144 +48,130 @@ pub fn version(
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
let tag = call.head;
let mut cols = vec![];
let mut vals = vec![];
let mut indexmap = IndexMap::with_capacity(4);
cols.push("version".to_string());
vals.push(Value::String {
val: env!("CARGO_PKG_VERSION").to_string(),
span: tag,
});
indexmap.insert(
"version".to_string(),
Value::String {
val: env!("CARGO_PKG_VERSION").to_string(),
span: tag,
},
);
cols.push("branch".to_string());
vals.push(Value::String {
val: shadow_rs::branch(),
span: call.head,
});
let branch: Option<&str> = Some(shadow::BRANCH).filter(|x| !x.is_empty());
if let Some(branch) = branch {
indexmap.insert(
"branch".to_string(),
Value::String {
val: branch.to_string(),
span: call.head,
},
);
}
cols.push("tag".to_string());
vals.push(Value::String {
val: shadow_rs::tag(),
span: call.head,
});
let short_commit: Option<&str> = Some(shadow::SHORT_COMMIT).filter(|x| !x.is_empty());
if let Some(short_commit) = short_commit {
indexmap.insert(
"short_commit".to_string(),
Value::String {
val: short_commit.to_string(),
span: call.head,
},
);
cols.push("short_commit".to_string());
vals.push(Value::String {
val: short_commit.to_string(),
span: call.head,
});
}
let commit_hash: Option<&str> = Some(shadow::COMMIT_HASH).filter(|x| !x.is_empty());
if let Some(commit_hash) = commit_hash {
indexmap.insert(
"commit_hash".to_string(),
Value::String {
val: commit_hash.to_string(),
span: call.head,
},
);
cols.push("commit_hash".to_string());
vals.push(Value::String {
val: commit_hash.to_string(),
span: call.head,
});
}
let commit_date: Option<&str> = Some(shadow::COMMIT_DATE).filter(|x| !x.is_empty());
if let Some(commit_date) = commit_date {
indexmap.insert(
"commit_date".to_string(),
Value::String {
val: commit_date.to_string(),
span: call.head,
},
);
cols.push("commit_date".to_string());
vals.push(Value::String {
val: commit_date.to_string(),
span: call.head,
});
}
let build_os: Option<&str> = Some(shadow::BUILD_OS).filter(|x| !x.is_empty());
if let Some(build_os) = build_os {
indexmap.insert(
"build_os".to_string(),
Value::String {
val: build_os.to_string(),
span: call.head,
},
);
cols.push("build_os".to_string());
vals.push(Value::String {
val: build_os.to_string(),
span: call.head,
});
}
let build_target: Option<&str> = Some(shadow::BUILD_TARGET).filter(|x| !x.is_empty());
if let Some(build_target) = build_target {
cols.push("build_os".to_string());
vals.push(Value::String {
val: build_target.to_string(),
span: call.head,
});
}
let rust_version: Option<&str> = Some(shadow::RUST_VERSION).filter(|x| !x.is_empty());
if let Some(rust_version) = rust_version {
indexmap.insert(
"rust_version".to_string(),
Value::String {
val: rust_version.to_string(),
span: call.head,
},
);
cols.push("rust_version".to_string());
vals.push(Value::String {
val: rust_version.to_string(),
span: call.head,
});
}
let rust_channel: Option<&str> = Some(shadow::RUST_CHANNEL).filter(|x| !x.is_empty());
if let Some(rust_channel) = rust_channel {
indexmap.insert(
"rust_channel".to_string(),
Value::String {
val: rust_channel.to_string(),
span: call.head,
},
);
cols.push("rust_channel".to_string());
vals.push(Value::String {
val: rust_channel.to_string(),
span: call.head,
});
}
let cargo_version: Option<&str> = Some(shadow::CARGO_VERSION).filter(|x| !x.is_empty());
if let Some(cargo_version) = cargo_version {
indexmap.insert(
"cargo_version".to_string(),
Value::String {
val: cargo_version.to_string(),
span: call.head,
},
);
cols.push("cargo_version".to_string());
vals.push(Value::String {
val: cargo_version.to_string(),
span: call.head,
});
}
let pkg_version: Option<&str> = Some(shadow::PKG_VERSION).filter(|x| !x.is_empty());
if let Some(pkg_version) = pkg_version {
indexmap.insert(
"pkg_version".to_string(),
Value::String {
val: pkg_version.to_string(),
span: call.head,
},
);
cols.push("pkg_version".to_string());
vals.push(Value::String {
val: pkg_version.to_string(),
span: call.head,
});
}
let build_time: Option<&str> = Some(shadow::BUILD_TIME).filter(|x| !x.is_empty());
if let Some(build_time) = build_time {
indexmap.insert(
"build_time".to_string(),
Value::String {
val: build_time.to_string(),
span: call.head,
},
);
cols.push("build_time".to_string());
vals.push(Value::String {
val: build_time.to_string(),
span: call.head,
});
}
let build_rust_channel: Option<&str> =
Some(shadow::BUILD_RUST_CHANNEL).filter(|x| !x.is_empty());
if let Some(build_rust_channel) = build_rust_channel {
indexmap.insert(
"build_rust_channel".to_string(),
Value::String {
val: build_rust_channel.to_string(),
span: call.head,
},
);
cols.push("build_rust_channel".to_string());
vals.push(Value::String {
val: build_rust_channel.to_string(),
span: call.head,
});
}
indexmap.insert(
"features".to_string(),
Value::String {
val: features_enabled().join(", "),
span: call.head,
},
);
cols.push("features".to_string());
vals.push(Value::String {
val: features_enabled().join(", "),
span: call.head,
});
// Get a list of command names and check for plugins
let installed_plugins = engine_state
@ -196,28 +181,12 @@ pub fn version(
.map(|x| x.name())
.collect::<Vec<_>>();
indexmap.insert(
"installed_plugins".to_string(),
Value::String {
val: installed_plugins.join(", "),
span: call.head,
},
);
cols.push("installed_plugins".to_string());
vals.push(Value::String {
val: installed_plugins.join(", "),
span: call.head,
});
let cols = indexmap.keys().cloned().collect::<Vec<_>>();
let vals = indexmap.values().cloned().collect::<Vec<_>>();
// Ok(Value::List {
// vals: vec![Value::Record {
// cols,
// vals,
// span: call.head,
// }],
// span: call.head,
// }
// .into_pipeline_data())
// List looks better than table, imo
Ok(Value::Record {
cols,
vals,
@ -256,7 +225,7 @@ fn features_enabled() -> Vec<String> {
names.push("uuid".to_string());
}
#[cfg(feature = "which")]
#[cfg(feature = "which-support")]
{
names.push("which".to_string());
}

View File

@ -16,7 +16,7 @@ impl Command for RenameDF {
}
fn usage(&self) -> &str {
"rename a dataframe column"
"Rename a dataframe column"
}
fn signature(&self) -> Signature {

View File

@ -17,7 +17,11 @@ impl Command for AsDate {
}
fn usage(&self) -> &str {
r#"Converts string to date. Format example:
r#"Converts string to date."#
}
fn extra_usage(&self) -> &str {
r#"Format example:
"%Y-%m-%d" => 2021-12-31
"%d-%m-%Y" => 31-12-2021
"%Y%m%d" => 2021319 (2021-03-19)"#
@ -25,7 +29,7 @@ impl Command for AsDate {
fn signature(&self) -> Signature {
Signature::build(self.name())
.required("format", SyntaxShape::String, "formating date string")
.required("format", SyntaxShape::String, "formatting date string")
.switch("not-exact", "the format string may be contained in the date (e.g. foo-2021-01-01-bar could match 2021-01-01)", Some('n'))
.category(Category::Custom("dataframe".into()))
}

View File

@ -18,7 +18,11 @@ impl Command for AsDateTime {
}
fn usage(&self) -> &str {
r#"Converts string to datetime. Format example:
r#"Converts string to datetime."#
}
fn extra_usage(&self) -> &str {
r#"Format example:
"%y/%m/%d %H:%M:%S" => 21/12/31 12:54:98
"%y-%m-%d %H:%M:%S" => 2021-12-31 24:58:01
"%y/%m/%d %H:%M:%S" => 21/12/31 24:58:01
@ -34,7 +38,7 @@ impl Command for AsDateTime {
fn signature(&self) -> Signature {
Signature::build(self.name())
.required("format", SyntaxShape::String, "formating date time string")
.required("format", SyntaxShape::String, "formatting date time string")
.switch("not-exact", "the format string may be contained in the date (e.g. foo-2021-01-01-bar could match 2021-01-01)", Some('n'))
.category(Category::Custom("dataframe".into()))
}

View File

@ -366,7 +366,8 @@ pub(super) fn compute_series_single_value(
rhs_span: right.span()?,
}),
},
Operator::Contains => match &right {
// TODO: update this to do a regex match instead of a simple contains?
Operator::RegexMatch => match &right {
Value::String { val, .. } => contains_series_pat(&lhs, val, lhs_span),
_ => Err(ShellError::OperatorMismatch {
op_span: operator.span,
@ -376,6 +377,19 @@ pub(super) fn compute_series_single_value(
rhs_span: right.span()?,
}),
},
Operator::StartsWith => match &right {
Value::String { val, .. } => {
let starts_with_pattern = format!("^{}", val);
contains_series_pat(&lhs, &starts_with_pattern, lhs_span)
}
_ => Err(ShellError::OperatorMismatch {
op_span: operator.span,
lhs_ty: left.get_type(),
lhs_span: left.span()?,
rhs_ty: right.get_type(),
rhs_span: right.span()?,
}),
},
_ => Err(ShellError::OperatorMismatch {
op_span: operator.span,
lhs_ty: left.get_type(),

View File

@ -18,7 +18,7 @@ impl Command for Date {
}
fn usage(&self) -> &str {
"date"
"Date-related commands"
}
fn run(

View File

@ -4,6 +4,7 @@ mod humanize;
mod list_timezone;
mod now;
mod parser;
mod to_record;
mod to_table;
mod to_timezone;
mod utils;
@ -14,6 +15,7 @@ pub use format::SubCommand as DateFormat;
pub use humanize::SubCommand as DateHumanize;
pub use list_timezone::SubCommand as DateListTimezones;
pub use now::SubCommand as DateNow;
pub use to_record::SubCommand as DateToRecord;
pub use to_table::SubCommand as DateToTable;
pub use to_timezone::SubCommand as DateToTimezone;
pub(crate) use utils::parse_date_from_string;

View File

@ -0,0 +1,164 @@
use crate::date::utils::parse_date_from_string;
use chrono::{DateTime, Datelike, FixedOffset, Local, Timelike};
use nu_protocol::ast::Call;
use nu_protocol::engine::{Command, EngineState, Stack};
use nu_protocol::{
Category, Example, PipelineData, ShellError::DatetimeParseError, Signature, Span, Value,
};
#[derive(Clone)]
pub struct SubCommand;
impl Command for SubCommand {
fn name(&self) -> &str {
"date to-record"
}
fn signature(&self) -> Signature {
Signature::build("date to-record").category(Category::Date)
}
fn usage(&self) -> &str {
"Convert the date into a structured table."
}
fn run(
&self,
engine_state: &EngineState,
_stack: &mut Stack,
call: &Call,
input: PipelineData,
) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
let head = call.head;
input.map(move |value| helper(value, head), engine_state.ctrlc.clone())
}
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "Convert the current date into a structured table.",
example: "date to-table",
result: None,
},
Example {
description: "Convert the current date into a structured table.",
example: "date now | date to-record",
result: None,
},
Example {
description: "Convert a given date into a structured table.",
example: " '2020-04-12 22:10:57 +0200' | date to-record",
result: {
let span = Span::test_data();
let cols = vec![
"year".into(),
"month".into(),
"day".into(),
"hour".into(),
"minute".into(),
"second".into(),
"timezone".into(),
];
let vals = vec![
Value::Int { val: 2020, span },
Value::Int { val: 4, span },
Value::Int { val: 12, span },
Value::Int { val: 22, span },
Value::Int { val: 10, span },
Value::Int { val: 57, span },
Value::String {
val: "+02:00".to_string(),
span,
},
];
Some(Value::Record { cols, vals, span })
},
},
]
}
}
fn parse_date_into_table(date: Result<DateTime<FixedOffset>, Value>, head: Span) -> Value {
let cols = vec![
"year".into(),
"month".into(),
"day".into(),
"hour".into(),
"minute".into(),
"second".into(),
"timezone".into(),
];
match date {
Ok(x) => {
let vals = vec![
Value::Int {
val: x.year() as i64,
span: head,
},
Value::Int {
val: x.month() as i64,
span: head,
},
Value::Int {
val: x.day() as i64,
span: head,
},
Value::Int {
val: x.hour() as i64,
span: head,
},
Value::Int {
val: x.minute() as i64,
span: head,
},
Value::Int {
val: x.second() as i64,
span: head,
},
Value::String {
val: x.offset().to_string(),
span: head,
},
];
Value::Record {
cols,
vals,
span: head,
}
}
Err(e) => e,
}
}
fn helper(val: Value, head: Span) -> Value {
match val {
Value::String {
val,
span: val_span,
} => {
let date = parse_date_from_string(&val, val_span);
parse_date_into_table(date, head)
}
Value::Nothing { span: _ } => {
let now = Local::now();
let n = now.with_timezone(now.offset());
parse_date_into_table(Ok(n), head)
}
Value::Date { val, span: _ } => parse_date_into_table(Ok(val), head),
_ => Value::Error {
error: DatetimeParseError(head),
},
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_examples() {
use crate::test_examples;
test_examples(SubCommand {})
}
}

View File

@ -19,7 +19,7 @@ impl Command for SubCommand {
}
fn usage(&self) -> &str {
"Print the date in a structured table."
"Convert the date into a structured table."
}
fn run(
@ -36,17 +36,17 @@ impl Command for SubCommand {
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "Print the date in a structured table.",
description: "Convert the date into a structured table.",
example: "date to-table",
result: None,
},
Example {
description: "Print the date in a structured table.",
description: "Convert the date into a structured table.",
example: "date now | date to-table",
result: None,
},
Example {
description: "Print the date in a structured table.",
description: "Convert a given date into a structured table.",
example: " '2020-04-12 22:10:57 +0200' | date to-table",
result: {
let span = Span::test_data();

View File

@ -80,11 +80,11 @@ pub fn create_default_context(cwd: impl AsRef<Path>) -> EngineState {
Headers,
Insert,
SplitBy,
Keep,
Take,
Merge,
Move,
KeepUntil,
KeepWhile,
TakeWhile,
TakeUntil,
Last,
Length,
Lines,
@ -106,6 +106,7 @@ pub fn create_default_context(cwd: impl AsRef<Path>) -> EngineState {
Skip,
SkipUntil,
SkipWhile,
Sort,
SortBy,
Transpose,
Uniq,
@ -142,7 +143,7 @@ pub fn create_default_context(cwd: impl AsRef<Path>) -> EngineState {
Sys,
};
#[cfg(feature = "which")]
#[cfg(feature = "which-support")]
bind_command! { Which };
// Strings
@ -165,7 +166,7 @@ pub fn create_default_context(cwd: impl AsRef<Path>) -> EngineState {
StrContains,
StrDowncase,
StrEndswith,
StrFindReplace,
StrReplace,
StrIndexOf,
StrKebabCase,
StrLength,
@ -192,6 +193,7 @@ pub fn create_default_context(cwd: impl AsRef<Path>) -> EngineState {
Rm,
Save,
Touch,
Glob,
};
// Platform
@ -217,6 +219,7 @@ pub fn create_default_context(cwd: impl AsRef<Path>) -> EngineState {
DateHumanize,
DateListTimezones,
DateNow,
DateToRecord,
DateToTable,
DateToTimezone,
};
@ -367,6 +370,10 @@ pub fn create_default_context(cwd: impl AsRef<Path>) -> EngineState {
MatchDeprecated,
NthDeprecated,
UnaliasDeprecated,
StrFindReplaceDeprecated,
KeepDeprecated,
KeepUntilDeprecated,
KeepWhileDeprecated,
};
#[cfg(feature = "dataframe")]

View File

@ -0,0 +1,36 @@
use nu_protocol::{
ast::Call,
engine::{Command, EngineState, Stack},
Category, PipelineData, Signature,
};
#[derive(Clone)]
pub struct KeepDeprecated;
impl Command for KeepDeprecated {
fn name(&self) -> &str {
"keep"
}
fn signature(&self) -> Signature {
Signature::build(self.name()).category(Category::Deprecated)
}
fn usage(&self) -> &str {
"Deprecated command"
}
fn run(
&self,
_engine_state: &EngineState,
_stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
Err(nu_protocol::ShellError::DeprecatedCommand(
self.name().to_string(),
"take".to_string(),
call.head,
))
}
}

View File

@ -0,0 +1,36 @@
use nu_protocol::{
ast::Call,
engine::{Command, EngineState, Stack},
Category, PipelineData, Signature,
};
#[derive(Clone)]
pub struct KeepUntilDeprecated;
impl Command for KeepUntilDeprecated {
fn name(&self) -> &str {
"keep until"
}
fn signature(&self) -> Signature {
Signature::build(self.name()).category(Category::Deprecated)
}
fn usage(&self) -> &str {
"Deprecated command"
}
fn run(
&self,
_engine_state: &EngineState,
_stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
Err(nu_protocol::ShellError::DeprecatedCommand(
self.name().to_string(),
"take until".to_string(),
call.head,
))
}
}

View File

@ -0,0 +1,36 @@
use nu_protocol::{
ast::Call,
engine::{Command, EngineState, Stack},
Category, PipelineData, Signature,
};
#[derive(Clone)]
pub struct KeepWhileDeprecated;
impl Command for KeepWhileDeprecated {
fn name(&self) -> &str {
"keep while"
}
fn signature(&self) -> Signature {
Signature::build(self.name()).category(Category::Deprecated)
}
fn usage(&self) -> &str {
"Deprecated command"
}
fn run(
&self,
_engine_state: &EngineState,
_stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
Err(nu_protocol::ShellError::DeprecatedCommand(
self.name().to_string(),
"take while".to_string(),
call.head,
))
}
}

View File

@ -1,16 +1,24 @@
mod keep_;
mod keep_until;
mod keep_while;
mod match_;
mod nth;
mod pivot;
mod str_datetime;
mod str_decimal;
mod str_find_replace;
mod str_int;
mod unalias;
pub use keep_::KeepDeprecated;
pub use keep_until::KeepUntilDeprecated;
pub use keep_while::KeepWhileDeprecated;
pub use match_::MatchDeprecated;
pub use nth::NthDeprecated;
pub use pivot::PivotDeprecated;
pub use str_datetime::StrDatetimeDeprecated;
pub use str_decimal::StrDecimalDeprecated;
pub use str_find_replace::StrFindReplaceDeprecated;
pub use str_int::StrIntDeprecated;
pub use unalias::UnaliasDeprecated;

View File

@ -0,0 +1,36 @@
use nu_protocol::{
ast::Call,
engine::{Command, EngineState, Stack},
Category, PipelineData, Signature,
};
#[derive(Clone)]
pub struct StrFindReplaceDeprecated;
impl Command for StrFindReplaceDeprecated {
fn name(&self) -> &str {
"str find-replace"
}
fn signature(&self) -> Signature {
Signature::build(self.name()).category(Category::Deprecated)
}
fn usage(&self) -> &str {
"Deprecated command"
}
fn run(
&self,
_engine_state: &EngineState,
_stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
Err(nu_protocol::ShellError::DeprecatedCommand(
self.name().to_string(),
"str replace".to_string(),
call.head,
))
}
}

View File

@ -35,7 +35,9 @@ impl Command for LetEnv {
) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
let env_var = call.req(engine_state, stack, 0)?;
let keyword_expr = call.positional[1]
let keyword_expr = call
.positional_nth(1)
.expect("checked through parser")
.as_keyword()
.expect("internal error: missing keyword");

View File

@ -76,7 +76,7 @@ impl Command for LoadEnv {
Ok(PipelineData::new(call.head))
}
_ => Err(ShellError::UnsupportedInput(
"Record not supported".into(),
"'load-env' expects a single record".into(),
span,
)),
},

View File

@ -99,7 +99,9 @@ fn with_env(
return Err(ShellError::CantConvert(
"string list or single row".into(),
x.get_type().to_string(),
call.positional[1].span,
call.positional_nth(1)
.expect("already checked through .req")
.span,
));
}
}
@ -123,7 +125,9 @@ fn with_env(
return Err(ShellError::CantConvert(
"string list or single row".into(),
x.get_type().to_string(),
call.positional[1].span,
call.positional_nth(1)
.expect("already checked through .req")
.span,
));
}
};

View File

@ -14,7 +14,7 @@ use crate::To;
#[cfg(test)]
use super::{
Ansi, Date, From, If, Into, Math, Path, Random, Split, SplitColumn, SplitRow, Str, StrCollect,
StrFindReplace, StrLength, Url, Wrap,
StrLength, StrReplace, Url, Wrap,
};
#[cfg(test)]
@ -31,7 +31,7 @@ pub fn test_examples(cmd: impl Command + 'static) {
working_set.add_decl(Box::new(Str));
working_set.add_decl(Box::new(StrCollect));
working_set.add_decl(Box::new(StrLength));
working_set.add_decl(Box::new(StrFindReplace));
working_set.add_decl(Box::new(StrReplace));
working_set.add_decl(Box::new(BuildString));
working_set.add_decl(Box::new(From));
working_set.add_decl(Box::new(If));

View File

@ -20,7 +20,6 @@ impl Command for ViewSource {
fn signature(&self) -> nu_protocol::Signature {
Signature::build("view-source")
.desc(self.usage())
.required("item", SyntaxShape::Any, "name or block to view")
.category(Category::Core)
}

View File

@ -28,7 +28,7 @@ impl Command for Cd {
call: &Call,
_input: PipelineData,
) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
let raw_path = call.nth(0);
let raw_path = call.positional_nth(0);
let path_val: Option<Value> = call.opt(engine_state, stack, 0)?;
let cwd = current_dir(engine_state, stack)?;

View File

@ -0,0 +1,128 @@
use nu_engine::env::current_dir;
use nu_engine::CallExt;
use nu_protocol::ast::Call;
use nu_protocol::engine::{Command, EngineState, Stack};
use nu_protocol::{
Category, Example, IntoInterruptiblePipelineData, PipelineData, ShellError, Signature, Spanned,
SyntaxShape, Value,
};
use wax::Glob as WaxGlob;
#[derive(Clone)]
pub struct Glob;
impl Command for Glob {
fn name(&self) -> &str {
"glob"
}
fn signature(&self) -> Signature {
Signature::build("glob")
.required("glob", SyntaxShape::String, "the glob expression")
.named(
"depth",
SyntaxShape::Int,
"directory depth to search",
Some('d'),
)
.category(Category::FileSystem)
}
fn usage(&self) -> &str {
"Creates a list of files and/or folders based on the glob pattern provided."
}
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "Search for *.rs files",
example: "glob *.rs",
result: None,
},
Example {
description: "Search for *.rs and *.toml files recursively up to 2 folders deep",
example: "glob **/*.{rs,toml} --depth 2",
result: None,
},
Example {
description:
"Search for files and folders that begin with uppercase C and lowercase c",
example: r#"glob "[Cc]*""#,
result: None,
},
Example {
description:
"Search for files and folders like abc or xyz substituting a character for ?",
example: r#"glob "{a?c,x?z}""#,
result: None,
},
Example {
description: "A case-insensitive search for files and folders that begin with c",
example: r#"glob "(?i)c*""#,
result: None,
},
Example {
description: "Search for files for folders that do not begin with c, C, b, M, or s",
example: r#"glob "[!cCbMs]*""#,
result: None,
},
Example {
description: "Search for files or folders with 3 a's in a row in the name",
example: "glob <a*:3>",
result: None,
},
Example {
description: "Search for files or folders with only a, b, c, or d in the file name between 1 and 10 times",
example: "glob <[a-d]:1,10>",
result: None,
},
]
}
fn extra_usage(&self) -> &str {
r#"For more glob pattern help please refer to https://github.com/olson-sean-k/wax"#
}
fn run(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
let span = call.head;
let path = current_dir(engine_state, stack)?;
let glob_pattern: Spanned<String> = call.req(engine_state, stack, 0)?;
let depth = call.get_flag(engine_state, stack, "depth")?;
let folder_depth = if let Some(depth) = depth {
depth
} else {
usize::MAX
};
let glob = match WaxGlob::new(&glob_pattern.item) {
Ok(p) => p,
Err(e) => {
return Err(ShellError::LabeledError(
"error with glob pattern".to_string(),
format!("{}", e),
))
}
};
#[allow(clippy::needless_collect)]
let glob_results: Vec<Value> = glob
.walk(path, folder_depth)
.flatten()
.map(|entry| Value::String {
val: entry.into_path().to_string_lossy().to_string(),
span,
})
.collect();
Ok(glob_results
.into_iter()
.into_pipeline_data(engine_state.ctrlc.clone()))
}
}

View File

@ -1,6 +1,6 @@
use crate::DirBuilder;
use crate::DirInfo;
use chrono::{DateTime, Utc};
use chrono::{DateTime, Local};
use nu_engine::env::current_dir;
use nu_engine::CallExt;
use nu_protocol::ast::Call;
@ -27,6 +27,10 @@ impl Command for Ls {
"List the files in a directory."
}
fn search_terms(&self) -> Vec<&str> {
vec!["dir"]
}
fn signature(&self) -> nu_protocol::Signature {
Signature::build("ls")
// Using a string instead of a glob pattern shape so it won't auto-expand
@ -462,6 +466,8 @@ pub(crate) fn dir_entry_dict(
} else {
vals.push(Value::nothing(span));
}
} else {
vals.push(Value::nothing(span));
}
} else {
vals.push(Value::nothing(span));
@ -471,9 +477,9 @@ pub(crate) fn dir_entry_dict(
if long {
cols.push("created".to_string());
if let Ok(c) = md.created() {
let utc: DateTime<Utc> = c.into();
let utc: DateTime<Local> = c.into();
vals.push(Value::Date {
val: utc.into(),
val: utc.with_timezone(utc.offset()),
span,
});
} else {
@ -482,9 +488,9 @@ pub(crate) fn dir_entry_dict(
cols.push("accessed".to_string());
if let Ok(a) = md.accessed() {
let utc: DateTime<Utc> = a.into();
let utc: DateTime<Local> = a.into();
vals.push(Value::Date {
val: utc.into(),
val: utc.with_timezone(utc.offset()),
span,
});
} else {
@ -494,9 +500,9 @@ pub(crate) fn dir_entry_dict(
cols.push("modified".to_string());
if let Ok(m) = md.modified() {
let utc: DateTime<Utc> = m.into();
let utc: DateTime<Local> = m.into();
vals.push(Value::Date {
val: utc.into(),
val: utc.with_timezone(utc.offset()),
span,
});
} else {

View File

@ -57,13 +57,18 @@ impl Command for Mkdir {
}
for (i, dir) in directories.enumerate() {
let span = call.positional[i].span;
let span = call
.positional_nth(i)
.expect("already checked through directories")
.span;
let dir_res = std::fs::create_dir_all(&dir);
if let Err(reason) = dir_res {
return Err(ShellError::CreateNotPossible(
format!("failed to create directory: {}", reason),
call.positional[i].span,
call.positional_nth(i)
.expect("already checked through directories")
.span,
));
}

View File

@ -1,5 +1,6 @@
mod cd;
mod cp;
mod glob;
mod ls;
mod mkdir;
mod mv;
@ -11,6 +12,7 @@ mod util;
pub use cd::Cd;
pub use cp::Cp;
pub use glob::Glob;
pub use ls::Ls;
pub use mkdir::Mkdir;
pub use mv::Mv;

View File

@ -6,7 +6,8 @@ use nu_engine::CallExt;
use nu_protocol::ast::Call;
use nu_protocol::engine::{Command, EngineState, Stack};
use nu_protocol::{
Category, Example, PipelineData, ShellError, Signature, Span, Spanned, SyntaxShape,
Category, Example, IntoInterruptiblePipelineData, PipelineData, ShellError, Signature, Span,
Spanned, SyntaxShape, Value,
};
const GLOB_PARAMS: nu_glob::MatchOptions = nu_glob::MatchOptions {
@ -40,6 +41,7 @@ impl Command for Mv {
SyntaxShape::Filepath,
"the location to move files/directories to",
)
.switch("quiet", "suppress output showing files moved", Some('q'))
// .switch("interactive", "ask user to confirm action", Some('i'))
// .switch("force", "suppress error when no file", Some('f'))
.category(Category::FileSystem)
@ -55,9 +57,12 @@ impl Command for Mv {
// TODO: handle invalid directory or insufficient permissions when moving
let spanned_source: Spanned<String> = call.req(engine_state, stack, 0)?;
let spanned_destination: Spanned<String> = call.req(engine_state, stack, 1)?;
let quiet = call.has_flag("quiet");
// let interactive = call.has_flag("interactive");
// let force = call.has_flag("force");
let ctrlc = engine_state.ctrlc.clone();
let path = current_dir(engine_state, stack)?;
let source = path.join(spanned_source.item.as_str());
let destination = path.join(spanned_destination.item.as_str());
@ -116,20 +121,35 @@ impl Command for Mv {
.collect();
}
for entry in sources.into_iter().flatten() {
move_file(
Spanned {
item: entry,
span: spanned_source.span,
},
Spanned {
item: destination.clone(),
span: spanned_destination.span,
},
)?
}
Ok(PipelineData::new(call.head))
let span = call.head;
Ok(sources
.into_iter()
.flatten()
.filter_map(move |entry| {
let result = move_file(
Spanned {
item: entry.clone(),
span: spanned_source.span,
},
Spanned {
item: destination.clone(),
span: spanned_destination.span,
},
);
if let Err(error) = result {
Some(Value::Error { error })
} else if quiet {
None
} else {
let val = format!(
"moved {:} to {:}",
entry.to_string_lossy(),
destination.to_string_lossy()
);
Some(Value::String { val, span })
}
})
.into_pipeline_data(ctrlc))
}
fn examples(&self) -> Vec<Example> {

View File

@ -1,4 +1,4 @@
use nu_engine::{get_full_help, CallExt};
use nu_engine::{eval_block, get_full_help, CallExt};
use nu_protocol::ast::Call;
use nu_protocol::engine::{Command, EngineState, Stack};
use nu_protocol::{
@ -141,12 +141,15 @@ impl Command for Open {
if let Some(ext) = ext {
match engine_state.find_decl(format!("from {}", ext).as_bytes()) {
Some(converter_id) => engine_state.get_decl(converter_id).run(
engine_state,
stack,
&Call::new(arg_span),
output,
),
Some(converter_id) => {
let decl = engine_state.get_decl(converter_id);
if let Some(block_id) = decl.get_block_id() {
let block = engine_state.get_block(block_id);
eval_block(engine_state, stack, block, output, false, false)
} else {
decl.run(engine_state, stack, &Call::new(arg_span), output)
}
}
None => Ok(output),
}
} else {

View File

@ -1,5 +1,9 @@
use std::collections::HashMap;
#[cfg(feature = "trash-support")]
#[cfg(all(
feature = "trash-support",
not(target_os = "android"),
not(target_os = "ios")
))]
use std::io::ErrorKind;
#[cfg(unix)]
use std::os::unix::prelude::FileTypeExt;
@ -35,7 +39,13 @@ impl Command for Rm {
}
fn signature(&self) -> Signature {
Signature::build("rm")
let sig = Signature::build("rm");
#[cfg(all(
feature = "trash-support",
not(target_os = "android"),
not(target_os = "ios")
))]
let sig = sig
.switch(
"trash",
"use the platform's recycle bin instead of permanently deleting",
@ -45,8 +55,8 @@ impl Command for Rm {
"permanent",
"don't use recycle bin, delete permanently",
Some('p'),
)
.switch("recursive", "delete subdirectories recursively", Some('r'))
);
sig.switch("recursive", "delete subdirectories recursively", Some('r'))
.switch("force", "suppress error when no file", Some('f'))
.switch("quiet", "suppress output showing files deleted", Some('q'))
// .switch("interactive", "ask user to confirm action", Some('i'))
@ -69,12 +79,18 @@ impl Command for Rm {
}
fn examples(&self) -> Vec<Example> {
vec![
let mut examples = vec![
Example {
description: "Delete or move a file to the system trash (depending on 'rm_always_trash' config option)",
example: "rm file.txt",
result: None,
},
}];
#[cfg(all(
feature = "trash-support",
not(target_os = "android"),
not(target_os = "ios")
))]
examples.append(&mut vec![
Example {
description: "Move a file to the system trash",
example: "rm --trash file.txt",
@ -85,12 +101,13 @@ impl Command for Rm {
example: "rm --permanent file.txt",
result: None,
},
Example {
description: "Delete a file, and suppress errors if no file is found",
example: "rm --force file.txt",
result: None,
}
]
]);
examples.push(Example {
description: "Delete a file, and suppress errors if no file is found",
example: "rm --force file.txt",
result: None,
});
examples
}
}
@ -100,7 +117,11 @@ fn rm(
call: &Call,
) -> Result<PipelineData, ShellError> {
let trash = call.has_flag("trash");
#[cfg(feature = "trash-support")]
#[cfg(all(
feature = "trash-support",
not(target_os = "android"),
not(target_os = "ios")
))]
let permanent = call.has_flag("permanent");
let recursive = call.has_flag("recursive");
let force = call.has_flag("force");
@ -116,20 +137,26 @@ fn rm(
let rm_always_trash = config.rm_always_trash;
#[cfg(not(feature = "trash-support"))]
#[cfg(any(
not(feature = "trash-support"),
target_os = "android",
target_os = "ios"
))]
{
if rm_always_trash {
return Err(ShellError::SpannedLabeledError(
"Cannot execute `rm`; the current configuration specifies \
`rm_always_trash = true`, but the current nu executable was not \
built with feature `trash_support`."
built with feature `trash_support` or trash is not supported on \
your platform."
.into(),
"trash required to be true but not supported".into(),
span,
));
} else if trash {
return Err(ShellError::SpannedLabeledError(
"Cannot execute `rm` with option `--trash`; feature `trash-support` not enabled"
"Cannot execute `rm` with option `--trash`; feature `trash-support` not \
enabled or trash is not supported on your platform"
.into(),
"this option is only available if nu is built with the `trash-support` feature"
.into(),
@ -241,7 +268,11 @@ fn rm(
|| is_empty()
{
let result;
#[cfg(feature = "trash-support")]
#[cfg(all(
feature = "trash-support",
not(target_os = "android"),
not(target_os = "ios")
))]
{
use std::io::Error;
result = if trash || (rm_always_trash && !permanent) {
@ -254,7 +285,11 @@ fn rm(
std::fs::remove_dir_all(&f)
};
}
#[cfg(not(feature = "trash-support"))]
#[cfg(any(
not(feature = "trash-support"),
target_os = "android",
target_os = "ios"
))]
{
result = if metadata.is_file() || is_socket || is_fifo {
std::fs::remove_file(&f)

View File

@ -4,7 +4,7 @@ use nu_protocol::engine::{Command, EngineState, Stack};
use nu_protocol::{
Category, Example, PipelineData, ShellError, Signature, Spanned, SyntaxShape, Value,
};
use std::io::Write;
use std::io::{BufWriter, Write};
use std::path::Path;
@ -125,39 +125,71 @@ impl Command for Save {
)),
}
} else {
match input.into_value(span) {
Value::String { val, .. } => {
if let Err(err) = file.write_all(val.as_bytes()) {
return Err(ShellError::IOError(err.to_string()));
}
match input {
PipelineData::ExternalStream { stdout: None, .. } => Ok(PipelineData::new(span)),
PipelineData::ExternalStream {
stdout: Some(mut stream),
..
} => {
let mut writer = BufWriter::new(file);
Ok(PipelineData::new(span))
stream
.try_for_each(move |result| {
let buf = match result {
Ok(v) => match v {
Value::String { val, .. } => val.into_bytes(),
Value::Binary { val, .. } => val,
_ => {
return Err(ShellError::UnsupportedInput(
format!("{:?} not supported", v.get_type()),
v.span()?,
));
}
},
Err(err) => return Err(err),
};
if let Err(err) = writer.write(&buf) {
return Err(ShellError::IOError(err.to_string()));
}
Ok(())
})
.map(|_| PipelineData::new(span))
}
Value::Binary { val, .. } => {
if let Err(err) = file.write_all(&val) {
return Err(ShellError::IOError(err.to_string()));
input => match input.into_value(span) {
Value::String { val, .. } => {
if let Err(err) = file.write_all(val.as_bytes()) {
return Err(ShellError::IOError(err.to_string()));
}
Ok(PipelineData::new(span))
}
Value::Binary { val, .. } => {
if let Err(err) = file.write_all(&val) {
return Err(ShellError::IOError(err.to_string()));
}
Ok(PipelineData::new(span))
}
Value::List { vals, .. } => {
let val = vals
.into_iter()
.map(|it| it.as_string())
.collect::<Result<Vec<String>, ShellError>>()?
.join("\n")
+ "\n";
if let Err(err) = file.write_all(val.as_bytes()) {
return Err(ShellError::IOError(err.to_string()));
Ok(PipelineData::new(span))
}
Value::List { vals, .. } => {
let val = vals
.into_iter()
.map(|it| it.as_string())
.collect::<Result<Vec<String>, ShellError>>()?
.join("\n")
+ "\n";
Ok(PipelineData::new(span))
}
v => Err(ShellError::UnsupportedInput(
format!("{:?} not supported", v.get_type()),
span,
)),
if let Err(err) = file.write_all(val.as_bytes()) {
return Err(ShellError::IOError(err.to_string()));
}
Ok(PipelineData::new(span))
}
v => Err(ShellError::UnsupportedInput(
format!("{:?} not supported", v.get_type()),
span,
)),
},
}
}
}

View File

@ -1,9 +1,20 @@
use std::fs::OpenOptions;
use std::path::Path;
use chrono::{DateTime, Datelike, Local};
use filetime::FileTime;
use nu_engine::CallExt;
use nu_protocol::ast::Call;
use nu_protocol::engine::{Command, EngineState, Stack};
use nu_protocol::{Category, Example, PipelineData, ShellError, Signature, SyntaxShape};
use nu_protocol::{Category, Example, PipelineData, ShellError, Signature, Spanned, SyntaxShape};
use crate::parse_date_from_string;
enum AddYear {
Full,
FirstDigits,
}
#[derive(Clone)]
pub struct Touch;
@ -20,6 +31,39 @@ impl Command for Touch {
SyntaxShape::Filepath,
"the path of the file you want to create",
)
.named(
"timestamp",
SyntaxShape::String,
"change the file or directory time to a timestamp. Format: [[CC]YY]MMDDhhmm[.ss]\n\n If neither YY or CC is given, the current year will be assumed. If YY is specified, but CC is not, CC will be derived as follows:\n \tIf YY is between [69, 99], CC is 19\n \tIf YY is between [00, 68], CC is 20\n Note: It is expected that in a future version of this standard the default century inferred from a 2-digit year will change",
Some('t'),
)
.named(
"date",
SyntaxShape::String,
"change the file or directory time to a date",
Some('d'),
)
.named(
"reference",
SyntaxShape::String,
"change the file or directory time to the time of the reference file/directory",
Some('r'),
)
.switch(
"modified",
"change the modification time of the file or directory. If no timestamp, date or reference file/directory is given, the current time is used",
Some('m'),
)
.switch(
"access",
"change the access time of the file or directory. If no timestamp, date or reference file/directory is given, the current time is used",
Some('a'),
)
.switch(
"no-create",
"do not create the file if it does not exist",
Some('c'),
)
.rest("rest", SyntaxShape::Filepath, "additional files to create")
.category(Category::FileSystem)
}
@ -35,18 +79,218 @@ impl Command for Touch {
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
let mut change_mtime: bool = call.has_flag("modified");
let mut change_atime: bool = call.has_flag("access");
let use_stamp: bool = call.has_flag("timestamp");
let use_date: bool = call.has_flag("date");
let use_reference: bool = call.has_flag("reference");
let no_create: bool = call.has_flag("no-create");
let target: String = call.req(engine_state, stack, 0)?;
let rest: Vec<String> = call.rest(engine_state, stack, 1)?;
for (index, item) in vec![target].into_iter().chain(rest).enumerate() {
match OpenOptions::new().write(true).create(true).open(&item) {
Ok(_) => continue,
Err(err) => {
return Err(ShellError::CreateNotPossible(
format!("Failed to create file: {}", err),
call.positional[index].span,
let mut date: Option<DateTime<Local>> = None;
let mut ref_date_atime: Option<DateTime<Local>> = None;
// Change both times if none is specified
if !change_mtime && !change_atime {
change_mtime = true;
change_atime = true;
}
if change_mtime || change_atime {
date = Some(Local::now());
}
if use_stamp || use_date {
let (val, span) = if use_stamp {
let stamp: Option<Spanned<String>> =
call.get_flag(engine_state, stack, "timestamp")?;
let (stamp, span) = match stamp {
Some(stamp) => (stamp.item, stamp.span),
None => {
return Err(ShellError::MissingParameter(
"timestamp".to_string(),
call.head,
));
}
};
// Checks for the seconds stamp and removes the '.' delimiter if any
let (val, has_sec): (String, bool) = match stamp.split_once('.') {
Some((dtime, sec)) => (format!("{}{}", dtime, sec), true),
None => (stamp.to_string(), false),
};
let size = val.len();
// Each stamp is a 2 digit number and the whole stamp must not be less than 4 or greater than 7 pairs
if (size % 2 != 0 || !(8..=14).contains(&size)) || val.parse::<usize>().is_err() {
return Err(ShellError::UnsupportedInput(
"input has an invalid timestamp".to_string(),
span,
));
}
let add_year: Option<AddYear> = if has_sec {
match size {
10 => Some(AddYear::Full),
12 => Some(AddYear::FirstDigits),
14 => None,
_ => unreachable!(), // This should never happen as the check above should catch it
}
} else {
match size {
8 => Some(AddYear::Full),
10 => Some(AddYear::FirstDigits),
12 => None,
_ => unreachable!(), // This should never happen as the check above should catch it
}
};
if let Some(add_year) = add_year {
let year = Local::now().year();
match add_year {
AddYear::Full => (format!("{}{}", year, val), span),
AddYear::FirstDigits => {
// Compliance with the Unix version of touch
let yy = val[0..2]
.parse::<u8>()
.expect("should be a valid 2 digit number");
let mut year = 20;
if (69..=99).contains(&yy) {
year = 19;
}
(format!("{}{}", year, val), span)
}
}
} else {
(val, span)
}
} else {
let date_string: Option<Spanned<String>> =
call.get_flag(engine_state, stack, "date")?;
match date_string {
Some(date_string) => (date_string.item, date_string.span),
None => {
return Err(ShellError::MissingParameter("date".to_string(), call.head));
}
}
};
date = if let Ok(parsed_date) = parse_date_from_string(&val, span) {
Some(parsed_date.into())
} else {
let flag = if use_stamp { "timestamp" } else { "date" };
return Err(ShellError::UnsupportedInput(
format!("input has an invalid {}", flag),
span,
));
};
}
if use_reference {
let reference: Option<Spanned<String>> =
call.get_flag(engine_state, stack, "reference")?;
match reference {
Some(reference) => {
let reference_path = Path::new(&reference.item);
if !reference_path.exists() {
return Err(ShellError::UnsupportedInput(
"path provided is invalid".to_string(),
reference.span,
));
}
date = Some(
reference_path
.metadata()
.expect("should be a valid path") // Should never fail as the path exists
.modified()
.expect("should have metadata") // This should always be valid as it is available on all nushell's supported platforms (Linux, Windows, MacOS)
.into(),
);
ref_date_atime = Some(
reference_path
.metadata()
.expect("should be a valid path") // Should never fail as the path exists
.accessed()
.expect("should have metadata") // This should always be valid as it is available on all nushell's supported platforms (Linux, Windows, MacOS)
.into(),
);
}
None => {
return Err(ShellError::MissingParameter(
"reference".to_string(),
call.head,
));
}
}
}
for (index, item) in vec![target].into_iter().chain(rest).enumerate() {
if no_create {
let path = Path::new(&item);
if !path.exists() {
continue;
}
}
if let Err(err) = OpenOptions::new().write(true).create(true).open(&item) {
return Err(ShellError::CreateNotPossible(
format!("Failed to create file: {}", err),
call.positional_nth(index)
.expect("already checked positional")
.span,
));
};
if change_mtime {
// Should not panic as we return an error above if we can't parse the date
if let Err(err) = filetime::set_file_mtime(
&item,
FileTime::from_system_time(date.expect("should be a valid date").into()),
) {
return Err(ShellError::ChangeModifiedTimeNotPossible(
format!("Failed to change the modified time: {}", err),
call.positional_nth(index)
.expect("already checked positional")
.span,
));
};
}
if change_atime {
// Reference file/directory may have different access and modified times
if use_reference {
// Should not panic as we return an error above if we can't parse the date
if let Err(err) = filetime::set_file_atime(
&item,
FileTime::from_system_time(
ref_date_atime.expect("should be a valid date").into(),
),
) {
return Err(ShellError::ChangeAccessTimeNotPossible(
format!("Failed to change the access time: {}", err),
call.positional_nth(index)
.expect("already checked positional")
.span,
));
};
} else {
// Should not panic as we return an error above if we can't parse the date
if let Err(err) = filetime::set_file_atime(
&item,
FileTime::from_system_time(date.expect("should be a valid date").into()),
) {
return Err(ShellError::ChangeAccessTimeNotPossible(
format!("Failed to change the access time: {}", err),
call.positional_nth(index)
.expect("already checked positional")
.span,
));
};
}
}
}
@ -65,6 +309,36 @@ impl Command for Touch {
example: "touch a b c",
result: None,
},
Example {
description: r#"Changes the last modified time of "fixture.json" to today's date"#,
example: "touch -m fixture.json",
result: None,
},
Example {
description: "Creates files d and e and set its last modified time to a timestamp",
example: "touch -m -t 201908241230.30 d e",
result: None,
},
Example {
description: "Changes the last modified time of files a, b and c to a date",
example: r#"touch -m -d "yesterday" a b c"#,
result: None,
},
Example {
description: r#"Changes the last modified time of file d and e to "fixture.json"'s last modified time"#,
example: r#"touch -m -r fixture.json d e"#,
result: None,
},
Example {
description: r#"Changes the last accessed time of "fixture.json" to a date"#,
example: r#"touch -a -d "August 24, 2019; 12:30:30" fixture.json"#,
result: None,
},
Example {
description: "Changes both last modified and accessed time of a, b and c to a timestamp only if they exist",
example: r#"touch -c -t 201908241230.30 a b c"#,
result: None,
},
]
}
}

View File

@ -16,12 +16,16 @@ impl Command for Append {
fn signature(&self) -> nu_protocol::Signature {
Signature::build("append")
.required("row", SyntaxShape::Any, "the row to append")
.required("row", SyntaxShape::Any, "the row, list, or table to append")
.category(Category::Filters)
}
fn usage(&self) -> &str {
"Append a row to the table."
"Append any number of rows to a table."
}
fn search_terms(&self) -> Vec<&str> {
vec!["add", "concatenate"]
}
fn examples(&self) -> Vec<Example> {
@ -81,12 +85,14 @@ impl Command for Append {
) -> Result<PipelineData, ShellError> {
let val: Value = call.req(engine_state, stack, 0)?;
let vec: Vec<Value> = process_value(val);
let metadata = input.metadata();
Ok(input
.into_iter()
.chain(vec)
.into_iter()
.into_pipeline_data(engine_state.ctrlc.clone()))
.into_pipeline_data(engine_state.ctrlc.clone())
.set_metadata(metadata))
}
}

View File

@ -1,7 +1,9 @@
use nu_engine::{eval_block, CallExt};
use nu_protocol::ast::Call;
use nu_protocol::engine::{CaptureBlock, Command, EngineState, Stack};
use nu_protocol::{Category, Example, PipelineData, Signature, SyntaxShape, Value};
use nu_protocol::{
Category, Example, IntoPipelineData, PipelineData, Signature, SyntaxShape, Value,
};
#[derive(Clone)]
pub struct Collect;
@ -37,11 +39,12 @@ impl Command for Collect {
let block = engine_state.get_block(capture_block.block_id).clone();
let mut stack = stack.captures_to_stack(&capture_block.captures);
let metadata = input.metadata();
let input: Value = input.into_value(call.head);
if let Some(var) = block.signature.get_positional(0) {
if let Some(var_id) = &var.var_id {
stack.add_var(*var_id, input);
stack.add_var(*var_id, input.clone());
}
}
@ -49,10 +52,11 @@ impl Command for Collect {
engine_state,
&mut stack,
&block,
PipelineData::new(call.head),
input.into_pipeline_data(),
call.redirect_stdout,
call.redirect_stderr,
)
.map(|x| x.set_metadata(metadata))
}
fn examples(&self) -> Vec<Example> {

View File

@ -77,31 +77,34 @@ pub fn compact(
input: PipelineData,
) -> Result<nu_protocol::PipelineData, ShellError> {
let columns: Vec<String> = call.rest(engine_state, stack, 0)?;
input.filter(
move |item| {
match item {
// Nothing is filtered out
Value::Nothing { .. } => false,
Value::Record { .. } => {
for column in columns.iter() {
match item.get_data_by_key(column) {
None => return false,
Some(x) => {
if let Value::Nothing { .. } = x {
return false;
let metadata = input.metadata();
input
.filter(
move |item| {
match item {
// Nothing is filtered out
Value::Nothing { .. } => false,
Value::Record { .. } => {
for column in columns.iter() {
match item.get_data_by_key(column) {
None => return false,
Some(x) => {
if let Value::Nothing { .. } = x {
return false;
}
}
}
}
// No defined columns contained Nothing
true
}
// No defined columns contained Nothing
true
// Any non-Nothing, non-record should be kept
_ => true,
}
// Any non-Nothing, non-record should be kept
_ => true,
}
},
engine_state.ctrlc.clone(),
)
},
engine_state.ctrlc.clone(),
)
.map(|m| m.set_metadata(metadata))
}
#[cfg(test)]

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