Maxim

Hey, I'm Max,

a Software Engineer working in Moldova at Crunchyroll

Blog
HTML tutorial

Friday (My Engineering Assistant!)

If you don’t have time for small things you, you won’t have time for the big things.

Richard Branson

Have you ever experienced that feeling when you got some interesting stuff to do, but you can’t find time for it just because you have small boring recurrent monkey tasks?

I’ve solved this issue by delegating all these recurrent tasks to Friday. Yeah, Friday is nothing else than a tool that I created for myself to automate some recurrent tasks and win some time for more unique, captivating ones.

The Roots

When I just started to learn software engineering, I was in love with every task I was doing because each of them was a new opportunity to form novel skills. With time a lot of tasks become recurrent, and you understand that you’re doing a monkey job instead of spending time on more valuable and interesting stuff.

After years of experience, you realize that you don’t feel like a more professional software engineer typing different complicated commands in your terminal. 😃

Hacking GIF

And at this point, I started to automate small and recurrent tasks to gain some time for more challenging ones. The easiest and simplest way to automate those small tasks was using bash scripts and aliases. But I quickly understood that this is not the best way to automate because bash script doesn’t have the most pleasant syntax, and I know other languages way better.

I started to think about a better tool/language that I should use to automate all monkey jobs I am doing, and I knew for sure that I want something that:

  1. will be easy to use even by a person who hasn’t built it;
  2. will be scalable and it would be easy to automate new stuff;
  3. will be easy to transfer from one machine to another;
  4. will support a modern IDE like IntelliJ IDEA.

Thus, I decided to write a Kotlin console app and call it Friday!

Project Setup

First off, let’s create a new Kotlin console application. This can be easily done using the iDea UI.

Project Stetup

After the project is ready and all the synching finishes we can hit the run button and run our Friday tool.

Friday Project Runned From IDE

This is nice, but it doesn’t look like a command-line tool yet. To do this, we need to make it runnable from the terminal.

The easiest way to achieve this is to build a Jar file. Thus, let’s adjust our build.gradle a little bit.

plugins {
    id 'org.jetbrains.kotlin.jvm' version '1.4.30'
}

group = 'com.maximbircu'
version = '1.0-SNAPSHOT'

repositories {
    mavenCentral()
}

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.4.30"
}

compileKotlin {
    kotlinOptions.jvmTarget = '1.8'
}

compileTestKotlin {
    kotlinOptions.jvmTarget = '1.8'
}

jar {
    manifest {
        attributes('Main-Class': 'com.maximbircu.friday.MainKt')
    }
    from { configurations.compileClasspath.collect { it.isDirectory() ? it : zipTree(it) } }
}

Now we can go to the terminal and run ./gradlew assemble and after the command will finish successfully, we should find our Jar file inside the build folder of our project.

Friday Jar Localtion

Now we can run our Friday tool from the terminal like this:

java -jar build/libs/friday-1.0-SNAPSHOT.jar

Let’s move it to a more common directory, the bin directory for instance, and also, let’s rename it to something more convenient and simple like friday.jar

Nice 👍
Now we can run it like that:

java -jar /usr/local/bin/friday.jar

But this is still not the easiest way. It would be nice if we could write just one single word to run the tool.

For this, we can create a bash executable file or a launcher, let’s call it friday.

#!/usr/bin/env bash

java -jar /usr/local/bin/friday.jar $@

We’re adding $@ at the end of the command to bypass all arguments from the terminal to our Friday main function; we’ll need this in the future.

Let’s locate the launcher at the same path where we placed the Jar file /usr/local/bin. Also, we need to make it executable by running chmod +x friday

Hurray, our terminal knows about Friday 🎉

Friday first run

Alright, all this looks good enough, but the installation process is composed of too many steps and is not straightforward. Let’s create an installer to simplify it. But before writing the installer, we need to add the friday launcher we wrote to the root of our project, we’ll always copy it to the bin directory.

Here is how the installer.sh file will look like:

#!/usr/bin/env bash

./gradlew assemble
chmod +x friday
cp build/libs/*.jar /usr/local/bin/friday.jar
cp friday /usr/local/bin

It will: assemble the project, make our launcher executable in case it’s not, and copy all files to the bin directory.

Friday Baby Steps

Now, when we have the tool up and running, we can start building its architecture and prepare the infrastructure.

First of all, we need a way to lunch bash commands because even if the big part of the work will be done in Kotlin, we might need to execute some bash commands as well. Here is how I did it:

fun String.executeBashSilently(): String {
    val process = Runtime.getRuntime().exec(this)
    val output = InputStreamReader(process.inputStream).readText()
    val error = InputStreamReader(process.errorStream).readText()
    if (error.isNotBlank()) throw Exception(error)
    return output.trim()
}

fun String.executeBash() {
    val proc = Runtime.getRuntime().exec(this)
    val stdInput = BufferedReader(InputStreamReader(proc.inputStream))
    val stdError = BufferedReader(InputStreamReader(proc.errorStream))

    var text: String?
    while (stdInput.readLine().also { text = it } != null) println(text)
    while (stdError.readLine().also { text = it } != null) println(text)
}

We can try and execute a command from Friday main function to check that everything works as expected.

fun main() {
    "ls".executeBash()
}

Friday running ls command

Cool, now, we need to teach Friday what is a command, and how to differentiate between them; we need a way to parse command-line arguments, too.

After some investigation, I decided to use https://github.com/ajalt/clikt.

Clikt is a Kotlin library that makes writing command-line interfaces simple and intuitive. Roughly speaking, it parses the arguments for you and generates a nice and neat help.

So, after adding all the required Clikt dependencies to the build.gradle file, we can define our first Friday command.

class FirstCommand : CliktCommand() {
    private val count: Int by option(help = "Number of greetings").int().default(1)
    private val name: String by option(help = "The person to greet").prompt("Your name")

    override fun run() {
        repeat(count) { echo("Hello $name!") }
    }
}

fun main(args: Array<String>) = FirstCommand().main(args)

And this is how we can use it from the terminal:

Running first command

Also, we’ve got a cool help command fully generated by the library:

Running help command

You can check out commands and passing-parameters to learn more about Clikt commands and parameters.

Let’s Make It More Scalable!

We can use Clikt subcommand API to make the tool a little bit more scalable and add more commands to it. So, firstly let’s add a root command (a container for all other commands).

class Friday : CliktCommand() {
    init {
        subcommands(
            FirstCommand()
        )
    }

    override fun run() = Unit
}

fun main(args: Array<String>) = Friday().main(args)

And to showcase the example better, I’ve written some more dummy commands. It doesn’t actually matter what they are doing. All of them are following the structure I explained above.

Friday commands

Let’s add them to the list of subcommands as well.

class Friday : CliktCommand() {
    init {
        subcommands(
            FirstCommand(),

            DeployCommand(),
            ProxyCommand(),
            DevicesCommand(),
            PackageCommand(),

            DockerCleanCommand(),
            CompareCommand(),
            OpenRullerCommand()
            JenkinsPluginsCommand(),
        )
    }

    override fun run() = Unit
}

Cool, now let’s test everything from CLI.

This is how our help message looks.

Multiple commands help

And this is how we can run our command.

Running first command

This structure allows to quickly add a new command to the list, and this is great, but with the number of commands growing, the help message and everything else might become a mess 🤷‍♂

It would be nice to have the commands grouped somehow and to do this we need to define one more class.

open class CommandsGroup(
    name: String = "",
    commands: List<CliktCommand> = emptyList()
) : CliktCommand(name = name) {
    init {
        subcommands(commands)
    }

    override fun run() = Unit
}

Then use it in the following way:

class Friday : CliktCommand() {
    init {
        subcommands(
            FirstCommand(),

            CommandsGroup(
                name = "android",
                commands = listOf(
                    DeployCommand(),
                    ProxyCommand(),
                    DevicesCommand(),
                    PackageCommand(),
                )
            ),
            CommandsGroup(
                name = "docker",
                commands = listOf(DockerCleanCommand())
            ),
            CommandsGroup(
                name = "git",
                commands = listOf(
                    OpenRulerCommand(),
                    CompareCommand()
                )
            ),
            CommandsGroup(
                name = "jenkins",
                commands = listOf(
                    JenkinsPluginsCommand()
                )
            )
        )
    }

    override fun run() = Unit
}

Now everything looks way more neat!

Grouped commands help

There Is Always Room for Improvement!

It seems that Friday is ready to be used now and there is no more room for improvement.

Actually, there is some!

I am too lazy to register subcommands manually each time I am adding a new one. That’s why I thought that it would be nice to teach it to find and register commands themselves.

After not too much thinking, I decided to use reflection for this Friday.kt.

Maybe it’s not the cleanest code I’ve ever written, but it works, and I don’t need to register the commands manually anymore. 😎

Alright, now, it looks like we’re done with the improvements.

Not yet! 😂

Creating the file and the class for every new command could also be automated, so I’ve developed a command that generates a new command GenerateNewCommand.kt.

Generate new command

After running it, a new class for the command is generated and placed inside the Friday project so that I’ll just need to write the command implementation.

New command clas

And Yeah, I have one more 😃

After a short period of using my new tool, I noticed that I need to write friday in front of each and every command I am running all the time. And I thought that this could be improved as well.

I fixed this by writing one more command that aggregates all Friday commands and generates a bash_profile file with aliases for each of them. GenerateBashProfileCommand.kt

Generate new bash profile

Also, I noticed that sometimes I want to see the whole tree of commands at once on the screen to find the one I need, and the Clikt can’t do this; that’s why I also implemented a more comprehensive help command HelpCommand.kt

Conclusion

Life is too short to lose it for small and recurrent tasks, and if you find the way I am trying to win some time interesting and useful, you don’t need to write everything from scratch.

You can simply fork https://github.com/maximbircu/friday and start automating your routine by adding your custom tasks.


Resources