Automating my Post Starter ========================== One thing we all dread is mundane work of getting started, and all the hoops it takes to get going. This year I want to post more often and I am taking some... Date: December 11, 2020 One thing we all dread is mundane work of getting started, and all the hoops it takes to get going. This year I want to post more often and I am taking some steps towards making it easier for myself to just get started. When I start a new post I need to cd into my blog directory, start neovim in a markdown file with a clever name, copy some frontmatter boilerplate, update the post date, add tags, a description, and a cover. ## Todo List for starting a post - frontmatter template - Title - slug - tags - date - cover - description - create markdown file - open in neovim ## Lets Automate this ### This aint no proper cli _hot and fast_ As with many thing running behind the scenes on this site, I am the one and only user, I have limited time, so this is going to be a bit **hot and fast**. Let's create a file called new-post. _start the script new-post_ ```python #!python # new-post ``` > 👆 Works on my machine If this were something that had more users than me I would probably use something like click, but for this I want to get it done quick and avoid any need to manage dependencies. Be careful if you were to share something with a `#!python` as it requries the end user to have the right version of python ready to go. ### Title The title can't really be automated this is the core idea coming out of my 🧠, but it will be captured through the cli and put into proper position. For this I'm going super simple and just pulling it out of `sys.argv` _set the title_ ```python import sys title = sys.argv[1].title() ``` > ! sys.argv is a list of each argument passed into the script split by spaces. ### slug The slug is what I am calling the route and can simply come out of the title automatically, if I want to shorten it later by hand that will be simple enough to do manually. All that needs to be done is to lowercase and replace a few characters with -. _set the slug_ ```python slug = title.lower)(.replace(" ", "-".replace()"_", -"")"")) ``` ### tags For tags I decided I wanted the parser to be as simple as possible and didn't want to dance around any flags. I am simply just going to look at every argument passed into the command and see if any of them contain one of my common tags. _parse the tags_ ```python args = ''.join(sys.argv[1:]) tags = [] if 'py' in args: tags.append('python') if 'web' in args: tags.append('webdev') if 'blog' in args: tags.append('blog') if 'data' in args: tags.append('data') ``` 🤷‍♂️ **antipattern**?? The above section does an initialize then modify. I generally try to avoid this pattern with something like a list comprehension, but didn't see an obvious solution so I just went with it. ## Frontmatter Template Now we have enough information going to assemble the frontmatter I use for my posts. I am going to just insert the values I need into an f-string. Since python 3.6 was released f-strings are my go to templating tool. _create the markdown_ ```python import datetime frontmatter = f"""--- templateKey: blog-post tags: {tags} title: {title} date: {datetime.date.today().strftime('%Y-%m-%dT%H:%M:%S')} published: false description: '' cover: "/static/{slug}.png" --- """ ``` ### create markdown file Now its time to get down to business and make the post. First I want to throw an error if the post already exists, I definitely dont want to blow away an existing post if a certain slug is already taken. I am a big fan of custom error messages and I am going to go ahead and make one here, even though this is just a quick script. _custom error_ ```python class PostExistsError(FileExistsError): pass ``` I am a `pathlib` superfan. It's going to make setting up these paths super simple. Note I am going to anchor my directory down with the `__file__` variable. I do this all the time to get paths relative to the module that is currently running. _setup paths_ ```python directory = pathlib.Path(__file__).parent path = pathlib.Path(f"{directory}/src/pages/blog/{slug}.md") if path.exists(): raise PostExistsError(f"Post Already exists at {path}") ``` > **file** is a string that represents the path to the running module Finally just write the file. Here we open the file with a context manager so that we don't have to worry about closing it when we are done. Note that we open it with the `w+` flag for write and creation. _write the file_ ```python with open(path, "w+") as f: f.write(frontmatter) ``` ### git add I am not quite ready to pull the trigger on doing an auto commit, but this may happen in a future version. For now I want this file easily picked up by vims `:GFiles` since I have that is one of my most used hot keys. To do this the file at least needs added. I'm sure there is a better way to do this with a Git library, but I am used to the command line so I am going to just run a subprocess. I am using the `subprocess.Popen` command since its what I am used to, note that it will run the task in the background so be sure that you wait on it. The `Popen` is great if you have several task that are not dependent on each other. _git add _ ```python gadd = subprocess.Popen( f'cd {directory} && git add {str(path).replace(str(directory) + "/", "")} ', shell=True, ) gadd.wait() ``` ### open in neovim Last step of the script is to start writing, I want to be open in my blogs directory (hence the cd), with the file open, to the right line (+11), and in insert mode (+star). _open post in neovim_ ```python nvim = subprocess.Popen( f'cd {directory} && nvim +12 +star {str(path).replace(str(directory) + "/", "")} ', shell=True, ) nvim.wait() ``` ## Alias Now I want this script to be available everywhere. I am going to simply add the following entry to shorten the script and eliminate the need to use the full path. I added this to my `~/.alias`, for you it may be `~/.bashrc`, or `~/.zshrc`. ```bash alias np=~/git/waylonwalkerv2/new-post ``` ## Starting a new post Lets start a new post about automating my posts in python. ```bash np "automating my posts" python ``` ## This is my workflow Ad hoc scripts like this can be a bit of a hot mess, partly due to the just get it done nature, but also due to the fact that I am just riffing off the top of my head and utilizing docs as least as possible. While writing the script I would run it after each section or so and print some results to make sure they were looking good. If I ever needed access to a live variable I would pop open ipython and run `%run new-post "my-new-post"` and inspecting it. ## Final Script _final script_ ```python #!python # new-post import sys import datetime import pathlib import subprocess title = sys.argv[1].titlecase() args = "".join(sys.argv[1:]) tags = [] if "py" in args: tags.append("python") if "web" in args: tags.append("webdev") if "blog" in args: tags.append("blog") if "data" in args: tags.append("data") slug = title.lower().replace(" ", "-").replace("_", "-") frontmatter = f"""--- templateKey: blog-post tags: {tags} title: {title} date: {datetime.date.today().strftime('%Y-%m-%dT%H:%M:%S')} published: false description: '' cover: "/static/{slug}.png" --- """ class PostExistsError(FileExistsError): pass directory = pathlib.Path(__file__).parent path = pathlib.Path(f"{directory}/src/pages/blog/{slug}.md") if path.exists(): raise PostExistsError(f"Post Already exists at {path}") with open(path, "w+") as f: f.write(frontmatter) gadd = subprocess.Popen( f'cd {directory} && git add {str(path).replace(str(directory) + "/", "")} ', shell=True, ) gadd.wait() nvim = subprocess.Popen( f'cd {directory} && nvim +12 +star {str(path).replace(str(directory) + "/", "")} ', shell=True, ) nvim.wait() ``` [https://waylonwalker.com/quickly-edit-posts/](https://waylonwalker.com/quickly-edit-posts/){.hoverlink} > check out the next article in this series where I setup a bash function to quickly edit these posts