<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">

  <title>semanticart - Jeffrey Chupp</title>
  <link href="https://blog.semanticart.com/"/>
  <updated>2024-01-02T00:00:00-05:00</updated>
  <author>
    <name>Jeffrey Chupp</name>
  </author>
  <id>https://blog.semanticart.com/</id>


  <entry>
    <title>fx</title>
    <link href="/2024/01/02/fx/"/>
    <id>https://blog.semanticart.com/2024/01/02/fx/</id>
    <updated>2024-01-02T00:00:00-05:00</updated>
    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
        <div class="yt-callout">
  <a href="https://youtu.be/I43vfzx7QUk" target="_blank">
    Visual learner? Watch this on YouTube!
    <picture>
  <source type="image/avif" srcset="/images/fx-thumbnail-small.avif" />
  <source type="image/webp" srcset="/images/fx-thumbnail-small.webp" />
  <img src="/images/fx-thumbnail-small.jpg" alt="YouTube thumbnail" />
</picture>
  </a>
</div><p><a href="https://fx.wtf/" title="">fx</a> is my favorite terminal-based JSON tool. Let&#39;s look at how it lets us interactively explore, query, filter, and edit JSON. To wrap things up, we&#39;ll compare it to jq, and I&#39;ll explain why I prefer fx.</p>
<p>To show off fx, we&#39;ll need some JSON. We&#39;ll start by fetching JSON of Harry Potter characters.</p>
<pre><code class="bash language-bash">curl https://hp-api.onrender.com/api/characters &gt; hp.json</code></pre>
<p>If we <code class="inline">cat hp.json</code>, we see that it is a single line of JSON (formatted for computers, not for human readability).</p>
<h2>Explore interactively</h2>
<p>Running fx with the JSON file&#39;s name will let you explore it interactively with human-friendly formatting.</p>
<pre><code class="bash language-bash">fx hp.json</code></pre>
<picture>
  <source type="image/avif" srcset="/images/fx-interactive.avif" />
  <source type="image/webp" srcset="/images/fx-interactive.webp" />
  <img src="/images/fx-interactive.png" alt="fx interactive view" />
</picture><p>You can navigate using up and down arrows or vim-style <code class="inline">j</code>/<code class="inline">k</code> navigation. You can search with <code class="inline">/</code> and go to previous/next matches with <code class="inline">n</code>/<code class="inline">N</code></p>
<p>Note that fx shows the current JSON path in the bottom left as you move around. Press <code class="inline">.</code> to enter &quot;dig&quot; mode to fuzzy-search for a path.</p>
<p>Pressing <code class="inline">y</code> lets you copy the value, path, or key under the cursor.</p>
<p><code class="inline">q</code>, <code class="inline">ctrl-c</code> or <code class="inline">esc</code> will exit fx.</p>
<p><code class="inline">fx --help</code> will show you how to collapse/expand nodes and the rest of the shortcuts.</p>
<h2>Querying and filtering</h2>
<p>fx querying will feel like the JavaScript map/filter/etc. you already know. fx also provides some optional <a href="https://fx.wtf/advanced-usage#syntactic-sugar" title="">syntactic-sugar</a>.</p>
<p>Here are some example commands with their non-sugary and sugary versions.</p>
<pre><code class="bash language-bash"># get a list of wand woods
fx hp.json &#39;.map(x =&gt; x.wand.wood)&#39;
fx hp.json .[].wand.wood
fx hp.json @.wand.wood

# wand woods without blank wood names
fx hp.json &#39;.map(x =&gt; x.wand.wood).filter(x =&gt; x)&#39;
fx hp.json .[].wand.wood &#39;.filter(x =&gt; x)&#39;
fx hp.json @.wand.wood &#39;.filter(x =&gt; x)&#39;

# living blonde wizard names
fx hp.json &#39;.filter(x =&gt; x.alive &amp;&amp; x.hairColour === &quot;blonde&quot; &amp;&amp; x.wizard).map(x =&gt; x.name).sort()&#39;
fx hp.json &#39;.filter(x =&gt; x.alive &amp;&amp; x.hairColour === &quot;blonde&quot; &amp;&amp; x.wizard)&#39; @.name sort</code></pre>
<h2>Built-ins and custom functions</h2>
<p><code class="inline">sort</code> is a <a href="https://fx.wtf/functions" title="">built-in function</a> (there&#39;s also <code class="inline">uniq</code> and more). fx also allows you to <a href="https://fx.wtf/advanced-usage#fxrc-js" title="">define your own functions</a> in <code class="inline">~/.fxrc.js</code> (or a local <code class="inline">.fxrc.js</code>).</p>
<p>Using <code class="inline">.filter(x =&gt; x)</code> to remove blanks is common enough that I&#39;ve written a custom <code class="inline">present</code> function in my <code class="inline">~/.fxrc.js</code></p>
<pre><code class="js language-js">// reject any falsy/blank values from an array
global.present = (arr) =&gt; arr.filter((x) =&gt; x);</code></pre>
<p>To use a custom function or built-in function, you add it as an argument to the fx command.</p>
<pre><code class="bash language-bash"># unique wand woods
fx hp.json @.wand.wood sort uniq present</code></pre>
<p>Because you can run any JavaScript in your custom functions, you can write functions to do things like filter out secrets or sensitive information.</p>
<p>Let&#39;s imagine we never want to see Voldemort&#39;s name, and we never want to leak our Stripe API key while recording a screencast. We can avoid both disasters by building a <code class="inline">confidential</code> function.</p>
<pre><code class="js language-js">// define secrets and their replacements
const secrets = {
  &quot;Lord Voldemort&quot;: &quot;He-Who-Must-Not-Be-Named&quot;,
  [process.env.STRIPE_API_KEY]: &quot;********&quot;,
};

// replace secrets with their replacements
global.confidential = (x) =&gt; {
  let stringVersion = JSON.stringify(x);

  Object.keys(secrets).forEach((secret) =&gt; {
    stringVersion = stringVersion.replaceAll(secret, secrets[secret]);
  });

  return JSON.parse(stringVersion);
};</code></pre>
<p>Here&#39;s some example usages:</p>
<pre><code class="bash language-bash"># without confidential
fx hp.json &#39;.filter(x =&gt; x.house === &quot;Slytherin&quot;)&#39; &#39;.[3].name&#39;
Lord Voldemort

# with confidential
fx hp.json &#39;.filter(x =&gt; x.house === &quot;Slytherin&quot;)&#39; confidential
He-Who-Must-Not-Be-Named

# Set a fake stripe API to test marking those as confidential
export STRIPE_API_KEY=&quot;Potter&quot;

# without confidential
fx hp.json .[0].name
Harry Potter

# with confidential
fx hp.json .[0].name confidential
Harry ********</code></pre>
<h2>Modifying JSON</h2>
<p>You use <code class="inline">map</code> to modify JSON.</p>
<p>e.g., here&#39;s how we&#39;d replace blank strings for <code class="inline">wand.wood</code> with nulls.</p>
<pre><code class="cmd language-cmd">fx hp.json &#39;.map(c =&gt; { c.wand.wood = c.wand.wood || null; return c })&#39; &gt; modified.json</code></pre>
<h2>fx versus jq</h2>
<p><a href="https://jqlang.github.io/jq/" title="">jq</a> is a great tool. If you already know it, you&#39;ll probably be happy continuing to use it (though you can benefit from the interactive parts of fx).</p>
<p>However, learning jq is daunting because it is a DSL. I&#39;ve found I can read a jq command reasonably easily but struggle to write one from scratch because I haven&#39;t internalized their DSL yet. I could learn jq&#39;s DSL, but I haven&#39;t made it a priority yet.</p>
<p>While jq is a DSL you have to learn, fx is just using JavaScript with some completely optional syntactic sugar.</p>
<p>Compare the following examples:</p>
<pre><code class="bash language-bash">jq &#39;[.[] | select(.hairColour == &quot;blonde&quot; and .alive == true and .wizard == true) | .name ] | sort&#39; hp.json
fx hp.json &#39;.filter(x =&gt; x.alive &amp;&amp; x.hairColour === &quot;blonde&quot; &amp;&amp; x.wizard).map(x =&gt; x.name).sort()&#39;</code></pre>
<p>Here&#39;s that same fx command with the sugar.</p>
<pre><code class="bash language-bash">fx hp.json &#39;.filter(x =&gt; x.alive &amp;&amp; x.hairColour === &quot;blonde&quot; &amp;&amp; x.wizard).map(x =&gt; x.name).sort()&#39;
fx hp.json &#39;.filter(x =&gt; x.alive &amp;&amp; x.hairColour === &quot;blonde&quot; &amp;&amp; x.wizard)&#39; @.name sort</code></pre>
<p>While the syntactic sugar lets you be more succinct, you don&#39;t have to learn it to use fx successfully.</p>
<hr class="thin"/>
<p>fx is a power tool with the ergonomics I&#39;m already familiar with. The ability to write custom functions is super compelling. You should give it a try.</p>

      </div>
    </content>
  </entry>

  <entry>
    <title>2023 Wrap Up</title>
    <link href="/2023/12/21/2023-wrap-up/"/>
    <id>https://blog.semanticart.com/2023/12/21/2023-wrap-up/</id>
    <updated>2023-12-21T00:00:00-05:00</updated>
    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
        <p>I haven&#39;t posted here all year, so I figured I could at least aggregate some links to other things I&#39;ve done this year.</p>
<p>2023 started with me going all-in with some cofounders on <a href="https://prefab.cloud" title="">Prefab</a>. I&#39;m super enjoying building <a href="https://prefab.cloud/features/developer-tools/">developer tools</a>. I&#39;ve been able to go deep into the Language Server Protocol, CLIs, and VS Code/Neovim tooling to help build <em>the best</em> feature flags, live config, and dynamic log levels for developers. You should try it out -- it&#39;d mean a lot to me 🫶</p>
<p>Here are some blog posts I wrote for the Prefab blog.</p>
<ul>
<li><a href="https://prefab.cloud/blog/ruby-logging-time-travel/">Time-traveling Ruby Logger</a>
</li>
<li><a href="https://prefab.cloud/blog/lsp-language-server-from-zero-to-completion/">Hands-On LSP Tutorial: Building a Custom Auto-Complete</a>
</li>
<li><a href="https://prefab.cloud/blog/lsp-language-server-configuration-example/">Hands-On LSP Tutorial: Configuration</a>
</li>
<li><a href="https://prefab.cloud/blog/writing-a-language-server-in-bash/">LSP: Writing a Language Server in Bash</a>
</li>
<li><a href="https://prefab.cloud/blog/save-money-on-browser-logging/">Making Front End Logging Useful</a>
</li>
<li><a href="https://prefab.cloud/blog/slow-docker-ruby-gems-solved/">Gem install sassc is always going to be slow. Your Docker builds don&#39;t have to be.</a>
</li>
</ul>
<p>I started a <a href="https://www.youtube.com/channel/UCLRwNm6-Xltyykt_gBujlvg">youtube channel</a>. There is only one video so far, but I plan to hit it hard in 2024. Like and subscribe and all that.</p>
<p>I&#39;ve also started an <a href="https://newsletter.semanticart.com">LSP Newsletter</a> if you&#39;d like to join me on my journey into developer tools.</p>

      </div>
    </content>
  </entry>

  <entry>
    <title>Layers</title>
    <link href="/2022/04/01/layers/"/>
    <id>https://blog.semanticart.com/2022/04/01/layers/</id>
    <updated>2022-04-01T00:00:00-05:00</updated>
    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
        <h2>The problem</h2>
<p>Pressing (not holding) ⌘-tab is useful for swapping between apps. If you ⌘-tab from your editor to your browser, you can then ⌘-tab back to your editor. You can jump back and forth between your editor and browser with just a quick ⌘-tab keystroke. It is a great workflow as long as you&#39;re using exactly two apps.</p>
<p>Then you get a Slack message and after giving Slack focus, ⌘-tab takes you back to your browser, the next ⌘-tab sends you back to Slack.</p>
<p>Sigh. OK, so we hold ⌘ and push tab until we&#39;re back to our editor and then hold ⌘ and push tab until we&#39;re back to our browser and now we can hit ⌘-tab to quickly jump between the two again. Order is restored.</p>
<p>This is mostly a minor nuisance except that this can happen dozens of times throughout the day. Little frictions add up and break flow.</p>
<h2>The journey</h2>
<p>I tried using <a href="https://support.apple.com/guide/mac-help/work-in-multiple-spaces-mh14112/mac" title="">Spaces</a> a few times before but the animation was honestly slightly nauseating. If you turn on <a href="https://support.apple.com/guide/mac-help/reduce-screen-motion-mchlc03f57a1/mac" title="">reduce motion</a> then Spaces changes from a dizzying sliding animation to a more subtle fade animation. The fade animation is pretty tolerable and you can drag whatever applications you want to the proper Spaces and jump around with the keyboard. Success!</p>
<p>Except that somehow the reduce motion setting stopped working for me after a reboot -- it is still checked, it just doesn&#39;t do anything and I&#39;m back to the abrasive sliding animation.</p>
<p>Gross. I started looking at other options. <a href="https://github.com/koekeishiya/yabai" title="">yabai</a> looks powerful but requires you to disable System Integrity Protection (permanently, not just during installation) and that&#39;s a non-starter for me.</p>
<h2>The solution</h2>
<p>Taking a step back, what am I really trying to accomplish? I want to be able to press a key and jump to an app (or group of apps). Ideally I don&#39;t have to wrangle app the apps into the right workspace like I did with Spaces after every reboot.</p>
<p>Fine. I&#39;ll write some AppleScript. After lots of Googling and some optimization, I ended up with a script I&#39;m calling <a href="https://github.com/semanticart/layers" title="">Layers</a>.</p>
<p>How does it work? Create a file in your home directory named <code class="inline">.layers</code>. Each line of the file should include a comma-separated list of applications in that layer. Running the <code class="inline">layers</code> script will generate <a href="https://blog.semanticart.com/2020/03/07/interpreted-applescript-startup-time/" title="">compiled AppleScript</a>s to focus applications and spit out a command to bind to a keyboard shortcut.</p>
<p>My <code class="inline">.layers</code> file looks like this:</p>
<pre><code class=" language-">Alacritty
Firefox, Google Chrome, Safari
Slack
Messages, Music, Stream Deck
zoom.us</code></pre>
<p>The <code class="inline">layers</code> output is</p>
<pre><code class=" language-">Add keyboard shortcuts for the following:
ls /Users/ship/.layers_scripts/layer_0*_compiled | xargs -L 1 -P 8 osascript
ls /Users/ship/.layers_scripts/layer_1*_compiled | xargs -L 1 -P 8 osascript
ls /Users/ship/.layers_scripts/layer_2*_compiled | xargs -L 1 -P 8 osascript
ls /Users/ship/.layers_scripts/layer_3*_compiled | xargs -L 1 -P 8 osascript
ls /Users/ship/.layers_scripts/layer_4*_compiled | xargs -L 1 -P 8 osascript</code></pre>
<p>I assign each line starting with <code class="inline">ls</code> to a shortcut via an <a href="https://www.alfredapp.com/" title="">Alfred</a> workflow but you can use any tool that lets you map bash commands to keyboard shortcuts. I&#39;m using ctrl-1 through ctrl-5 since those worked well enough for me with Spaces.</p>
<p>The <code class="inline">ls ...</code> command finds all the compiled scripts for a layer and passes them to <code class="inline">xargs</code> to run in parallel (up to 8 at a time).</p>
<p>The compilation + parallelization felt like overengineering at first, but it took little time to implement and has a real impact on responsiveness.</p>
<p>Pressing ctrl-2 will bring Firefox, Google Chrome, and Safari to the forefront of my screen. It skips any apps that aren&#39;t running. I can press ctrl-1 to get back to Alacritty where my editor is running.</p>
<p>This is better for me than ⌘-tab because it is predictable -- the positions of things never change. This is better for me than Spaces because there&#39;s no distracting animations.</p>
<p>There&#39;s one downside I can think of when compared to Spaces -- you can&#39;t have different windows of an Application mapped to different Layers. I didn&#39;t use this functionality in Spaces so I don&#39;t miss it.</p>
<p>Why &quot;Layers&quot;? Spaces are effectively distinct virtual screens. Layers don&#39;t have this same isolation and apps overlap. This overlap allows for composability and use-cases not allowed by space.</p>
<p>I&#39;ve been using it a few days now and it feels fantastic. Take it for a spin and let me know how it works for you.</p>

      </div>
    </content>
  </entry>

  <entry>
    <title>Ruby Custom Code Actions Plugin</title>
    <link href="/2022/01/30/ruby-custom-code-actions-plugin/"/>
    <id>https://blog.semanticart.com/2022/01/30/ruby-custom-code-actions-plugin/</id>
    <updated>2021-12-31T00:00:00-05:00</updated>
    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
        <p>I&#39;ve packaged up the code action from the <a href="https://blog.semanticart.com/2021/12/31/null-ls-nvim-custom-code-actions/">last post</a> into a neovim plugin: <a href="https://github.com/semanticart/ruby-code-actions.nvim">ruby-code-actions.nvim</a>. Since publishing the plugin, I&#39;ve added new code actions for correcting the current line, selected lines, or entire file with RuboCop.</p>
<p>I wrote it using <a href="https://github.com/nvim-lua/plenary.nvim">plenary</a> for unit testing and found the experience lovely. It has good mocking support for <code class="inline">vim.api</code> and your custom code. I couldn&#39;t out the correct way to mock things like <code class="inline">vim.cmd</code> and such but I worked around this by using <code class="inline">vim.api</code> equivalents or wrapping calls and mocking the wrappers. I can&#39;t say enough nice things about plenary or <a href="https://github.com/tjdevries">TJ DeVries</a>&#39; work on <a href="https://github.com/nvim-telescope/telescope.nvim">telescope</a> and neovim itself.</p>
<p>In the end, I have a <a href="https://github.com/semanticart/ruby-code-actions.nvim/blob/main/lua/spec/ruby-code-actions/module_spec.lua">spec file</a> I can run via <code class="inline">make</code> and the development process feels more like the comfortable TDD I&#39;m used to.</p>
<picture>
  <source type="image/avif" srcset="/images/plenary-make-results.avif" />
  <source type="image/webp" srcset="/images/plenary-make-results.webp" />
  <img src="/images/plenary-make-results.png" alt="spec results" />
</picture>
      </div>
    </content>
  </entry>

  <entry>
    <title>null-ls.nvim custom code actions</title>
    <link href="/2021/12/31/null-ls-nvim-custom-code-actions/"/>
    <id>https://blog.semanticart.com/2021/12/31/null-ls-nvim-custom-code-actions/</id>
    <updated>2021-12-31T00:00:00-05:00</updated>
    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
        <p>NOTE: since this was published, I&#39;ve released a plugin with this code action and more: <a href="https://github.com/semanticart/ruby-code-actions.nvim">ruby-code-actions.nvim</a>.</p>
<p><a href="https://microsoft.github.io/language-server-protocol/" title="">Language servers</a> are a great tool for empowering your editor, but your language server might not support every feature you want. So what can you do? Bring your own functionality!</p>
<h2>What are my options for custom LSP features?</h2>
<p>So far, I&#39;ve looked into efm-langserver and null-ls.nvim.</p>
<p><a href="https://github.com/mattn/efm-langserver" title="">efm-langserver</a> is an editor-agnostic language server meant for helping you wire up external commands as diagnostics, code actions, etc. It looks promising but I&#39;ve had trouble getting it working with neovim. YMMV.</p>
<p>Fortunately, for neovim we have <a href="https://github.com/jose-elias-alvarez/null-ls.nvim" title="">null-ls.nvim</a>. It allows you to &quot;Use Neovim as a language server to inject LSP diagnostics, code actions, and more via Lua.&quot;</p>
<p>You might worry that writing custom actions in Lua means you&#39;re building something less portable than what you&#39;d get with a script wired up to efm-langserver. Maybe. But you could always throw your Lua code in a shell script and call that from null-ls or from efm-langserver. The benefit of null-ls running Lua directly in neovim is that rather than communicating by shelling out processes, you&#39;re keeping everything in-process.</p>
<h2>My null-ls setup</h2>
<p>Here&#39;s my current <a href="https://github.com/wbthomason/packer.nvim" title="">packer.nvim</a> config for null-ls:</p>
<pre><code class="lua language-lua">use {
    &#39;jose-elias-alvarez/null-ls.nvim&#39;,
    requires = {{&#39;neovim/nvim-lspconfig&#39;}, {&#39;nvim-lua/plenary.nvim&#39;}},
    config = function()
        local null_ls = require(&quot;null-ls&quot;)
        local sources = {
            null_ls.builtins.code_actions.gitsigns,
            null_ls.builtins.formatting.prettier,
            null_ls.builtins.formatting.mix,
            null_ls.builtins.formatting.rubocop,
            null_ls.builtins.diagnostics.rubocop,
            null_ls.builtins.diagnostics.shellcheck
        }
        require(&quot;custom_code_actions&quot;)
        null_ls.setup({sources = sources, debug = true})
    end
}</code></pre>
<p>You&#39;ll note that null-ls provides some nice builtins for formatting, diagnostics, and code actions. Unfortunately there&#39;s no builtins for ruby code actions. </p>
<p>I keep my custom code actions in <code class="inline">custom_code_actions.lua</code> and register them with the <code class="inline">require(&quot;custom_code_actions&quot;)</code> line.</p>
<p>Let&#39;s write a simple code action to see how it works. We&#39;ll write a code action to insert the <a href="https://docs.ruby-lang.org/en/3.0.0/doc/syntax/comments_rdoc.html#label-frozen_string_literal+Directive" title=""><code class="inline">frozen_string_literal</code> directive</a> in our ruby file if it isn&#39;t already present.</p>
<h2>How do we define a custom action?</h2>
<p>Let&#39;s look at the skeleton of a code action:</p>
<pre><code class="lua language-lua">local null_ls = require(&quot;null-ls&quot;)

local frozen_string_actions = {
    method = null_ls.methods.CODE_ACTION,
    filetypes = {&quot;ruby&quot;},
    generator = {
        fn = function(context)
          -- ...
        end
    }
}
null_ls.register(frozen_string_actions)</code></pre>
<p>What&#39;s going on here?</p>
<ul>
<li>We require null_ls to reference.
</li>
<li>We define a table named <code class="inline">frozen_string_actions</code>.
</li>
<li>It has a <code class="inline">method</code> key to specify that we&#39;re defining a code action.
</li>
<li>It has a <code class="inline">filetypes</code> array that specifies which file types we want this to apply to.
</li>
<li>It has a <code class="inline">generator</code> function that takes a param (here named <code class="inline">context</code>) describing the current context of the file in your editor (current selection, etc).
</li>
<li>Finally, we register our code action with null-ls
</li>
</ul>
<h2>The generator function</h2>
<p>The generator function is invoked when the editor is considering which code actions to present to the user. The function should consider the <code class="inline">context</code> of the current file and return a table of relevant actions. It could return nothing if no actions are relevant.</p>
<h2>Context params</h2>
<p>Let&#39;s see what the <code class="inline">context</code> argument passed to our generator contains.</p>
<p>We create <code class="inline">test.rb</code> with the content</p>
<pre><code class="ruby language-ruby">puts &quot;Hello world&quot;</code></pre>
<p>If we change the body of our generator body to be <code class="inline">print(vim.inspect(context))</code>, open our <code class="inline">test.rb</code> in neovim, and request available code actions (I&#39;m using <code class="inline">:Telescope lsp_code_actions</code>), our generator will be invoked, and we&#39;ll see the table passed in as <code class="inline">context</code>:</p>
<pre><code class=" language-">{
  bufname = &quot;/private/tmp/out.rb&quot;,
  bufnr = 1,
  client_id = 1,
  col = 16,
  content = { &#39;puts &quot;Hello world&quot;&#39;, &quot;&quot; },
  ft = &quot;ruby&quot;,
  lsp_method = &quot;textDocument/codeAction&quot;,
  method = &quot;NULL_LS_CODE_ACTION&quot;,
  range = {
    col = 17,
    end_col = 17,
    end_row = 1,
    row = 1
  },
  row = 1
}</code></pre>
<p>For our needs, the important part is <code class="inline">content</code>. If the first line of the file, <code class="inline">context.content[1]</code>, is something other than the frozen_string_literal directive, we should return an action to allow the user to insert the directive. If it does match the directive, our action isn&#39;t applicable so we return nothing.</p>
<h2>The implementation</h2>
<p>Here&#39;s an implementation of the generator function to conditionally insert the directive:</p>
<pre><code class="lua language-lua">fn = function(context)
    frozen_string_literal_comment = &quot;# frozen_string_literal: true&quot;

    first_line = context.content[1]

    if first_line ~= frozen_string_literal_comment then
        return {
            {
                title = &quot;🥶Add frozen string literal comment&quot;,
                action = function()
                    lines = {frozen_string_literal_comment, &quot;&quot;, first_line}

                    vim.api
                        .nvim_buf_set_lines(context.bufnr, 0, 1, false, lines)
                end
            }
        }
    end
end</code></pre>
<p>Walking through, we see that:</p>
<ol>
<li>We declare a variable with the directive content
</li>
<li>We get the first line of the file
</li>
<li>We check to see if they differ
</li>
<li>If they do, we return a table with a <code class="inline">title</code> that shows up as a prompt for the user and an <code class="inline">action</code> that is executed if the user selects our code action.
</li>
</ol>
<p>Here&#39;s what the <code class="inline">title</code> looks like in <code class="inline">:Telescope lsp_code_actions</code>:</p>
<picture>
  <source type="image/avif" srcset="/images/frozen_string_literal_code_action.avif" />
  <source type="image/webp" srcset="/images/frozen_string_literal_code_action.webp" />
  <img src="/images/frozen_string_literal_code_action.png" alt="our title in Telescope lsp_code_actions" />
</picture><p>After selecting our code action, we see our file has been updated with the directive:</p>
<picture>
  <source type="image/avif" srcset="/images/frozen_string_literal_code_action_after.avif" />
  <source type="image/webp" srcset="/images/frozen_string_literal_code_action_after.webp" />
  <img src="/images/frozen_string_literal_code_action_after.png" alt="the result of our code action" />
</picture><p>There&#39;s not much more to say here that you can&#39;t learn with <code class="inline">:help</code> (e.g. <code class="inline">:help nvim_buf_set_lines</code>).</p>
<p>Here&#39;s a <a href="https://gist.github.com/semanticart/7f2f838b41521a6d20ca50eda6458969" title="">gist with the entire implementation</a>.</p>
<h2>What&#39;s next?</h2>
<p>Since we determine in our generator if the code action should apply, we might also want to consider the user&#39;s selection. If they&#39;ve selected multiple lines of text, they&#39;re <em>probably</em> not intending to invoke a code action to add the frozen_string_literal directive. We could return nothing if we saw that the <code class="inline">context.range.row</code> differs from the <code class="inline">context.range.end_row</code>.</p>
<p>In a standalone ruby script, the frozen_string_literal directive <a href="magic">can also appear on the second line</a>. We could update our generator to insert the directive on the second line if there&#39;s a shebang line on line 1. We could return no action if the directive is on either the first or second line.</p>
<p>Have fun!</p>

      </div>
    </content>
  </entry>

  <entry>
    <title>Improving AppleScript Startup Time with Compilation</title>
    <link href="/2020/03/07/interpreted-applescript-startup-time/"/>
    <id>https://blog.semanticart.com/2020/03/07/interpreted-applescript-startup-time/</id>
    <updated>2020-03-07T00:00:00-05:00</updated>
    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
        <p><a href="https://developer.apple.com/library/archive/documentation/AppleScript/Conceptual/AppleScriptLangGuide/introduction/ASLR_intro.html">Applescript</a> is useful but interpreted scripts are fairly slow to start.</p>
<p>Consider a script that only has the number <code class="inline">1</code> in it. </p>
<pre><code class=" language-">$ echo &quot;1&quot; &gt; example.scpt</code></pre>
<p>When run with <code class="inline">osascript example.scpt</code>, this will output <code class="inline">1</code>.</p>
<p>A timed run of the script shows it takes roughly 430 milliseconds on my machine.</p>
<pre><code class=" language-">$ time osascript example.scpt
1

real    0m0.429s
user    0m0.053s
sys     0m0.053s</code></pre>
<p>We can&#39;t have a much simpler script than this, so it looks like the price of using interpreted Applescript is a minimum of ~430ms. If you have a script that runs frequently, this might be too slow.</p>
<p>You might not be aware that you can compile AppleScript. Not surprisingly, this compilation improves the startup time.</p>
<pre><code class=" language-">$ osacompile -x -o compiled-example example.scpt
$ time osascript compiled-example
1

real    0m0.252s
user    0m0.034s
sys     0m0.036s</code></pre>
<p>We&#39;re down from over 400ms to 250ms. That&#39;s still not ideal to run in a tight loop, but the savings might be compelling depending on your usage.</p>

      </div>
    </content>
  </entry>

  <entry>
    <title>Tracking down where a property changes in JavaScript</title>
    <link href="/2020/02/20/tracking-down-where-a-property-changes-in-javascript/"/>
    <id>https://blog.semanticart.com/2020/02/20/tracking-down-where-a-property-changes-in-javascript/</id>
    <updated>2020-02-22T00:00:00-05:00</updated>
    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
        <p>Without strict enforcement of boundaries, it can be a mystery where changes happen in the state in your JavaScript application (this is a reason tools like Redux are compelling). I was recently investigating a bug in an app where the source of a state change was a mystery. The app keeps track of the <code class="inline">count</code> of videos uploaded. Deleting a video could sometimes cause the <code class="inline">count</code> to decrement <em>twice</em> when it should only decrement once. I would be able to find this bug quickly if I could get a stack trace every time the state was modified.</p>
<p>This isn&#39;t a new problem. You may remember the promise of <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/watch"><code class="inline">obj.watch</code></a> and <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/observe"><code class="inline">Object.observe()</code></a> before they were deprecated.</p>
<p>Let&#39;s talk about some current options.</p>
<h2>Proxy</h2>
<p><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy">Proxy</a> allows you to create a replacement for your object that wraps the original object. You can define your own <code class="inline">set</code> &quot;trap&quot; to log the stack trace (or break into a debugger, etc.).</p>
<p>example:</p>
<pre><code class="javascript language-javascript">let proxy = new Proxy(objectYouWantToObserve, {
  set: function(obj, prop, value) {
    if (prop === &#39;thePropertyWeWantToObserve&#39;) {
      console.trace(`set: ${prop} -&gt; ${value}`)
    }

    // The default behavior to store the value
    obj[prop] = value;

    // Indicate success
    return true;
  }

  // you can optionally define an additional trap for `get` if you&#39;re also
  // interested in tracking reads for the property
});</code></pre>
<p>You would then need to use your <code class="inline">proxy</code> instance wherever you would have used <code class="inline">objectYouWantToObserve</code>.</p>
<p>The downside of this approach is that, depending on your app design, it isn&#39;t always convenient to replace references to the original object.</p>
<h2>breakOn</h2>
<p>Paul Irish&#39;s library <a href="https://github.com/paulirish/break-on-access">break-on-access</a> exposes a <code class="inline">breakOn</code> function that will happily invoke the debugger whenever a property is modified (or, optionally, accessed for read).</p>
<p>example:</p>
<pre><code class="javascript language-javascript">breakOn(objectYouWantToObserve, &quot;thePropertyWeWantToObserve&quot;)</code></pre>
<p>The upside of this approach is that it requires no additional modification to your code. Any changes to <code class="inline">thePropertyWeWantToObserve</code> will invoke the debugger.</p>
<p>I keep <code class="inline">break-on-access</code> in my Chrome snippets and reach for it first when I need to track down a change in and object&#39;s top-level property.</p>
<h2>What if I need to observe changes in dynamically nested objects?</h2>
<p>If you need to observe properties changing in nested objects, <code class="inline">break-on-access</code> won&#39;t help you. With some effort, you can use nested <code class="inline">Proxy</code>s by returning a new <code class="inline">Proxy</code> from the <code class="inline">Proxy</code>&#39;s <code class="inline">set</code> trap. I&#39;d also consider something like <a href="https://github.com/ElliotNB/observable-slim">Observable Slim</a> since it allows observing even deeply-nested changes to an object and its usage is similar to that of <code class="inline">Proxy</code>.</p>

      </div>
    </content>
  </entry>

  <entry>
    <title>CI Results in Your tmux Status Line</title>
    <link href="/2020/02/13/ci-results-in-your-tmux-status-line/"/>
    <id>https://blog.semanticart.com/2020/02/13/ci-results-in-your-tmux-status-line/</id>
    <updated>2020-02-13T00:00:00-05:00</updated>
    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
        <p>I use a custom status line in <a href="https://github.com/tmux/tmux/wiki" title="">tmux</a> to show the CI test results of my current branch. This post will talk a little about tmux status lines, <a href="https://hub.github.com/" title=""><code class="inline">hub</code></a>&#39;s <code class="inline">ci-status</code> command, and my <code class="inline">ci-status-indicator</code> scripts.</p>
<p>Example status line:</p>
<picture>
  <source type="image/avif" srcset="/images/tmux-colored-status-full.avif" />
  <source type="image/webp" srcset="/images/tmux-colored-status-full.webp" />
  <img src="/images/tmux-colored-status-full.png" alt="full tmux status preview" />
</picture><h2>Tmux status lines</h2>
<p>The tmux status line lets you show information about the current tmux session. You can also use the <code class="inline">#()</code> syntax to pull in content from external programs. For example, you might want to <a href="http://www.tylerewing.co/tmux-now-playing" title="">show the currently playing track from Spotify</a>.</p>
<p>I recommend <a href="https://leanpub.com/the-tao-of-tmux/read#status-bar" title="">The Tao of tmux</a> for a further introduction to the tmux status line (also known as the &quot;status bar&quot;).</p>
<p>There are four ways the status line is updated:</p>
<ol>
<li>Automatically every 15 seconds (this frequency can be changed with <code class="inline">status-interval</code> in your tmux configuration).
</li>
<li>When you move between panes or windows.
</li>
<li>After a command finishes in a tmux-owned shell pane.
</li>
<li>On-demand when you invoke <code class="inline">refresh-client</code> (Note: passing <code class="inline">-S</code> to <code class="inline">refresh-client</code> will update <em>only</em> the status line).
</li>
</ol>
<details>
<summary>Tmux appears to have an internal throttle that limits updating the status line to once per second (this throttle is ignored by `refresh-client`).</summary>

To test this, I set my <code class="inline">status-right</code> in my tmux conf to be <code class="inline">set -g status-right '#(date >> /tmp/debug.txt)'</code>. This appends the current time to a temporary file whenever the status line is updated.

Moving from one tmux window to another slowly, you can see that you get a new line written to the debug file for each window change.

I then ran <code class="inline">ruby -e '100.times { system("tmux next-window") }'</code> to cycle through tmux windows 100 times. Only one line was written to the debug file per second.
</details><p><a href="https://github.com/tmux/tmux/blob/486ce9b09855ae30a2bf5e576cb6f7ad37792699/regress/format-strings.sh#L128-L133" title="">The status line runs <code class="inline">#()</code> commands asynchronously</a>, but because your status line can potentially be updated every second, you should still keep the contents speedy. Avoid content that makes network calls or is cpu intensive. Where network calls or expensive computation are unavoidable, consider throttling or caching.</p>
<h2>hub ci-status</h2>
<p><a href="https://hub.github.com/" title="">hub</a> lets you &quot;do everyday GitHub tasks without leaving the terminal.&quot;</p>
<p>Running <code class="inline">hub ci-status</code> with no options will return <code class="inline">pending</code>, <code class="inline">failure</code>, <code class="inline">success</code>, etc. to indicate the status of the GitHub checks associated with your branch.</p>
<p>Running <code class="inline">hub ci-status -v</code> will show each current check with the associated status.</p>
<pre><code class=" language-">✔︎       unit-tests              https://url-to-see-unit-test-details/
●       e2e-tests               https://url-to-see-e2e-test-details/</code></pre>
<p>You can see additional options with <code class="inline">hub ci-status --help</code>.</p>
<h2>ci-status-indicator</h2>
<p>Showing the current GitHub checks results in the tmux status line requires a little massaging. The tmux status line doesn&#39;t support ANSI colors. If it did, we could reach for <code class="inline">hub ci-status -f &quot;%sC%t &quot;</code> which prints out the names of each check color-coded depending on state (failure = red, pending = yellow, success = green).</p>
<p>We could do some clever work to translate the ANSI color output of <code class="inline">hub</code> to tmux colors, but, since the number of colors here is limited, I&#39;m going to use a lazy <code class="inline">sed</code> command.</p>
<p><code class="inline">hub ci-status -f &quot;%S, %t &quot; | sed &#39;s/failure, /#[fg=red]/g; s/success, /#[fg=green]/g; s/pending, /#[fg=yellow]/g&#39;</code></p>
<picture>
  <source type="image/avif" srcset="/images/tmux-colored-status-wordy.avif" />
  <source type="image/webp" srcset="/images/tmux-colored-status-wordy.webp" />
  <img src="/images/tmux-colored-status-wordy.png" alt="initial colored status output" />
</picture><p>That gets us close, but we can do better. The <code class="inline">-tests</code> part of the GitHub check name isn&#39;t adding any value, so we can remove it. Finally, I&#39;ll use a white <code class="inline">|</code> to delimit the checks rather than a space.</p>
<p>Since this command is getting long, let&#39;s extract it to a script.</p>
<pre><code class="bash language-bash">#!/usr/bin/env bash

hub ci-status -f &quot;%S, %t|&quot; | \
  gsed -r &#39;
    s/failure, /#[fg=red]/g;
    s/success, /#[fg=green]/g;
    s/pending, /#[fg=yellow]/g;
    s/\|$//;
    s/\|/#[fg=white]|/g;
    s/-tests//g;&#39;</code></pre>
<p>This outputs <code class="inline">#[fg=yellow]e2e#[fg=white]|#[fg=green]unit</code> which looks like this:</p>
<picture>
  <source type="image/avif" srcset="/images/tmux-colored-status-final.avif" />
  <source type="image/webp" srcset="/images/tmux-colored-status-final.webp" />
  <img src="/images/tmux-colored-status-final.png" alt="final colored status output" />
</picture><p>Much better.</p>
<p>I&#39;m using <code class="inline">gsed</code> here rather than OSX&#39;s built-in <code class="inline">sed</code>. This isn&#39;t required, but I find <code class="inline">gsed</code>&#39;s extended regular expressions more useful than the built-in as these regular expressions grow. <code class="inline">brew install gnu-sed</code> and you&#39;re good to go.</p>
<p>You&#39;ll likely want to customize this script to make the output more succinct as we did by removing <code class="inline">-tests</code>. Perhaps replacing <code class="inline">codecov</code> with <code class="inline">cc</code> or similar could help you conserve real estate.</p>
<p>For customization&#39;s sake, since different projects can have different checks, I like to have a copy of this script local to each project. I name it <code class="inline">.ci-status-indicator</code>, <code class="inline">chmod +x</code> it, and add <code class="inline">.ci-status-indicator</code> to my <a href="https://help.github.com/en/github/using-git/ignoring-files#create-a-global-gitignore" title="">global gitignore</a>. The final tmux configuration line looks like this:</p>
<pre><code class=" language-">set -g status-right &#39; #(cd #{pane_current_path} &amp;&amp; [ -f &quot;.ci-status-indicator&quot; ] &amp;&amp; ./.ci-status-indicator) #[fg=white]%Y-%m-%d %H:%M&#39;</code></pre>
<p>This runs <code class="inline">.ci-status-indicator</code> if it exists and also shows the time. You need to use <code class="inline">pane_current_path</code> because the default current working directory of the status line process is whatever directory you initially started tmux from.</p>
<h2>Caching / Throttling</h2>
<p>Remember what I said earlier about keeping the status line speedy and avoiding network calls? <code class="inline">hub ci-status</code> is pretty fast, but we can be avoid hammering the GitHub API with a little caching. I&#39;ve recently been writing about a <a href="https://blog.semanticart.com/tags/cli-cache/" title="">general purpose cli caching script</a> and that script will work great here.</p>
<p>Since the tmux config line is already a bit long, I put the <code class="inline">cache</code> usage in the <code class="inline">.ci-status-indicator</code> script itself.</p>
<pre><code class="bash language-bash">#!/usr/bin/env bash

cache tmux-ci-status --ttl 30 \
  hub ci-status -f &quot;%S, %t|&quot; | \
    gsed -r &#39;
      s/failure, /#[fg=red]/g;
      s/success, /#[fg=green]/g;
      s/pending, /#[fg=yellow]/g;
      s/\|$//;
      s/\|/#[fg=white]|/g;
      s/-tests//g;&#39;</code></pre>
<p>You might also be asking yourself &quot;Won&#39;t this get stale when I change branches?&quot; Yes, until the <code class="inline">cache</code> TTL expires and the status line updates. Think for a moment about ways to avoid stale content.</p>
<h2>Avoiding stale results</h2>
<p>We&#39;re working against two caches here:</p>
<ol>
<li>tmux&#39;s <code class="inline">status-interval</code> (15 seconds by default)
</li>
<li><code class="inline">cache</code>&#39;s 30 second TTL (customizable to whatever we want)
</li>
</ol>
<p>We&#39;ll need to solve for both.</p>
<p><a href="https://git-scm.com/docs/githooks" title="">Git hooks</a> are the best place to start. Issuing <code class="inline">tmux refresh-client -S</code> in the <code class="inline">post-checkout</code> hook handles the <code class="inline">status-interval</code> cache. We <em>could</em> use this same git hook to first purge the cache key on every branch change. That would work fine but is wastefully throwing away our previous information.</p>
<p>We can be a little more clever and use the current git SHA as part of our cache key. This has the advantage of not needing to re-query GitHub if you change between known commits within <code class="inline">cache</code>&#39;s TTL.</p>
<pre><code class="bash language-bash">#!/usr/bin/env bash

cache_key=&quot;tmux-ci-status-$(git rev-parse HEAD)&quot;

cache $cache_key --ttl 30 --cache-status &quot;*&quot; \
  hub ci-status -f &quot;%S, %t|&quot; | \
    gsed -r &#39;
      s/failure, /#[fg=red]/g;
      s/success, /#[fg=green]/g;
      s/pending, /#[fg=yellow]/g;
      s/\|$//;
      s/\|/#[fg=white]|/g;
      s/-tests//g;&#39;</code></pre>
<p>With a per-SHA cache and a hook-triggered instant status line refresh, we&#39;ll never see stale content from another branch/SHA.</p>
<h2>Next steps</h2>
<p>This works great, and hopefully you&#39;ve learned several new things along the way.</p>
<p>There&#39;s always room for improvement, so here&#39;s something to ponder and work on if you wish: Under what circumstances can the GitHub checks results change for a single SHA? Put another way: are there scenarios where you can cache the results indefinitely and avoid pinging <code class="inline">hub ci-status</code> again for the SHA?</p>
<p>If the check&#39;s result is <code class="inline">pending</code> then it makes sense to query again until the check finishes (the <code class="inline">pending</code> state will transition to <code class="inline">failure</code> or <code class="inline">success</code>).  If the results are all <code class="inline">success</code> then querying <code class="inline">hub ci-status</code> again feels wasteful. If the result is <code class="inline">failure</code> then you <em>might</em> want to re-check <code class="inline">hub ci-status</code> if you manually re-trigger a build (e.g. for flaky tests) but perhaps it is otherwise a final state.</p>
<p>I leave this as an exercise to the reader. I&#39;ve not yet added this optimization since I&#39;ve found the cache TTL sufficient.</p>
<p>Shaving that yak is tempting though. It could lead to some fun side paths (e.g. it might be neat to annotate your git history with <a href="https://git-scm.com/docs/git-notes" title="">git notes</a> regarding the CI status of each commit).</p>

      </div>
    </content>
  </entry>

  <entry>
    <title>entr</title>
    <link href="/2020/02/07/entr/"/>
    <id>https://blog.semanticart.com/2020/02/07/entr/</id>
    <updated>2020-02-07T00:00:00-05:00</updated>
    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
        <p>There&#39;s no shortage of ways to automatically run your tests when files change (e.g. jest has <code class="inline">jest --watch</code>). I find these mechanisms useful for maintaining flow. Having Vim open on one screen and a test-runner open on another screen is fantastic.</p>
<p>There&#39;s not a custom runner for everything, but you might not need one. I&#39;ve found that <a href="http://entrproject.org/" title="">entr</a> often gets the job done. entr lets you &quot;run arbitrary commands when files change.&quot;</p>
<p>When I was writing the <a href="https://blog.semanticart.com/tags/cli-cache/" title="">cli caching script</a>, I kept <code class="inline">git ls-files | entr bats test</code> running. Every time I saved my script or the test, the tests would run.</p>
<p>The entr homepage specifies several good use-cases and how to watch for new files, etc. Give it a try.</p>

      </div>
    </content>
  </entry>

  <entry>
    <title>TDD a CLI Caching Script - Part Six - Utility</title>
    <link href="/2020/02/05/tdd-a-cli-caching-script-part-six/"/>
    <id>https://blog.semanticart.com/2020/02/05/tdd-a-cli-caching-script-part-six/</id>
    <updated>2020-02-05T00:00:00-05:00</updated>
    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
        <p>This is part six in a <a href="https://blog.semanticart.com/tags/cli-cache/" title="">series</a> about writing a general-purpose script to cache CLI output. In this brief post we&#39;ll make our script more useful as a utility to other scripts by adding some new options.</p>
<p>If you want to try your hand at implementing these features, checkout <a href="https://github.com/semanticart/cli-cache/commit/780d577e003fff0da5e48694c290caa51102c8bf" title="">780d577</a> locally. I&#39;ll omit my implementation here but will link to the appropriate diffs.</p>
<h2><code class="inline">--check</code></h2>
<p>We can implement <code class="inline">--check</code> to allow checking to see if the cache is fresh per the provided <code class="inline">--ttl</code> and <code class="inline">--stale-while-revalidate</code> options. The best way to communicate this freshness is with exit status, IMHO. This will allow a shell script to use <code class="inline">cache --check</code> as a conditional. We&#39;ll return <code class="inline">0</code> (truthy) if the cache is fresh and <code class="inline">1</code> (falsy) if it is not.</p>
<p>Example usage:</p>
<pre><code class="bash language-bash">fresh() {
    cache --check --ttl 1 test-cache-key
}

if fresh; then
    echo &quot;FRESH&quot;
else
    echo &quot;NOT FRESH&quot;
fi</code></pre>
<p>Here&#39;s the tests for the feature:</p>
<pre><code class="bash language-bash">@test &quot;--check returns 0 if the content exists and is inside the TTL/SWR&quot; {
  run ./cache $TEST_KEY echo 1
  [ &quot;$status&quot; -eq 0 ]
  [ &quot;$output&quot; = &quot;1&quot; ]

  run ./cache --stale-while-revalidate 1 --ttl 1 --check $TEST_KEY
  [ &quot;$status&quot; -eq 0 ]
  [ &quot;$output&quot; = &quot;&quot; ]
}

@test &quot;--check returns 1 if the content exists but is outside the TTL/SWR&quot; {
  run ./cache $TEST_KEY echo 1
  [ &quot;$status&quot; -eq 0 ]
  [ &quot;$output&quot; = &quot;1&quot; ]

  wait_for_second_to_pass

  run ./cache --stale-while-revalidate 1 --ttl 0 --check $TEST_KEY
  [ &quot;$status&quot; -eq 1 ]
  [ &quot;$output&quot; = &quot;&quot; ]
}

@test &quot;--check returns 1 if the content is not yet cached&quot; {
  run ./cache --check $TEST_KEY
  [ &quot;$status&quot; -eq 1 ]
  [ &quot;$output&quot; = &quot;&quot; ]
}</code></pre>
<p>Between the feature description and the tests, you can probably guess the implementation. You can <a href="https://github.com/semanticart/cli-cache/commit/ae8b56f9fe855dd469d7dbadbba2be21f736f100" title="">check out the <code class="inline">--check</code> implementation</a> as you wish.</p>
<h2><code class="inline">--purge</code></h2>
<p>Every cache needs a good mechanism for clearing the cached content on-demand. Maybe your cache key wasn&#39;t specific enough or outside forces have made the content stale before the TTL naturally expires. Whatever your reason, we&#39;ll support <code class="inline">cache --purge &lt;cache-key&gt;</code> to remove the content currently cached at the provided key.</p>
<p>Here&#39;s the tests for <code class="inline">--purge</code>:</p>
<pre><code class="bash language-bash">@test &quot;--purge exits with 0 if the content is not yet cached&quot; {
  [ ! -f &quot;$CACHE_DIR$TEST_KEY&quot; ]

  run ./cache --purge $TEST_KEY
  [ &quot;$status&quot; -eq 0 ]
  [ &quot;$output&quot; = &quot;&quot; ]

  [ ! -f &quot;$CACHE_DIR$TEST_KEY&quot; ]
}

@test &quot;--purge exits with 0 and removes the file if the content is cached&quot; {
  touch &quot;$CACHE_DIR$TEST_KEY&quot;

  [ -f &quot;$CACHE_DIR$TEST_KEY&quot; ]

  run ./cache --purge $TEST_KEY
  [ &quot;$status&quot; -eq 0 ]
  [ &quot;$output&quot; = &quot;&quot; ]

  [ ! -f &quot;$CACHE_DIR$TEST_KEY&quot; ]
}</code></pre>
<p>Try implementing this feature and then check out <a href="https://github.com/semanticart/cli-cache/commit/13dfde193c7c409de76d40aa8ca2739bd8176874" title="">my implementation</a> afterwards.</p>
<h2>Closing</h2>
<p>With a few small tweaks we&#39;ve made our <code class="inline">cache</code> script more useful. This will wrap up the <a href="https://blog.semanticart.com/tags/cli-cache/" title="">series</a> for now, but I&#39;m sure you&#39;ll see the <code class="inline">cache</code> script appear as a background character in future posts.</p>

      </div>
    </content>
  </entry>

</feed>
