ABS programming language: the joy of shell scripting.
According to the words of his creator:
It allows the mixing of convenience of the shell with the syntax of a modern programming language.
The way i see ABS is like a layer on top of bash. It fills that gap for when Bash feels awkward and Python (or Node) is just too much. It features a simplified C-like syntax and an easy way of calling shell commands and handle their output.
What's good about it?
- It's writing in go.
- There are binaries for Windows, Linux and macOS.
- Easy to learn, as in there isn't anything extraordinary in the syntax. It is very similar to many mainstream programming languages.
- The documentation is good enough.
What's the catch?
It's new, very new. Like 5 months old at the time of this writing.
There isn't any tool around this (yet), no fancy plugins for editors, no syntax highlight, no linters or language server protocol stuff.
Community is basically none existing.
ABS & Bash side by side
This snippets are taken from the homepage, they show how to fetch our IP address and print the sum of its parts.
This is how you could do it using good old Bash.
# Simple program that fetches your IP and sums it up
RES=$(curl -s 'https://api.ipify.org?format=json' || "ERR")
if [ "$RES" = "ERR" ]; then
echo "An error occurred"
exit 1
fi
IP=$(echo $RES | jq -r ".ip")
IFS=. read first second third fourth <<EOF
${IP##*-}
EOF
total=$((first + second + third + fourth))
if [ $total -gt 100 ]; then
echo "The sum of [$IP] is a large number, $total."
fi
I don't do much shell scripting these days so i'm kinda lost in here. I know a little bit about what's going on but only because of the variable names and some common commands. This syntax is so different from mainstream programming languages that it feels like dark magic to many of us.
Now lets have a look at the "translated" version in ABS.
res = `curl -s 'https://api.ipify.org?format=json'`
if !res.ok {
echo("An error occurred: %s", res)
exit(1)
}
ip = res.json().ip
total = ip.split(".").map(int).sum()
if total > 100 {
echo("The sum of [%s] is a large number, %s.", ip, total)
}
Since i spend most of my time writing Javascript and PHP i feel right at home in here. Notice that is so similar to other languages that the go
syntax highlight works fine-ish in that markdown block.
What's going on in there?
I'll explain the second snippet now.
Lets start at the beginning.
res = `curl -s 'https://api.ipify.org?format=json'`
That is one way of executing a command in ABS. Anything inside the backticks in considered a bash command. And this is just half of the awesomeness, the other half is in the return value of this command.
What you get from the command is like a javascript object, it has methods and properties, it also uses the dot notation to access gain access to them. Because this is a command result it has an ok
property that lets you know if the command ended successfully or with an error, so you can do a validation like this:
if !res.ok {
echo("An error occurred: %s", res)
exit(1)
}
Our old friend the if
statement does exactly what you think, and you also have access to the logic operators that you know and love. Inside if
block you can see two builtin functions that can call common commands without a special syntax. As you might expect echo
and exit
do the same thing as their Bash counterpart.
And the goodness doesn't stop there. This res
variable is a String so that means it has other methods, including .json()
that can convert a string type into a Hash (if is a valid json string). Once we have a Hash type that represents the http request made by curl
we can start processing the IP value.
ip = res.json().ip
total = ip.split(".").map(int).sum()
I want you to pay attention to this part right here: .map(int)
, if you take a look at the Array type methods you'll see that map
takes a function as its argument, this is big deal because that means that higher order functions are possible (check this article to know why this is awesome.)
Lets play with it
I use cmus as my music player. It is so "lightweight" that it doesn't even show desktop notifications when it starts playing a song, but it does give you the chance to execute a program every time it does, by setting the status_display_program
option. We'll try to do almost the same thing this python script does.
I'll keep this simple so i will ignore a lot of things in the python version, we'll focus on the notification part. This is what i'm interested in from the python script:
# See if script is being called by cmus before proceeding.
if sys.argv[1].startswith("status"):
#... moar code ...
# We only display a notification if something is playing.
if status_data("status") == "playing":
#... moar code ...
# Execute notify-send with our song data.
subprocess.call('notify-send -u low -t 500 "' + \
notify_summary + '" "by ' + \
notify_body + ' "', shell=True)
That is what i want to achieve. Our main block in ABS should look like this:
if is_playing {
summary, body = get_data(process_args())
`notify-send "$summary" "$body"`
}
Our script will have more than that but it is basically what i want to do. This are the steps we are going to make.
- Figure out how we can get the status.
- Gather the rest of the arguments that cmus gives us.
- Get the important stuff.
- Send the data to the
notify-send
command.
Lets start with is_playing
.
If you remember in the python version we get the status from the sys.argv
property and it checks if the arguments list start with the string 'status'. In Bash you would check the special variables $1
or $2
or $3
... you get the point, each one of these hold the value of a positional argument given to the script. In ABS we have the arg
function, you give it a number and it returns one of the arguments. The arguments lists starts at 0 and the first two are always the binary of the interpreter and the script path so we should check argument with the index 2 and 3, they will give us the status string and the status value.
We define our condition like this:
is_playing = arg(2) == "status" && arg(3) == "playing"
Now lets get the rest of the data the cmus provides. At this point it would be nice to have a function that returns an array with the arguments list, but we don't have it. Instead, we will create a function that calls arg
with a number until we get an empty string which will mean we have reach the end of the list. This function will store the arguments in a Hash and return it.
process_args = f() {
result = {}
index = 4
current_arg = arg(index)
while current_arg != "" {
result[current_arg] = arg(index + 1)
index += 2
current_arg = arg(index)
}
result
}
This is a user defined function. There are a couple funny things in it, first is that we create a function with the "keyword" f
and some parenthesis. The other funny thing is that we don't have a return
keyword, instead we have lonely hash just sitting there at the end of our function. There is a return
keyword but is only necessary when you want an early return or when the interpreter doesn't know what to return in the last statement of the function.
The last piece of the puzzle is get_data
, this function will handle the result from process_args
and ensure that we always get a summary
and a body
for our notification.
get_data = f(args) {
title = args.title || args.file.split("/").pop()
artist = args.artist || "Unknown"
album = args.album || false
msg = fmt("<b>Artist:</b> %s", artist)
if album {
msg += fmt("\n<b>Album:</b> %s", album)
}
return [title, msg]
}
We can't trust the argument that cmus gives us because most of the data that we want is in the metadata of the file and it might be missing, that's why we evaluate every property and have an "alternate" value with the ||
operator just in case. Next, we create a nice message for the body of the notification, we use the fmt
function that is almost like printf
in other languages. Once we have everything we need we return an array.
And so the final product would be this:
process_args = f() {
result = {}
index = 4
current_arg = arg(index)
while current_arg != "" {
result[current_arg] = arg(index + 1)
index += 2
current_arg = arg(index)
}
result
}
get_data = f(args) {
title = args.title || args.file.split("/").pop()
artist = args.artist || "Unknown"
album = args.album || false
msg = fmt("<b>Artist:</b> %s", artist)
if album {
msg += fmt("\n<b>Album:</b> %s", album)
}
return [title, msg]
}
is_playing = arg(2) == "status" && arg(3) == "playing"
if is_playing {
summary, body = get_data(process_args())
`notify-send "$summary" "$body"`
}
Still want to know more?
Be sure to visit the author's site, the documentation or the examples directory in the github repository.
If you know go
consider contributing to the project.
The ABS programming language
ABS is a programming language that works best when you're scripting on your terminal. It tries to combine the elegance of languages such as Python, or Ruby with the convenience of Bash.
tz = `cat /etc/timezone`
continent, city = tz.split("/")
echo("Best city in the world?")
selection = stdin()
if selection == city {
echo("You might be biased...")
}
See it in action:
Let's try to fetch our IP address and print the sum of its parts if it's higher than 100. Here's how you do it in Bash:
# Simple program that fetches your IP and sums it up
RES=$(curl -s 'https://api.ipify.org?format=json' || "ERR")
if [ "$RES" = "ERR" ]; then
echo "An error occurred"
exit 1
…Thank you for your time. If you find this article useful and want to support my efforts, consider leaving a tip in ko-fi.com/vonheikemen.
Top comments (2)
Just...Thanks for this :)
And thank you for creating ABS. I actually had fun writing scripts in it.