Skip to content


Switch branches/tags

Name already in use

A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?

Latest commit


Git stats


Failed to load latest commit information.
Latest commit message
Commit time


This is a small utility I am using to auto-configure my workstations: Installing packages, linking dotfiles etc.

Design goals (aka "why not a shell script?"):

  • Provide a tagging system to select which commands to run on a particular system.
  • Provide high-level commands, so the same script may on different operating systems with less duplication.
  • Simplify certain things, like symlinking: ln on Linux requires knowledge of the relative path between target and source.

I put my wsconfig script together with my dotfiles and even some binaries in Dropbox.


Put each command on it's own line:

mkdir ~/bla
link vimrc ~/.vimrc
$ echo 'hello world' >> /tmp/test

mkdir and link are high-level commands implemented in Python. $ is run directly in the shell. See further below for all commands.

To restrict commands to a specific operating system:

sys:osx {
   $ defaults write NSGlobalDomain AppleShowScrollBars -string "Always"

The list of predefined tags on the current system can be displayed by running wsconfig --defaults. Tags that you might see there are sys:windows, sys:linux, sys:macos, but also sys:ubuntu, sys:ubuntu:natty or sys:windows:7.

Custom tags can be used:

DevEnvironment {
    dpkg python-setuptools

Because the tag in the above example starts with an uppercase letter, wsconfig will consider it "public" and present it to you as a choice to define on the command line. You can use lowercase tags internally to split commands into blocks:

DevEnvironment {
    define php
    define python

python {
    dpkg python-setuptools
    $ easy_install pip

php {
    dpkg php5-cli php5-xdebug

At this point it is worth pointing out that even though php and python above appears to look like "packages" of some sort, thinking about them in thta way is not correct. They are really "if conditions", and the commands are guaranteed to run in the order they appear in the script file - i.e., first the commands in the python block, then those in the php block.

What's more, the define statements are executed sequentially as well, thus the following will not be want you want, because the define appears to late to have any actual effect:

php {
    dpkg php5-cli
DevEnvironment {
    define php

You can nest conditions:

python {
    sys:linux { dpkg: python-setuptools }
    sys:osx { $ brew install python-setuptools }

A condition can also specify multiple tags. The following is the exact equivalent to the above. What you prefer is a matter of style:

python sys:linux { dpkg: python-setuptools }
python sys:osx { $ brew install python-setuptools }

If you combine a capitalized tag with a system tag, the capitalized tag will only be offered as choice when running on that system:

sys:linux VirtualMachine {
    $ gconftool-2 -s /apps/gnome-screensaver/lock_enabled --type=bool false

When running the above on Windows, wsconfig is smart enough to realize that there are no commands backing the VirtualMachine tag, and will ignore it.

Nested conditions, and tags combined with whitespace or both treated as AND. You can als do OR, by using a comma:

sys.linux, sys.osx { link: ssh/config ~/.ssh/config }

AND and OR can be combined (but complex expressions using brackets are currently not supported):

sys.linux, Cygwin {
    define tarsnap

Above, the tag tarsnap will only be defined if we're on Linux, or if we're on Windows and the Cygwin tag has been selected (remember, since it's uppercase, the user will be presented Cygwin as a choice).

Tags can also be negated. If you want to install Thunderbird only when not in a virtualized environment:

sys:linux !Vm {
    dpkg thunderbird

Finally, you can also use comments, of course:

# To fix monospace fonts in Java apps # https// dpkg ttf-dejavu

There is no syntax for multiline comments, but if you're paying attention, there's an obvious way to implement them: Use a tag selector to disable a block of statements:

comment {
sys:linux (


Sometimes you want to use machine-specific values in the script; wsconfig has a variable system that allows you to do this. You will be asked to provide values for all the variables used in the effective script (that is, you won't be bothered with variables that are only used in commands that won't run) at the start of an apply run.

The syntax uses a double-@ notation:

$ sudo scutil --set ComputerName "@@hostname@@"

Variables are case-sensitive.

Root usage

You'll want to run some commands as root, but usually not all - you want your config files to be created with you as the owner. wsconfig uses sudo to run commands as root.

Some commands, like dpkg, use sudo by default. Others, like link or mkdir, to run them as root, you can prefix them with the term sudo:

sudo mkdir /opt/foo

For shell commands, you are free to do whatever you like, since they will be piped directly to the shell:

$ sudo apt-get update
$ su -c "apt-get update"

Available commands


Execute something in the shell. These are not parsed like other commands - instead, content is given to the shell as-is. A multiline shell syntax is also supported:

$: set -e
   echo $FOO

Whitespace is significant here. After the colon, every line that is indented at least as many characters as the position of the colon will be considered part of the shell command. The first line with an indentation level equal or lower than the column will be the next regular command:

    echo $FOO
 remind "This is no longer shell"
Install dpkg packages on Debian-systems, using apt-get.
Installs a formula via homebrew; Preferred over the native command because the latter returns an error code if the requested formula is already installed.

Create a symbolic link. Both pathnames can be relative to the config file itself, wsconfig will properly construct the link target path.

The command will fail if the target file already exists with a different link target than the one you wish to say. You can add an -f option to force a link overwrite:

link -f virtualenvs/postmkvirtualenv ~/.virtualenvs/postmkvirtualenv
Creates a directory, if it does't exist yet.
Install a Python package using "pip". pip needs to be available.
Run a windows executable via wine.
Remind yourself of some manual setup step. These will be collected and presented at the end of the script.

Add a line to the given file, but only if it doesn't exist yet:

ensure_line ~/.bashrc "~/.bashrc_michael"

Applying a config file:

$ wsconfig my_config_file
Available choices:
$ wsconfig my_config_file apply Development

Tagging in-depth

Here are some extended thoughts on the tagging system, and my thinking about it (currently still an ongoing process).

Initially, the define command was considered out-of-sequence. It was being preprocessed such that the following worked as expected:

foo bar qux { remind "Stop drinking" }
bar { define qux }
foo { define bar }
define foo

We would traverse the document until no new defines are activated, and then use all discovered tags as the starting set. However, this seemed kid of schizophrenic. The inclination would be to use it like this:

sys.linux {

    define chrome


I.e., as a sort of "call" or "include", with the chrome selector serving to encapsulate the relevant commands visually/structurally. And while the above does indeed work, even now, if the chrome block comes after it, the whole point of this being supposed to be an include is that it shouldn't matter where in the file it is located. But that's not really what define is. If above the foo command fails, and the script is aborted at this point, you'd expect a chrome block to not be processed. However, if defines are preprocessed as was the case, then such a block might have already run.

So to combat that, I wanted to add restrictions on define, such that they may only be used in selectors that have no other commands:

sys:linux {
    define base-linux
    define foo
Development {
    define base-development
    define python
    define php

It would be an artificial restriction intended to make things clearer, but as you can see, it leads to an entirely different style of writing config files. You'd be forced to put ALL commands within faux selectors (like base-linux), which is ugly, while at best making the problem, that here is no longer a clear order of execution, only somewhat more bearable (if the above looks clear, think about a large file with sequential commands being intermixed with such packages.

It just doesn't make sense to encourage using define as an inclusion concept, which is what preprocessing them in this way does. It's schizophrenic because it is confused about whether tag selectors are what the claim to be, "if conditions", or whether they should be viewed as "packages".

Instead, if needed, a package concept could be introduced separately:

@chrome (

sys:linux {
    @chrome  # Include the chrome package.

The @()-syntax could indicate a package, NOT a selector, and they would only ever run when included (but only once). These could also have other uses, like indicating a "unit of execution", where errors would be caught, such that an error in the package causes subsequent statements in the package to be skipped, but further statements outside to be run.

On the other hand, introducing a different type of syntax might already be too much. This is supposed to be simple after all. There is another potential solution: A multi-pass apply process. So if we take the example from before:

sys.linux {

    define chrome

Then chrome would not be preprocessed. If the script ends with foo, then no chrome block will have run. Instead, code processing the document comes across the define only when foo has already run, and when it does, it schedules another document traverse. The second time, commands that have already run skipped, but commands newly unlocked by the tag are run now.

This might be the perfect solution because:
  • No extra syntax.
  • The order in which commands run is not any more confusing then with @(), and it could be used equally as effectively to structure code.
  • It avoids the main conceptional issue with the original define - that it was processed out-of-order.
  • The @() syntax would need to implement code to avoid running multiple times as well.
  • It fixes the problem that defines have now, that they have no effect if in the wrong order.

There's a further aspect that I'm currently not happy with. Take the following pieces of code:

DevEnviron {
    Python {}
    Php {}
DevEnviron {
    define python
python {
    Python3 {}

In both cases, only the DevEnviron tag will be presented as a choice. Why? wsconfig would either have to indiscriminately present all such tags as choices, as a flat list, without recognizing the dependencies, even though, in the first example, defining Python has no effect without also defining DevEnviron (this could be an optional --all switch). Or it would have to present you with a tree of choices, i.e. recognizing the dependency between Dev and Python. This could happen through a smart algorithm, or by going through a multi-step choice process (choose DevEnviron, then choose Python, after each step traversing the tree for new tags that become available).

Initially, I thought about validation rules that prevented such tags from being hidden, but that doesn't really make a lot of sense, and one reason is how easy it can be worked around. If this fails validation:

Python {
    Dev {}

Then the following would bypass it, but have the same effect (the Python tag being useless without the Dev tag):

Dev {
    python { noop }
Python { define python }

Similar tools


A tiny utility to automatize setting up a new workstation; linking config files and installing packages.






No packages published