---
title: Setting Up a Blog in the Worst Way Possible
subtitle: Or Why I should not be Let Loose with an Idea and a Terminal
author:
 - Choss
date: 2026-08-14
---

# The Motivation

Hey future me! As you know, I've been toying with the idea of starting a blog for the past few years now.
I say "blog".
That's probably stretching the definition a little.
I want a place to store my notes that's accessible and public.
Why?
Well, there are a few reasons.

## 1. Why Make Notes at All?

Some people seem to have a magic gift whereby they can read a chapter of a textbook or watch a tutorial video and remember every little detail for the next 50 years.
Alas, I am not one of those people.
Heck, if I'm reading a textbook, 90% of the info has already left my brain by the time I turn the page!
And, from listening to peoples experiences online, I think a lot of folks are in a similar boat.
There's a reason you're told to make notes in lectures - not because the output is a top tier learning aid, but because the very act of making notes improves retention.

When I was at uni, I found a system that worked really well for me.
During lecture, I would scrawl down roughly what was said in my "Rough Notes" book.
If there was something I didn't get, I'd draw a box around it and come back to it later.
Needless to say, these notes were *rough*.
Really *rough*.
Practically unintelligable.
But that was ok, because that was the point.
After the lecture I would go and sit down in the library or common area or wherever and open up my laptop.
Now was the time for the "Clean Notes".
I would try, from memory, to create a well formatted, readable, referencable set of notes for what was covered.
And it was these notes that I would review when it came time for the exam.

That system worked well at the time, but my uni days are (far) behind me now and I feel like I am stuck in a place where I can't really learn.
I am finding this very frustrating, and to be honest, difficult to face.
So we're going to have a little experiment here.
I'm going to try making notes again.

## 2. Why Make it Public?

Humans are lazy, and I am no exception to that rule.
We do not, generally, like putting in the work, going the distance so to speak; making notes is certainly a lot more effort than watching the video and telling oneself that they have "learned something".
I also know that I struggle to maintain motivation for things.
In all honesty, I've done this whole "let's try making notes again" before.
Four and a half years ago I started making clean notes in a nice clean notebook.
It is still only a third full - I lost the motivation.
So I hope, by putting my notes online, I will feel that there is some stakes involved and from this I can derrive the motivation to keep sticking to it.
Because nobody knows about my notebook, and no one knows how full it is.
But I hope that by showing people, by making it plain to all "how full" the notebook is I can muster the motivation and build the habit.
But we'll see.
Perhaps it will just end up being a mess... but it is better to make a mess than make nothing at all.

# The Setup

Ok, I'll stop it with the whimsy now.
How are we actually going to go about *putting* notes online?
Now I don't know my wordpresses from my bloggers and have never managed any kind of site or server before... but I do have a [Tilde](tilde.club) account.
We can work backwards from this.
Tilde.club provides each user with a webpage; all we need do is get some .html files into ~/public\_html, and the simplest way to do that is to hand write them in direct in the folder.
Doable, but a little laborious - making the notes is already a high effort task, having to wrangle HTML at the same time may be unwise.

Enter [pandoc](pandoc.org), which is a nice tool that can convert MarkDown to HTML (among several dozen other formats), and MarkDown is lot more pleasant to write than raw HTML.
It seems sensible to me to have the source .md files kept seperate from output .html, to keep things clean.
This means we need a process to pandoc the source and copy the output to ~/public\_html/, which could be handled by a script (perhaps python or bash).

When it comes to actually creating the .md notes, I would like to be able to do this from my home PC and push them up to tilde.club.
And the easiest way I can think to do that is via git.
This also buys us a lot of extra nice to haves for free - primarily change tracking and branching should I want to run some potentially dangerous experiments.

So, we need to:
 1. Create a git repository on tilde.club
 2. Create a script that pandocs the source and copies it into ~/public\_html/

Bonus points for:
 1. Only pandocing source files that have been updated since the script was last run
 2. Using githooks to automatically run the deployment script

Is this the correct way to do all this?
Almost certainly not.
But I think it best to give it a go and learn from the mistakes rather than deliberate for hours over if I should use wordpress or blogger.

# Step 1. Creating a git Repo on tilde.club

[This](https://tilde.club/wiki/cgit.html) tilde.club wiki page talks about setting up a git repo for use with cgit, but dictates that only bare repos can be used.
A bare repo is, as far as I can tell, a git repository that cannot be worked in.
There is no working tree, you cannot checkout branches, edit files or merge things.
It is merely a *container* for the git repository, that other folks pull and push to.
This is not great for us, as we want to be able to check out a branch and run a script that access the source files.
No can do with a bare repo.
I did think about having a second folder in which we clone from the bare repo and do our script work from there, which is not ideal as it requires ~2x the disk space.
But this bare repo business is only for cgit, which while cool is far from required.
So I tried creating a non-bare repo:

``git init --initial-branch=main ~/git/hello``

And from my local machine I can

``git clone ssh://chosstoton@tilde.club/~/git/hello``

However, my joy was short lived, as when I tried pushing changes back up, I got this error:

``Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Writing objects: 100% (3/3), 242 bytes | 242.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
remote: error: refusing to update checked out branch: refs/heads/main
remote: error: By default, updating the current branch in a non-bare repository
remote: is denied, because it will make the index and work tree inconsistent
remote: with what you pushed, and will require 'git reset --hard' to match
remote: the work tree to HEAD.
remote: 
remote: You can set the 'receive.denyCurrentBranch' configuration variable
remote: to 'ignore' or 'warn' in the remote repository to allow pushing into
remote: its current branch; however, this is not recommended unless you
remote: arranged to update its work tree to match what you pushed in some
remote: other way.
remote: 
remote: To squelch this message and still keep the default behaviour, set
remote: 'receive.denyCurrentBranch' configuration variable to 'refuse'.
To ssh://tilde.club/~/git/hello
 ! [remote rejected] main -> main (branch is currently checked out)
error: failed to push some refs to 'ssh://tilde.club/~/git/hello'``

Which is basically saying I cannot push to ``main`` on the remote as the remote has ``main`` checked out, so this action would clobber any untracked changes in their working tree.
This makes sense, as the remote repo is non-bare, it has a working tree and can make changes on the ``main`` branch.
However, we know that we will not be doing that, all we need is read access to the source .md and the ability to execute a script, and by following the instructions in the error message we can get around this.
On the remote, I ran:

``git config set receive.denyCurrentBranch warn``

Which allowed me to push.
But indeed, after the push the remote's working tree had not updated to reflect the changes and a ``git reset --hard`` was required.
This will be a problem, but I have a gut feel that we should be able to fix this with githooks.
So let's put a pin in that and come back to it later.

# Step 2. Creating a Deployment Script

The idea here is to have a script that runs (possibly with githooks) that can pandoc the source .md files and copy the output to an identically structured directory tree under some other specified root directory - namely /public\_html.
Seeing as this script spends most of it's time running external tools and manipulating files, Bash seems like the right choice here.
Now I am not very familar with Bash, but how are we to become familiar without playing in the unfamilar?

The first unfamiliar problem I must tackle is parsing of command line arguments.
In Python, we can use the ``argparse`` module to handle all of this for us, but Bash is not Python.
(Shocking, I know).
There is, however, a program that ships as standard on most Linux distros called ``getopt`` (not to be confused with the Bash builtin ``getopts`` with an **s**).
I have never used this before, so I guess it is time to learn.

## A Whistlestop Tour of getopt

The problem that ``getopt`` tries to solve is that of standardisation.
You see, there are many ways to specify command line arguments; compare the following:
 - ``script.sh -a -b -v high --directory /some/path``
 - ``script.sh -abv high -directory /some/path``
 - ``script.sh -a -b -vhigh --directory=/some/path``
You can find programs in the wild that use all of these syntaxes, or a mix thereof, and writing the code to parse every possible syntax is laborious.
Enter ``getopt``.
It's purpose is to take a series of flags and arguments, in any format shown above, and reformat them to a standard format that is easy for a bash script to interpret.
Notice that this is quite different from Python's ``argparse``, which handles interpretation *and* assignments.
With ``getopt``, the assignment (or ingestion) of the arguments is still left to the script.

So how do we actually use it?
A glance at the ``man`` page (``man 1 getopt``, not ``man 3 getopt`` - which is for the C function) reveals it to be a relatively simple interface.
We pass in an optstring (the possible options that ``getopt`` will scan for), plus a longopts string if we want, followed be the ``--`` demarker and finally the options that we want parsed.
The program will return a string where:
 - all single character flags are seperated (``-abc`` → ``-a -b -c``)
 - all single character flags that take an argument are space seperated (``-lWarn`` → ``-l Warn``)
 - all long options that take an argument are space seperated (``--directory=/some/path`` → ``--directory /some/path``)

It's worth noting that ``getopt`` does not handle single dash long options without specifying the ``--alternative`` flag.
In the example above, passing ``-directory /some/path`` without the flag will not be transformed into ``--directory /some/path``, but rather ``-d -i -r -e ...``.

``getopt`` will also inject a ``--`` demarker after all the parsed arguments, which is convetion in the command line to tell the program "Hey, you've seen all the flags now, everything after this point is not a flag so don't try and interpret it as such".
It is also interpretted by some programs as "start reading from stdin now please".
Either way, we can make good use of it as when we find ``--`` we know that there are no more flags that we need to parse.

## The Great eval/set/getopt Tangent

While ``getopt`` neatly formates our arguments, we still need to do the parsing.
The standard code block one finds online has a ``while true`` loop shifting through the arguments, inspecting them with ``case``.
Once the end is hit (as marked with ``--``), the loop breaks and the program continues.
But for this to work, we need to place the nicely formatted arguments from ``getopt`` back into the argument vector so they are accessible via ``$1`` etc.
And that is done with the following line.

``eval set -- "$(getopt ... )"``

Now I was greatly confused when I first read this.
I understood that ``set`` sets the argument vector but why the ``--`` here, and what's up with ``eval``, and are the quotes really necessary?

Let's start by looking at that ``--``.
We have already seen how it can be interpretted as a way to say "No more flags for you here".
Now looking at the help text, we see that ``set`` can take some flags, such as ``-B`` and ``-p``, and thus if we did not have the ``--`` then some of the nicely formatted flags destined for our script may get consumed and interpretted by ``set`` itself, never making it into the argument vector.
Not good.
Thus we insert a ``--`` before all of our arguments to ensure that they are not interpretted by ``set`` but make it into the argument vector for our program to parse.

The next two things that I found odd are all to do with bash variable expansion - that is how ``$(getopt ... )`` gets expanded into the actual text that ``set`` sees.
First let's take a look at those quotes, which are, as it would happen, very important.
You see, variable expansion in bash does more than just a find and replace - it will also perform globbing!
If you've ever used the wildcard character ``*`` on the commandline then you know what globbing is - it is the process of matching file names according to a pattern.

 - * matches any string
 - ? matches any single character
 - [a-z] matches any lowercase letter etc.

If the variable expansion is not quoted, then globbing will take place.
For example, in an empty directory, create three files - say foo.txt, bar.txt and baz.txt.
Next set a variable with ``TEST="wow * wildcard"``.
Running ``echo $TEST`` will expand the wildcard character and you will see the output ``wow bar.txt baz.txt foo.txt wildcard``.
Whereas running with quotes as in ``echo "$TEST"`` will yield the raw string ``wow * wildcard``.

You may think, as I did, that we want to run globbing.
If the user wants to pass in a bunch of filenames then they will likely glob.
However, bash will have already run globbing before the input was passed into our script, so any rogue wildcards or questionmarks are intentional and not to do with filename matching.
This is all to say, the quotes are important as they prevent certain characters from being consumed by globbing rather than our script.

However, if we try this with ``set``, we bump into some slightly annoying behavior.
Running ``set -- $(getopt --options abc: -- -a -c "wow * wildcard")`` we find that the wildcard gets globbed: ``echo "$@"`` yields ``-a -c 'wow bar.txt baz.txt foo.txt wildcard' --``.
What's more, those quotes aren't denoting a single argument.
If we  ``echo "$3"`` we see ``'wow``, not ``'wow bar.txt baz.txt foo.txt wildcard'``. 

Doing the same but with quotes surrounding the command expansion superficially fixes this problem.
``set -- "$(getopt --options abc: -- -a -c 'wild * card')"; echo "$@"`` yield ``-a -c 'wow * wildcard' --``.
But looking at ``echo $1`` reveals the issue: ``-a -c 'wow * wildcard' --``.
It's *all* in the first argument!
All of it!
This is not what we want at all.

It may seem that we are stuck between a rock and a hard place.
But fret not, we still have one more thing to look into, and it may just be the answer to all our problems.
But before we look at that, we must first take a moment to look at what bash is actually doing when we hit enter on our keyboards.
According to the [documentation](https://www.gnu.org/software/bash/manual/bash.html#Shell-Operation), bash goes through a seven step process:

1. Reads the input
2. Breaks it down into words and tokens, keeping quoted items together in a single token
3. Parses these tokens and words into simple and compound commands and does any alias replacements
4. Performs shell expansions with word splitting and globbing (which we looked at above) before finally removing any quote characters
5. Processes input and output redirections and removes the characters associated with that
6. Executes the command
7. Gathers the return status

For our purposes here, we only really care about steps 2 and 4.
Let's follow along with the process for the command ``set -- "$(getopt --options abc: -- -a -c 'wild * card')"``.
First we read the input, then split it up into tokens.
The first token is ``set``, the second is ``--`` and the third, due to the quotes is ``"$(getopt --options abc: -- -a -c 'wild * card')"``.
The next step determines that this is a simple command as we have no looping or branching.
Now we move onto step 4, which executes the ``getopt`` and replaces the third token with it's output.
This triggers the whole pipeline again: breaking the command down into tokens, aliasing, globbing (which does not find the asterisk as it is contained in the quotes surrounding ``wild * card``).
The output of ``getopt`` is substituted in so that the third token becomes ``"-a -c 'wild * card' --"``.
Again, globbing does not find the asterisk because it is in quotes.
It is at this point that any quotes and escape characters get removed, so the third token becomes ``-a -c 'wild * card' --``.
The inner quotes are left untouched as they were escaped by the outer quotes which have just been stripped.

So at the end of all this we end up with three tokens:

1. ``set``
2. ``--``
3. ``-a -c 'wild * card' --``

The ``set`` command sees this third token as a single string, so does what it is meant to do and simply sets a single place in the argument vector as this entire string.
If only there were a way to split that last string up into four tokens.
It's almost as if we need to pass it through the pipeline again.

Enter ``eval``, a tool to pass a string through the bash interpreter pipeline.
When it is executed, it will pass all of it's input through the pipeline as if the line had been passed directly to bash.
The trick is, we've already gone through once, and have already expanded the ``getopt`` command.
So, let's pretend we've just gone through the pipeline for the command ``eval set -- "$(getopt --options abc: -- -a -c 'wild * card')"``.
We have four tokens at the end, which are as follows:

1. ``eval``
2. ``set``
3. ``--``
4. ``-a -c 'wild * card' --``

Now bash executes ``eval``, which takes all of the remaining tokens, mashes them into a single string and reruns the pipeline.
So we start with ``set -- -a -c 'wild * card' --``, which gets split into tokens again.
But, as the quotes around the output of our previous ``getopt`` command have been stripped, the output is now handled as several tokens, not just one.
We end up with the tokens ``set`` ``--`` ``-a`` ``-c`` ``'wild * card`` ``--``.
When these are passed through step 4, the asterisk is not globbed as it is enclosed in quotes.
These quotes are then stripped off at the end of this step leaving us with the tokens:

1. ``set``
2. ``--``
3. ``-a``
4. ``-c``
5. ``wild * card``
6. ``--``

Which, when executed, sets the first item in the argument vector to ``-a``, the second to ``-c`` and the third to the single, unglobbed string ``wild * card``!
(and the fourth to ``--``... that will be important later).
Now, in practice, we would run a command like ``eval set -- "$(getopt --options abc: -- "$@")"`` and the ``"$@"`` would get expanded into ``-a -c "wild * card"`` during the processing of the ``getopt`` command.
We now know exactly what is going on with this command and can sleep easy at night knowing that it won't install viruses while we're not looking.
This concludes the Great ``eval``/``set``/``getopt`` Tangent!

## Security Implications of eval

... but what if it does install viruses while we're not looking.
Anything that passes a user controlled string into some execution environment should be a massive red flag.
It certainly set alarm bells off in my head, so let's tanget this tanget and take a little walk down security lane.

Bash has this handy way to pass several commands in a single line by seperating them with a semicolon.
Thus, ``echo 1;echo 2`` will print a ``1`` followed by a ``2``.
Let us pretend that a rather neffarious user got their hands on our hypotical program that we've been working with and instead of etering ``-a -c 'wild * card'``, they "mistyped" and "accidetly" entered ``-a -c 'wild * card;rm -rf /'``.
Easy mistake to make.
Let's follow through the pipeline and see where we get to.

The line get's split up into tokens and the fourth one gets expanded into ``"-a -c 'wild * card;rm -rf /' --"``.
Both the asterisk and the semicolon do not have any special meaning here due to the quotes, which are removed at the end of step 4.
Now, when ``eval`` comes to execute it forms the string ``set -- -a -c 'wild * card;rm -rf /' --``, which splits into tokens, keeping ``wild * card;rm -rf /`` as one.
Again, the asterisk and the semicolon do not have special meaning due to the quotes so we are safe!
``set`` goes on to execute and our pesky "typo" does not explode on us.

In order to have that bomb explode, we need ``eval`` to form a string with the ``rm -rf /`` outside of the quotes.
Having played around for a bit, this seems harder said than done (at least with the version of bash that I am useing - GNU bash, version 5.2.37(1)-release (x86\_64-pc-linux-gnu)).
If we strip out all the quotes and run (don't do this) ``eval set -- $(getopt --options abc: -- -a -c somthing ;rm -rf /)`` we find that the ``rm -rf /`` does execute but only becuase it is part of the ``$(...)`` substitution.
When bash runs throug the pipeline during the command subsitution it find the unquoted semicolon and says "I have two commands to run", ``getopt --options abc: -- -a -c something`` and ``rm -rf /``.
This won't happen in practice as we would be substituting this string in via ``$@`` which happens _after_ bash looks to split up on semicolons.
You can test this by setting some varialble with ``foo="echo 1;echo 2"`` then executing it with ``$foo``.
The text that get's printed is ``1;echo 2`` - the semicolon was treated as a literal semicolon, not a command delimiter.

## Finally Writing the Script

Now that we have (labouriously and with great effort) clambered over the hurdle of argument parsing, we can get on with writing the script.
The basic structure is something like this:

``for each file in the tree:
    if it is an .md file:
        pass it through pandoc
        copy the output to the same place in the new tree
    else:
        copy the file to the same place in the new tree
``

