Scripting

Scripting

What is scripting ?

"In computing, a script is a relatively short and simple set of instructions that typically automate an otherwise manual process.". Thus, increasing the productivity of a worklfow.

On the same page we can also read "Scripts are usually interpreted rather than compiled.", which might already give us an hint as if Zig is the right tool for the job or not.

But even after reading this, it is still beter to try by ourselves.

The main reasons why I still want to try Zig is because it is a modern language compared to other compiled languages like C or C++ which are not very used in the scripting world as far as I know. So with the aspects of a modern language that makes the programmer life easier and with compiled code performances, I still thought Zig could have a shot at this task.

Zig also has the advantage of being a very familiar language compared to typical scripting languages like bash. So instead of learning a new language, I could just use Zig which I am familiar with already.

Project description

i3 is a tiling window manager for linux, it allows you to have multiple workspaces and to switch between them easily. Those workspaces can be renamed so that you can find yourself better in your workflow.

In this exemple we have a few workspaces named: 1, 2, 3, 9:other and 10:telegram.

/HEIG_ZIG/images/i3.png

We can interact with i3 using the i3-msg command.

  i3-msg -t get_workspaces # return in a json format the list of workspaces and their properties

To rename a workspace you have to run a command like this one:

  i3-msg 'rename workspace "4: surfing-the-web" to "4: zig-project"'

The main problem with this command is that you have to rewrite the number of the workspace twice, which is not very practical.

This project allow the user to just write the number of the workspace he wants to rename and the new name without the number and the script will do the rest.

Which looks like this:

  my-i3-rename-project 4 zig-project

What was needed was a script that would as an input only take the number of the workspace that is going to be renamed and the new name without the number (eg. "4: ").

In order to do that, we have to interact with processes and manipulate strings, we are going to see step by step how to do that in our script.

Implementation and comparaison with an other scripting language

I decided to write the same script twice, first in Zig and then in Python, in order to compare the two languages and see if Zig is a good choice for scripting purposes.

We are going to go through the different steps of the script and see how it is done in Zig and in Python, compare the 2 and see which is best suited for the job.

Part 1: Declaring constants

First we are going to define our constants and our allocator

  const allocator = std.heap.page_allocator;
  const app_name: []const u8 = "/bin/i3-msg";
  var params: []const []const u8 = &.{ app_name, "-t", "get_workspaces" };

In python we don't need the allocator since it is a garbage collected language, but we still need to define our constants and we see that the syntax is quite nicer in Python

  app_name = "/bin/i3-msg"
  params = app_name + " -t get_workspaces"

Part 2: Getting the output of the process

Then we are going to call the process and get the output

  const result = try std.ChildProcess.run(.{
      .allocator = allocator,
      .argv = params,
      .cwd = null,
      .cwd_dir = null,
      .env_map = null,
      .max_output_bytes = 51200,
      .expand_arg0 = .no_expand,
  });
  defer {
      allocator.free(result.stdout);
      allocator.free(result.stderr);
  }
  const text_result = result.stdout;

The python code contains way less parameters, notably those related to memory management.

  result = subprocess.run(params, shell=True, capture_output=True, text=True)

Part 3: Making the new name

Here we are allocating dynamic memory to the arguments and then declaring the new name of the workspace

  const args = try std.process.argsAlloc(allocator);
  defer std.process.argsFree(allocator, args);
  const new_name = try std.fmt.allocPrint(allocator, "{s}: {s}", .{ args[1], args[2] });
  defer allocator.free(new_name);

Again, we don't have to worry about allocation and deallocating memory. Moreover, string maniuplation are way easier in Python.

  new_name = sys.argv[1] + ": " + sys.argv[2]

Part 4: Getting the current name of the workspace from the id of the workspace we want to rename

There is no std regex functions or anything that contains string manipulations in Zig, so we have to do it by hand by analyzing the characters of the string. Obviously, this is a very error-prone and hard task.

  const needle: []const u8 = try std.fmt.allocPrint(allocator, "num\":{s}", .{args[1]});
  defer allocator.free(needle);
  var pos_in_text_result = std.mem.indexOf(u8, text_result, needle);
  pos_in_text_result.? += 15;
  if (std.mem.eql(u8, args[1], "10")) {
      pos_in_text_result.? += 1;
  }
  
  var start_of_name = std.mem.indexOf(u8, text_result[pos_in_text_result.? .. pos_in_text_result.? + 50], ",\"");
  start_of_name.? += 2;
  start_of_name.? += pos_in_text_result.?;
  
  var end_of_name = std.mem.indexOf(u8, text_result[pos_in_text_result.? .. pos_in_text_result.? + 50], "\"");
  end_of_name.? += start_of_name.?;
  
  const diff = end_of_name.? - start_of_name.?;
  const name = text_result[start_of_name.? - 3 - diff .. end_of_name.? - 3 - diff];
  const old_name = try std.fmt.allocPrint(allocator, "\"{s}\"", .{name});
  defer allocator.free(old_name);

In Python, we the help of a regex library we can easily extract the name of the workspace from the json output.

  pattern = fr'"num":{sys.argv[1]},"name":"([^"]+)"'
  match = re.search(pattern, result.stdout)
  
  if match:
      old_name = match.group(1)
  
  print(old_name)

Part 5: Re running a process to rename the workspace

Finally, we are going to run the process to rename the workspace using the inputs we have extracted and created. Here we arrive at the same conclusions that we had in Part 2

  params = &.{ app_name, "rename", "workspace", old_name, "to", new_name };
  
  const modif_result = try std.ChildProcess.run(.{
      .allocator = allocator,
      .argv = params,
      .cwd = null,
      .cwd_dir = null,
      .env_map = null,
      .max_output_bytes = 51200,
      .expand_arg0 = .no_expand,
  });
  defer {
      allocator.free(modif_result.stdout);
      allocator.free(modif_result.stderr);
  }

And again in Python.

  result = subprocess.run(params, shell=True, capture_output=True, text=True)

Part 6: Printing the result message

Here we are going to have to format our string in order to print it.

  std.debug.print("Should have changed workspace {s} from {s} -> {s}", .{ args[1], name, new_name });

In Python, we can just print the result without the need for special formatting, which is way faster and nicer to write.

  print("Should have changed the name of the workspace with id " + sys.argv[1] + "from" + old_name + " to " + new_name)

Overall we have 76 lines for the Zig implementation and 39 lines for the Python implementation, the syntax is also way more readable in Python. One of the main reasons we could save up so many lines in Python is because we have a lot of fine libraries that are simple to use, notably re and subprocess.

Bash implementation

I also made with the help of ChatGPT a bash implementation of the script, which truly is a scripting language by excellence. This implementation is 32 lines long and is quite is easy to read and understand the purpose.

  #!/bin/bash
  
  # Check if the correct number of arguments is provided
  if [ "$#" -ne 2 ]; then
      echo "Usage: $0 <workspace_number> <new_name>"
      exit 1
  fi
  
  # Variables
  workspace_number=$1
  new_name="$workspace_number: $2"
  app_name="/bin/i3-msg"
  
  # Get the current workspaces
  result=$($app_name -t get_workspaces)
  
  # Extract the current name of the workspace
  # Typical substring of the result = "num":1,"name":"1"
  pattern="\"num\":$workspace_number,\"name\":\"([^\"]+)\""
  if [[ $result =~ $pattern ]]; then
      old_name="${BASH_REMATCH[1]}"
  else
      echo "Workspace number $workspace_number not found."
      exit 1
  fi
  
  echo "Old workspace name: $old_name"
  
  # Change the name of the workspace to the new name
  params="$app_name 'rename workspace \"$old_name\" to \"$new_name\"'"
  echo "Executing: $params"
  eval $params

Benchmark

I used hyperfine in order to benchmark and see which implementation is faster.

Zig implementation3.0 ms ± 0.5 ms
Python implementation28.4 ms ± 4.9 ms
Bash implementation3.7 ms ± 0.3 ms

The results are what we excepted for Zig and Python, Zig is a compiled language and is way faster than Python. I am a bit more suprised by how the Bash implementation is that fast. I excepted it to be faster than Python because the latest has many drawbacks:

  • Overhead of the runtime environment with the Python VM
  • Python uses subprocess to run the commands when Bash directly runs the commands with no IPC.
  • Startup time of the Python interpreter.
  • Memory usage is far more heavy in the Python runtime environment.

The reason why Zig is still a bit faster than Bash is because it is compiled language there is no need to interpret the code, it is directly executed by the machine. I think that Zig in this particular case still has the drawbacks of having to start processes to run the commands and do IPC.

Conclusion

Was Zig the good choice ? Mostly no, Zig is not a good choice for scripting, it is way too verbose and not suited for the task. Bash or even is way better suited for the job, it is way more readable and way easier to use. There are a lot of things that Zig is not really good at if you want to go fast and notably do string manipulation and memory management.

Doing things like string manipulation by hand was not an easy task and it was very error-proned, it is way better to use a language that has libraries that are already made for you and that are easy to use.

As far as my knowledge goes, most scripts are not really designed to be very fast and scalable, if you run your script occasionaly on your local machine like I did in this example, a 20ms difference is not going to change anything.

To conclude I would not recommend using Zig for scripting, it simply is not the right tool for the job.

You can find the GitHub repository of the project here if you want to take a look at the whole code.

If the project seems cool to you can also install the scripts on your system by following the instructions in the README of the repository.

I also made an other script in Zig to monitor battery levels and send system notifications when the battery is low, you can find the repository here.