Package: wsconfig
wsconfig========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 particularsystem.- Provide high-level commands, so the same script may on different operatingsystems with less duplication.- Simplify certain things, like symlinking: ``ln`` on Linux requires knowledgeof the relative path between target and source.I put my wsconfig script together with my dotfiles and even some binariesin Dropbox.Language--------Put each command on it's own line::mkdir ~/blalink vimrc ~/.vimrc$ echo 'hello world' >> /tmp/test``mkdir`` and ``link`` are high-level commands implemented in Python. ``$`` isrun 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 todefine on the command line. You can use lowercase tags internally to splitcommands into blocks::DevEnvironment {define phpdefine 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 inthta way is not correct. They are really "if conditions", and the commands areguaranteed to run in the order they appear in the script file - i.e., firstthe commands in the ``python`` block, then those in the ``php`` block.What's more, the ``define`` statements are executed sequentially as well, thusthe following will not be want you want, because the ``define`` appears to lateto 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 exactequivalent 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 willonly 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 realizethat there are no commands backing the ``VirtualMachine`` tag, and willignore 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 bracketsare currently not supported)::sys.linux, sys.windows Cygwin {define tarsnap}Above, the tag ``tarsnap`` will only be defined if we're on Linux, or if we'reon Windows *and* the ``Cygwin`` tag has been selected (remember, since it'suppercase, the user will be presented ``Cygwin`` as a choice).Tags can also be negated. If you want to install Thunderbird only when not ina virtualized environment::sys:linux !Vm {dpkg thunderbird}Finally, you can also use comments, of course:# To fix monospace fonts in Java apps# https//bugs.launchpad.net/ubuntu/+source/sun-java6/+bug/569396dpkg ttf-dejavuThere 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 ablock of statements::comment {sys:linux (...}}Variables---------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 providevalues for all the variables used in the effective script (that is, you won'tbe bothered with variables that are only used in commands that won't run) atthe 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 yourconfig 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/fooFor shell commands, you are free to do whatever you like, since they will bepiped 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 syntaxis also supported::$: set -eFOO=barecho $FOOWhitespace is significant here. After the colon, every line that isindented at least as many characters as the position of the colon willbe considered part of the shell command. The first line with an indentationlevel equal or lower than the column will be the next regular command::$:FOO=barecho $FOOremind "This is no longer shell"dpkgInstall dpkg packages on Debian-systems, using apt-get.brewInstalls a formula via homebrew; Preferred over the native commandbecause the latter returns an error code if the requested formulais already installed.linkCreate a symbolic link. Both pathnames can be relative to the configfile itself, wsconfig will properly construct the link target path.The command will fail if the target file already exists with a differentlink target than the one you wish to say. You can add an ``-f`` optionto force a link overwrite::link -f virtualenvs/postmkvirtualenv ~/.virtualenvs/postmkvirtualenvmkdirCreates a directory, if it does't exist yet.pipInstall a Python package using "pip". pip needs to be available.wineRun a windows executable via wine.remindRemind yourself of some manual setup step. These will be collected andpresented at the end of the script.ensure_lineAdd 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_fileAvailable choices:DevVm$ wsconfig my_config_file apply DevelopmentTagging in-depth----------------Here are some extended thoughts on the tagging system, and my thinking aboutit (currently still an ongoing process).Initially, the ``define`` command was considered out-of-sequence. It was beingpreprocessed such that the following worked as expected::foo bar qux { remind "Stop drinking" }bar { define qux }foo { define bar }define fooWe would traverse the document until no new ``defines`` are activated, and thenuse all discovered tags as the starting set. However, this seemed kid ofschizophrenic. The inclination would be to use it like this::sys.linux {...foo...define chrome...}I.e., as a sort of "call" or "include", with the ``chrome`` selector servingto encapsulate the relevant commands visually/structurally. And while the abovedoes indeed work, even now, if the ``chrome`` block comes after it, the wholepoint of this being supposed to be an include is that it shouldn't matter wherein 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 notbe processed. However, if ``defines`` are preprocessed as was the case, thensuch a block might have already run.So to combat that, I wanted to add restrictions on ``define``, such that theymay only be used in selectors that have no other commands::sys:linux {define base-linuxdefine foo}Development {define base-developmentdefine pythondefine php}It would be an artificial restriction intended to make things clearer, but asyou 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 aclear order of execution, only somewhat more bearable (if the above looks clear,think about a large file with sequential commands being intermixed with suchpackages.It just doesn't make sense to encourage using ``define`` as an inclusionconcept, which is what preprocessing them in this way does. It's schizophrenicbecause 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 onlyever run when included (but only once). These could also have other uses, likeindicating a "unit of execution", where errors would be caught, such that anerror 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 toomuch. This is supposed to be simple after all. There is another potentialsolution: A multi-pass apply process. So if we take the example from before::sys.linux {...foo...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 documentcomes across the ``define`` only when ``foo`` has already run, and when itdoes, it schedules another document traverse. The second time, commands thathave 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 multipletimes as well.- It fixes the problem that defines have now, that they have no effectif in the wrong order.----There's a further aspect that I'm currently not happy with. Take the followingpieces 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 tagsas 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 thedependency between ``Dev`` and ``Python``. This could happen through a smartalgorithm, or by going through a multi-step choice process (choose``DevEnviron``, then choose ``Python``, after each step traversing the tree fornew 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 howeasy it can be worked around. If this fails validation::Python {Dev {}}Then the following would bypass it, but have the same effect (the Pythontag being useless without the Dev tag)::Dev {python { noop }}Python { define python }Similar tools-------------https://github.com/technicalpickles/homesick