2 个版本

0.1.1 2021 年 1 月 8 日
0.1.0 2019 年 12 月 11 日

#2775命令行工具

Apache-2.0

460KB
3K SLoC

atom

令人惊艳的 Shell 脚本。

注意:点击上面的图片查看视频演示。

为什么编写 Shell 呢?

Bash 在编程世界中无处不在。实际上,真的是无处不在。如果您下载并安装此项目,您很可能会使用 bash 来这样做(或某些 bash 衍生版本/兼容的 shell)。

通常,我 非常喜欢 这样一个小巧简单的平台如 bash 在如此广泛的应用。它通常使大家的生活更轻松。

通常。

Bash 脚本需要被取缔

说真的,人们究竟是如何编写 bash 脚本的??? 对于我们绝大多数不是绝对 200-IQ 天才的人来说,它绝对是 无法使用 的。每次我想在目录中的每个文件上执行一个 简单的 for 循环 时,我必须查阅它,然后 仍然 放弃,因为太难了。

我们怎么能期望用户编写这样的代码呢?

shopt -s nullglob
for path in ./*; do
    [[ ${path##*/} != *.* ]] && rm "$path"
done

或者这样的代码??

oIFS=${IFS+_${IFS}}
IFS=/; echo "${array[*]}"
${oIFS:+'false'} unset -v IFS || IFS=${oIFS#_}

答案: 他们不能。

bash 脚本为什么如此糟糕的核心原因是什么?

bash 脚本功能中最糟糕的部分如下

  1. Bash 代码被设计成以难以阅读的方式快速组合在一起,这并不是脚本功能的好设计目标。当命令意味着典型的文件导航和运行程序时,它们应该是快速和简单的,而 脚本功能应该是更易于阅读和一致的。
  2. Bash 甚至没有实现其 一个 设计目标,因为脚本如此难以阅读,以至于根本无法进行任何组合。

atom 如何解决这些问题?

在我看来,shell 有两个方面。交互式方面和脚本方面。创建一个 优秀的 shell 意味着很好地平衡这两种模式。如果你使一种语言非常适合 脚本,那么文件导航和其他交互式功能就会受到影响。另一方面,如果你使一种语言非常适合交互式命令(如 bash),那么脚本就变得不可能。

Atom 尝试在两种模式之间寻求更好的平衡,在我看来,它做得相当成功。它似乎更适合脚本编写而不是交互式编程,但我并不介意为此做出牺牲。

Atom 的设计目标是

  1. 外壳脚本 必须 足够强大,能够作为传统的高级语言。
  2. 同时,编写 交互式 命令不应该有太多的语法糖。
  3. 应拒绝不正确的代码。不应尝试理解用户的错误代码 (参见 JavaScript坏代码就是坏代码
  4. 声明式和函数式编程优先,命令式编程最后

我认为这些目标非常适合 Atom。

为了展示 Atom 的脚本功能,我用它编写了一个完整的卡片游戏!

实际上,CPU 的表现比我好得多,它多次以两倍或三倍于我的分数击败了我。它根本不欺骗,它只是根据协同作用来排列其卡片,鼓励选择与其最佳卡片协同作用良好的卡片,并丢弃最差的卡片。

如果您想尝试我所有的自定义宏,并使用我的启动屏幕,请在您的家目录中使用我的 .atom-prelude 文件并尽情实验!要玩我的卡片游戏,请运行 rummy@play'

关于作者

我在大学里无聊地隔离着。如果您喜欢我的项目,请考虑通过给我买咖啡来支持我!

用法

Atom 与其他任何外壳都截然不同,无论是其外部语法还是内部功能。

例如,Atom 支持传统的表格、列表、字符串、整数、浮点数、布尔值等,就像 这个大大优于且更专业的外壳,我本应该一开始就使用它(但那样没有乐趣。“非原创综合症”真的在我身上有影响力,不是吗?)

但 Atom 也直接受到像 lisp 这样的语言的影响,通过包括符号作为一等类型,并通过实现迭代结构如 forwhile 循环作为 而不是值的操作来实施。

此外,它还添加了 lambda(可以捕获其环境)和宏(可以更改其父环境)。

交互式语法

尽管这种语法在所有脚本中都是有效的语句,但我的意图主要是用于交互式编程。

通常,在 Atom 和 bash 中执行程序(以及函数)仅相差一个字符。

Bash

$ g++ main.cpp -o main

Atom

$ g++' main.cpp -o main

您可能会想,在程序名称后编写引号并不那么方便,但实际上并不坏。我发现现在当我使用 bash 时,我似乎无法不意外地输入任何命令 而不 使用它。

我选择 ' 字符的原因是它是除了分号之外,没有在符号中使用(且不使用 shift 键访问)的最易获得的字符。命令 g++' main.cpp -o main 实际上只是多按了一个键。

为了赋予上帝赋予的脚本功能,这是一个微不足道的牺牲。

别名

您可能会发现,您已经定义了一个带有不可调用值(不是宏或函数)的符号 g++,例如 5

如果情况是这样,只需将符号 g++ 用引号括起来,然后按照如下方式运行: "g++"' main.cpp -o main

这也意味着你可以通过定义一个字符串或路径的符号来简单地创建别名。

这个片段将 ls 定义为 lsd 程序的别名,并将 cat 定义为 bat 程序的别名。

ls := "lsd";
ls'

脚本语法

再次强调,脚本语法实际上只是鼓励用于脚本的语法。本 README 中的所有语法都将在任何地方工作。

为了从简单开始,让我们定义一个变量。

x := 5;

哇!我们已经改变了我们的环境!这意味着每次计算符号 x 时,它都会变成 5

现在让我们定义其他一些东西。

WEEKDAYS := [
	"Sunday",
	"Monday",
	"Tuesday",
	"Wednesday",
	"Thursday",
	"Friday",
	"Saturday"
];

grades := {
    "adam": 50,
    "literally everyone else": 100
};

太棒了!现在符号 WEEKDAYS 被绑定到一个包含所有工作日名称的列表,而符号 grades 被绑定到一个包含学生成绩的表中!

现在让我们尝试使用 lambda 函数做一些有趣的事情。

min := \x,y -> x < y? x : y;
max := fn(x, y) -> x < y? x : y;

# You can put brackets around the lambda body for multiple statements
return-five :=   () -> { print("returning 5"); 5 };
return-six  := fn() -> 6;

increment := x -> x + 1;
decrement := fn(x) -> x - 1;

正如你所看到的,我们可以使用 ... -> fn(...) -> 语法糖来创建具有多个参数的 lambda 函数,() -> 来创建一个不接受任何参数的 lambda,或者只需一个符号和一个箭头,来创建一个接受单个参数的 lambda。多么方便!

你还可以使用 fn 关键字来定义函数,而不将它们分配。

fn is-leapyear(year) {
	if year % 4 = 0 and year % 100 != 0 {
		true
	} else if year % 100 = 0 and year % 400 = 0 {
		true
	} else {
		false
	}
};

fn days-in-month(month, year) {
	month = 2? 28 + to-int(is-leapyear(year)) : 31 - (((month - 1) % 7) % 2)
};

fn day-of-week(m, d, y) {
	t := [0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4];

    # Notice that semicolons go on the end of if statements too
	if m < 3 { y := y - 1 };
    
    # The last expression in a function is the value that is returned
	(((y + to-int(y / 4)) - to-int(y / 100)) + to-int(y / 400) + t[m - 1] + d) % 7
};

请注意,在 每个表达式 的末尾都有一个分号。它对于除了块中的最后一个值之外的所有内容都是必需的!

你还可以使用 macro 关键字以类似的方式定义宏

# We write `; nil` at the end of the macro so that
# the macro returns nil, instead of the new CWD value.
root := macro() -> { CWD := ROOT; nil };

macro quit() {
	print("Goodbye!👋");
	sleep(0.4);
	exit();
};

这难道不令人愉快吗???

函数与宏

函数和宏看起来非常相似,并且执行几乎相同的任务,但它们的影响截然不同。

简单来说,函数创建自己的局部作用域,当函数返回时,局部变量会被丢弃。然而,宏使用它在其中被调用的 当前作用域 作为其作用域。

那些将被宏的参数覆盖的变量会被保存。以下代码为例

x := 5;
y := nil;
macro test(x) { y := x };

print(x, y);
test("x is still 5, but y is not nil");
print(x, y);

宏允许你读取和写入全局状态,而(用户定义的)函数对程序的全球状态没有副作用。

然而,内置函数完全是另一种情况...

内置函数与函数

内置函数有点名不副实。它们是半宏,半函数,还有一点点其他无法量化的东西。它们是如何工作的?也许世界永远也不会知道。

它们之所以如此神秘,是因为它们可以改变它们被调用的范围(就像宏一样),它们可以选择不评估它们的参数(与宏不同),并且它们可以接受不同数量的参数。例如,考虑内置函数cd。它不仅评估其参数(使用cdDesktop不会评估符号Desktop),它还修改了当前作用域中的CWD变量。这有多奇怪?

$ home'
$ cd' Desktop

此外,像echoprintto-str这样的内置函数可以接受不同数量的参数。

$ print("x is equal to", x := 5, "and y is equal to", y := 6)

尽管内置函数的这些功能很酷且奇特,但它们在许多情况下并不常用。printcd这样的函数是规则的例外,你可以期待大多数函数的行为与常规用户定义函数完全相同,没有任何奇怪的陷阱。

模块

Atom有一个非常丰富的内置库列表(对于一个用一周左右写成的shell来说)。以下是内置模块的列表。

模块(这只是一个用来表示作为库功能的表的时髦名称),可以通过@member运算符或["member"]运算符来访问。

示例用法

shuffled-deck := rand@shuffle(cards@deck@all);
echo(shuffled-deck[0]);
模块 描述 成员
rand 一个包含混乱的模块。明智地使用你的熵的力量,年轻的脚本编写者。 {int: fn(int,int) ->int,shuffle: fn([any]) -> [any],choose: fn([any]) ->any}
fmt 一个用于格式化字符串的模块。到目前为止,它只包含操作颜色、粗体、下划线等的函数。 {red: fn(str) -> str,green: fn(str) -> str,blue: fn(str) -> str,yellow: fn(str) -> str,magenta: fn(str) -> str,cyan: fn(str) -> str,black: fn(str) -> str,gray: fn(str) -> str,grey: fn(str) -> str,white: fn(str) -> str,dark: {red: fn(str) -> str,green: fn(str) -> str,blue: fn(str) -> str,cyan: fn(str) -> str,yellow: fn(str) -> str,magenta: fn(str) -> str, },bold: fn(str) -> str,invert: fn(str) -> str,underline: fn(str) -> str }
widget 一个用于创建在终端中显示文本的小型模块。小部件有一个标题、一个内容字符串、一个宽度和一个高度。 {create: fn(str, str,int,int) -> str,add-horizontal: fn(str...) -> str,add-vertical: fn(str...) -> str }
math 一个用于各种数学函数的模块。三角学、多对数等。 {E:float, PI:float, TAU:float,pow: fn(float,float) ->float,log: fn(float,float) ->float,log10: fn(float) ->float,log2: fn(float) ->float,sqrt: fn(float) ->float,cbrt: fn(float) ->float,sin: fn(float) ->float,cos: fn(float) ->float,tan: fn(float) ->float,asin: fn(float) ->float,acos: fn(float) ->float,atan: fn(float) ->float}
os 一个用于获取操作系统信息的小模块。对于创建跨平台脚本很有用。 {name: str,family: str,version: str }
sh 一个用于获取有关shell信息的小模块,例如版本、可执行文件的路径、可执行文件的父目录以及预置脚本(shell启动时运行的脚本,如.bashrc)的路径。成员version包含主版本、次版本和补丁整数。 {exe:path,dir:path,version: [int],prelude:path}
file 一个用于文件操作的小模块。目前功能不多。简单点。 {read: fn(path orstror sym) -> str,write: fn(path orstror sym, str) ->nil,append: fn(path orstror sym, str) ->nil}
date 日期模块有点奇怪。它与其说是一个模块,不如说是一个隐藏的函数。每次访问date时,它都会使用一个不断更新的表格,其中包含当前的日期信息和日期字符串。 {day:int,weekday:int,month:int,year:int, str: str }
time 时间模块的功能就像日期模块,但针对时间。到目前为止,我最喜欢这个模块的功能是编写宏,当用户试图在分钟的第一个秒编译时,会打印出伪造和奇异的 g++ 错误。这个模块充满了纯粹邪恶的东西。 {小时:int,分钟:int,小时:int, str: str }
这是一个用于牌类游戏的模块。牌只是具有相应Unicode表示的字符串。例如,值 cards@deck@aces[0]"🂡"。在包含多个花色的模块中的每个列表中,它们交替出现黑桃、红心、方块、梅花。因此,cards@deck@all["🂡", "🂱", "🃁", "🃑", "🂢", ..., "🃞"] {牌组: {全部: [str],Ace: [str],国王: [str],王后: [str],杰克: [str],人头: [str],数字: [str] },花色: {黑桃: str,梅花: str,红心: str,方块: str },花色: fn(str) -> str,: fn(str) ->int,name: fn(str) -> str,来自-name: fn(str) -> str,背面: str }
国际象棋 这是一个用于国际象棋的模块。棋盘以行列表的形式存储,行列表是棋子列表。与牌类似,棋子只是具有相应Unicode表示的字符串。因此,cards@white@king"",而 cards@black@king"" {white: {国王: str,王后: str,: str,: str,: str,: str },black: {国王: str,王后: str,: str,: str,: str,: str },空间: str,-棋子: fn(str) -> 布尔型,-空间: fn(str) -> 布尔型,-white: fn(str) -> 布尔型,-black: fn(str) -> 布尔型,create: fn() -> [[str]],翻转: fn([[str]]) -> [[str]],获取: fn([[str]], str) -> str,fmt: fn([[str]]) -> str,打印: fn([[str]]) ->nil,移动: fn([[str]], str, str) -> [[str]],add: fn([[str]], str, str) -> [[str]],删除: fn([[str]], str) -> [[str]] }

这些都是为了使脚本编写变得极其便捷。有了内置的广泛任务库,编写脚本将变得非常容易。

我们提供积木,你提供胶水。

常量和内置函数

这些常量和内置函数旨在在脚本编写和交互式提示符中频繁使用,因此它们都包含在全局作用域中。

如果愿意,可以覆盖它们。在使用宏将这些值赋值时要小心!在运行代码之前,请确保您的代码不会破坏某些东西!

名称 描述 类型
nil() Python 的 None 的原子等价物 nil nil
truetruth 布尔值,表示真 布尔型 true
false 布尔值,表示假 布尔型 false
CWD 当前工作目录的路径 path 见描述
HOME 主目录的路径 ^ ^
VIDS 视频目录的路径 ^ ^
DESK 桌面目录的路径 ^ ^
PICS 图片目录的路径 ^ ^
DOCS 文档目录的路径 ^ ^
DOWN 下载目录的路径。 ^ ^
report 用于打印命令结果的函数。可以用来以自定义的方式格式化结果,或用于不打印nil值。 fn(any) ->nil 默认情况下,fn(val) -> print(" =>", val)
prompt 用于生成用户输入命令的提示信息的函数。它接受当前工作目录作为参数。 fn(path) -> str 默认情况下,fn(cwd) -> to-str(cwd) + "> "
incomplete-prompt 用于在用户输入不完整的代码行后生成用户输入命令的提示信息的函数。它接受当前工作目录作为参数。 ^ 默认情况下,fn(cwd) -> " " * len(cwd) + "> "
absolute 此函数接受一个路径,移除路径中的任何无关部分(例如 foo/../bar),并将路径转换为绝对路径。例如,在主目录中的 ./testing 会变成 /home/adam/testing fn(path) -> pathfn(sym) -> pathfn(str) -> path 本地代码。
exists 此函数返回是否任何路径存在。 fn(path) -> boolfn(sym) -> boolfn(str) -> bool ^
is-err 此函数返回内层表达式的评估是否返回错误。 fn(any) -> 布尔型 ^
is-syntax-err 此函数返回错误是否为语法错误。这主要用于与 report 函数一起使用。 fn(any) -> 布尔型 ^
sleep 使shell暂停指定的秒数。 fn(float) ->nil ^
to-path 将字符串或符号转换为路径。 fn(path orstror sym) ->path ^
to-float 将字符串、整数、浮点数或布尔值转换为浮点值。 fn(str或 int 或 float 或布尔型) ->float ^
to-int 将字符串、整数、浮点数或布尔值转换为整数。 fn(str或 int 或 float 或布尔型) ->int ^
input 使用提示信息获取用户输入。 fn(any...) -> str ^
rev 反转字符串或列表。 fn(str) -> strfn([任何]) -> [any] ^
分割 使用给定分隔符分割字符串。 fn(str, str) -> str ^
排序 对整数列表进行排序。 fn([int]) -> [int] ^
连接 使用分隔符连接列表。 fn([any],any) -> str ^
环境 包含作用域内所有绑定的表。 () -> ^
HOMEVIDSDESKPICSDOCSDOWN 相应目录的路径。 path ^
homevidsdeskpicsdocsdown 将当前工作目录设置为相应目录的宏。 () ->nil () ->CWD:= ...
exitquit 退出当前shell会话。 fn() ->nil 本地代码。
解绑 解绑具有给定名称的符号。 (str) ->nil ^
打印 打印一个或多个值,并返回最后一个。 fn(any...) ->any ^
echo 打印一个或多个值,并返回 nil fn(any...) ->nil ^
pwdcwd 打印当前工作目录。 (路径或字符串或符号) ->nil ^
cd 更改目录。此宏是一个特殊形式,它不会评估其参数。例如,如果 x 被定义为 5,则 cd' x不会 执行 cd' 5,它将执行 cd' "x" (路径或字符串或符号) ->nil ^
cd-eval 将目录更改为一个已评估的值。当您在编写脚本时希望 cd 进入一个未知名称的文件夹时使用。 (any) ->nil ^
clearcls 清除控制台。 fn() ->nil fn() -> { ifos@family= "linux"或 os@family= "unix" {clear' } else ifos@family= "windows" {cls' } else { 打印("\n" * 255) } }
keys 获取表中的键列表。 fn() -> [str] 本地代码。
vals 获取表中的值列表。 fn() -> [any] ^
insert 返回一个表,其中插入具有给定键的值。 fn(, str,any) -> ^
remove 返回一个表,其中已移除具有给定键的值。 fn(, str) -> ^
len 获取列表或字符串的长度,表中的对数,或路径的组件数。 fn([any]或表或str或路径) ->int ^
push 将给定元素添加到列表中。 fn([any],any) -> [any] ^
pop 返回列表的最后一个元素。 fn([any]) ->any ^
zip 将两个列表组合在一起。这将创建一个包含对的列表(长度为两的列表),每个对包含第一个列表的一个元素和第二个列表的一个元素。 fn([any], [any]) -> [[any,any]] ^
head 获取列表的第一个元素。 fn([any]) ->any fn(list) ->list[0]
tail 获取没有第一个元素的列表。 fn([any]) -> [any] 本地代码。
map 将函数映射到列表。 fn(fn(any) ->any, [any]) -> [any] fn(f,list) -> {结果:= []; forxinlist{结果:= push(结果, f(x)); };结果}
filter 使用给定的函数过滤列表。 fn(fn(any) -> 布尔型, [any]) -> [any] fn(f,list) -> {结果:= []; forxinlist{ if f(x) {结果:= push(结果,x); }; };结果}
reduce 使用一个函数将列表减少到一个原子值,该函数接受一个累加器和列表的一个元素,并返回新的累加器。Reduce有三个参数,函数、累加器的初始值和要减少的列表。 fn(fn(any,any) ->any,any, [any]) ->any fn(f,acc,list) -> { forxinlist{acc:= f(acc,x); };acc}
背面 一个宏,它将当前工作目录设置为当前工作目录的父目录。 () ->nil () -> {cd' .. }
add 一个函数,用于添加两个值。 fn(any,any) ->any fn(x,y) ->x+y
mul 一个函数,用于乘以两个值。 ^ fn(x,y) ->x*y
sub 一个函数,用于减去两个值。 fn(int 或 float,int 或 float) ->int 或 float fn(x,y) ->x-y
div 一个用于除两个值的函数 ^ fn(x,y) ->x/y
rem 获取两个值余数的函数。 ^ fn(x,y) ->x%y
sum 求一系列值的和。 fn([any]) ->any fn(list) ->reduce(add, 0, list)
prod 获取一系列值的乘积。 ^ fn(list) ->reduce(mul, 1, list)
inc 增加一个数字。 fn(int) ->int orfn(float) ->float fn(x) ->x+ 1
dec 减少一个数字。 ^ fn(x) ->x- 1
double 将数字翻倍。 ^ fn(x) ->x* 2
triple 将数字乘三。 ^ fn(x) ->x* 3
quadruple 将数字乘四。 ^ fn(x) ->x* 4
quintuple 将数字乘五。 ^ fn(x) ->x* 5

安装

开发版本

git clone https://github.com/adam-mcdaniel/atom
cd atom
cargo install -f --path .

版本发布

要获取当前版本的开发版本,请从crates.io安装。

# Also works for updating oakc
cargo install -f atom

依赖项

~14–24MB
~353K SLoC