DEV Community

Heiker
Heiker

Posted on • Updated on

ABS, a scripting language with a simple syntax and the convenience of bash

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
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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'`
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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"`
}
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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]
}
Enter fullscreen mode Exit fullscreen mode

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"`
}
Enter fullscreen mode Exit fullscreen mode

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.

GitHub logo abs-lang / abs

Home of the ABS programming language: the joy of shell scripting.

abs language logo

GitHub Workflow Status (branch) License Version undefined undefined
undefined undefined Coverage Status
undefined undefined undefined undefined

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...")
}
Enter fullscreen mode Exit fullscreen mode

See it in action:

asciicast

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
Enter fullscreen mode Exit fullscreen mode

Thank you for reading. If you find this article useful and want to support my efforts, buy me a coffee ☕.

buy me a coffee

Top comments (2)

Collapse
 
odino profile image
Alessandro Nadalin

Just...Thanks for this :)

Collapse
 
vonheikemen profile image
Heiker

And thank you for creating ABS. I actually had fun writing scripts in it.