yun@dicom

导航

Nushell 使用说明及总结

介绍

作为 Shell 语言, 我特别喜欢 Nushell 的如下几个特点:
  • 来自 UNIX Shell 的管道, 把多个命令连接在一起
  • 函数式编程风格
   事实上, 管道操作符 (|) 也是很多函数式编程的操作符
  • 丰富的对象化的数据结构
  • 对结构化文件的处理, 比如 JSON, XML, CSV, TOML 等
  • 基于模板的字符串解析
  • 在线帮助
  • 输出结果是彩色的, 而且是表格化的, 还自带编号 ! 真贴心 !
  • Footprint 很小, 安装后 < 30M (0.85 版本)
本文内容大多数来自
(文中简称 book), 但是也有很多我自己的理解.
鉴于 book 中, 已经把 Nushell 的介绍和使用说的非常详细了, 本文不打算重复或大批量抄录其内容, 更多是补充和总结.
 

基础知识

 首先

1. 数据类型

关于 Nushell 的数据类型, book 中也说的很详细了. 这里只强调和补充几个要点:
  • 可以用 describe 命令来获得前一个管道输出的数据类型描述
比如 ls | describe, 就可以知道 ls 输出的 table 的表头的名称和类型, 在编写后面的语句时, 能参考这些名字
  • 可以用 into <type> 来把前一个管道输出的数据类型转换成当前引用的数据类型
  • 字符串可以用单引号, 也可以用双引号, 两者稍有不同
  • 字符串也可以不用任何引号, 这叫裸字符串. 比引号更好的地方在于, 裸串不需要转义
例如:
open d:\tmp\aa.txt
对比: 
open "d:\\tmp\\aa.txt"
   
  • null 是特殊的,内定的数据, 表示 "没数据/未定义", 类似 C 中的 void, C# 中的 null, 或 Python 中的 None.
例如下面语句中的 null
[[meal size]; [arepa, null] ] | is-empty meal # false
   
  • 区间还可以用负数表示, 但是需要加上括号, 否则容易引起编译器误解. 例如下面的语句: 
'abcd12345678' | str substring (-5..-2)  #456
(-5..-2) 的意思是, 从倒数第五开始, 到倒数第二结束.
也可以省略区间的结尾. 比如:
(-3..)   意思是, 从倒数第三个位置开始取, 直到末尾. 即等于 (-3, -2, -1)
这个设计理念应该是从 Python 借用过来的.
负数区间在切片时非常有用
  • 用字符串的 parse 命令, 可以把一个字符串解释成若干列. 这个 "按模式切片" 的功能, 真是日志分析的利器.

2. 变量

  • Nu 中的变量,其实是 "常量", 一旦赋值后就不允许修改
  • 与常规编程语言类似, 变量有作用域, 子域可以使用同名变量, 不会覆盖父域的同名变量
  • 变量支持路径.
比如 
print $val.name
  

3. 子表达式 

  • 可以通过圆括号 () 来执行一个子表达式并使用其结果.
例如
(ls)
  • 子表达式也支持路径
例如
(ls).name
 
  • 子表达式可以简化
例如
ls | where size > 10kb
 其实是简化的子表达式. 完整的语法应该是 
ls | where {|it| $it.size > 10k}
上述语法也可以部分简化成:
ls | where $it.size > 10k

 或

ls | where ($it.size > 10k)

 

  • 简化后的子表达式, 路径名必须写在前面.
以下语句非法: 
ls | where 10k < size

但以下语句合法: 
ls | where 10k < $it.size
ls | where (10k < $it.size)

 

ls 命令介绍

列出指定目录中的目录名/文件名, 大小, 修改时间

完整格式

ls {flags} (pattern) 
 

Flags:

--all, -a : 显示隐藏文件
--long, -l : 长格式, 显示所有的列 (稍慢, 且列内容依赖于平台)
--short-names, -s : 仅显示文件名, 不显示路径
--full-paths, -f : 把路径显示成绝对路径
--du, -d : 在目录尺寸列的位置, 显示整个目录所有文件和目录占用的空间 (disk usage)
--directory, -D : 显示指定的目录, 而不是其内容
--mime-type, -m : 针对文件, 在类型列, 显示其 mime 类型, 而不是 'file'.
只根据文件名决定其 mime 类型, 不检查文件内容
 

参数:

-- pattern:
 

输入输出:

输入: 无
输出: table (表格)
可以用如下语句获得输出类型:
ls | describe
上述语句输出如下 
table <
name: string, // 文件名/目录名
type: string, // 类型 (file/dir)
size: filesize, // 文件大小
modified: date > // 最后一次修改日期+时间
如果是长格式, 输出的表格包含更多内容 (长格式依赖于平台)
ls -l | describe 
上述语句输出如下:
table <
name: string, // 文件名/目录名
type: string, // 类型 (file/dir)
target: nothing, // ??
readonly: bool, // 是否只读
size: filesize, // 文件大小
created: date, // 创建日期+时间
accessed: date, // 访问日期+时间
modified: date> // 最后一次修改日期+时间
 

例子:

列出指定类型的文件

ls *.txt

列出子目录下的文件/目录

ls out 

列出文件及目录, 但是其名称中不包含 bar

ls -s | where name !~ bar 

只列出目录, 忽略文件类型

ls | where type == 'dir' 

列出最近 7 天修改过的所有文件, 并且递归子目录

ls **\*.* | where type == 'file' and modified > ((date now) - 7day)

把目标目录放在变量中

let dir = "E:\\Work";
ls $"($dir)\\**\\*.cpp"

按大小排序, 倒序

ls | sort-by size | reverse 

显示部分列 - 文件名及大小 (忽略修改时间等)

ls | select name, size 

把输出重定向到一个文件

ls | get name | save a:\aa.txt 

注意: 重定向也要通过管道操作符, 不能用传统 shell 的 >. 

列出特定条件的文件, 条件由我指定

ls | where ($it.name | str contains -i "txt") 
 

表格 (table) 及处理

 

Nu 提供了许多处理表格的命令. 本章总结一下.

ls 命令的输出就是一个表格, 因此本节的大多数示例都用 ls 命令生成表格

排序 - sort-by

可以用 sort-by 命令对一个表进行排序, 参数为列名.
例如

ls | sort-by size

 

选取 - select

可以从表中选择特定的列或行来形成新的表格

选取 - 列. select 后面带一个或多个列名

ls | select name size


选取 - 行:

first 数字: 指定选择开始的 N 行
skip 数字: 跳过不需要的 N 行
select 数字: 选择指定的一行

例子:

ls | sort-by size | first 5
ls | sort-by size | skip 2

 

从表格提取数据 - get

参数: 列名. 返回类型: 列表

例子:

ls | get name

上述 get 操作返回一个列表. 可以用如下语句确认输出类型:

ls | get name | describe

 返回如下:

list <string>

 

 

列表 (list) 及处理

 

什么是列表 - List

列表 (List) 是一个有序的值的集合.
可以用方括号创建一个列表, 元素之间的间隔可以是空格或逗号.
例如

[1 2 3]
[a, b, c]

 

迭代列表 - each

要遍历一个列表中的元素, 可以用 each 命令与 Nu 代码块来指定对一个元素可以做什么操作.

块参数 (例如 { |it| echo $it } 中的 |it|) 通常是当前的元素.

ls | get name | each {|it| echo $it}

 可以用 where 命令来过滤, 得到列表的子集. 例如

let colors = [red orange yellow green blue purple]
$colors | where ($it | str ends-with 'e')

又如 

let lst = [1, 3, 5, 7, 9]
$lst | where ($it > 5)

 

访问单个元素

可以用 $lst.index 来获得指定索引的单个元素. 其中 index 是给定的索引
例如

let lst = [1, 3, 5, 7, 9]
$lst.1 # 返回 3

 如果索引在某个变量中, 可以使用 get 命令从列表中提前元素. 例如

let lst = [1, 3, 5, 7, 9]
let index = 2
$lst | get $index # 返回 5

 当然, 下面的代码也是可以的, 虽然有点啰嗦

let lst = [1, 3, 5, 7, 9]
$lst | get 2 # 返回 5

 

获取元素个数 - length

length 命令可以获得列表中的元素个数

let lst = [1, 3, 5, 7, 9]
$lst | length

 当然, length 命令也可以用于获得表格的行数

 

判空 - is-empty

is-empty 命令可以判定一个列表是否为空

例如:

let lst = [1, 3, 5, 7, 9]
$lst | is-empty # 返回 false

 

let lst = []
$lst | is-empty # 返回 true

 

又, is-empty 命令也可以用于字符串或表格

   

字符串及处理

字符串既可以用单引号也可以用双引号, 但两者的语法方面有差异
单引号字符串不能转义, 而双引号字符串可以转义.
 

字符串插值 (String interpolation) - 用对象名称甚至表达式来直接替换原位

这是一种从原始文本和执行表达式的结果中构建文本的方法.
字符串插值将这些结果结合在一起, 返回新的字符串.
在单引号或双引号前加入 $ 字符, 就表示字符串插值.
原位对象用 ($name) 表示, 原位表达式用 (exp) 表示

以下是几个例子

let name = "Alice"
$"greeting, ($name)"

 输出:

greeting, Aliceing>

 

$"Do you know that 2+2 is (2 + 2) ?"

 输出:

Do you know that 2+2 is 4 ?

 注意: 上述语句中的 (2 + 2) 中的两个空格不可少, 否则就不是合法表达式

字符串分割 - split row

split row 命令从一个基于分隔符的字符串来创建一个列表.
例如:

let colors = "red orange yellow green blue purple"
$colors | split row ' '

返回:

│ 0 │ red │
│ 1 │ orange │
│ 2 │ yellow │
│ 3 │ green │
│ 4 │ blue │
│ 5 │ purple │

 

split column

从一个基于分隔符的字符串来创建一个表, 并为每个元素添加一列
例如:

let colors = "red orange yellow green blue purple"
$colors | split column ' '

 返回:

│ # │ column1 │ column2 │ column3 │ column4 │ column5 │ column6 │
│ 0 │ red │ orange │ yellow │ green │ blue │ purple │

 

split chars

将一个字符串分割成一个字符列表

split words

将一个字符串分隔成单词列表
例如:

$colors | split words

 返回:

│ 0 │ red │
│ 1 │ orange │
│ 2 │ yellow │
│ 3 │ green │
│ 4 │ blue │
│ 5 │ purple │

可以用 help split 来显示完整的命令


str 命令

许多字符串函数是 str 命令的子命令.

可以用 help str 来显示完整的命令

str contains

检查字符串中是否包含某个字符或子串
例如:

"hello world" | str contains 'w' #返回: true
"hello world" | str contains 'wor' #返回: true
"hello world" | str contains 'xor' #返回: false

 

可以忽略大小写:

"hello world" | str contains 'W' #返回: false
"hello world" | str contains -i 'W' #返回: true

 

str trim

可以修剪字符串两边的空白
例如:

' My string ' | str trim

 返回

My string

带 -l 或 --left 参数以指定只修剪左侧

带 -r 或 --right 参数以指定只修剪右侧

 

str substring

截取子串

例如:

'Hello World!' | str substring 4..8 #o Wo
'abcd1234' | str substring 2.. #cd1234
'abcd1234' | str substring ..4 #abcd

范围中, 还可以用负数, 类似于 Python. 例如

'abcd1234' | str substring ..-2 #abcd12
'abcd1234' | str substring ..-3 #abcd1

取最右边的 3 个字符, 也类似 Python

'abcd12345678' | str substring (-3..) #678

 上述语句中:

  • 因为首字为负数, 因此范围必须用圆括号 (), 否则被当成 flag
  • (-3..) 意思是, 从倒数第三个位置开始取, 直到末尾. 即等于 (-3, -2, -1)
  • (-3..-1) 意思是, 从倒数第三个位置开始取, 直到倒数第一. 即等于 (-3, -2)

str start-with / str end-with

str start-with 命令可以判断字符串是否以指定的子串开头
str end-with 命令可以判断字符串是否以指定的子串结尾
例如:

"hello world" | str starts-with 'xor'
"hello world" | str starts-with 'h'
"hello world" | str starts-with 'he'

"hello world" | str ends-with 'd'
"hello world" | str ends-with 'ld'
"hello world" | str ends-with 'orld'

 

str index-of sub 

str index-of sub 命令可以返回子串在长串中的位置. 例如:

'123.456' | str index-of '1'

如果想反向搜索-从字符串尾部开始搜索, 加上参数 -e. 例如:

'123.4156' | str index-of '1' -e

-e 写前面也是可以的. 例如

'123.4156' | str index-of -e '1'

如果找到, 返回 0 开始的索引; 如果找不到, 返回 -1

 

str replace

str replace 'old' 'new' 命令可以进行子串替换
例如:

"hello world" | str replace 'hello' 'good'

返回: 

good world

 

str upcase / str downcase : 大小写转换

 

'NU' | str downcase # nu
'nu' | str upcase # NU

 

str length : 字符串长度

'Hello World!' | str length #12

 

字符串转换

有多种方法可以将字符串转换为其他类型,或者反过来.

 

转换为字符串的若干方法:

  1. 使用 into string. 例如 123 | into string
  2. 通过字符串插值. 例如 $'(123)'
  3. 使用 build-string. 例如 build-string (123)

字符串转换为其他类型:

使用 into <type>. 例如

'123' | into int

  

path 命令

path 用来探索和维护文件路径.
使用 path 命令时, 需要带上子命令.
例如

'a:\tmp\2\aa\readme.txt' | path basename

将返回 readme.txt

 

path 支持如下子命令

path basename

返回 strPath 的最后一个元素, 一般是文件名 readme.txt

path dirname

返回 strPath 的父目录

path exists

检查文件是否存在

path expand

相对路径转为绝对路径

path join

把 list/records 转换成 字符串

path parse

把 strPath 转换成结构化的 records 类型的数据

例如

│ prefix │ a: │
│ parent │ a:\tmp\2\aa │
│ stem │ abc │
│ extension │ txt │

 

path relative-to

 

path split

把 strPath 切分成 list, 每个元素都是最小成分

例如

│ 0 │ a:\ │
│ 1 │ tmp │
│ 2 │ 2 │
│ 3 │ aa │
│ 4 │ abc.txt │

 

path type

返回路径的类型, 比如 file, dir, symlink 等. 输入参数 (strPath) 必须在盘上存在.

 
 

系统命令

 

Nushell 内置了一批系统命令, 主要有

  • 关于文件和目录: 复制文件, 移动文件, 删除文件, 创建目录, 删除目录.

特别提出的是, 支持修改文件时间, 监视文件变更, 搜索文件.
搜索文件还支持两种场景:
1) 根据确定的文件名, 搜索文件位置和类型. 适用场景: 文件名确定, 但文件位置不确定
2) 在指定目录下, 按通配符搜索. 适用场景: 目录确定, 通配符确定.

  • 目录变更, 目录列举
  • 打开文件/保存文件. 特别点赞的是, 打开文件还支持 JSON/XML 等常规的结构化文件.
  • 关于进程: 列举进程, 执行外部进程, 用默认执行器打开指定文件/打开文件夹/打开 URL
  • 注册表查询

 

复制文件 - cp


cp 命令的完整格式:
cp {flags} (source) (destination)

Flags:
--recursive, -r : 递归子目录
--verbose, -v : 显示每个处理结果
--update, -u : 仅当 source 比 dest 新时, 或者 dest 不存在时, 才复制
--interactive, -i : 交互式, 每个文件都问一下
--no-symlink, -n : 忽略快捷方式或符号链接
--progress, -p : 显示进度条

例子:
简单复制文件

cp a.txt b.txt

  递归子目录

cp -r dir_a dir_b

递归子目录, 并且显示每个处理结果

cp -r -v dir_a dir_b

通配符:

cp *.txt dir_a

仅当原文件比目标文件新时, 才复制

cp -u a b

   

移动文件 - mv


mv 命令的完整格式:
mv {flags} (source) (destination)

Flags:
--force, -f : 强制覆盖目标文件
--verbose, -v : 显示每个处理结果
--update, -u : 仅当 source 比 dest 新时 (此时务必 -f), 或者 dest 不存在时, 才移动
--interactive, -i : 交互式, 每个文件都问一下

例子:
简单移动文件

mv a.txt b.txt

移动到子目录下

mv a.txt dir_a\dir_b

通配符:

mv *.txt dir_a

仅当原文件比目标文件新时, 才移动

mv -u a b

  


删除文件 - rm


rm 命令的完整格式:
rm {flags} (filename) ...rest

Flags:
--recursive, -r : 递归子目录
--verbose, -v : 显示每个处理结果
--trash, -t : 移到平台的回收站, 不是永久删除. 对 Android 和 ios 不适用
--permanent, -p : 永久删除. 也忽略 'always_trash' 配置项
--force, -f : 抑制错误信息 (即是文件不存在, 也不要告诉我)
--interactive, -i : 交互式, 每个文件都问一下
--interactive-once, -I : 交互式, 但是只问一次

例子:
简单删除文件

rm a.txt

 移动到回收站

rm --trash a.txt

 永久删除

rm -p *.txt

  强行删除, 忽略 文件不存在 的警告

rm -f *.txt

  删除当前目录中, 文件长度为 0 的所有文件

ls | where size == 0KB and type == file | each { rm -t $in.name }

  

创建或修改文件时间 - touch

既能创建一个空白文件, 也能修改文件时间

touch 命令的完整格式:
touch {flags} (filename) ...rest

Flags:
--reference, -r {to} : 把所有文件的修改时间都同步成与 to 一致
--modified, -m : 更新文件的最后修改时间. 如果未指定参数, 就把修改时间更新为当前时间
--access, -a : 更新文件的最后访问时间. 如果未指定参数, 就把修改时间更新为当前时间
--no-create, -c : 如果文件不存在, 就不要创建文件了

例子:
简单创建文件

touch a.txt

 同时创建多个文件

touch a.txt b.txt c.json

更新文件的修改时间, 改为当前时间

touch -m a.txt

更新文件的修改时间, 改为昨天

touch -m -d "yesterday" *.txt

  把文件的更新时间同步到与 test 一致

touch -m -r test a.txt b.txt c.txt

  把文件的最后访问时间改为指定时间

touch -a -d "August 24, 2023; 17:12:56" a.txt b.txt c.txt

  

搜索文件 - which

which 用于搜索可执行文件或 DLL 的位置. 需要给定一个确切的文件名, 不能是通配符.

貌似是在环境变量之 PATH 给出的列表中搜索

which 命令的完整格式:
which {flags} (filename) ...rest

Flags:
--all, -a : 列出所有的可执行文件

 

例子:

which cmd.exe

  返回:

│ # │ command │ path │ type │
│ 0 │ cmd.exe │ C:\WINDOWS\system32\cmd.exe │ external │

 

which gdi32.dll

返回:

│ # │ command │ path │ type │
│ 0 │ gdi32.dll │ C:\WINDOWS\system32\gdi32.dll │ external │

  


打开目录/文件/URL - start


start 可以用默认的应用程序或 Viewer 来打开: 目录, 文件, 或 URL

start 命令的完整格式:
start (path)

例子:

start a.txt
start b.jpg

用默认的文件管理器打开当前目录

start .

用默认的浏览器打开 website

start www.ibm.com

  

执行外部命令 - run-external

run-external 命令的完整格式:
run-external {flags} (command) ...rest

Flags:
--redirect-stdout : 把 stdout 重定向到 pipeline
--redirect-stderr : 把 stderr 重定向到 pipeline
--redirect-combine : 把 stdout 和 stderr 都重定向到 pipeline
--trim-end-newline : 删除尾部的新行

例子:

run-external "echo" "-n" "hello"

 返回:

-n hello

 例子:

run-external --redirect-stdout "echo" "-n" "hello" | split chars

  返回:

│ 0 │ - │
│ 1 │ n │
│ 2 │ │
│ 3 │ h │
│ 4 │ e │
│ 5 │ l │
│ 6 │ l │
│ 7 │ o │
│ 8 │ │
│ 9 │ │
│ │ │

明显看到, 上述返回结果中, 有空白字符

修改命令如下, 可以删除空白字符

run-external --redirect-stdout --trim-end-newline "echo" "-n" "hello" | split chars

返回:

│ 0 │ - │
│ 1 │ n │
│ 2 │ │
│ 3 │ h │
│ 4 │ e │
│ 5 │ l │
│ 6 │ l │
│ 7 │ o │

 

文件 IO - open/save

 

打开文件 - open

这个操作很神奇, 能根据扩展名判断文件类型, 然后把文件内容解释为表格 (table),随后就可以用命令来操纵表格啦

扩展名最好是小写.
如果无法自动判断, 可以用 from 命令作为管道来强行指定类型
如果编码不正确, 可以用 decode 命令作为管道.

如何处理结果? - Filter
打开文件之后, 返回的结果, 要么是半结构化的, 要么是全结构化的. 这时就需要用 filter 命令来处理结果. 参见章节: 常用的 filter 命令


open 命令的完整格式:
open {flags} (filename) ...rest

Flags:
--raw, -r: 作为原始格式打开

关于 from 命令支持的文件格式, 参见章节: from 命令

 

例子:
打开文本文件

open aa.txt

 打开 json 文件

open aa.json

 打开文件, 并强行作为 json 来解释

open aa.json | from json

 打开文件, 并指定编码:

open aa.txt -- raw | decode utf-8
open aa.txt -- raw | decode GB18030

 可以用如下命令来查看结果的格式

open package.json | describe

 结果为 (删除了一部分, 否则实在太长, 分行是我手工加的):

record <
name: string, publisher: string, description: string, displayName: string,
engines: record <vscode: string>,
categories: list <string>,
activationEvents: list <string>,
capabilities: record <virtualWorkspaces: bool, untrustedWorkspaces: record <supported: bool>>,
contributes: record <configuration: record <...>>,
taskDefinitions: table <
type: string,
required: list <string>,
properties: record <...>
when: string>
>,
repository: record <type: string, url: string>
>

 

结构非常复杂...


from 命令


把字符串或 BIN 数据解释成结构化的数据

目前支持以下子命令
Subcommands:
from csv - 解释为 .csv, 然后创建 table.
from json - 解释为 json, 然后转化为结构化数据
from nuon - 解释为 nuon, 然后转化为结构化数据
from ods - 解释为 OpenDocument 的数据表 (.ods), 然后创建 table.
from ssv - 解释为空白分隔的数据, 然后创建 table. 默认的空白间隔是 2
from toml - 解释为 tomo, 然后创建 record
from tsv - 解释为 tsv, 然后创建 table.
from xlsx - 解释为 Excel (.xlsx), 然后创建 table
from xml - 解释为 xml, 然后创建 record.
from yaml - 解释为 yaml, 然后创建 table
from yml - 同 yaml

 

保存文件 - save

save 命令的完整格式:
save {flags} (filename)

Flags:
--raw, -r : 作为原始格式保存
--append, -a : 把结果添加到文件结尾
--force, -f : 覆盖目标文件
--progress, -p : 限制进度条
--stderr, -e {path} : 把错误输出重定向到 path 指定的文件中

例子:
把字符串保存到文件中

"save me" | save aa.txt

把字符串添加到文件中

"append me" | save --append aa.txt

 把 list 保存到文件中

[a b c d x y] | save aa.json

 运行程序, 并且把 stderr 保存到文件中 (这两个语句没弄懂差别在哪)

do -i {} | save foo.txt --stderr foo.txt
do -i {} | save foo.txt --stderr bar.txt

 save 文件前, 也许希望把数据转换成指定的格式. 可以用下面的 to 命令 (与 from 相反)


to 命令

把结构化的数据转换成指定格式
子命令与 from 中的子命令相同
例子:

ls | to json | save "aa.txt"

  

常用的 Filter 命令

 

1. each for filters


each 的输入参数为 list 或 table 的每一行,
用这个参数来运行随后的闭包,
运行完毕后, 所有结果再形成一个新的 list
完整格式:
each {flags} (closure)

Flags:
--keep-empty, -k : 保留空白结果

 

例子:

[1 2 3] | each {|e| 2 * $e }

输出:

│ 0 │ 2 │
│ 1 │ 4 │
│ 2 │ 6 │

 

扫描输入的 list, 如果发现 2, 就在输出的 list 中返回 found (否则就啥都不做):

[1 2 3 2 1] | each {|it| if $it == 2 { "found"}}

输出:

│ 0 │ found │
│ 1 │ found │

 

上述命令, 如果用 -k 参数:

[1 2 3 2 1] | each-k {|it| if $it == 2 { "found"}}

结果如下:

│ 0 │ │
│ 1 │ found │
│ 2 │ │
│ 3 │ found │
│ 4 │ │

 

以下例子引用了索引值 (参见 enumerate for filters)

[1 2 3 2 1] | enumerate | each {|e| if $e.item == 2 { $"found 2 at ($e.index)!"} }

输出:

│ 0 │ found 2 at 1! │
│ 1 │ found 2 at 3! │

 

列出目录中的文件名, 并且把文件名存入另一个文件中

ls | each {|e| $e.name} | save -f a:\aa.txt

 

注意:
因为 table 是由 record 组成的 list, 因此, 如果对一个 table 调用 each,
那么传递给闭包的参数将是一个 record, 而不是一个 cell.

另外, 也要避免将一个 record 传递给 each. 因为一个 record 只有一行.
如果将 record 传递给 each, each 将只运行一次, 而不是把记录中的每个元素运行一次 !
如果非要迭代 record, 可以先把 record 转换成 table, 再迭代这个 table. 例如

{name: sam, rank: 10} | transpose key value

返回如下 table: 

│ # │ key │ value │
│ 0 │ name │ sam │
│ 1 │ rank │ 10 │

 

2. enumerate for filters


某些情况下, 我们希望迭代时, 除了值以外, 还能获得索引. enumerate 可以满足这种要求.
对一个 list 执行 enumerate, 可以返回一个 table, 其中 index 是索引, item 是值

[1 2 3 2 1] | enumerate | describe

返回:

table <index: int, item: int>

 

3. where for filters


可以用于 list, table, range
用于 list 时, 返回 list
用于 table 时, 返回 table
用于 range 时, 返回 list

 

例子:

1..5 | where {|x| $x > 2} 

返回

│ 0 │ 3 │
│ 1 │ 4 │
│ 2 │ 5 │

 

[1, 2, 3, 4, 5] | where {|x| $x > 2}

也返回

│ 0 │ 3 │
│ 1 │ 4 │
│ 2 │ 5 │

 

4. length for filters


返回 list 或 table 的数量.

例子:

[a b c d] | length # 4

 

注意:
length 不适用于字符串, 如果希望获得字符串的长度, 请用 str length

 

5. select for filters


选择指定的列.
既可以用于 record, 也可以用于 table, 还可以用于 list
参数可以是列名, 也可以是数字

 

例如:

ls | select name
ls | select 0 1 2 3 # 选择开始的 4 行, 等效于 ls | first 4

 

与 get 不同的是, 用于 table 时, 返回还是 table, 用于 list 时, 返回还是 list
例如, 比较如下两行

[a b c d] | select 1 # [b]
[a b c d] | get 1 # b

 

前者返回一个 list, 但只有一个元素: [b]. 后者直接返回值

如果超过 1 个, 两者返回相同的类型

[a b c d] | select 1 2 # [b c]
[a b c d] | get 1 2 # [b c]

 

6. get for filters

抽取数据
既可以用于 record, 也可以用于 table, 还可以用于 list

 

例子:

ls | get name # 返回 list <name>
ls | get 2.name # 返回 string
ls | get name.2 # 返回 string, 与上相同
ls | get name | get 2 # 返回 string, 与上相同
ls | get 2 | get name # 返回 string, 与上相同

 

get 语法还可以缩写为如下形式, 相当于用索引访问 list

[a b c].2 # c

 


7. items for filters


给定一个 record, 迭代每个 (key, value)

完整的语法为:
items (closure)

 

例子:

ls | get 2 | items {|key, value| echo $'($key) = ($value)' }

返回:

name = DB.txt
type = file
size = 40 B
modified = Mon, 16 Oct 2023 12:15:34 +0800 (a day ago)

 

8. lines for filters


把输入转换为 list <string>. 以换行作为分隔符.
对 open 打开的 raw text file 特别有用, 把长串转换为短串 list

完整语法为:
lines {flags}

Flags:
--skip-empty, -s : 跳过空行

 

例子:

"two\n\nlines" | lines

返回

│ 0 │ two │
│ 1 │ │
│ 2 │ lines │

  

"two\nlines" | lines

 返回:

│ 0 │ two │
│ 1 │ lines │

 

"two\n\nlines" | lines -s

返回:

│ 0 │ two │
│ 1 │ lines │

 

9. values for filters

给定一个 record 或 table, 用 values 过滤器可以生成一个 list, 每个元素来自于指定的列值

 

例子:

ls | get 2 | values

返回

│ 0 │ DB.txt │
│ 1 │ file │
│ 2 │ 40 B │
│ 3 │ a day ago │

 

如果输入是一个 table, 将生成 list <list <...>>.
例如:

ls | values

 

注:
与此对应的是 columns, 用 columns 过滤器可以生成一个 list, 每个元素来自于指定的列名

 

10. columns for filters

给定一个 record 或 table, 用 columns 过滤器可以生成一个 list, 每个元素来自于指定的列名

 

例子:

ls | get 2 | values

输出:

│ 0 │ name │
│ 1 │ type │
│ 2 │ size │
│ 3 │ modified │

 

11. 对 list 或 table 的 "掐头去尾"


skip : 忽略开头的 N 行, 如果无参数, 默认跳过开头的第一行
drop : 忽略结尾的 N 行, 如果无参数, 默认跳过结尾的最后一行
first: 返回开头的 N 行, 如果无参数, 默认返回第一行
last : 返回结尾的 N 行, 如果无参数, 默认返回最后一行

输出与输入相同: 对 list 返回 list, 对 table 返回 table

 

例子:

 

ls | first
ls | last 3
ls | skip
ls | drop 6

 

[a b c d ] | drop 2

返回:

│ 0 │ a │
│ 1 │ b │

 

12. 对 list 或 table, 用区间指定选择范围


除了掐头去尾以外, 有时我们还希望用一个区间来指定选择范围. 可以用 range 过滤器来达成这个目的

 

例子:

[a b c d e f] | range 2..4

返回

│ 0 │ c │
│ 1 │ d │
│ 2 │ e │

 

返回最后两项:

[a b c d e f] | range (-2..)

返回

│ 0 │ e │
│ 1 │ f │

 

返回倒数第3-倒数第2:

[a b c d e f] | range (-3..-2)

返回:

│ 0 │ d │
│ 1 │ e │

 

13. uniq for filters - 返回值去重


完整的格式:
uniq {flags}

Flags:
--count, -c: 返回 table, 其中 value 列给出去重后的值, count 列给出重复次数
--repeated, -d: 仅返回重复出现的项 (即 count > 1 的项), 与 -u 相反
--unique, -u: 仅返回出现一次的项 (即 count = 1 的项), 与 -d 相反
--ignore-case, -i: 忽略大小写

 

例子:

[a b a x b w c x] | uniq

返回:

│ 0 │ a │
│ 1 │ b │
│ 2 │ x │
│ 3 │ w │
│ 4 │ c │

 

[a b a x b w c x] | uniq -c

返回:

│ # │ value │ count │
│ 0 │ a │ 2 │
│ 1 │ b │ 2 │
│ 2 │ x │ 2 │
│ 3 │ w │ 1 │
│ 4 │ c │ 1 │

  

[a b a x b w c x] | uniq -d

返回:

│ 0 │ a │
│ 1 │ b │
│ 2 │ x │

 

[a b a x b w c x] | uniq -u

 返回:

│ 0 │ w │
│ 1 │ c │

 

14. uniq-by for filters - 返回值去重


如果输入是 table, 我们希望可以按列名来去重, 这时可以用 uniq-by 过滤器. 参数中可以指定一个或多个列
完整格式:
uniq-by {flags} ...rest

Flags 与 uniq 相同:

 

例子:

 

按 fruit 去重

[[fruit count]; [apple 9] [apple 2] [pear 3] [orange 2]] | uniq-by fruit

返回:

│ # │ fruit │ count │
│ 0 │ apple │ 9 │
│ 1 │ pear │ 3 │
│ 2 │ orange │ 2 │

 

按 count 去重

[[fruit count]; [apple 9] [apple 2] [pear 3] [orange 2]] | uniq-by count

返回:

│ # │ fruit │ count │
│ 0 │ apple │ 9 │
│ 1 │ apple │ 2 │
│ 2 │ pear │ 3 │

 

15. sort for filters


排序.
适用于 list 或 record

完整格式:
sort {flags}

Flags:
--reverse, -r: 逆序
--ignore-case, -i: 忽略大小写
--values, -v: 如果输入是单个 record, 将按其 value 排序; 否则忽略此选项
--natural, -n: 如果输入项是字符串组成的数字, 将转成数字来排序

 

例子:

[2 0 1] | sort

返回:

│ 0 │ 0 │
│ 1 │ 1 │
│ 2 │ 2 │

 忽略大小写排序

[airplane Truck Car] | sort -i

 返回:

│ 0 │ airplane │
│ 1 │ Car │
│ 2 │ Truck │

record 排序, 按名称

{b: 4, a: 3, c:1} | sort

 返回:

│ a │ 3 │
│ b │ 4 │
│ c │ 1 │

 

record 排序, 按值

{b: 4, a: 3, c:1} | sort -v

返回:

│ c │ 1 │
│ a │ 3 │
│ b │ 4 │

按字母顺序排序

["4", "300", "100"] | sort

返回:

│ 0 │ 100 │
│ 1 │ 300 │
│ 2 │ 4 │

 改成按数字顺序排序:

["4", "300", "100"] | sort -n

 返回:

│ 0 │ 4 │
│ 1 │ 100 │
│ 2 │ 300 │

 

16. sort-by for filters


排序.
适用于 table

完整格式:
sort-by {flags} ...rest

Flags:
--reverse, -r: 逆序
--ignore-case, -i: 忽略大小写
--natural, -n: 如果输入项是字符串组成的数字, 将转成数字来排序

 

例子

ls | sort-by modified

 

[[fruit count]; [apple 9] [apple 2] [pear 3] [orange 2]] | sort-by fruit

返回:

│ # │ fruit │ count │
│ 0 │ apple │ 9 │
│ 1 │ apple │ 2 │
│ 2 │ orange │ 2 │
│ 3 │ pear │ 3 │

  

[[fruit count]; [apple 9] [apple 2] [pear 3] [orange 2]] | sort-by count

 返回:

│ # │ fruit │ count │
│ 0 │ apple │ 2 │
│ 1 │ orange │ 2 │
│ 2 │ pear │ 3 │
│ 3 │ apple │ 9 │

 

17. find for filters

搜索


完整格式:
find {flags} ...rest

Flags:
--regex, -r {string}: 用正则表达式来匹配
--ignore-case, -i: 忽略大小写的正则; 等效于 (?i)
--multiline, -m: 多行正则模式: 用 ^ and $ 来匹配行初/行尾; 等效于 (?m)
--dotall, -s: dotall 正则模式; 等效于 (?s)
--columns, -c {list<string>}: 指定用哪些列名来搜索, 不支持正则
--invert, -v: 反向匹配

 

例子:
字符串中搜索

"abcdef" | find b

返回:

abcdef

在 file size 的 list 中搜索

[1 5 3kb 4 3Mb] | find 5 3kb

返回:

│ 0 │ 5 │
│ 1 │ 2.9 KiB │

 用正则来搜索

[abc bde arc abf] | find --regex "ab"

返回:

│ 0 │ abc │
│ 1 │ abf │

 用列名来搜索

ls | find -c [name] age

返回:

│ # │ name │ type │ size │ modified │
│ 0 │ package.json │ file │ 1.9 KB │ a day ago │

 

18. is-empty for filters


用来判断某个项是否为空.
输入参数可以是: 字符串, list, table
完整格式:
is-empty ...rest

参数可以输入列名, 用于判断特定的某一列

 

例子:

ls | is-empty

 

[[meal size]; [arepa, null] ] | is-empty meal # false
[[meal size]; [arepa, null] ] | is-empty size # true

 

 

系统或平台相关的命令

 

1. sleep - 等待/延迟


完整格式:
sleep (duration) ...rest

 

例子:

sleep 1sec # 延迟 1秒
sleep 3sec 5min # 延迟 5分 + 3秒

 

2. ps - 显示进程信息


完整格式:
ps {flags}

Flags:
--long, -l: 显示尽可能多的列

如果不加选项, ps 输出如下 table

table <pid: int, ppid: int, name: string, cpu: float, mem: filesize, virtual: filesize>

 

如果加上选项 -l, ps 输出如下 table:

table <pid: int, ppid: int, name: string, cpu: float, mem: filesize, virtual: filesize,
command: string, start_time: date, user: string, user_sid: string, priority: int,
cwd: string, environment: list<string>>

 针对 Windows 平台:

  • mem : 对应了任务管理器中的 WorkingSet
  • command : App 的完整名称: FullPath + App Name
  • user : 用户名
  • user_sid : 用户的 GUID
  • cwd : 此文件所在的目录
  • environment : 运行环境


例子:
显示占用内存最多的最后 5 个

ps | sort-by mem | last 5

可能的输出:

│ # │ pid │ ppid │ name │ cpu │ mem │ virtual │
│ 0 │ 17896 │ 17312 │ chrome.exe │ 0.00 │ 244.7 MB │ 154.3 MB │
│ 1 │ 17240 │ 3000 │ msedge.exe │ 0.00 │ 276.4 MB │ 166.9 MB │
│ 2 │ 15840 │ 3000 │ WXWork.exe │ 0.00 │ 376.7 MB │ 396.5 MB │
│ 3 │ 14132 │ 3000 │ WeChat.exe │ 0.00 │ 430.8 MB │ 363.7 MB │
│ 4 │ 3000 │ 5536 │ Explorer.EXE │ 0.00 │ 513.0 MB │ 1.7 GB │

 

3. kill - 杀死指定的进程


完整格式
kill {flags} pid ...rest

Flags:
--force, -f: 强行杀死
--quiet, -q: 安静! 不要在 Console 上显示任何信息

kill 通常与 ps 一起使用, 以便根据 name 获得 pid

 

例子:

杀死占用内存最多的进程:

ps | sort-by mem | last | kill $in.pid

 

 杀死含有指定名称的所有进程:

ps | where ($it.name | str contains -i "code.exe") | get pid | each {|it| kill -f $it }

上述语句中, where 后面的圆括号不能省略

 
 

核心命令

 

1. 用 let 定义变量


在 nu 中, 可以用 let 来定义一个 '变量'. 但其实是定义了一个常量, 一旦赋值就不能修改.
但是可以重新定义.
创建常量后, 可以通过 $来引用

以下语句是合法的

let x = 0; echo $x; let x = 'hello'; echo $x

 

另外, 变量是有作用域的, 在嵌套块中, 可以定义同名的变量, 这个变量不会影响上层的同名变量.
例如:

let my_value = 4
do { let my_value = 5; echo $my_value }
echo $my_value

先输出 5, 然后输出 4

 

后续语句不能修改的变量, 不能用于循环控制. 因此, 我们需要一个常规编程语言意义上的变量.
这种情况下, 就要用 mut 来定义真正意义上的变量

 

2. 用 mut 定义变量

例如:

mut a = 1; $a = $a + 10; echo $a

将输出 11

有了真正的 '变量' 之后, 就可以开始探索循环了

 

3. for 循环


for 的完整格式:
for {flags} (var_name) (range) (block)

Flags:
--numered, -n : 同时返回索引和值 ($it.index 和 $it.item)

参数:
var_name : 循环变量名
range : 循环的范围
block : 要运行的语句块

 

例子:

for x in [1 2 3] { print $x * $x}
for $x in 1..4 { print $x}

上述两个语句, 循环变量用 x 和 $x 都可以

 

举一个同时引用索引和值的例子:

for -n $it in ['a' 'b' 'c' 'd'] { print $"[($it.index)] is ($it.item)" }

 输出:

[0] is a
[1] is b
[2] is c
[3] is d

 

之前的 ls 语句还可以写成这样:

for it in (ls | get name) { print $it }

或者写成这样, 更简短易懂

for it in (ls).name { print $it }


4. loop 循环


loop 的完整格式:
loop (block)

参数:
block : 要运行的语句块

loop 的语法特别简单(粗暴): 参数只有一个语句块.
通常情况下, 需要配合 mut 定义的变量来控制循环何时结束

 

例子:

mut x = 0; loop { if $x > 10 { break }; $x = $x + 1 }; $x

 

5. while 循环


while 的完整格式:
while (cond) (block)

参数:
cond : 要检测的条件
block : 要运行的语句块

通常情况下, 需要配合 mut 定义的变量来控制循环何时结束


例子:

mut x = 0; while $x < 10 { $x = $x + 1; print $"running on ($x)" }

 


6. do 语句


do 语句用于执行一个闭包, 自动把管道输入 ($in) 作为参数传入

完整语法为:
do {flags} (closure) ...rest

Flags:
--ignore-errors, -i: 闭包运行时, 忽略其错误
--ignore-shell-errors, -s: 闭包运行时, 忽略 shell 错误
--ignore-program-errors, -p: 闭包运行时, 忽略外部程序错误
--capture-errors, -c: 闭包运行时, 捕获错误, 并且返回错误

参数:
closure : 要运行的闭包
...rest : 给闭包的参数

 

例子:


运行闭包:

do { echo hello }

 

把闭包保存为变量, 然后运行闭包: 

let text = "I am enclosed"; let hello = {|| echo $ text}; do $hello

 

带参数的闭包:

do {|x| 100 + $x } 77

 

从输入管道中获取数据

77 | do {|| 100 + $in }

上述语句中, $in 是系统自动传入的管道

 
 

几个综合应用的例子

 

1. 列出目录下的所有文件, 递归子目录

如果文件名中包含 hello, 就把这些文件复制到另一个目录下, 同时显示这些文件名

ls -f **/*.* | where ($it.name | str contains -i "txt") | get name | each {|it| cp -v $it "a:\\tmp25"}

 

如果希望把满足条件的文件名保存起来, 可以利用 each 的特征: 把结果合成一个 list

ls -f **/*.* | where ($it.name | str contains -i "txt") | get name | each {|it| cp -v $it "a:\\tmp25"; $it} | save -f "a:\\tmp25\\out.txt"

 

写成如下语句也可以:

ls -f **/*.* | get name | each {|it| if ($it | str contains -i "txt") {$it}} | each {|it| cp -v $it "a:\\tmp25"; $it} | save -f "a:\\tmp25\\out.txt"

或者

ls -f **/*.* | get name | where (str contains -i "txt") | each {|it| cp -v $it "a:\\tmp25"; $it} | save -f "a:\\tmp25\\out.txt"

 

2. 中的情形, 把不满足条件的文件作为列表, 存为另一个文件


ls -f **/*.* | where not ($it.name | str contains -i "txt") | get name | save -f "a:\\tmp25\\not.txt"

 

3. 搜索一系列文件, 对每个文件进行模式匹配

软件开发过程和产品生命周期中, 以下场景不可避免:

  • 功能逐渐增加
  • 需求不断变更
  • 越来越多的客户化和定制

随着代码和工程项目的分支越来越多, 版本也就越来越多, "这个功能我做过了,但是,代码到底在哪个该死的版本中?", 这个问题经常问.

我们可以用 Nu 来编写脚本, 根据蛛丝马迹, 从浩如烟海的源文件中, 找到文件名和修改时间.

 

3.1 首先设计一个从文件中搜索字符串的自定义函数

book 中叫自定义命令, 我更喜欢按传统编程语言思路, 叫做函数.

 1 def on-file [filename: string subString: string] {
 2     #print $"processing ($filename)" 
 3     let ln = open $filename -r | decode GB18030 | lines;
 4     for -n $it in ($ln) {
 5         if ($it.item | str contains -i $subString) {
 6             print $"Found at line ($it.index) of file ($filename)";
 7             return true 
 8         }
 9     };
10     return false;
11 }

 输入参数是两个: 文件名和待匹配的字符串

行 3: 定义一个变量, 保存打开文件后的行列表

行 4: 用 for 迭代, flag 为 -n, 以便获得迭代索引

行 5,6,7: 如果字符串匹配成功, 就显示行位置和文件名,然后提前返回


以下是单元测试代码: 

on-file "d:\\works\\main.cpp" "RemoveFile"

 返回:

Found at line 13 of file main.cpp

 

3.2 在 ls 命令中, 把上述辅助函数挂到管道 | 中, 就可以搜索整个目录了.

代码如下:

ls **\*.cpp | get name | each {|it| if (on-file $it "RemoveTempFile") {$it} }

 在我机器上的运行结果如下:

Found at line 131 of file Works.51\MainWindow.InitExit.cpp
│ 0 │ Works.51\MainWindow.InitExit.cpp │
Found at line 63 of file Works.51\Tool\Utility.TempFile.cpp
Found at line 245 of file Works.51\Test\TestFrameWnd.Show.cpp
│ 1 │ Works.51\Tool\Utility.TempFile.cpp │
│ 2 │ Works.51\TestFrameWnd.Show.cpp │
Found at line 149 of file Works.51\Test.InitExit.cpp
│ 3 │ Works.51\Test.InitExit.cpp │

 print 的结果与最终 list 混合在一起了, 因为 3.2 代码里面, each 每次迭代都执行一遍 print, 之后把管道输出的 list 的变更也输出

 

3.3 换一个思路, 把 on-file 改成从管道获得文件名

 

 1 def on-file [subString: string] {
 2     let filename = $in;
 3     #print $"processing ($filename)"
 4     let lines = open $filename -r | decode GB18030 | lines;
 5     for -n $it in ($lines) {
 6         if ($it.item | str contains -i $subString) {
 7             print $"Found at line ($it.index) of file ($filename)";
 8             return true 
 9         }
10     };
11     return false;
12 }

 

现在, 输入参数只有一个了, 就是待匹配的子串. 而文件名来自管道输入

行 2, 把管道输入保留在 filename 变量中.

因为有两个地方要引用文件名 (如果包括注释就是三个引用).

函数中如果多个地方引用 $in, 所有后续的引用, 要么为 null, 要么发生代码解释错误

单元测试代码需要改成:

"d:\\works\\main.cpp" | on-file "RemoveFile"

 

3.4 ls 命令改成:

ls **\*.cpp | where ($it.name | on-file "RemoveTempFile") | select name

 在我机器上的运行结果如下:

Found at line 131 of file Works.51\MainWindow.InitExit.cpp
Found at line 63 of file Works.51\Tool\Utility.TempFile.cpp
Found at line 245 of file Works.51\Test\TestFrameWnd.Show.cpp
Found at line 149 of file Works.51\Test.InitExit.cpp
│ # │ name |
│ 0 │ Works.51\MainWindow.InitExit.cpp │
│ 1 │ Works.51\Tool\Utility.TempFile.cpp │
│ 2 │ Works.51\TestFrameWnd.Show.cpp │
│ 3 │ Works.51\Test.InitExit.cpp │

可以看到, 最终的输出结果是一个 table. 而且不会跟 print 结果混合了.

 

总结

 
2023-10-20
 

posted on 2023-10-18 20:03  yun@dicom  阅读(2046)  评论(0编辑  收藏  举报