Shell scripting that will knock your socks off

Overview

atom

Shell scripting that will knock your socks off.

NOTE: Click the image above for a video demonstration.

About the Author

I'm a freshman in college bored in quarantine, and looking for any work. If you enjoy my projects, consider supporting me by buying me a coffee!

Why write a shell?

As I see it, there are two sides of a shell. The interactive side, and the scripting side. Creating a good shell means balancing these two modes well. If you make a language too well suited to scripting, then file navigation, and other interactive features will suffer. On the other hand, if you make a language too well suited for interactive commands (like bash), then scripting becomes very difficult.

Atom tries to strike a better balance between the two modes than bash, and, in my opinion, does so rather successfully. It does seem to be more well suited to scripting instead of interactive programming, but I don't really mind the sacrifice all that much.

Atom's design goals are:

  1. Shell scripting must be powerful enough to function as a traditional high level language.
  2. At the same time, there must not be a lot of syntactic sugar for writing interactive commands.
  3. Incorrect code should be rejected. There should be no attempt to understand the user's incorrect code (see JavaScript). Bad code is bad code.
  4. Declarative and functional programming first, imperative last.

I would say these goals serve atom very well.

To show Atom's scripting capabilities, I wrote an entire card game using it!

The CPU is actually much better than I am, and has beaten me with twice or thrice my score multiple times. It doesn't cheat at all, it only ranks its cards by synergy, encourages picking up cards that synergize well with its best cards, and discarding its worst cards.

If you want to try all of my custom macros, and to have my splash screen, use my .atom-prelude file in your home directory and experiment away! To play my card game specifically, run rummy@play'.

Usage

Atom is drastically different than any other shell, both with its outward syntax and internal functionality.

For example, atom supports traditional tables, lists, strings, ints, floats, bools, etc. like this drastically better and more professional shell that I should have just started using to begin with (but there's no fun in that. "Not invented here syndrome" really does have a hold on me doesn't it?).

But atom also takes direct inspiration from languages like lisp by including symbols as first class types, and by implementing iterative constructs like for and while loops as values, not operations on values.

Additionally, it adds lambdas (which can capture their environment), and macros (which can change their parent environment).

Interactive Syntax

While this syntax will work in all of your scripts as valid statements, my intent is for this to be mainly used for interactive programming.

Generally, executing programs (and functions too) in atom and in bash is only different by one character.

Bash:

$ g++ main.cpp -o main

Atom:

$ g++' main.cpp -o main

You might be thinking that writing a quote after every program name is not so ergonomic, but it actually isn't half bad. I find now that when I use bash, I can't seem to type any commands without it accidentally.

The reason I chose the ' character is because it is the most readily available character that isn't used in any symbols (besides the semicolon) that is also not accessed with the shift key. The command g++' main.cpp -o main really is just one extra keystroke.

Not that much of a sacrifice for scripting power bestowed by the gods.

Aliases

You might find that you have defined a symbol g++ with a non-callable value (not a macro or a function), like 5.

If this is the case, simply wrap the symbol g++ in quotes, and then run it like so: "g++"' main.cpp -o main.

This also means that you can make aliases rather simply by defining a symbol with a string or a path.

This snippet defines ls as an alias for the lsd program:

ls := "lsd";
ls'

Scripting Syntax

Again, scripting syntax is really just syntax encouraged for scripting. All syntax you see on this README will work anywhere.

To start off simple, let's define a variable.

x := 5;

Wow! We've changed our environment! This means that whenever the symbol x is evaluated, it becomes 5 instead!

Now let's define something else.

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

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

Bravo! Now the symbol WEEKDAYS is bound to a list containing all the weekdays' names, and the symbol grades is bound to a table containing grades for students!

Now let's try to do some fun stuff with lambdas.

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;

As you can see, we can use the \... -> or fn(...) -> syntactic sugar to create lambda functions with multiple arguments, () -> to create a lambda that takes no arguments, or just a symbol and an arrow to create a lambda that takes a single argument. How handy!

You can also use the fn keyword to define functions without assigning them.

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
};

Please note the semicolon at the end of every expression. It is required for everything except for the last value of a block!

You can also define macros in a similar manner with the macro keyword:

# 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();
};

Isn't this nice???

Functions vs. Macros

Functions and macros look very similar, and perform almost identical tasks, but they have drastically different implications.

To explain it simply, a function creates its own local scope, with local variables that are dropped when the function returns. Macros, however, use the current scope that the macro is called in as its scope.

Variables that would have been overwritten by the macro's parameters, are saved. Take the following code for example:

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);

Macros let you read and write global state, while (user defined) functions have no side effects on the global state of the program.

Builtin functions are a different beast entirely, though...

Builtin Functions vs. Functions

Builtin functions are a bit of a misnomer. They're half macro, half function, and a little bit of something else not quite quantifiable. How do they work? The world may never know.

The reason they're so mystical is because they can change the scope they were called in (like macros), they have the option to not evaluate their arguments (unlike macros), and they can take varying numbers of arguments.. For example, take the cd builtin function. Not only does it not evaluate its arguments (calling cd with Desktop doesn't evaluate the symbol Desktop), it modifies the CWD variable in the current scope. How weird is that?

$ home'
$ cd' Desktop

Additionally, builtin functions like echo, print, and to-str can take varying numbers of arguments.

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

Although these features of builtin functions are cool and bizarre, they really aren't used in many instances. Functions like print and cd are exceptions to the rule, and you can expect most functions to behave exactly as regular user defined functions without any bizarre catches.

Modules

Atom has an extensive list of builtin libraries (for a shell written in a week or so, that is). Here's a list of builtin modules.

Modules (which is just a fancy name for tables intended to function as libraries), can be accessed with the @member operator or the ["member"] operators.

Example usage:

shuffled-deck := rand@shuffle(cards@deck@all);
echo(shuffled-deck[0]);
Module Description Members
rand A module embodied with chaos. Use your power of entropy wisely, young scripters. { int: fn(int, int) -> int, shuffle: fn([any]) -> [any], choose: fn([any]) -> any }
fmt A module for formatting strings. As of right now, there are only functions that manipulate color, boldness, underlining, etc. { 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 A small module for creating widgets for displaying text in the terminal. Widgets have a title, a content string, a width, and a height. { create: fn(str, str, int, int) -> str,add-horizontal: fn(str...) -> str,add-vertical: fn(str...) -> str }
math A module for various math functions. Trigonometry, multiple logarithms, etc. { 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 A small module for getting info about the operating system. Useful for creating cross-platform scripts. { name: str, family: str, version: str }
sh A small module for getting info about the shell, such as the version, the path to the executable, the executable's parent directory, and the path the to prelude script (the script run at the shell's startup, like .bashrc). The version member contains the major, minor, and patch integers. { exe: path, dir: path, version: [int], prelude: path }
file A small module for file manipulation. It's not much yet. Keep it simple. { read: fn(path or str or sym) -> str, write: fn(path or str or sym, str) -> nil, append: fn(path or str or sym, str) -> nil }
date The date module is a bit weird. It's not so much a module, more of a hidden function. Every time date is accessed, it uses a constantly updating table with the current date info, and the date as a string. { day: int, weekday: int, month: int, year: int, str: str }
time The time module functions just like the date module, but for time. My favorite thing so far about this module is writing macros that print fake and bizarre g++ errors if the user tries to compile on the first second of the minute. Purely evil stuff waiting to happen, this module is. { hour: int, minute: int, hour: int, str: str }
cards A module for card games. Cards are just strings with their respective Unicode representation. So, for example, the value cards@deck@aces[0] is "πŸ‚‘". In every list containing multiple suites in the module, they alternate between Spades, Hearts, Diamonds, Clubs. So, cards@deck@all is ["πŸ‚‘", "πŸ‚±", "πŸƒ", "πŸƒ‘", "πŸ‚’", ..., "πŸƒž"]. { deck: { all: [str], aces: [str], kings: [str], queens: [str], jacks: [str], faces: [str], numbers: [str] }, suites: { spades: str, clubs: str, hearts: str, diamonds: str }, suite: fn(str) -> str, value: fn(str) -> int, name: fn(str) -> str, from-name: fn(str) -> str, back: str }
chess A module for chess. Chess boards are stored as lists of rows, which are lists of pieces. Pieces, similar to cards, are just strings with their respective Unicode representation. So, cards@white@king is "β™”", and cards@black@king is "β™š". { white: { king: str, queen: str, rook: str, bishop: str, knight: str, pawn: str }, black: { king: str, queen: str, rook: str, bishop: str, knight: str, pawn: str }, space: str, is-piece: fn(str) -> bool, is-space: fn(str) -> bool, is-white: fn(str) -> bool, is-black: fn(str) -> bool, create: fn() -> [[str]], flip: fn([[str]]) -> [[str]], get: fn([[str]], str) -> str, fmt: fn([[str]]) -> str, print: fn([[str]]) -> nil, mv: fn([[str]], str, str) -> [[str]], add: fn([[str]], str, str) -> [[str]], rm: fn([[str]], str) -> [[str]] }

These are all intended to make scripting extremely ergonomic. With builtin libraries for a wide variety of tasks, making scripts will be incredibly easy.

We bring the blocks, you bring the glue.

Constants and Builtin Functions

These constants and builtin functions are intended to be used extremely often in scripting and in the interactive prompt, so they are all included in the global scope.

They can all be overwritten, if you wish. I would be careful about using macros to assign to these values! Be absolutely sure that your code won't break something before you run it!

Name Description Type Value
nil or () The atom equivalent of python's None nil nil
true and truth The boolean value for true. bool true
false The boolean value for false. bool false
CWD The path of the current working directory. path See description.
HOME The path of the home directory. ^ ^
VIDS The path of the videos directory. ^ ^
DESK The path of the desktop directory. ^ ^
PICS The path of the pictures directory. ^ ^
DOCS The path of the documents directory. ^ ^
DOWN The path of the downloads directory. ^ ^
report The function that's called to print the result of a command. This can be written to format results in a custom way, or to not print nil values fn(any) -> nil By default, fn(val) -> print(" =>", val)
prompt The function used to generate the prompt for the user to enter commands on. It takes the current working directory as a parameter. fn(path) -> str By default, fn(cwd) -> to-str(cwd) + "> "
incomplete-prompt The function used to generate the prompt for the user to enter commands on after they've entered an incomplete line of code. It takes the current working directory as a parameter. ^ By default, fn(cwd) -> " " * len(cwd) + "> "
absolute This function takes a path, removes any extraneous portions of the path (such as foo/../bar), and also makes the path an absolute path. So ./testing in the home directory would become /home/adam/testing, for example. fn(path) -> path or fn(sym) -> path or fn(str) -> path Native code.
exists This function returns whether or not any path exists. fn(path) -> bool or fn(sym) -> bool or fn(str) -> bool ^
is-err This function returns whether or not the evaluation of the inner expression returns an error. fn(any) -> bool ^
is-syntax-err This function returns whether or not an error is a syntax error. This is mainly intended for use with the report function. fn(any) -> bool ^
sleep Make the shell pause for a given number of seconds. fn(float) -> nil ^
to-path Convert a string or symbol to a path. fn(path or str or sym) -> path ^
to-float Convert a string, integer, float, or boolean to a floating point value. fn(str or int or float or bool) -> float ^
to-int Convert a string, integer, float, or boolean to an integer. fn(str or int or float or bool) -> int ^
input Get user input with a prompt. fn(any...) -> str ^
rev Reverse a string or a list. fn(str) -> str or fn([any]) -> [any] ^
split Split a string with a given delimiter. fn(str, str) -> str ^
sort Sort a list of integers. fn([int]) -> [int] ^
join Join a list with a separator. fn([any], any) -> str ^
env A table containing all bindings in scope. macro() -> table ^
HOME, VIDS, DESK, PICS, DOCS, DOWN The path to the respective directory. path ^
home, vids, desk, pics, docs, down Macros that set the current working directory to the respective directory. macro() -> nil macro() -> CWD := ...
exit or quit Exit the current shell session. fn() -> nil Native code.
unbind Unbind a symbol with a given name. macro(str) -> nil ^
print Print one or more values, and return the last one. fn(any...) -> any ^
echo Print one or more values, and return nil. fn(any...) -> nil ^
pwd or cwd Print the current working directory. macro(path or string or sym) -> nil ^
cd Change the directory. This macro is a special form, it does not evaluate its argument. For example, if x is defined as 5, cd' x will NOT perform cd' 5, it will perform cd' "x". macro(path or string or sym) -> nil ^
cd-eval Change the directory to an evaluated value. This is used when you want to cd into a folder with an unknown name while writing the script. macro(any) -> nil ^
clear or cls Clear the console. fn() -> nil fn() -> { if os@family = "linux" or os@family = "unix" { clear' } else if os@family = "windows" { cls' } else { print("\n" * 255) } }
keys Get the list of keys in a table. fn(table) -> [str] Native code.
vals Get the list of values in a table. fn(table) -> [any] ^
insert Return a table with a value inserted with a given key. fn(table, str, any) -> table ^
remove Return a table with a value removed with a given key. fn(table, str) -> table ^
len Get the length of a list or string, the number of pairs in a table, or the number of components to a path. fn([any] or table or str or path) -> int ^
push Add a given element to a list. fn([any], any) -> [any] ^
pop Return the last element of a list. fn([any]) -> any ^
zip Zip two lists together. This creates a list of pairs (lists of length two), with each pair containing an element of the first list and an element of the second list. fn([any], [any]) -> [[any, any]] ^
head Get the first element of a list. fn([any]) -> any fn(list) -> list[0]
tail Get the list without the first element. fn([any]) -> [any] Native code.
map Map a function over a list. fn(fn(any) -> any, [any]) -> [any] fn(f, list) -> { result := []; for x in list { result := push(result, f(x)); }; result }
filter Filter a list with a given function. fn(fn(any) -> bool, [any]) -> [any] fn(f, list) -> { result := []; for x in list { if f(x) { result := push(result, x); }; }; result }
reduce Reduce a list to an atomic value with a function that takes an accumulator and an element of the list, and returns the new accumulator. Reduce takes three arguments, the function, the initial value of the accumulator, and the list to reduce. fn(fn(any, any) -> any, any, [any]) -> any fn(f, acc, list) -> { for x in list { acc := f(acc, x); }; acc }
back A macro that sets the current working directory to the parent of the current working directory. macro() -> nil macro() -> { cd' .. }
add A function that adds two values. fn(any, any) -> any fn(x, y) -> x + y
mul A function that multiplies two values. ^ fn(x, y) -> x * y
sub A function that subtracts two values. fn(int or float, int or float) -> int or float fn(x, y) -> x - y
div A function that divides two values ^ fn(x, y) -> x / y
rem A function that gets the remainder of two values. ^ fn(x, y) -> x % y
sum Sum a list of values. fn([any]) -> any fn(list) -> reduce(add, 0, list)
prod Get the product of a list of values. ^ fn(list) -> reduce(mul, 1, list)
inc Increment a number. fn(int) -> int or fn(float) -> float fn(x) -> x + 1
dec Decrement a number. ^ fn(x) -> x - 1
double Double a number. ^ fn(x) -> x * 2
triple Triple a number. ^ fn(x) -> x * 3
quadruple Quadruple a number. ^ fn(x) -> x * 4
quintuple Quintuple a number. ^ fn(x) -> x * 5

Installation

To install, you must download Rust from here.

Development Build

# Download the repo and install from source
git clone https://github.com/adam-mcdaniel/atom
cd atom
cargo install -f --path .

Releases

To get the current release build, install from crates.io.

# Also works for updating atomsh
cargo install -f atomsh

After Install

# Just run the atom executable!
atom
You might also like...
McFly - fly through your shell history
McFly - fly through your shell history

McFly - fly through your shell history McFly replaces your default ctrl-r shell history search with an intelligent search engine that takes into accou

Raw C Shell: interact with your operating system using raw C code, because why not?
Raw C Shell: interact with your operating system using raw C code, because why not?

rcsh Raw C Shell is a minimalist shell with no built in commands. You write entirely in C code and use return; to execute your code. Unlike that silly

Ask ChatGPT for a shell script, code, or anything, directly from your terminal πŸ€–πŸ§ πŸ‘¨β€πŸ’»
Ask ChatGPT for a shell script, code, or anything, directly from your terminal πŸ€–πŸ§ πŸ‘¨β€πŸ’»

ShellGPT Ask ChatGPT for a shell script, code, or anything, directly from your terminal πŸ€– 🧠 πŸ‘¨β€πŸ’» Demo Install The binary is named gpt when installe

Animated app icons in your Dock that can run an arbitrary shell script when clicked.
Animated app icons in your Dock that can run an arbitrary shell script when clicked.

Live App Icon for Mac Animated app icons in your Dock that can run an arbitrary shell script when clicked. Requirements macOS 13 (Ventura) or higher X

A simple, human-friendly, embeddable scripting language

Mica Language reference Β· Rust API A simple, human-friendly scripting language, developed one feature at a time. Human-friendly syntax inspired by Rub

The GameLisp scripting language

GameLisp GameLisp is a scripting language for Rust game development. To get started, take a look at the homepage. Please note that GameLisp currently

Open-source compiler for the Papyrus scripting language of Bethesda games.

Open Papyrus Compiler This project is still WORK IN PROGRESS. If you have any feature requests, head over to the Issues tab and describe your needs. Y

A safe, fast, lightweight embeddable scripting language written in Rust.

Bud (budlang) A safe, fast, lightweight embeddable scripting language written in Rust. WARNING: This crate is not anywhere near being ready to publish

A plugin system for the Rhai embedded scripting language.

Rhai Dylib This crate exposes a simple API to load dylib Rust crates in a Rhai engine using Rhai modules. 🚧 This is a work in progress, the API is su

Comments
  • Initial CI: Check style and build

    Initial CI: Check style and build

    First of all, awesome project!

    I wrote a quick CI setup for checking style and building Atom. It currently runs a basic format and lint job, and a build job on Ubuntu, macOS and Windows.

    As you can see, rustfmt already catched two trailing whitespaces!

    There are a lot of possibilities from here:

    • Cargo bench, test, check, doc, etc.
    • Cross compilation to different targets (arm64 for example)
    • Compilations on different toolchains (beta, nightly)
    • Running rustfmt and clippy with more arguments

    GitHub Actions needs to be activated.

    Let me know what you think!

    opened by EwoutH 4
  • [ImgBot] Optimize images

    [ImgBot] Optimize images

    Losslessly compress images to reduce their file sizes

    Beep boop. Your images are optimized!

    Your image file size has been reduced by 13% πŸŽ‰

    Details

    | File | Before | After | Percent reduction | |:--|:--|:--|:--| | /assets/atom-about.png | 133.63kb | 114.55kb | 14.27% | | /assets/rummy-game.png | 61.12kb | 53.00kb | 13.29% | | /assets/atom-splash.png | 63.10kb | 55.31kb | 12.35% | | /assets/rummy-splash.png | 20.99kb | 20.30kb | 3.32% | | | | | | | Total : | 278.84kb | 243.15kb | 12.80% |


    πŸ“ docs | :octocat: repo | πŸ™‹πŸΎ issues | πŸ… swag | πŸͺ marketplace

    opened by EwoutH 0
Owner
adam mcdaniel
Programmer and Musician
adam mcdaniel
Simple low-level web server to serve file uploads with some shell scripting-friendly features

http_file_uploader Simple low-level web server to serve file uploads with some shell scripting-friendly features. A bridge between Web's multipart/for

Vitaly Shukela 2 Oct 27, 2022
🎨✨ Show off your soothing color palette

?? Show off your soothing color palette ✨ Palettes · install · contribute · Gratitute ?? Palettes Rust C Lua Ruby Go sh js ?? install Installing this

BinaryBrainiacs 4 Jan 28, 2023
🐒 Atuin replaces your existing shell history with a SQLite database, and records additional context for your commands

Atuin replaces your existing shell history with a SQLite database, and records additional context for your commands. Additionally, it provides optional and fully encrypted synchronisation of your history between machines, via an Atuin server.

Ellie Huxtable 4.6k Jan 1, 2023
CLI program for sending one-off requests to the VTube Studio API

vtubestudio-cli (vts) CLI program for sending one-off requests to the VTube Studio API. It connects to the websocket, authenticates, performs one or t

null 2 Nov 24, 2021
An easy-to-use TUI crate for Rust, based off of the Elm architecture.

Rustea An easy-to-use TUI crate for Rust, based off of the Elm architecture. This is a re-implementation of Go's Tea, created by TJ Holowaychuk. Featu

Laz 82 Dec 21, 2022
Minimal server (with maximal security) for turning off an X10-controlled fan over HTTP

"Fan Remote" A self-contained Rust binary to expose a single X10 command (turn off that fan) as an HTML form button. In its current form, it's highly

Stephan Sokolow 2 Oct 23, 2022
Following "ZK HACK III - Building On-chain Apps Off-chain Using RISC Zero"

RISC Zero Rust Starter Template Welcome to the RISC Zero Rust Starter Template! This template is intended to give you a starting point for building a

drCathieSo.eth 3 Dec 22, 2022
oneoff is a library for one-off types

OneOff OneOff is a library for one-off types use oneoff::OneOff; let left = OneOff::Left(1); let right = OneOff::Right(2); assert_eq!(left, OneOff:

null 3 Nov 15, 2024
bevy_scriptum is a a plugin for Bevy that allows you to write some of your game logic in a scripting language

bevy_scriptum is a a plugin for Bevy that allows you to write some of your game logic in a scripting language. Currently, only Rhai is supported, but more languages may be added in the future.

JarosΕ‚aw Konik 7 Jun 24, 2023
Explore from the safety of your shell

turtlescan Explore from the safety of your shell Installation: cargo install turtlescan tui Starts a little tui which connects to your JSON-RPC server

Brian Cloutier 4 Sep 13, 2022