nushell/crates/nu-utils/standard_library/std.nu
Antoine Stevan 22142bd4ae
add a cross platform clip command to the standard library (#8695)
Should close the associated poin in #8311

# Description
this PR adds a `clip` command to `std` which
- has error support in case the clipboard system command is not
installed
- removes any ANSI escape sequences from the input to have a clean
copied text
- can throw desktop notifications on linux in case of a long pipeline
which needs to be copied (can be disabled with `--no-notify`)
- echoes the copied data to the output of the terminal (can be disabled
with --silent)
- has examples and dependencies listed
- has "charpage" 65001 (a.k.a. utf-8) support for windows

## new additions from 9cd3c951f to
ad3e8de25b
- better OS context error support
- use of the `match` statement for scalability
- `match` the value of the `$nu.os-info.name`
- add `macOS` with `pbcopy` to the list of supported systems
- throw an error when the OS is not supported
- ~~add simple tests to make sure `clip` works on all main OSes with the
CI~~ had to be removed in 6028b98483 (see
[the failing
test](https://github.com/nushell/nushell/actions/runs/4610091544/jobs/8148126550?pr=8695#step:6:35))

# User-Facing Changes
users can now access a `clip` to copy the output of any pipeline to the
system clipboard, on any system

# Tests + Formatting
```
$nothing
```

# After Submitting
```
$nothing
```
2023-04-05 13:19:42 -05:00

496 lines
15 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# std.nu, `used` to load all standard library components
# Universal assert command
#
# If the condition is not true, it generates an error.
#
# # Example
#
# ```nushell
# >_ assert (3 == 3)
# >_ assert (42 == 3)
# Error:
# × Assertion failed:
# ╭─[myscript.nu:11:1]
# 11 │ assert (3 == 3)
# 12 │ assert (42 == 3)
# · ───┬────
# · ╰── It is not true.
# 13 │
# ╰────
# ```
#
# The --error-label flag can be used if you want to create a custom assert command:
# ```
# def "assert even" [number: int] {
# assert ($number mod 2 == 0) --error-label {
# start: (metadata $number).span.start,
# end: (metadata $number).span.end,
# text: $"($number) is not an even number",
# }
# }
# ```
export def assert [
condition: bool, # Condition, which should be true
message?: string, # Optional error message
--error-label: record # Label for `error make` if you want to create a custom assert
] {
if $condition { return }
let span = (metadata $condition).span
error make {
msg: ($message | default "Assertion failed."),
label: ($error_label | default {
text: "It is not true.",
start: (metadata $condition).span.start,
end: (metadata $condition).span.end
})
}
}
# Assert that executing the code generates an error
#
# For more documentation see the assert command
#
# # Examples
#
# > assert error {|| missing_command} # passes
# > assert error {|| 12} # fails
export def "assert error" [
code: closure,
message?: string
] {
let error_raised = (try { do $code; false } catch { true })
assert ($error_raised) $message --error-label {
start: (metadata $code).span.start
end: (metadata $code).span.end
text: $"There were no error during code execution: (view source $code)"
}
}
# Assert $left == $right
#
# For more documentation see the assert command
#
# # Examples
#
# > assert equal 1 1 # passes
# > assert equal (0.1 + 0.2) 0.3
# > assert equal 1 2 # fails
export def "assert equal" [left: any, right: any, message?: string] {
assert ($left == $right) $message --error-label {
start: (metadata $left).span.start
end: (metadata $right).span.end
text: $"They are not equal. Left = ($left). Right = ($right)."
}
}
# Assert $left != $right
#
# For more documentation see the assert command
#
# # Examples
#
# > assert not equal 1 2 # passes
# > assert not equal 1 "apple" # passes
# > assert not equal 7 7 # fails
export def "assert not equal" [left: any, right: any, message?: string] {
assert ($left != $right) $message --error-label {
start: (metadata $left).span.start
end: (metadata $right).span.end
text: $"They both are ($left)."
}
}
# Assert $left <= $right
#
# For more documentation see the assert command
#
# # Examples
#
# > assert less or equal 1 2 # passes
# > assert less or equal 1 1 # passes
# > assert less or equal 1 0 # fails
export def "assert less or equal" [left: any, right: any, message?: string] {
assert ($left <= $right) $message --error-label {
start: (metadata $left).span.start
end: (metadata $right).span.end
text: $"Left: ($left), Right: ($right)"
}
}
# Assert $left < $right
#
# For more documentation see the assert command
#
# # Examples
#
# > assert less 1 2 # passes
# > assert less 1 1 # fails
export def "assert less" [left: any, right: any, message?: string] {
assert ($left < $right) $message --error-label {
start: (metadata $left).span.start
end: (metadata $right).span.end
text: $"Left: ($left), Right: ($right)"
}
}
# Assert $left > $right
#
# For more documentation see the assert command
#
# # Examples
#
# > assert greater 2 1 # passes
# > assert greater 2 2 # fails
export def "assert greater" [left: any, right: any, message?: string] {
assert ($left > $right) $message --error-label {
start: (metadata $left).span.start
end: (metadata $right).span.end
text: $"Left: ($left), Right: ($right)"
}
}
# Assert $left >= $right
#
# For more documentation see the assert command
#
# # Examples
#
# > assert greater or equal 2 1 # passes
# > assert greater or equal 2 2 # passes
# > assert greater or equal 1 2 # fails
export def "assert greater or equal" [left: any, right: any, message?: string] {
assert ($left >= $right) $message --error-label {
start: (metadata $left).span.start
end: (metadata $right).span.end
text: $"Left: ($left), Right: ($right)"
}
}
# Assert length of $left is $right
#
# For more documentation see the assert command
#
# # Examples
#
# > assert length [0, 0] 2 # passes
# > assert length [0] 3 # fails
export def "assert length" [left: list, right: int, message?: string] {
assert (($left | length) == $right) $message --error-label {
start: (metadata $left).span.start
end: (metadata $right).span.end
text: $"Length of ($left) is ($left | length), not ($right)"
}
}
# Assert that ($left | str contains $right)
#
# For more documentation see the assert command
#
# # Examples
#
# > assert str contains "arst" "rs" # passes
# > assert str contains "arst" "k" # fails
export def "assert str contains" [left: string, right: string, message?: string] {
assert ($left | str contains $right) $message --error-label {
start: (metadata $left).span.start
end: (metadata $right).span.end
text: $"'($left)' does not contain '($right)'."
}
}
# Add the given paths to the PATH.
#
# # Example
# - adding some dummy paths to an empty PATH
# ```nushell
# >_ with-env [PATH []] {
# std path add "foo"
# std path add "bar" "baz"
# std path add "fooo" --append
#
# assert equal $env.PATH ["bar" "baz" "foo" "fooo"]
#
# print (std path add "returned" --ret)
# }
# ╭───┬──────────╮
# │ 0 │ returned │
# │ 1 │ bar │
# │ 2 │ baz │
# │ 3 │ foo │
# │ 4 │ fooo │
# ╰───┴──────────╯
# ```
export def-env "path add" [
--ret (-r) # return $env.PATH, useful in pipelines to avoid scoping.
--append (-a) # append to $env.PATH instead of prepending to.
...paths # the paths to add to $env.PATH.
] {
let-env PATH = (
$env.PATH
| if $append { append $paths }
else { prepend $paths }
)
if $ret {
$env.PATH
}
}
# Maintain a list of working directories and navigates them
# the directory stack
export-env {
let-env DIRS_POSITION = 0
let-env DIRS_LIST = [($env.PWD | path expand)]
}
# Add one or more directories to the list.
# PWD becomes first of the newly added directories.
export def-env "dirs add" [
...paths: string # directory or directories to add to working list
] {
mut abspaths = []
for p in $paths {
let exp = ($p | path expand)
if ($exp | path type) != 'dir' {
let span = (metadata $p).span
error make {msg: "not a directory", label: {text: "not a directory", start: $span.start, end: $span.end } }
}
$abspaths = ($abspaths | append $exp)
}
let-env DIRS_LIST = ($env.DIRS_LIST | insert ($env.DIRS_POSITION + 1) $abspaths | flatten)
let-env DIRS_POSITION = $env.DIRS_POSITION + 1
_fetch 0
}
# Advance to the next directory in the list or wrap to beginning.
export def-env "dirs next" [
N:int = 1 # number of positions to move.
] {
_fetch $N
}
# Back up to the previous directory or wrap to the end.
export def-env "dirs prev" [
N:int = 1 # number of positions to move.
] {
_fetch (-1 * $N)
}
# Drop the current directory from the list, if it's not the only one.
# PWD becomes the next working directory
export def-env "dirs drop" [] {
if ($env.DIRS_LIST | length) > 1 {
let-env DIRS_LIST = (
($env.DIRS_LIST | take $env.DIRS_POSITION)
| append ($env.DIRS_LIST | skip ($env.DIRS_POSITION + 1))
)
}
_fetch 0
}
# Display current working directories.
export def-env "dirs show" [] {
mut out = []
for $p in ($env.DIRS_LIST | enumerate) {
$out = ($out | append [
[active, path];
[($p.index == $env.DIRS_POSITION), $p.item]
])
}
$out
}
# fetch item helper
def-env _fetch [
offset: int, # signed change to position
] {
# nushell 'mod' operator is really 'remainder', can return negative values.
# see: https://stackoverflow.com/questions/13683563/whats-the-difference-between-mod-and-remainder
let pos = ($env.DIRS_POSITION
+ $offset
+ ($env.DIRS_LIST | length)
) mod ($env.DIRS_LIST | length)
let-env DIRS_POSITION = $pos
cd ($env.DIRS_LIST | get $pos )
}
def CRITICAL_LEVEL [] { 50 }
def ERROR_LEVEL [] { 40 }
def WARNING_LEVEL [] { 30 }
def INFO_LEVEL [] { 20 }
def DEBUG_LEVEL [] { 10 }
def parse-string-level [level: string] {
(
if $level == "CRITICAL" { (CRITICAL_LEVEL)}
else if $level == "CRIT" { (CRITICAL_LEVEL)}
else if $level == "ERROR" { (ERROR_LEVEL) }
else if $level == "WARNING" { (WARNING_LEVEL) }
else if $level == "WARN" { (WARNING_LEVEL) }
else if $level == "INFO" { (INFO_LEVEL) }
else if $level == "DEBUG" { (DEBUG_LEVEL) }
else { (INFO_LEVEL) }
)
}
def current-log-level [] {
let env_level = ($env | get -i NU_LOG_LEVEL | default (INFO_LEVEL))
try {
($env_level | into int)
} catch {
parse-string-level $env_level
}
}
def now [] {
date now | date format "%Y-%m-%dT%H:%M:%S%.3f"
}
# Log critical message
export def "log critical" [message: string] {
if (current-log-level) > (CRITICAL_LEVEL) { return }
print --stderr $"(ansi red_bold)CRT|(now)|($message)(ansi reset)"
}
# Log error message
export def "log error" [message: string] {
if (current-log-level) > (ERROR_LEVEL) { return }
print --stderr $"(ansi red)ERR|(now)|($message)(ansi reset)"
}
# Log warning message
export def "log warning" [message: string] {
if (current-log-level) > (WARNING_LEVEL) { return }
print --stderr $"(ansi yellow)WRN|(now)|($message)(ansi reset)"
}
# Log info message
export def "log info" [message: string] {
if (current-log-level) > (INFO_LEVEL) { return }
print --stderr $"(ansi white)INF|(now)|($message)(ansi reset)"
}
# Log debug message
export def "log debug" [message: string] {
if (current-log-level) > (DEBUG_LEVEL) { return }
print --stderr $"(ansi default_dimmed)DBG|(now)|($message)(ansi reset)"
}
# print a command name as dimmed and italic
def pretty-command [] {
let command = $in
return $"(ansi default_dimmed)(ansi default_italic)($command)(ansi reset)"
}
# give a hint error when the clip command is not available on the system
def check-clipboard [
clipboard: string # the clipboard command name
--system: string # some information about the system running, for better error
] {
if (which $clipboard | is-empty) {
error make --unspanned {
msg: $"(ansi red)clipboard_not_found(ansi reset):
you are running ($system)
but
the ($clipboard | pretty-command) clipboard command was not found on your system."
}
}
}
# put the end of a pipe into the system clipboard.
#
# Dependencies:
# - xclip on linux x11
# - wl-copy on linux wayland
# - clip.exe on windows
#
# Examples:
# put a simple string to the clipboard, will be stripped to remove ANSI sequences
# >_ "my wonderful string" | clip
# my wonderful string
# saved to clipboard (stripped)
#
# put a whole table to the clipboard
# >_ ls *.toml | clip
# ╭───┬─────────────────────┬──────┬────────┬───────────────╮
# │ # │ name │ type │ size │ modified │
# ├───┼─────────────────────┼──────┼────────┼───────────────┤
# │ 0 │ Cargo.toml │ file │ 5.0 KB │ 3 minutes ago │
# │ 1 │ Cross.toml │ file │ 363 B │ 2 weeks ago │
# │ 2 │ rust-toolchain.toml │ file │ 1.1 KB │ 2 weeks ago │
# ╰───┴─────────────────────┴──────┴────────┴───────────────╯
#
# saved to clipboard
#
# put huge structured data in the clipboard, but silently
# >_ open Cargo.toml --raw | from toml | clip --silent
#
# when the clipboard system command is not installed
# >_ "mm this is fishy..." | clip
# Error:
# × clipboard_not_found:
# │ you are using xorg on linux
# │ but
# │ the xclip clipboard command was not found on your system.
export def clip [
--silent: bool # do not print the content of the clipboard to the standard output
--no-notify: bool # do not throw a notification (only on linux)
] {
let input = $in
let input = if ($input | describe) == "string" {
$input | ansi strip
} else { $input }
match $nu.os-info.name {
"linux" => {
if ($env.WAYLAND_DISPLAY? | is-empty) {
check-clipboard xclip --system $"('xorg' | pretty-command) on linux"
$input | xclip -sel clip
} else {
check-clipboard wl-copy --system $"('wayland' | pretty-command) on linux"
$input | wl-copy
}
},
"windows" => {
chcp 65001 # see https://discord.com/channels/601130461678272522/601130461678272524/1085535756237426778
check-clipboard clip.exe --system $"('xorg' | pretty-command) on linux"
$input | clip.exe
},
"macos" => {
check-clipboard pbcopy --system macOS
$input | pbcopy
},
_ => {
error make --unspanned {
msg: $"(ansi red)unknown_operating_system(ansi reset):
'($nu.os-info.name)' is not supported by the ('clip' | pretty-command) command.
please open a feature request in the [issue tracker](char lparen)https://github.com/nushell/nushell/issues/new/choose(char rparen) to add your operating system to the standard library."
}
},
}
if not $silent {
print $input
print --no-newline $"(ansi white_italic)(ansi white_dimmed)saved to clipboard"
if ($input | describe) == "string" {
print " (stripped)"
}
print --no-newline $"(ansi reset)"
}
if (not $no_notify) and ($nu.os-info.name == linux) {
notify-send "std clip" "saved to clipboard"
}
}