Nushell 使用说明及总结
介绍
- 来自 UNIX Shell 的管道, 把多个命令连接在一起
- 函数式编程风格
- 丰富的对象化的数据结构
- 对结构化文件的处理, 比如 JSON, XML, CSV, TOML 等
- 基于模板的字符串解析
- 在线帮助
- 输出结果是彩色的, 而且是表格化的, 还自带编号 ! 真贴心 !
- Footprint 很小, 安装后 < 30M (0.85 版本)
基础知识
1. 数据类型
- 可以用 describe 命令来获得前一个管道输出的数据类型描述
- 可以用 into <type> 来把前一个管道输出的数据类型转换成当前引用的数据类型
- 字符串可以用单引号, 也可以用双引号, 两者稍有不同
- 字符串也可以不用任何引号, 这叫裸字符串. 比引号更好的地方在于, 裸串不需要转义
open d:\tmp\aa.txt
open "d:\\tmp\\aa.txt"
- null 是特殊的,内定的数据, 表示 "没数据/未定义", 类似 C 中的 void, C# 中的 null, 或 Python 中的 None.
[[meal size]; [arepa, null] ] | is-empty meal # false
- 区间还可以用负数表示, 但是需要加上括号, 否则容易引起编译器误解. 例如下面的语句:
'abcd12345678' | str substring (-5..-2) #456
- 用字符串的 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:
--long, -l : 长格式, 显示所有的列 (稍慢, 且列内容依赖于平台)
--short-names, -s : 仅显示文件名, 不显示路径
--full-paths, -f : 把路径显示成绝对路径
--du, -d : 在目录尺寸列的位置, 显示整个目录所有文件和目录占用的空间 (disk usage)
--directory, -D : 显示指定的目录, 而不是其内容
--mime-type, -m : 针对文件, 在类型列, 显示其 mime 类型, 而不是 'file'.
只根据文件名决定其 mime 类型, 不检查文件内容
参数:
输入输出:
输出: 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
字符串转换
有多种方法可以将字符串转换为其他类型,或者反过来.
转换为字符串的若干方法:
- 使用 into string. 例如 123 | into string
- 通过字符串插值. 例如 $'(123)'
- 使用 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 结果混合了.
总结
posted on 2023-10-18 20:03 yun@dicom 阅读(2046) 评论(0) 编辑 收藏 举报