Onyx Mueller

Scheduling Commands With Cron On Your Mac

by , on
August 31, 2018

I use a variety of CLI software for day to day development. One I use heavily is Homebrew. I use it to install various libraries and applications on my Mac. Among benefits, it provides an easy way to install and manage software, without the need of administrative access. That’s important to me.

However, two caveats of Homebrew are that 1) it does not automatically remove old installation files (which can result in significant disk space over time/usage) and 2) the Homebrew software database is not automatically updated. To achieve both of these, you have to run commands periodically.

I’m lazy and would prefer if these commands were run on a regular basis on my Mac. So I turned to cron for help.

What is cron?

cron is a time-based job scheduler found in most Unix-like computer operating systems, like macOS. cron is driven by a crontab (cron table) file, a configuration file that specifies commands to run periodically on a given schedule.

Creating a crontab file

To see if you have an existing crontab file, run the following at the command-line:

$ crontab -l

To create a new crontab file or edit your existing, simply run:

$ crontab -e

This will launch your default editor (typically VI) and open the crontab file to edit it. Here’s an example job you can save to the crontab file.

* * * * * echo 'Hello, world!'

Note: On macOS Mojave (10.14), and maybe older versions too, saving a crontab file and attempting to install it will trigger a security dialog like the following. You’ll need to hit OK to allow the scheduling to occur.

Crontab usage

The syntax of a crontab file has five fields for specifying time, date, & day followed by the command to be run at that interval.

* * * * * command to be executed
- - - - -
| | | | |
| | | | +----- day of week (0 - 6) (where 0 represents Sunday and 6 represents Saturday)
| | | +------- month (1 - 12)
| | +--------- day of month (1 - 31)
| +----------- hour (0 - 23)
+------------- min (0 - 59)

Use an * (asterisk) for one of the five time, date, or day fields to mean every value should be applied (which would be every value in the ranges I provided above in parentheses). Use a , (comma) to delimit multiple values. And use a – (hyphen) to denote a range of values.

Here are some examples:

0 8 * * 1 echo 'every Monday at 8AM'
0 8 * * 1,5 echo 'every Monday and Friday at 8AM'
0 8 * * 1-5 echo 'every weekday (Mon - Fri) at 8AM'

On a Mac, instead of the first five fields, one of eight special strings can be used, which is very useful.

@reboot     Run once, at startup.
@yearly     Run once a year, "0 0 1 1 *".
@annually   (sames as @yearly)
@monthly    Run once a month, "0 0 1 * *".
@weekly     Run once a week, "0 0 * * 0".
@daily      Run once a day, "0 0 * * *".
@midnight   (same as @daily)
@hourly     Run once an hour, "0 * * * *".

Setting environment variables

cron executes jobs under its own environment variables and PATH, which defaults to:


This means that if you try to create a job that will run commands that are not in cron’s PATH (e.g., Homebrew commands), cron will complain that it can’t find the command and fail to run. To get around this, you can specify the PATH in the crontab file before the command.

* * * * * brew update

Running commands from a separate file

In my case, I wanted to run a few Homebrew commands on a regularly basis, so I placed all my commands in a separate shell script and added the path to my shell script. My crontab file looks something like this:

@daily /path/to/shell/commands.sh

My shell script file looks something like this:

brew cleanup
brew cask cleanup
brew update

To allow cron to run a shell script, make sure you’ve modified the permissions to make it executable:

chmod u+x /path/to/shell/commands.sh

Hey, you’ve got mail

Cron is a daemon, so you won’t be able to see a it run in the terminal/command-line. So how do you know if a cron job has been run successfully?

Well, it turns out that cron sends out an email each time the scheduler runs. You can access these messages through the mail command:

$ mail

This lists all the messages you’ve received. If you prefer to have them emailed to an actual email account, you can add the following to your crontab file:



My Favorite Announcements From Google I/O 2017

by , on
May 24, 2017

I’ve attended Google I/O, Google’s annual developer conference, for the last 4 years in a row. I/O 2017 was hands down the best of them all. The biggest reason for saying that is not because of new products and features, but because we saw Google take big strides towards listening to and embracing the developer and user communities. This was apparent not only in the operation of the conference, but also in the direction Google is taking in many of its products, platforms, and services.

As both an Android developer and user of Google products and services, here are some of my favorite announcements.

  1. Kotlin Support
    • Android developers have been begging for an improved way to develop apps. Google has answered that call by announcing official support for the Kotlin language. Kotlin brings a modern, pragmatic, and concise approach to writing code, and will make for a more satisfying and efficient experience for developers.
  2. Android Architecture Components
  3. Google Play Updates
  4. Google Lens
    • I recently had trouble coming up with the name of a favorite restaurant in Italy for a colleague that was going to visit. The photos I took didn’t help to narrow it down. Google Lens will be able to help by identifying data in my photos that helps recognize the restaurant.
  5. Google Home Improvements
    • I wasn’t interested in buying a Google Home after it was announced last year at I/O 2016, not even after it gained a few more skills through software updates. But with the addition of hands-free calling & visual responses, this has changed my mind.
  6. Google Photos Sharing
    • My family and I are big fans and users of Google Photos since launch, but one pain point my wife and I have had is being able share all photos we take, regardless of which device it was taken on. My solution was to place my Google account on my wife’s phone, so that we have a central account where all photos are uploaded. Google has finally solved this problem for me with the announcement of Shared Libraries.
  7. Instant Apps

While many of the announcements Google made at their annual developer conference might not have been exciting to consumers and may not have wowed the press, they excited me and many others I know. I’m looking forward to seeing the future Google creates.

Using Drawables In TextViews

by , on
September 28, 2013

A common mistake I notice when reviewing Android development code is the use of more views than what is needed for the design. A good example of this is the use of three views (or more!) to create a very common pattern.

For instance, say the following was intended.

take_photo_buttonDevelopers have commonly implemented this using something that essentially has the following layout.

<RelativeLayout /* container */ >
   <ImageView /* image */  />
   <TextView /* text */ />

Android provides a much easier means to accomplish this. The same design above can be done using a single TextView.

    android:text="Take photo" />

Since API level 1, TextView has allowed setting Drawables either to the left of, to the right of, above, or below the text. These Drawables, also know as Compound Drawables, can be specified in XML or using methods. In addition to location of where the Drawable is placed, you can also indicate the padding to use. Using this functionality on simple designs allows you to place an image near text without the need of an ImageView. In many cases, you can greatly simplify the amount of views needed to create your Android designs.

My New Commuting Bicycle

by , on
March 19, 2012

After having my commuting bicycle stolen outside of a Denver restaurant in autumn of last year (bah!), I’ve been in need of a replacement.  But after the check for my home insurance claim arrived, I decided to hold off from buying a new one immediately since winter was around the corner.

Fast-forward to three days ago…

Motivated by the recent warm weather we’re getting before spring, the will to keep back in shape, and the sum of money saved from the home insurance check along with some recent birthday cash, I ventured out to find a new commuting bicycle.

I wasn’t sure what I wanted this time around, but I knew I didn’t want the same style of bike I had before.  Previously, I had bought a dual-sport.  Back then, I figured that since I loved MTBing, I wanted a bike with similar features but with better road efficiency.  While the dual-sport did just that, I decided not to go that direction again.

After spending parts of two days, going through the inventory at my local REI, Wheat Ridge Cyclery, and some random smaller bike shops, I found a bicycle that I really liked at the Westminster Bicycle Village: a 2011 Trek (1st) District in Matte Platinum.


There were several features/aspects of the Trek District that I loved:

  • It’s a single-speed bicycle, which means it doesn’t have the additional gears you normally find on bicycles.
  • It uses a belt-drive, so it replaces your standard oily, noisy bike chain with a totally silent, maintenance-free belt made of Kevlar.
  • The carbon fork, aluminum frame, single gear system, and aggressive road bike inspired geometry help to make the bike extremely light.
  • It has a really cool “urban”, yet vintage design/look.

Along with that, I was happy to discover Bicycle Village was having a clearance sale, which knocked the price down to $790 (vs. $1100-1200 MSRP).  So, after a quick test spin, I bought and brought home my new commuting bicycle. 🙂

WarBall and the Muellers

by , on
December 22, 2011

For about 7 years now, my family/friends and I have had fun battling it out in a fantasy football league called WarBall.  To steal some text from the league’s manifesto, “WarBall is a highly competitive, private fantasy football league… If you have the intelligence, strategy, and decision-making to be a true general in the game of fantasy football, then WarBall is for you.”

“Highly competitive” is an understatement to me.  I’ve played in many other leagues, but none of them were as challenging to succeed in as WarBall.  I can honestly say I’ve had a lot of success in leagues outside of WarBall.  But in the 7 years of WarBall, not so much!  I finally did earn my first championship (last year!  yay!), but it wasn’t easy.  The runner-up and I continued a trend of having a different WarBall champion and runner-up every single season (in other words, no one has been the champion or runner-up twice).

Perhaps one could say that the competitiveness of WarBall is directly linked to the size of the prize pot (paid out to both the champion and the runner-up in a 70/30 split).  While that may be true, I’d like to attain that WarBall’s challengingly nature is attributed to the seasoned fantasy football gamers that play the game.

Why am I telling you this?  Well, considering my perception of WarBall’s competitiveness, it brings me joy to see my family members in the top ranks this season!  All 4 of the Muellers playing are part of the top 5 WarBall teams in our 14-team league this season.

This Mueller dominance of WarBall is very cool to see!  (At least to me!  :-p)   What’s even more neat is the fact that we are all roughly sitting around 7 – 4 (hats off to my sister with the slight lead!) heading into the last two games before the playoffs.  You can’t make this up people.

Go Muellers!

Update: After another round of games, which we won or tied, we all have the same winning percentage!

Update: I was the only one *not* to make the playoffs!  😀  Congratulations goes out to my brother for winning WarBall this year!

AdvancedDataGrid CSV Export Utility Class

by , on
August 20, 2011

Recently, I needed a way to export data from Adobe Flex’s AdvancedDataGrid control to CSV-formatted files for use in Microsoft Excel.  Since Flex did not provide a native method, I looked into some existing third-party libraries.  Those did not work out, so I ended up building my own AdvancedDataGrid CSV export utility class.

I first experimented with the excellent AlivePDF library.  I had used AlivePDF in the past to…well…create PDFs.  One of the things I remembered from using it was that there was CSV export functionality.  But after spending some time experimenting, I was disappointed to find it required quite a bit of overhead and would not be able to fully satisfy my needs.  Next, I turned my attention towards the AS3XLS library.  From what I could gather, the library seems like what you’d want if you were trying to read & write native Excel files, but it didn’t provide CSV support.

So, I started searching the intertubes for another option.  I found a great  post by Abdul Qabiz, demonstrating a home baked solution he had come up with.  The code he presented was quite dated (it was written for Flex 2, targeting the DataGrid control) and was missing some features I needed.  However, it was exactly what I needed as a starting point.  Learning from what he provided, I rolled my own solution that provided everything I was looking for.

Below is the AdvancedDataGrid CSV export utility class I came up with.  It compiles in Flex 3.5, Flex 3.6 and Flex 4.5, handles grouped column headers, can double-quote values, and is capable of saving the CSV data of any language to a file (through the use of UTF-16 encoding).  I am posting this simple class in case somebody else might find it useful.  If you do, please leave a comment.  Also, let me know if you find ways to improve it! 

Note: The final version of the code uses a tab as the default CSV delimiter.  This is due to a limitation in Excel when dealing with a comma-separated value file in UTF-16/Unicode encoding.  Despite the use of a tab character, the format of the file is still viewed as a “CSV”.  🙂

Download the code.


package net.onyxmueller.util
import flash.net.FileReference;
import flash.utils.ByteArray;

import mx.collections.ICollectionView;
import mx.collections.IViewCursor;
import mx.controls.AdvancedDataGrid;
import mx.controls.advancedDataGridClasses.AdvancedDataGridColumn;
import mx.controls.advancedDataGridClasses.AdvancedDataGridColumnGroup;

public class CSVUtil
private var _csvSeparator:String;
private var _lineSeparator:String;
private var _doubleQuoteValues:Boolean;
private var _doubleQuoteRexExPattern:RegExp = /\"/g;
private var _encoding:String;

public function CSVUtil(csvSeparator:String = "\t", lineSeparator:String = "\n", doubleQuoteValues:Boolean = true)
_csvSeparator = csvSeparator;
_lineSeparator = lineSeparator;
_doubleQuoteValues = doubleQuoteValues;

public function formatAsCSVString(items:Array):String
if (_doubleQuoteValues)
// escape any existing double quotes then place double quotes around values
for (var i:int = 0; i < items.length; i++) { items[i] = items[i] ? items[i].replace(_doubleQuoteRexExPattern, "\"\"") : ""; items[i] = "\"" + items[i] + "\""; } } return items.join(_csvSeparator) + _lineSeparator; } public function advancedDataGridToCSVString(dg:AdvancedDataGrid):String { var headerCSV:String = ""; var headerItems:Array; var dataCSV:String = ""; var dataItems:Array; var columns:Array = dg.groupedColumns ? dg.groupedColumns : dg.columns; var column:AdvancedDataGridColumn; var headerGenerated:Boolean = false; var cursor:IViewCursor = (dg.dataProvider as ICollectionView).createCursor(); // loop through rows while (!cursor.afterLast) { var obj:Object = null; obj = cursor.current; dataItems = new Array(); headerItems = new Array(); // loop through all columns for the row for each (column in columns) { // if the column is not visible or the header text is not defined (e.g., a column used for a graphic), // do not include it in the CSV dump if (!column.visible || !column.headerText) continue; // depending on whether the current column is a group or not, export the data differently if (column is AdvancedDataGridColumnGroup) { for each (var subColumn:AdvancedDataGridColumn in (column as AdvancedDataGridColumnGroup).children) { // if the sub-column is not visible or the header text is not defined (e.g., a column used for a graphic), // do not include it in the CSV dump if (!subColumn.visible || !subColumn.headerText) continue; dataItems.push(obj ? subColumn.itemToLabel(obj) : ""); if (!headerGenerated) headerItems.push(column.headerText + ": " + subColumn.headerText); } } else { dataItems.push( obj ? column.itemToLabel(obj) : ""); if (!headerGenerated) headerItems.push(column.headerText); } } // append a CSV generated line of our data dataCSV += formatAsCSVString(dataItems); // if our header CSV has not been generated yet, do so; this should only occur once if (!headerGenerated) { headerCSV = formatAsCSVString(headerItems); headerGenerated = true; } // move to our next item cursor.moveNext(); } // set references to null: headerItems = null; dataItems = null; columns = null; column = null; cursor = null; // return combined string return headerCSV + dataCSV; } public function saveAdvancedDataGridAsCSVFile(dg:AdvancedDataGrid, fileName:String, encoding:String = "utf-16"):void { var csvString:String = advancedDataGridToCSVString(dg); var bytes:ByteArray = new ByteArray(); // if using UTF-16, prefix file with BOM (little-endian) if (encoding == "utf-16") { bytes.writeByte(0xFF); bytes.writeByte(0xFE); bytes.writeMultiByte(csvString, encoding); } else bytes.writeMultiByte(csvString, encoding); // prompt the user with a save location // note: FileReference requires a minimum flash player version of 10 var fileReference:FileReference = new FileReference(); fileReference.save(bytes, fileName); fileReference = null; } } }

How to remove unused import statements in Flash Builder

by , on
August 11, 2011

Here’s a quick tip for Flex developers using Flash Builder.  For a given .AS file, you can remove/clean-up any unused import statements by hitting Ctrl+Shift+O or by clicking on Source –> Organize Imports.

Improve Visual Studio 2010 Build Times

by , on
July 28, 2011

A co-worker pointed me to a great tip that improved my build times within Visual Studio 2010.  If you work with a large solution/project, this might help you.

Under Tools –> Options –> ‘Projects and Solutions’ grouping –> Build and Run –> Check ‘Only build startup projects and dependencies on Run’.

Only build startup projects and dependencies on Run

Check 'Only build startup projects and dependencies on Run'

Again, this is probably only useful if you have a larger solution like mine.  In my case, it brought my wait time down from minutes to under a minute.

Thanks Andy!

I have to give T-Mobile props

by , on
March 9, 2011

Note: This is a test post from the WordPress app for Android. Please excuse any mistakes caused by my “fat fingers.”

T-Mobile has been going from city to city, upgrading their network infrastructure to enable HSPA+/4G speeds and become one of the largest (if not the largest) HSPA+/4G networks out there.  I’ve been with T-Mobile for a long time (7+ years?) and it’s kind of cool that without having to pay a extra dime, my phone’s bandwidth in Denver went from fast to really fast without having to do anything.  Check out the snapshot of my recent bandwidth test below:

2011.03.03 T-Mobile Denver speed test on Nexus One.

2011.03.03 T-Mobile Denver speed test on Nexus One.

Throw in better customer service (this is my opinion – I’ve had both AT&T and Nextel in the past) and the peace of mind that I’m getting a good deal on my cell phone plan, I can’t help but give T-Mobile props.

How to transfer your Google Voice number to a Google Apps account

by , on
February 24, 2011

Update: This is no longer a valid method.  Google has “retired” the form.

I’ve been using Google Voice for a number of years now.  I actually started using the service back when it was owned and branded as GrandCentral.   When I received word that Google would be acquiring them, I was delighted.  Since then, Google Voice has gotten progressively better and it has become a service I use nearly every single day.

However, once I moved my personal domain to GApps and started using my GApps account exclusively as my primary Google account, this was the one service that I was not able to use with my new GApps account.  Well, let me be clear…I was able to use the Google Voice service with my GApps account, but not with the Google Voice number/account I had been using for years.  This was because my number/account was tied to my old personal Google account.  Unfortunately, the Google Voice interface does not provide a way (as of the timing of this writing) to transfer your number/account to a different Google account.  So this problem meant that I could not fully abandon my old personal Google account since I still needed it to access my Google Voice account.  That was until…

I found a way to resolve this issue.  Google provides a form you can fill out in case “you accidentally associated your Google Voice account with the wrong Google Account.”  You can find it here:


Now, of course I didn’t accidentally associate my Google Voice number/account with the wrong account.  I had been happily using my number under the right account for years.  But I filled out the form anyway and indicated that I was not requesting this transfer due to a mistake, but that I was now using a different Google account (a GApps account) as my primary, and would like to have my account transferred to it.  Lo and behold, a couple of days later, I received an email indicating the transfer of the account was complete!


Google Voice Account Transfer Complete

Now, this might not be the “correct” way this should be done.  But it worked for me.  I would imagine Google will eventually have a better way of doing this kind of thing down the road.  Until then, if you have a need for transferring your Google Voice number to a Google Apps account, filling out and submitting the form might work for you.