Slack Status Updater (CLI) - Connecting Cores

It's time to actually build a CLI!

March 23rd 2018

I've just completed a few features in the core and now want to integrate it into the CLI. Yesterday, I ran some experiments that I stupidly forgot to commit and don't have access to on this machine. But whatever. The result of those experiments was that it's pretty easy to get started with integrating the core that I've built. The core itself carries just these two features for now:

  1. Read a yaml document of statuses and return a list of them.
  2. Take a status and a Slack API token and use those to actually change the status.

I probably should have added the feature to take a given status name (lunch), a list of available statuses, and pick the status from the list if it exists. But since I've got the testing down right now, I'm going to skip over that, write that logic in the CLI and then port it over to the core because I don't want to let the momentum die off. So! To work. But first, I'll update my little project site :).

11:44 Jumping in for a quick dev session. Noting down what use cases I want to pass and therefore the tests:

  1. CLI should be able to read a file from ~/.config/ssucli/statuses.yaml and also read a config file from ~/.config/ssucli/config
  2. CLI should be able to take in a given status name, search for it and pick it from a given list of statuses
  3. CLI should take the picked status and use the core to update the status on Slack.

11:49 Let's start with being able to read a file from ~/.config/ssucli/statuses.yaml

  • First order of business are the tests. The first tests would be passing a file name into a function, and seeing if the function can return the data in that file. To do that, I'd need to set up a test file in a test folder with some test data before the test runs. And once the test is done running, I delete the file and the folder. Remember that when I laid out the MVP goals, I wasn't going to auto setup the folders or files. So I don't need to test for that just yet.
  • I don't know how to read files in go. So off to the docs we go.

12:03 Just when I though Go couldn't surprise me anymore. No I'm kidding. I knew I'd be shocked. Repeatedly. Like from first reading, I'm trying to figure out how I check if a directory exists and how to make a directory. It seems like os is the package I want. That makes sense and is expected. But here's the kicker:

  • The os package contains a function called IsExists. Naturally I'd expect to pass a path into it and receive an error or bool in response. Instead, the function is some weird definition which requires an error as an input and receives a bool?

func IsExist(err error) bool
IsExist returns a boolean indicating whether the error is known to report that a file or directory already exists. It is satisfied by ErrExist as well as some syscall errors.

What??? ARRRGH!! I'm going to Google.

12:12 Nope. I'm not crazy. You use os.Stat on the path you want to check. And then you use err with os.IsExist to see if you get a true bool value back. The convolution is real.

13:45 Back. And just added some code to the set up and tear down functionality of the test file. Turns out that os.Remove(<path>) doesn't work if there are alredy files in it. And I'm not sure I can see a recursive remove function either. Sigh.

  • Oh wait. Scratch that. RemoveAll looks like it might work. If it does, that's a real contradiction since MkdirAll makes a complete path including subfolders specified in the path passed to the function. Because of that, my expectation of os.RemoveAll(~/.config/ssuclitest) is that it will remove .config as well. Which isn't what I want.
  • Ok. It just removed ssuclitest. I'll concede that does make sense. Bah humbug. Sorry.

13:53 Time to run the first test then

  • Ok that's good. Test failed. Function not defined yet. Here's what the test looks like for now:
func TestIfFileExists(t *testing.T){
	homeDirectory := os.Getenv("HOME")
	fileNameToSearchFor := homeDirectory + "/.config/ssuclitest/statuses.yaml"
	_, err := getStatusesFromFile(fileNameToSearchFor)
	if err != nil {
		t.Fatalf("An error occurred. Expected no error. Error was %s", err.Error())

Right now I'm not storing the value I get from getStatusesFromFile. I'll fill that in as I go along.

14:01 And we have code that passes. Discovered a quick one on how to test a specific file only. If I want to test filereader_test.go, then I have to also specify all the files that it depends on. Previously when I ran go test filereader_test.go it gave me an error that the function I'm running in the test cannot be found. This is despite it being in the same pacakge. So to get it to work, you have to use go test filereader_test.go filereader.go. More details in this wonderful Stackoverflow answer. Code to pass the test is:

func getStatusesFromFile(pathToStatusFile string) ([] ssucore.Status, error) {
	_, err := os.Stat(pathToStatusFile)
	if os.IsNotExist(err) {
		return nil, err
	} else {
		return nil, nil

Now to actually return something from the function. But before that, I need to update my setup function to create a file with some mock data.

14:24 I'm now writing dummy data to the file. That was an interesting expedition. I can use f, err := os.OpenFile('path-to-file', os.O_WRONLY|os.CREATE, 0644) to open the file and create it if it doesn't exist. Lovely. And then I can use f.Write([]bytes("string-to-cast-to-byte-array") to write the contents. And maybe I can use f.Read() to get a byte array containing the contents of the file back? NO!!! f.Read() returns the number of bytes!!! Bloody hell!

  • So to read a file, I either use io/ioutil to read the whole file, OR I use f.Read and pass a known size byte array in which will then get filled up with contents from the file. So if I pass a byte array that can hold 5 items, I'll receive the first 5 bytes from the file. I die.

14:44 So using the ioutil.ReadFile method, I can see that the content I'm receiving from it is not nil. In fact it's perfect! Could have had this done sooner if not for a silly if condition error by me.

  • And the CLI officially reads a yaml file and returns the results I want!! woot! Time to make a commit.
  • Commit is up

Stopping for the day (week) here. Back at it on Monday or early morning during the weekend if I feel like it.

March 25th 2018

Not sure I can get much done since I'm on only till my son and/or wife wakes up. So let's just jump into it.

05:49 Just spent some minutes writing a test to ensure that I pick the correct status out of a given list of statuses. One thing I'm doing as I go along is to write all the functions as private. If I find that I need to make them public, I will. I'm not sure if this is the "right" way to do it or if there is any such "right" way at all. But at this point I'd rather just get my CLI working :).

05:54 Writing the function to satisfy the test. Think I need to refresh my memory on how to write a loop.

  • Oh. Looks like there might not be a for in concept in Go. Cool.

06:03 Done! I have a function that checks for an existing status, and returns it if it exists, and returns an empty status and an error if it does not. Commit

06:04 Onwards to the final part. Tie it all together with a CLI. urfave/cli. Let me downloadeth you.

06:11 Read through the documentation on how to setup the CLI app. Going step by step. I now have my first "useless" app running.


06:21 While writing the code for the CLI to access the core's method to set the status, I just realised I've named it using the private convention. Went ahead and fixed that along with the test. Commit is here

06:44 And it is done!



Some caveats here.

  • The config of the slack token is currently read from the environment variable. The choice to pass the slack token into the core is really paying off. I'll add the reading from config file functionality later.
  • There's no listing function available. There's hardly any error messaging. Even the result being printed to the terminal is done via log instead of fmt.
  • But it works! :)

Commit is here

I'll be back on Monday for polishing :)

Monday 26th March 2018

It's 6 AM so I'm not going to bother jumping into anything serious since I step away to start the day with family at 6 30. Just reviewing what needs to be done today based off the MVP list and general observations about the app.

  • Primary focus: Needs some form of config file. This will be the first thing I work on today. I'm not fond of keeping the Slack token in the environment variable.
  • Proper error messages and some documentation around the command. I think this should be about 20 minutes of works tops.
  • Update the command to be slackstatus set <status-name> instead of slackstatus <status-name>.
  • Generating binaries.

And once that is done, I'll be ready to release. With that I want to do a quick review of how long this has taken me. It's a bit iffy to measure that since this is not primary work and I allow for work related stuff or anything else like calls to interrupt the flow but I'll try an estimation. I really should have Toggl'd this.

Update - By my most pessimistic estimate where I don't even take into consideration some of the breaks I had to take in between to attend to work related stuff, it's been 22 hours of work so far which is easily within the allocated 56 hours of work I set aside for this. Yey!

09:55 Pausing regular work to ado a quick dev session on error messages and to generate a binary for Mac. I'll do the config file stuff when I'm back home.

10:05 Done with the error message formatting. Now to add some helpful stuff around the CLI itself.

10:10 While I was at it I built myself a binary for Mac using the following command inside the cli source directory

env GOOS=darwin GOARCH=amd64 go build .

10:19 I got distracted! Added new statuses to my list and was too busy playing with that. Eeeks.

10:26 That was unproductive. Anyways. I can cross off helpful messages from my list. When I get back home I'll quickly work through adding in a config file.


12:50 Getting back to it after checking in on communication at work. It's time to work on the config file. For the moment, I've dropped the idea of having a config.ini file. I don't know what I'd put in it except for SLACKTOKEN=<my-personal-token>. So if I'm going to have a single value only, then I might as well just have that file be the token itself. But for future compatibility and not having to ask people to immediately change the format of their config file, I'll be getting the token from a file called ~/.config/ssucli/tokenconfig. Done! And the best part is that the CLI package actually supports configuring parameters through a file. Yey!

13:06 Ran into a strange error where a certain attribute was not being detected. The CLI package I'm using (urfave/cli) provides something called a cli.StringFlag type. Within that type I can specify FilePath as one of the properties to say where the string is. I was getting an error telling me that theproperty does not exist. When I checked the github releases, it turned out that v1.20.0 was made in August 2017 while this property was introduce in October 2017. What this also told me is that dep ensure -add actually checks github's releases instead of grabbing the master branch by default. It'll only use the master branch if no releases exist. Fascinating. So I went into Gopkg.toml and changed the constraint to look like this:

  branch = "master"
  name = ""

13:14 Annnd we have blast off! Now to move it over to use a subcommand (set) and we just need to update the README to be done.

13:41 Took me a little longer than I expected due to a misunderstanding of how Flags work. I don't really understand how it works still but I modified my code to get things working for now. About to head out to have lunch. Will be back to dig into this later. Not sure I'll dig into it at all today though. Commit is here

  • On second thoughts, I'll call it quits for the day :)

15:47 Just came back to this to complete the README, annnnd do my first release. Woo!! Shipping is fun :)

This post is part of a series of logs exploring how I'm building a CLI for updating my slack status. You can follow the full project here

Posted on March 23 2018 by Adnan Issadeen