<?xml version="1.0" encoding="UTF-8"?>
<rss  xmlns:atom="http://www.w3.org/2005/Atom" 
      xmlns:media="http://search.yahoo.com/mrss/" 
      xmlns:content="http://purl.org/rss/1.0/modules/content/" 
      xmlns:dc="http://purl.org/dc/elements/1.1/" 
      version="2.0">
<channel>
<title>rgtlab — Ronald G. Thomas</title>
<link>https://rgtlab.org/blog/</link>
<atom:link href="https://rgtlab.org/blog/index.xml" rel="self" type="application/rss+xml"/>
<description>Writing on statistical computing, reproducible research, clinical-trial methodology, and open-source R software from the lab of Ronald G. Thomas at UC San Diego.
</description>
<generator>quarto-1.5.45</generator>
<lastBuildDate>Mon, 08 Jun 2026 07:00:00 GMT</lastBuildDate>
<item>
  <title>Functional Plot Generation with purrr</title>
  <dc:creator>Ronald G. Thomas</dc:creator>
  <link>https://rgtlab.org/posts/pp-grouped-plots-with-purrr/</link>
  <description><![CDATA[ 




<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/pp-grouped-plots-with-purrr/media/images/hero.png" class="img-fluid figure-img" style="width:80.0%"></p>
<figcaption>Functional programming tools bring order to repetitive plotting tasks.</figcaption>
</figure>
</div>
<p><em>Generating publication-quality plot grids without writing the same code three times.</em></p>
<section id="introduction" class="level1">
<h1>Introduction</h1>
<p>I did not really know how to programmatically generate multiple plots from grouped data until I started working with the Palmer Penguins dataset. The task seemed straightforward: produce scatter plots for every pairwise combination of numeric variables, separately for each penguin species, and then assemble them into a single coherent grid.</p>
<p>My first instinct was to copy-paste code for each species and each variable pair. That approach collapsed quickly once I counted the combinations: three species, four numeric variables, six pairwise combinations per species – eighteen plots total. Copy-pasting eighteen ggplot calls is tedious, brittle, and nearly impossible to maintain.</p>
<p>The <code>purrr</code> package solved this problem cleanly. By splitting the data by species and mapping a plotting function over every variable combination, I could generate all eighteen plots with a single pipeline and assemble them into a structured grid using <code>patchwork</code>. This post documents that workflow.</p>
<section id="motivations" class="level2">
<h2 class="anchored" data-anchor-id="motivations">Motivations</h2>
<ul>
<li>I was tired of manually duplicating ggplot code for each subgroup in a dataset.</li>
<li>I wanted to see all pairwise relationships between numeric features at a glance, stratified by species.</li>
<li>I needed a workflow that could scale: if a fourth species appeared in the data, the code should handle it without modification.</li>
<li>I was curious whether <code>purrr::pmap</code> could handle the three-argument case (x variable, y variable, grouping variable) cleanly.</li>
<li>I wanted practice assembling complex multi-panel layouts with <code>patchwork</code> beyond simple <code>+</code> and <code>/</code> operators.</li>
</ul>
</section>
<section id="objectives" class="level2">
<h2 class="anchored" data-anchor-id="objectives">Objectives</h2>
<ol type="1">
<li>Split a data frame by a categorical variable (species) and iterate over the resulting list with <code>map2</code>.</li>
<li>Use <code>pmap</code> to map a plotting function across all pairwise combinations of numeric columns.</li>
<li>Assemble the resulting list of plots into a structured grid using <code>patchwork::wrap_plots</code> with a custom design layout string.</li>
<li>Produce a final composite figure with species labels, shared legends, and collected axis titles.</li>
</ol>
<p>I am documenting my learning process here. Errors and better approaches are welcome; see the contact section below.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/pp-grouped-plots-with-purrr/media/images/ambiance1.png" class="img-fluid figure-img"></p>
<figcaption>Functional programming and visualisation.</figcaption>
</figure>
</div>
</section>
</section>
<section id="prerequisites-and-setup" class="level1">
<h1>Prerequisites and Setup</h1>
<p>This post assumes familiarity with <code>ggplot2</code> and basic <code>tidyverse</code> operations. The key packages are:</p>
<ul>
<li><code>purrr</code> for functional iteration (<code>map2</code>, <code>pmap</code>)</li>
<li><code>patchwork</code> for plot composition (<code>wrap_plots</code>, <code>plot_layout</code>, <code>plot_spacer</code>)</li>
<li><code>palmerpenguins</code> for the dataset</li>
<li><code>rlang</code> for tidy evaluation (<code>.data[[var]]</code> pronoun)</li>
</ul>
<div class="cell">
<div class="sourceCode cell-code" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb1-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">library</span>(palmerpenguins)</span>
<span id="cb1-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">library</span>(tidyverse)</span>
<span id="cb1-3"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">library</span>(purrr)</span>
<span id="cb1-4"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">library</span>(patchwork)</span>
<span id="cb1-5"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">library</span>(rlang)</span>
<span id="cb1-6"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">library</span>(grid)</span></code></pre></div>
</div>
<p>The Palmer Penguins dataset contains measurements for 344 penguins across three species (Adelie, Chinstrap, Gentoo) from three islands in the Palmer Archipelago, Antarctica.</p>
<div class="cell">
<div class="sourceCode cell-code" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb2-1">penguins <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span></span>
<span id="cb2-2">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">drop_na</span>() <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span></span>
<span id="cb2-3">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">count</span>(species) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span></span>
<span id="cb2-4">  knitr<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">kable</span>(</span>
<span id="cb2-5">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">caption =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Observations per species after</span></span>
<span id="cb2-6"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">    removing missing values."</span></span>
<span id="cb2-7">  )</span></code></pre></div>
<div class="cell-output-display">
<table class="caption-top table table-sm table-striped small">
<caption>Observations per species after removing missing values.</caption>
<thead>
<tr class="header">
<th style="text-align: left;">species</th>
<th style="text-align: right;">n</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td style="text-align: left;">Adelie</td>
<td style="text-align: right;">146</td>
</tr>
<tr class="even">
<td style="text-align: left;">Chinstrap</td>
<td style="text-align: right;">68</td>
</tr>
<tr class="odd">
<td style="text-align: left;">Gentoo</td>
<td style="text-align: right;">119</td>
</tr>
</tbody>
</table>
</div>
</div>
</section>
<section id="what-is-functional-plot-generation" class="level1">
<h1>What is Functional Plot Generation?</h1>
<p>Functional plot generation means treating plot creation as a function and then applying that function systematically across combinations of inputs. Instead of writing:</p>
<div class="sourceCode" id="cb3" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb3-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Do not do this for 18 combinations</span></span>
<span id="cb3-2">plot_adelie_bill_flipper <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">ggplot</span>(...)</span>
<span id="cb3-3">plot_adelie_bill_body   <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">ggplot</span>(...)</span>
<span id="cb3-4">plot_adelie_bill_depth  <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">ggplot</span>(...)</span>
<span id="cb3-5"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># ... 15 more times</span></span></code></pre></div>
<p>one writes a single plotting function and lets <code>purrr</code> call it for every combination of species, x-variable, and y-variable. The output is a list of ggplot objects that can be arranged into any layout.</p>
<p>Think of it like a mail merge: the template is defined once and the data fills in the blanks.</p>
</section>
<section id="getting-started-preparing-the-data" class="level1">
<h1>Getting Started: Preparing the Data</h1>
<p>The first step is to sample from the penguins data and split it into a named list by species. We also compute all pairwise combinations of the four numeric columns (<code>bill_length_mm</code>, <code>bill_depth_mm</code>, <code>flipper_length_mm</code>, <code>body_mass_g</code>).</p>
<div class="cell">
<div class="sourceCode cell-code" id="cb4" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb4-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">set.seed</span>(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">42</span>)</span>
<span id="cb4-2">df_sampled <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> penguins <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span></span>
<span id="cb4-3">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sample_n</span>(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">50</span>) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span></span>
<span id="cb4-4">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">drop_na</span>()</span>
<span id="cb4-5"></span>
<span id="cb4-6">df_by_species <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">split</span>(df_sampled, df_sampled<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">$</span>species)</span>
<span id="cb4-7"></span>
<span id="cb4-8">numeric_cols <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">names</span>(df_sampled)[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">6</span>]</span>
<span id="cb4-9">pairs <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">t</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">combn</span>(numeric_cols, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>))</span>
<span id="cb4-10"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">colnames</span>(pairs) <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">c</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"x_var"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"y_var"</span>)</span>
<span id="cb4-11">pair_grid <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">as.data.frame</span>(pairs) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span></span>
<span id="cb4-12">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">mutate</span>(<span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">group_var =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"sex"</span>)</span></code></pre></div>
</div>
<p>The <code>pair_grid</code> data frame now holds six rows, one for each pairwise combination. Each row names an x-variable, a y-variable, and a grouping variable (sex) for colour coding within each species panel.</p>
<div class="cell">
<div class="sourceCode cell-code" id="cb5" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb5-1">pair_grid <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span></span>
<span id="cb5-2">  knitr<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">kable</span>(</span>
<span id="cb5-3">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">caption =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"All pairwise variable combinations</span></span>
<span id="cb5-4"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">    to plot."</span></span>
<span id="cb5-5">  )</span></code></pre></div>
<div class="cell-output-display">
<table class="caption-top table table-sm table-striped small">
<caption>All pairwise variable combinations to plot.</caption>
<thead>
<tr class="header">
<th style="text-align: left;">x_var</th>
<th style="text-align: left;">y_var</th>
<th style="text-align: left;">group_var</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td style="text-align: left;">bill_length_mm</td>
<td style="text-align: left;">bill_depth_mm</td>
<td style="text-align: left;">sex</td>
</tr>
<tr class="even">
<td style="text-align: left;">bill_length_mm</td>
<td style="text-align: left;">flipper_length_mm</td>
<td style="text-align: left;">sex</td>
</tr>
<tr class="odd">
<td style="text-align: left;">bill_length_mm</td>
<td style="text-align: left;">body_mass_g</td>
<td style="text-align: left;">sex</td>
</tr>
<tr class="even">
<td style="text-align: left;">bill_depth_mm</td>
<td style="text-align: left;">flipper_length_mm</td>
<td style="text-align: left;">sex</td>
</tr>
<tr class="odd">
<td style="text-align: left;">bill_depth_mm</td>
<td style="text-align: left;">body_mass_g</td>
<td style="text-align: left;">sex</td>
</tr>
<tr class="even">
<td style="text-align: left;">flipper_length_mm</td>
<td style="text-align: left;">body_mass_g</td>
<td style="text-align: left;">sex</td>
</tr>
</tbody>
</table>
</div>
</div>
</section>
<section id="building-the-plotting-function" class="level1">
<h1>Building the Plotting Function</h1>
<p>The core of the workflow is a single function that accepts variable names as strings and a data frame, then returns a ggplot object. The <code>.data[[var]]</code> pronoun from <code>rlang</code> allows column selection by string name inside <code>aes()</code>.</p>
<div class="cell">
<div class="sourceCode cell-code" id="cb6" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb6-1">make_scatter <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">function</span>(x_var, y_var,</span>
<span id="cb6-2">                         group_var, species_name,</span>
<span id="cb6-3">                         df) {</span>
<span id="cb6-4">  df <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span></span>
<span id="cb6-5">    <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">ggplot</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">aes</span>(</span>
<span id="cb6-6">      <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">x =</span> .data[[x_var]],</span>
<span id="cb6-7">      <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">y =</span> .data[[y_var]]</span>
<span id="cb6-8">    )) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span></span>
<span id="cb6-9">    <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">geom_point</span>(</span>
<span id="cb6-10">      <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">aes</span>(<span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">color =</span> .data[[group_var]]),</span>
<span id="cb6-11">      <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">alpha =</span> <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.5</span></span>
<span id="cb6-12">    ) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span></span>
<span id="cb6-13">    <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">geom_smooth</span>(</span>
<span id="cb6-14">      <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">method =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"loess"</span>,</span>
<span id="cb6-15">      <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">se =</span> <span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">TRUE</span>,</span>
<span id="cb6-16">      <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">linewidth =</span> <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.7</span></span>
<span id="cb6-17">    ) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span></span>
<span id="cb6-18">    <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">scale_color_manual</span>(</span>
<span id="cb6-19">      <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">values =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">c</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"purple"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"green"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"red"</span>)</span>
<span id="cb6-20">    ) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span></span>
<span id="cb6-21">    <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">theme_bw</span>() <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span></span>
<span id="cb6-22">    <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">theme</span>(<span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">text =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">element_text</span>(<span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">size =</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">8</span>))</span>
<span id="cb6-23">}</span></code></pre></div>
</div>
<p>Two design choices deserve explanation. First, <code>geom_smooth(method = "loess")</code> adds a local regression line so we can see nonlinear trends. Second, the function does not set titles (those come from the grid layout labels). Keeping the function minimal makes it reusable across different layouts.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/pp-grouped-plots-with-purrr/media/images/ambiance2.png" class="img-fluid figure-img"></p>
<figcaption>Iterating with purpose across data strata.</figcaption>
</figure>
</div>
</section>
<section id="mapping-across-species-and-variable-pairs" class="level1">
<h1>Mapping Across Species and Variable Pairs</h1>
<p>This is the heart of the approach. We use <code>map2</code> to iterate over the list of species data frames and their names simultaneously. Inside that outer loop, <code>pmap</code> iterates over every row of <code>pair_grid</code>, calling <code>make_scatter</code> with the appropriate variable names.</p>
<div class="cell">
<div class="sourceCode cell-code" id="cb7" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb7-1">all_plots <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> df_by_species <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span></span>
<span id="cb7-2">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">map2</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">names</span>(df_by_species), <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">function</span>(df, spc) {</span>
<span id="cb7-3">    pair_grid <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span></span>
<span id="cb7-4">      <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">pmap</span>(<span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">function</span>(x_var, y_var, group_var) {</span>
<span id="cb7-5">        <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">make_scatter</span>(</span>
<span id="cb7-6">          x_var, y_var, group_var, spc, df</span>
<span id="cb7-7">        )</span>
<span id="cb7-8">      })</span>
<span id="cb7-9">  })</span></code></pre></div>
</div>
<p>The result <code>all_plots</code> is a nested list: the outer level has three elements (one per species), and each inner level has six elements (one per variable pair). That gives us eighteen ggplot objects total, organized by species.</p>
<div class="cell">
<div class="sourceCode cell-code" id="cb8" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb8-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">cat</span>(</span>
<span id="cb8-2">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Species:"</span>, <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">length</span>(all_plots), <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">\n</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span>,</span>
<span id="cb8-3">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Plots per species:"</span>, <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">length</span>(all_plots[[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>]]), <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">\n</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span>,</span>
<span id="cb8-4">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Total plots:"</span>, <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sum</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">lengths</span>(all_plots)), <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">\n</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb8-5">)</span></code></pre></div>
<div class="cell-output cell-output-stdout">
<pre><code>Species: 3 
 Plots per species: 6 
 Total plots: 18 </code></pre>
</div>
</div>
</section>
<section id="assembling-the-grid-with-patchwork" class="level1">
<h1>Assembling the Grid with Patchwork</h1>
<p>The final step is arranging all eighteen plots into a structured grid. I want each species to occupy one row group, with a text label identifying the species. The <code>patchwork</code> package supports custom layout strings where each letter maps to a named plot element.</p>
<section id="extracting-individual-plots" class="level2">
<h2 class="anchored" data-anchor-id="extracting-individual-plots">Extracting Individual Plots</h2>
<p>First, we pull every plot out of the nested list and assign readable names.</p>
<div class="cell">
<div class="sourceCode cell-code" id="cb10" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb10-1">p1  <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> all_plots[[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>]][[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>]]</span>
<span id="cb10-2">p2  <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> all_plots[[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>]][[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>]]</span>
<span id="cb10-3">p3  <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> all_plots[[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>]][[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>]]</span>
<span id="cb10-4">p4  <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> all_plots[[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>]][[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">4</span>]]</span>
<span id="cb10-5">p5  <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> all_plots[[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>]][[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">5</span>]]</span>
<span id="cb10-6">p6  <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> all_plots[[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>]][[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">6</span>]]</span>
<span id="cb10-7">p7  <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> all_plots[[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>]][[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>]]</span>
<span id="cb10-8">p8  <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> all_plots[[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>]][[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>]]</span>
<span id="cb10-9">p9  <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> all_plots[[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>]][[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>]]</span>
<span id="cb10-10">p10 <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> all_plots[[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>]][[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">4</span>]]</span>
<span id="cb10-11">p11 <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> all_plots[[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>]][[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">5</span>]]</span>
<span id="cb10-12">p12 <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> all_plots[[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>]][[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">6</span>]]</span>
<span id="cb10-13">p13 <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> all_plots[[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>]][[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>]]</span>
<span id="cb10-14">p14 <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> all_plots[[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>]][[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>]]</span>
<span id="cb10-15">p15 <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> all_plots[[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>]][[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>]]</span>
<span id="cb10-16">p16 <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> all_plots[[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>]][[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">4</span>]]</span>
<span id="cb10-17">p17 <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> all_plots[[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>]][[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">5</span>]]</span>
<span id="cb10-18">p18 <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> all_plots[[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>]][[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">6</span>]]</span></code></pre></div>
</div>
</section>
<section id="defining-the-layout" class="level2">
<h2 class="anchored" data-anchor-id="defining-the-layout">Defining the Layout</h2>
<p>The layout string below arranges plots in an upper triangular pattern for each species, with text labels (X, Y, Z) marking each species group. Each letter in the string maps to one named plot element in <code>wrap_plots</code>.</p>
<div class="cell">
<div class="sourceCode cell-code" id="cb11" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb11-1">layout_string <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb11-2"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">X##</span></span>
<span id="cb11-3"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">ABC</span></span>
<span id="cb11-4"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">#DE</span></span>
<span id="cb11-5"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">##F</span></span>
<span id="cb11-6"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">Y##</span></span>
<span id="cb11-7"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">GHI</span></span>
<span id="cb11-8"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">#JK</span></span>
<span id="cb11-9"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">##L</span></span>
<span id="cb11-10"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">Z##</span></span>
<span id="cb11-11"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">MNO</span></span>
<span id="cb11-12"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">#PQ</span></span>
<span id="cb11-13"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">##R</span></span>
<span id="cb11-14"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span></code></pre></div>
</div>
<p>The <code>#</code> characters represent empty cells. This produces a staircase pattern: three plots in the first row, two in the second, one in the third (mirroring the upper triangle of a pairwise comparison matrix).</p>
</section>
<section id="composing-the-final-figure" class="level2">
<h2 class="anchored" data-anchor-id="composing-the-final-figure">Composing the Final Figure</h2>
<p>We create text labels for each species using <code>grid::textGrob</code> and pass everything to <code>wrap_plots</code>.</p>
<div class="cell">
<div class="sourceCode cell-code" id="cb12" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb12-1">label_sp1 <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> grid<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">textGrob</span>(</span>
<span id="cb12-2">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">names</span>(df_by_species)[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>],</span>
<span id="cb12-3">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">gp =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">gpar</span>(<span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">fontsize =</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">10</span>, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">fontface =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"bold"</span>)</span>
<span id="cb12-4">)</span>
<span id="cb12-5">label_sp2 <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> grid<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">textGrob</span>(</span>
<span id="cb12-6">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">names</span>(df_by_species)[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>],</span>
<span id="cb12-7">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">gp =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">gpar</span>(<span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">fontsize =</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">10</span>, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">fontface =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"bold"</span>)</span>
<span id="cb12-8">)</span>
<span id="cb12-9">label_sp3 <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> grid<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">textGrob</span>(</span>
<span id="cb12-10">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">names</span>(df_by_species)[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>],</span>
<span id="cb12-11">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">gp =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">gpar</span>(<span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">fontsize =</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">10</span>, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">fontface =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"bold"</span>)</span>
<span id="cb12-12">)</span>
<span id="cb12-13"></span>
<span id="cb12-14">composite <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">wrap_plots</span>(</span>
<span id="cb12-15">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">X =</span> label_sp1,</span>
<span id="cb12-16">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">A =</span> p1,  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">B =</span> p2,  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">C =</span> p3,</span>
<span id="cb12-17">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">D =</span> p4,  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">E =</span> p5,  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">F =</span> p6,</span>
<span id="cb12-18">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">Y =</span> label_sp2,</span>
<span id="cb12-19">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">G =</span> p7,  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">H =</span> p8,  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">I =</span> p9,</span>
<span id="cb12-20">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">J =</span> p10, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">K =</span> p11, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">L =</span> p12,</span>
<span id="cb12-21">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">Z =</span> label_sp3,</span>
<span id="cb12-22">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">M =</span> p13, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">N =</span> p14, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">O =</span> p15,</span>
<span id="cb12-23">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">P =</span> p16, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">Q =</span> p17, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">R =</span> p18,</span>
<span id="cb12-24">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">design =</span> layout_string</span>
<span id="cb12-25">) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span></span>
<span id="cb12-26">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">plot_layout</span>(</span>
<span id="cb12-27">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">guides =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"collect"</span>,</span>
<span id="cb12-28">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">axis_titles =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"collect"</span></span>
<span id="cb12-29">  ) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span></span>
<span id="cb12-30">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">theme</span>(</span>
<span id="cb12-31">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">legend.position =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"bottom"</span>,</span>
<span id="cb12-32">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">legend.direction =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"horizontal"</span>,</span>
<span id="cb12-33">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">text =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">element_text</span>(<span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">size =</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">8</span>)</span>
<span id="cb12-34">  )</span>
<span id="cb12-35"></span>
<span id="cb12-36">composite</span></code></pre></div>
<div class="cell-output-display">
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/pp-grouped-plots-with-purrr/index_files/figure-html/final-composite-1.png" class="img-fluid figure-img" alt="A grid of 18 scatter plots arranged in three upper-triangular blocks, one per penguin species, showing pairwise relationships between bill length, bill depth, flipper length, and body mass with LOESS smoothing lines." width="768"></p>
<figcaption>Pairwise scatter plots for four numeric penguin measurements, stratified by species. Each row group shows one species with a LOESS smooth and points coloured by sex.</figcaption>
</figure>
</div>
</div>
</div>
<p>The <code>guides = "collect"</code> argument consolidates duplicate legends into a single shared legend at the bottom. The <code>axis_titles = "collect"</code> argument prevents repeated axis labels across panels. Together, these two settings produce a clean, readable composite figure.</p>
</section>
<section id="things-to-watch-out-for" class="level2">
<h2 class="anchored" data-anchor-id="things-to-watch-out-for">Things to Watch Out For</h2>
<ol type="1">
<li><p><strong>Variable types matter.</strong> The <code>.data[[var]]</code> pronoun works only when the column name is a string. Passing a symbol instead produces cryptic subscription errors.</p></li>
<li><p><strong>Missing data propagates.</strong> Omitting <code>drop_na()</code> before splitting leaves <code>NA</code> rows in some species subsets, causing <code>geom_smooth</code> to warn about removed observations.</p></li>
<li><p><strong>Layout string alignment.</strong> Every row in the <code>patchwork</code> layout string must have the same number of characters. A misaligned row silently produces an incorrect layout.</p></li>
<li><p><strong>Global environment side effects.</strong> The original draft used <code>assign(..., envir = .GlobalEnv)</code> inside the plotting function. This is fragile and pollutes the workspace. Returning the plot object and storing it in a list is cleaner and more predictable.</p></li>
<li><p><strong>Sample size per species.</strong> When sampling 50 rows from the full dataset, some species may end up with very few observations. For production analyses, use the full dataset or stratified sampling to ensure adequate representation.</p></li>
</ol>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/pp-grouped-plots-with-purrr/media/images/ambiance3.png" class="img-fluid figure-img"></p>
<figcaption>Assembling the pieces into a coherent whole.</figcaption>
</figure>
</div>
</section>
</section>
<section id="what-did-we-learn" class="level1">
<h1>What Did We Learn?</h1>
<section id="lessons-learnt" class="level2">
<h2 class="anchored" data-anchor-id="lessons-learnt">Lessons Learnt</h2>
<p><strong>Conceptual Understanding:</strong></p>
<ul>
<li>Pairwise scatter plot matrices provide a rapid visual summary of relationships between continuous features, but they scale quadratically: four variables produce six pairs, five produce ten.</li>
<li>Stratification by species reveals within-group patterns that pooled analyses obscure (Simpson’s paradox).</li>
<li>LOESS smoothers are useful for initial exploration but carry no inferential interpretation without further modeling.</li>
<li>The upper-triangular layout avoids redundancy by showing each pair exactly once.</li>
</ul>
<p><strong>Technical Skills:</strong></p>
<ul>
<li><code>purrr::map2</code> iterates over two lists in parallel – ideal for pairing data frames with their names.</li>
<li><code>purrr::pmap</code> generalises to arbitrary numbers of arguments by iterating over rows of a data frame.</li>
<li><code>rlang::.data[[var]]</code> enables column selection by string name inside <code>aes()</code>, which is essential for programmatic ggplot construction.</li>
<li><code>patchwork::wrap_plots</code> with a custom <code>design</code> string provides fine-grained control over multi-panel layouts.</li>
</ul>
<p><strong>Gotchas and Pitfalls:</strong></p>
<ul>
<li>Forgetting <code>drop_na()</code> before <code>split()</code> produces species-level data frames with missing rows that cause warnings in <code>geom_smooth</code>.</li>
<li>The <code>plot_spacer()</code> function is useful for empty cells, but the <code>#</code> character in the design string is more readable for fixed layouts.</li>
<li><code>assign()</code> to <code>.GlobalEnv</code> inside mapped functions creates hard-to-debug side effects. Always return values from functions and collect them in lists.</li>
<li><code>scale_color_manual</code> requires the correct number of colour values matching the levels of the grouping variable.</li>
</ul>
</section>
<section id="limitations" class="level2">
<h2 class="anchored" data-anchor-id="limitations">Limitations</h2>
<ul>
<li>The sample of 50 penguins is small. Some species may have fewer than 10 observations, making LOESS fits unreliable.</li>
<li>The grouping variable (sex) is hard-coded. A more general approach would accept the grouping variable as a parameter.</li>
<li>The layout string is manually crafted for exactly three species and six variable pairs. Adding a fourth species or fifth numeric variable requires rewriting the layout.</li>
<li>No statistical tests accompany the visual exploration. The plots show associations but do not quantify significance or effect size.</li>
<li>The colour palette (purple, green, red) is not colourblind-friendly. Production code should use a palette validated for accessibility.</li>
</ul>
</section>
<section id="opportunities-for-improvement" class="level2">
<h2 class="anchored" data-anchor-id="opportunities-for-improvement">Opportunities for Improvement</h2>
<ol type="1">
<li>Replace the hard-coded layout string with a function that generates the design string dynamically based on the number of species and variable pairs.</li>
<li>Add correlation coefficients as text annotations inside each scatter plot panel.</li>
<li>Use <code>GGally::ggpairs</code> as a comparison point – it handles pairwise plots natively, though with less layout flexibility.</li>
<li>Implement stratified sampling with <code>dplyr::slice_sample(n, by = species)</code> to ensure balanced representation.</li>
<li>Replace the manual plot extraction (p1 through p18) with a programmatic approach using <code>list_flatten</code> and named assignment.</li>
<li>Switch to a colourblind-friendly palette such as <code>viridis</code> or the Okabe-Ito palette.</li>
</ol>
</section>
</section>
<section id="wrapping-up" class="level1">
<h1>Wrapping Up</h1>
<p>This post demonstrated how <code>purrr::map2</code> and <code>purrr::pmap</code> can replace copy-pasted ggplot code with a single functional pipeline. The key insight is that plots are objects that can be stored in lists, passed to functions, and assembled programmatically.</p>
<p>The approach worked well for the Palmer Penguins data. Starting from a single plotting function and a table of variable combinations, we generated eighteen scatter plots and arranged them into a structured grid that reveals within-species patterns. The total code is compact enough to fit in a single script, yet flexible enough to adapt to new datasets.</p>
<p>When working with grouped data and finding repetitive ggplot code, this pattern is worth trying:</p>
<ul>
<li><strong>Split</strong> the data by your grouping variable.</li>
<li><strong>Define</strong> a single plotting function that accepts column names as strings.</li>
<li><strong>Map</strong> the function across all combinations using <code>pmap</code>.</li>
<li><strong>Assemble</strong> the resulting list with <code>patchwork</code>.</li>
</ul>
</section>
<section id="see-also" class="level1">
<h1>See Also</h1>
<ul>
<li><a href="https://aosmith.rbind.io/2018/08/20/automating-exploratory-plots/">Automating exploratory plots with ggplot2 and purrr</a>: Ariel Muldoon’s tutorial on the same pattern with detailed examples.</li>
<li><a href="https://www.r-bloggers.com/2021/06/principal-components-and-penguins-by-ellis2013nz/">Principal components and penguins</a>: PCA as an alternative to pairwise scatter plots for high-dimensional penguin data.</li>
<li><a href="https://purrr.tidyverse.org/">purrr documentation</a>: Official reference for <code>map</code>, <code>map2</code>, and <code>pmap</code>.</li>
<li><a href="https://patchwork.data-imaginist.com/">patchwork documentation</a>: Guide to plot composition and layout design.</li>
<li><a href="https://dplyr.tidyverse.org/articles/programming.html">Programming with dplyr</a>: Background on <code>.data[[var]]</code> and tidy evaluation.</li>
</ul>
</section>
<section id="reproducibility" class="level1">
<h1>Reproducibility</h1>
<p>This analysis uses the <code>palmerpenguins</code> built-in dataset and requires no external data files. To reproduce:</p>
<div class="sourceCode" id="cb13" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb13-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">quarto</span> render index.qmd</span></code></pre></div>
<p><strong>Session information:</strong></p>
<div class="cell">
<div class="sourceCode cell-code" id="cb14" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb14-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sessionInfo</span>()</span></code></pre></div>
<div class="cell-output cell-output-stdout">
<pre><code>R version 4.5.3 (2026-03-11)
Platform: aarch64-apple-darwin25.3.0
Running under: macOS Tahoe 26.5

Matrix products: default
BLAS:   /opt/homebrew/Cellar/openblas/0.3.32/lib/libopenblasp-r0.3.32.dylib 
LAPACK: /opt/homebrew/Cellar/r/4.5.3/lib/R/lib/libRlapack.dylib;  LAPACK version 3.12.1

locale:
[1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8

time zone: America/Los_Angeles
tzcode source: internal

attached base packages:
[1] grid      stats     graphics  grDevices utils     datasets  methods  
[8] base     

other attached packages:
 [1] rlang_1.2.0          patchwork_1.3.2      lubridate_1.9.5     
 [4] forcats_1.0.0        stringr_1.5.1        dplyr_1.2.1         
 [7] purrr_1.1.0          readr_2.2.0          tidyr_1.3.1         
[10] tibble_3.3.1         ggplot2_4.0.0        tidyverse_2.0.0     
[13] palmerpenguins_0.1.1

loaded via a namespace (and not attached):
 [1] Matrix_1.7-4       gtable_0.3.6       jsonlite_2.0.0     compiler_4.5.3    
 [5] tidyselect_1.2.1   parallel_4.5.3     splines_4.5.3      scales_1.4.0      
 [9] yaml_2.3.10        fastmap_1.2.0      lattice_0.22-9     R6_2.6.1          
[13] labeling_0.4.3     generics_0.1.4     knitr_1.50         htmlwidgets_1.6.4 
[17] pillar_1.11.1      RColorBrewer_1.1-3 tzdb_0.5.0         stringi_1.8.7     
[21] xfun_0.56          S7_0.2.0           timechange_0.4.0   cli_3.6.6         
[25] mgcv_1.9-4         withr_3.0.2        magrittr_2.0.5     digest_0.6.37     
[29] hms_1.1.4          nlme_3.1-168       lifecycle_1.0.5    vctrs_0.7.3       
[33] evaluate_1.0.5     glue_1.8.0         farver_2.1.2       rmarkdown_2.29    
[37] tools_4.5.3        pkgconfig_2.0.3    htmltools_0.5.8.1 </code></pre>
</div>
</div>
</section>
<section id="lets-connect" class="level1">
<h1>Let’s Connect</h1>
<ul>
<li><strong>GitHub:</strong> <a href="https://github.com/rgt47">rgt47</a></li>
<li><strong>Twitter/X:</strong> <a href="https://twitter.com/rgt47"><span class="citation" data-cites="rgt47">@rgt47</span></a></li>
<li><strong>LinkedIn:</strong> <a href="https://linkedin.com/in/rgthomaslab">Ronald Glenn Thomas</a></li>
<li><strong>Email:</strong> <a href="https://rgtlab.org/contact">rgtlab.org/contact</a></li>
</ul>
<p>I would enjoy hearing from you if:</p>
<ul>
<li>An error or a better approach to any of the code in this post comes to mind.</li>
<li>There are suggestions for topics to see covered.</li>
<li>The interest is in discussing R programming, data science, or reproducible research.</li>
<li>There are questions about anything in this tutorial.</li>
<li>The goal is simply to say hello and connect.</li>
</ul>
<section id="related-posts-in-this-cluster" class="level2">
<h2 class="anchored" data-anchor-id="related-posts-in-this-cluster">Related posts in this cluster</h2>
<p>This post is part of the <em>Palmer Penguins Analysis Arc</em> series. Recommended reading order:</p>
<ol type="1">
<li>Post 50: <a href="../50-pp-eda/">Palmer Penguins Part 1: Exploratory Data Analysis</a></li>
<li>Post 51: <a href="../51-pp-multiple-regression/">Palmer Penguins Part 2: Multiple Regression</a></li>
<li>Post 52: <a href="../52-pp-cross-validation/">Palmer Penguins Part 3: Cross-Validation</a></li>
<li>Post 53: <a href="../53-pp-diagnostics/">Palmer Penguins Part 4: Model Diagnostics</a></li>
<li>Post 54: <a href="../54-pp-random-forest/">Palmer Penguins Part 5: Random Forest versus Linear</a></li>
<li>Post 55: <a href="../55-pp-body-mass-prediction/">Predictive Modeling of Penguin Body Mass</a></li>
<li><strong>Post 56: Functional Plot Generation with purrr</strong> (this post)</li>
</ol>


</section>
</section>

 ]]></description>
  <category>r</category>
  <category>data-visualization</category>
  <guid>https://rgtlab.org/posts/pp-grouped-plots-with-purrr/</guid>
  <pubDate>Mon, 08 Jun 2026 07:00:00 GMT</pubDate>
  <media:content url="https://rgtlab.org/posts/pp-grouped-plots-with-purrr/media/images/hero.png" medium="image" type="image/png" height="79" width="144"/>
</item>
<item>
  <title>Setting Up Multi-Language Quarto Documents on macOS</title>
  <dc:creator>Ronald G. Thomas</dc:creator>
  <link>https://rgtlab.org/posts/pub-multi-language-quarto/</link>
  <description><![CDATA[ 




<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/pub-multi-language-quarto/media/images/hero.png" class="img-fluid figure-img" style="width:80.0%"></p>
<figcaption>Quarto logo: the open-source publishing system that ties multiple languages together in a single document.</figcaption>
</figure>
</div>
<p><em>Quarto provides a unified authoring framework for scientific and technical publishing.</em></p>
<section id="introduction" class="level1">
<h1>Introduction</h1>
<p>The plumbing behind a multi-language Quarto document is more extensive than the Quarto website suggests. The documentation makes it look effortless: drop an R chunk here, a Python chunk there, maybe some Julia for good measure, and hit Render. The reality involves an afternoon of chasing PATH variables, fixing reticulate configurations, and troubleshooting why Julia refuses to find its own packages.</p>
<p>The problem is not that any single language is hard to install. Each one has a clean installer and decent documentation. The difficulty is making them all coexist inside a single rendering pipeline where R orchestrates Python through reticulate, Julia through JuliaCall, and Observable JS runs natively in the browser. Small misconfigurations in any layer can cascade into cryptic errors.</p>
<p>We document here the lessons learned getting all four languages to cooperate in a single Quarto document on macOS, written from the perspective of working through the process rather than having mastered it.</p>
<p>More formally, this post documents the polyglot capability of the Document layer of the Workflow Construct described in <a href="../../posts/52-workflow-construct/">post 52</a>. Quarto is the construct’s recommended document layer, subsuming the roles previously divided between R Markdown, Jupyter, and standalone LaTeX. This post addresses the specific case in which a single document spans R, Python, Julia, and Observable, which is the configuration that surfaces the most layer-interaction subtle failures.</p>
<section id="motivations" class="level2">
<h2 class="anchored" data-anchor-id="motivations">Motivations</h2>
<p>The following considerations motivated this exploration:</p>
<ul>
<li>Comparing plotting libraries across languages side by side, using the same dataset, in a single rendered document.</li>
<li>Collaborators who work in Python need a way to view analyses that mix R and Python code without installing R themselves.</li>
<li>Julia’s speed for numerical computation makes it worth exploring, particularly when called from inside an R session.</li>
<li>Observable JS examples in Quarto documentation suggest useful integration patterns worth understanding.</li>
<li>Documenting the setup process forces understanding of each component rather than blindly following a tutorial.</li>
<li>A reproducible multi-language environment can be reused across future projects.</li>
</ul>
</section>
<section id="objectives" class="level2">
<h2 class="anchored" data-anchor-id="objectives">Objectives</h2>
<ol type="1">
<li>Install and configure R, Python, Julia, and Observable JS to work within a single Quarto document on macOS.</li>
<li>Verify each language integration independently before combining them.</li>
<li>Render a minimal multi-language Quarto document that executes code in all four languages.</li>
<li>Document common setup pitfalls and their solutions so future attempts take minutes rather than hours.</li>
</ol>
<p>Errors and better approaches are welcome; see the Feedback section at the end.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/pub-multi-language-quarto/media/images/ambiance1.png" class="img-fluid figure-img"></p>
<figcaption>A workspace ready for multi-language exploration.</figcaption>
</figure>
</div>
</section>
</section>
<section id="prerequisites-and-setup" class="level1">
<h1>Prerequisites and Setup</h1>
<p>Before starting, the following should be available on the macOS system:</p>
<ul>
<li><strong>macOS</strong> (tested on Monterey and later)</li>
<li><strong>At least 8 GB RAM</strong> (each language runtime consumes memory; running all four simultaneously is demanding)</li>
<li><strong>Roughly 5 GB free disk space</strong> for all installations</li>
<li><strong>Admin privileges</strong> for Homebrew and installer packages</li>
<li><strong>Internet connection</strong> for downloading packages</li>
</ul>
<p><strong>Background.</strong> This guide assumes familiarity with the macOS Terminal and basic comfort installing software from the command line. Prior experience with all four languages is not required; that is the whole point.</p>
</section>
<section id="what-is-multi-language-quarto" class="level1">
<h1>What is Multi-Language Quarto?</h1>
<p>Quarto is an open-source scientific publishing system that extends the idea behind R Markdown to multiple languages. A single <code>.qmd</code> file can contain code chunks written in R, Python, Julia, and Observable JS. When the document is rendered, Quarto routes each chunk to the appropriate language engine, collects the output, and weaves everything into a unified HTML, PDF, or Word document.</p>
<p>The key insight is that R acts as the orchestration layer. Python chunks are executed through the reticulate package, Julia chunks through JuliaCall, and Observable JS chunks run natively in the browser when producing HTML output. This means the R installation needs to know where Python and Julia live on the system, and each language needs its own packages installed separately.</p>
</section>
<section id="getting-started-basic-setup" class="level1">
<h1>Getting Started: Basic Setup</h1>
<p>The setup proceeds in four stages. Completing and verifying each stage before moving to the next is recommended. Debugging a four-language environment is much harder than debugging a single-language problem.</p>
<section id="stage-1-r-and-rstudio" class="level2">
<h2 class="anchored" data-anchor-id="stage-1-r-and-rstudio">Stage 1: R and RStudio</h2>
<ol type="1">
<li>Install R from <a href="https://cran.r-project.org/">CRAN</a></li>
<li>Install <a href="https://posit.co/download/rstudio-desktop/">RStudio</a></li>
<li>Install <a href="https://quarto.org/docs/get-started/">Quarto</a></li>
</ol>
<p>Then install the R packages that bridge to other languages:</p>
<div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb1-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">install.packages</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">c</span>(</span>
<span id="cb1-2">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"dplyr"</span>,</span>
<span id="cb1-3">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"ggplot2"</span>,</span>
<span id="cb1-4">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"scales"</span>,</span>
<span id="cb1-5">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"reticulate"</span>,</span>
<span id="cb1-6">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"JuliaCall"</span></span>
<span id="cb1-7">))</span></code></pre></div>
<p>The first three packages are for data work. The last two are the bridge packages: reticulate connects R to Python, and JuliaCall connects R to Julia.</p>
</section>
<section id="stage-2-python" class="level2">
<h2 class="anchored" data-anchor-id="stage-2-python">Stage 2: Python</h2>
<ol type="1">
<li>Install <a href="https://www.anaconda.com/products/distribution">Anaconda</a> (recommended) or Miniconda</li>
<li>Install required Python packages:</li>
</ol>
<div class="sourceCode" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb2-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">conda</span> install pandas seaborn matplotlib</span></code></pre></div>
<ol start="3" type="1">
<li>Verify Python configuration in R:</li>
</ol>
<div class="sourceCode" id="cb3" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb3-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">library</span>(reticulate)</span>
<span id="cb3-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">py_config</span>()</span></code></pre></div>
<p>The output of <code>py_config()</code> should show your Anaconda Python path. If it does not, you need to tell reticulate where to look.</p>
<div class="callout callout-style-default callout-important callout-titled">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon"></i>
</div>
<div class="callout-title-container flex-fill">
Common Python Issues
</div>
</div>
<div class="callout-body-container callout-body">
<ul>
<li><p>If <code>py_config()</code> does not show your Anaconda installation, specify it explicitly:</p>
<div class="sourceCode" id="cb4" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb4-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">use_python</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"/path/to/anaconda/bin/python"</span>)</span></code></pre></div></li>
<li><p>Seaborn styling requires specific syntax:</p>
<div class="sourceCode" id="cb5" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb5-1">sns.set_theme(style<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"whitegrid"</span>)</span></code></pre></div>
<p>Do not use <code>plt.style.use()</code> for Seaborn themes.</p></li>
</ul>
</div>
</div>
</section>
<section id="stage-3-julia" class="level2">
<h2 class="anchored" data-anchor-id="stage-3-julia">Stage 3: Julia</h2>
<ol type="1">
<li>Install Julia from <a href="https://julialang.org/downloads/">julialang.org</a></li>
<li>Add Julia to PATH (usually automatic with the installer)</li>
<li>Install required Julia packages:</li>
</ol>
<div class="sourceCode" id="cb6" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb6-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">using</span> Pkg</span>
<span id="cb6-2"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">Pkg.add</span><span class="er" style="color: #AD0000;
background-color: null;
font-style: inherit;">(</span><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">[</span></span>
<span id="cb6-3">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"UnicodePlots"</span>,</span>
<span id="cb6-4">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"DataFrames"</span>,</span>
<span id="cb6-5">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"CSV"</span>,</span>
<span id="cb6-6">  <span class="er" style="color: #AD0000;
background-color: null;
font-style: inherit;">"Statistics"</span></span>
<span id="cb6-7"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">]</span><span class="er" style="color: #AD0000;
background-color: null;
font-style: inherit;">)</span></span></code></pre></div>
<ol start="4" type="1">
<li>Set up the Julia-R connection:</li>
</ol>
<div class="sourceCode" id="cb7" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb7-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">library</span>(JuliaCall)</span>
<span id="cb7-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">julia_setup</span>()</span></code></pre></div>
<div class="callout callout-style-default callout-warning callout-titled">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon"></i>
</div>
<div class="callout-title-container flex-fill">
Julia Gotchas
</div>
</div>
<div class="callout-body-container callout-body">
<ul>
<li>Some Julia plotting backends have system dependencies that may not be present on macOS by default.</li>
<li>Use UnicodePlots for the most reliable terminal-based results.</li>
<li>Ensure the Julia working directory has write permissions.</li>
</ul>
</div>
</div>
</section>
<section id="stage-4-observable-js" class="level2">
<h2 class="anchored" data-anchor-id="stage-4-observable-js">Stage 4: Observable JS</h2>
<p>No separate installation is needed. Observable JS runs in the browser when Quarto produces HTML output. Your Quarto document must include the dependency declaration in the YAML header:</p>
<div class="sourceCode" id="cb8" style="background: #f1f3f5;"><pre class="sourceCode yaml code-with-copy"><code class="sourceCode yaml"><span id="cb8-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">dependencies</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span></span>
<span id="cb8-2"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">  </span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">-</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> </span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">name</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> </span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"@observablehq/plot"</span></span>
<span id="cb8-3"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">    </span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">version</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> latest</span></span></code></pre></div>
<p>That is it. Observable chunks will render automatically when you build to HTML.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/pub-multi-language-quarto/media/images/ambiance2.png" class="img-fluid figure-img"></p>
<figcaption>Python logo: the second language engine in the multi-language Quarto setup.</figcaption>
</figure>
</div>
<p><em>Python integration through reticulate is often the first multi-language feature Quarto users encounter.</em></p>
</section>
</section>
<section id="verifying-each-language" class="level1">
<h1>Verifying Each Language</h1>
<p>Before combining all four languages in a single document, verify that each integration works independently. This saves considerable debugging time.</p>
<section id="r-environment-check" class="level2">
<h2 class="anchored" data-anchor-id="r-environment-check">R Environment Check</h2>
<div class="sourceCode" id="cb9" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb9-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sessionInfo</span>()</span></code></pre></div>
<p>This confirms your R version and loaded packages. Look for the version number and ensure the base packages are listed.</p>
</section>
<section id="python-environment-check" class="level2">
<h2 class="anchored" data-anchor-id="python-environment-check">Python Environment Check</h2>
<div class="sourceCode" id="cb10" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb10-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">library</span>(reticulate)</span>
<span id="cb10-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">py_config</span>()</span></code></pre></div>
<p>Confirm that the Python path points to your Anaconda installation, not the system Python. The output should show the version number and the path to the Python binary.</p>
</section>
<section id="julia-environment-check" class="level2">
<h2 class="anchored" data-anchor-id="julia-environment-check">Julia Environment Check</h2>
<div class="sourceCode" id="cb11" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb11-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">library</span>(JuliaCall)</span>
<span id="cb11-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">julia_eval</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"versioninfo()"</span>)</span></code></pre></div>
<p>This should print the Julia version and system information. If JuliaCall cannot find Julia, check that the Julia binary is on your PATH.</p>
</section>
</section>
<section id="testing-a-minimal-example" class="level1">
<h1>Testing a Minimal Example</h1>
<p>Once all four verifications pass, test a minimal multi-language document. Create a new <code>.qmd</code> file with these chunks:</p>
<div class="cell">
<details class="code-fold">
<summary>Code</summary>
<div class="sourceCode cell-code" id="cb12" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb12-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">print</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Hello from R!"</span>)</span></code></pre></div>
</details>
</div>
<div class="cell" data-python.reticulate="false">
<details class="code-fold">
<summary>Code</summary>
<div class="sourceCode cell-code" id="cb13" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb13-1"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Hello from Python!"</span>)</span></code></pre></div>
</details>
</div>
<div class="sourceCode" id="cb14" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb14-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">println</span><span class="er" style="color: #AD0000;
background-color: null;
font-style: inherit;">(</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Hello from Julia!"</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">)</span></span></code></pre></div>
<div class="cell">
<details class="code-fold">
<summary>Code</summary>
<div class="sourceCode cell-code" id="cb15" data-startfrom="326" data-source-offset="0" style="background: #f1f3f5;"><pre class="sourceCode js code-with-copy"><code class="sourceCode javascript" style="counter-reset: source-line 325;"><span id="cb15-326"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">md</span><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">`Hello from Observable JS!`</span></span></code></pre></div>
</details>
<div class="cell-output cell-output-display">
<div id="ojs-cell-1" data-nodetype="expression">

</div>
</div>
</div>
<p>If all four chunks produce output when you render, your environment is properly configured. If any chunk fails, go back to the verification step for that specific language.</p>
</section>
<section id="full-example-palmer-penguins-visualization" class="level1">
<h1>Full Example: Palmer Penguins Visualization</h1>
<p>Below is a complete, documented version of a multi-language visualization example. Each section creates the same scatterplot — bill depth versus bill length by species — in a different language, showcasing how the same data can flow through four ecosystems.</p>
<section id="data-preparation-in-r" class="level3">
<h3 class="anchored" data-anchor-id="data-preparation-in-r">Data Preparation in R</h3>
<p>R loads the data and exports a CSV that the other languages can read:</p>
<div class="cell">
<details class="code-fold">
<summary>Code</summary>
<div class="sourceCode cell-code" id="cb16" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb16-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">library</span>(dplyr)</span>
<span id="cb16-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">library</span>(ggplot2)</span>
<span id="cb16-3"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">library</span>(scales)</span>
<span id="cb16-4"></span>
<span id="cb16-5">data <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> penguins <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">na.omit</span>()</span>
<span id="cb16-6"></span>
<span id="cb16-7"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">write.csv</span>(data, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"penguins.csv"</span>, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">row.names =</span> <span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">FALSE</span>)</span>
<span id="cb16-8"></span>
<span id="cb16-9"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">glimpse</span>(data)</span></code></pre></div>
</details>
</div>
<p>R prepares and exports the clean data. The other languages read from this shared CSV, which ensures consistency across all four visualizations.</p>
</section>
<section id="visualization-in-r-ggplot2" class="level3">
<h3 class="anchored" data-anchor-id="visualization-in-r-ggplot2">Visualization in R (ggplot2)</h3>
<div class="cell">
<details class="code-fold">
<summary>Code</summary>
<div class="sourceCode cell-code" id="cb17" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb17-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">ggplot</span>(data, <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">aes</span>(<span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">x =</span> bill_length_mm,</span>
<span id="cb17-2">                 <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">y =</span> bill_depth_mm,</span>
<span id="cb17-3">                 <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">color =</span> species,</span>
<span id="cb17-4">                 <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">shape =</span> species)) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span></span>
<span id="cb17-5">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">geom_point</span>(<span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">size =</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">alpha =</span> <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.7</span>) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span></span>
<span id="cb17-6">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">geom_smooth</span>(<span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">method =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"lm"</span>,</span>
<span id="cb17-7">              <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">se =</span> <span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">FALSE</span>,</span>
<span id="cb17-8">              <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">linewidth =</span> <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.7</span>,</span>
<span id="cb17-9">              <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">alpha =</span> <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.5</span>) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span></span>
<span id="cb17-10">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">scale_color_brewer</span>(<span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">palette =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Set1"</span>) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span></span>
<span id="cb17-11">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">labs</span>(</span>
<span id="cb17-12">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">title =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Bill Depth vs Length (R)"</span>,</span>
<span id="cb17-13">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">x =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Bill Length (mm)"</span>,</span>
<span id="cb17-14">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">y =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Bill Depth (mm)"</span>,</span>
<span id="cb17-15">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">color =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Species"</span>,</span>
<span id="cb17-16">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">shape =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Species"</span></span>
<span id="cb17-17">  ) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span></span>
<span id="cb17-18">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">theme_minimal</span>() <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span></span>
<span id="cb17-19">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">theme</span>(</span>
<span id="cb17-20">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">plot.title =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">element_text</span>(</span>
<span id="cb17-21">      <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">hjust =</span> <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.5</span>, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">size =</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">14</span>, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">face =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"bold"</span></span>
<span id="cb17-22">    ),</span>
<span id="cb17-23">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">legend.position =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"bottom"</span>,</span>
<span id="cb17-24">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">panel.grid.minor =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">element_blank</span>(),</span>
<span id="cb17-25">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">axis.text =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">element_text</span>(<span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">size =</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">10</span>),</span>
<span id="cb17-26">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">axis.title =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">element_text</span>(<span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">size =</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">12</span>)</span>
<span id="cb17-27">  )</span></code></pre></div>
</details>
</div>
<p>The R version uses ggplot2 with a colorblind-friendly palette. The <code>geom_smooth()</code> layer adds per-species trend lines that reveal Simpson’s paradox: the overall negative correlation between bill length and depth reverses when one conditions on species.</p>
</section>
<section id="visualization-in-python-seaborn" class="level3">
<h3 class="anchored" data-anchor-id="visualization-in-python-seaborn">Visualization in Python (Seaborn)</h3>
<div class="cell" data-python.reticulate="false">
<details class="code-fold">
<summary>Code</summary>
<div class="sourceCode cell-code" id="cb18" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb18-1"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> pandas <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">as</span> pd</span>
<span id="cb18-2"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> seaborn <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">as</span> sns</span>
<span id="cb18-3"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> matplotlib.pyplot <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">as</span> plt</span>
<span id="cb18-4"></span>
<span id="cb18-5">sns.set_theme(style<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"whitegrid"</span>)</span>
<span id="cb18-6">sns.set_palette(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Set1"</span>)</span>
<span id="cb18-7"></span>
<span id="cb18-8">penguins <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> pd.read_csv(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"penguins.csv"</span>).dropna()</span>
<span id="cb18-9"></span>
<span id="cb18-10">plt.figure(figsize<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">10</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">6</span>))</span>
<span id="cb18-11"></span>
<span id="cb18-12">sns.scatterplot(</span>
<span id="cb18-13">    data<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>penguins,</span>
<span id="cb18-14">    x<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'bill_length_mm'</span>,</span>
<span id="cb18-15">    y<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'bill_depth_mm'</span>,</span>
<span id="cb18-16">    hue<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'species'</span>,</span>
<span id="cb18-17">    style<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'species'</span>,</span>
<span id="cb18-18">    s<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">100</span>,</span>
<span id="cb18-19">    alpha<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.7</span></span>
<span id="cb18-20">)</span>
<span id="cb18-21"></span>
<span id="cb18-22">sns.regplot(</span>
<span id="cb18-23">    data<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>penguins,</span>
<span id="cb18-24">    x<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'bill_length_mm'</span>,</span>
<span id="cb18-25">    y<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'bill_depth_mm'</span>,</span>
<span id="cb18-26">    scatter<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">False</span>,</span>
<span id="cb18-27">    color<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'gray'</span>,</span>
<span id="cb18-28">    line_kws<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>{<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'alpha'</span>: <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.5</span>}</span>
<span id="cb18-29">)</span>
<span id="cb18-30"></span>
<span id="cb18-31">plt.title(</span>
<span id="cb18-32">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'Bill Depth vs Length (Python)'</span>,</span>
<span id="cb18-33">    pad<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">20</span>, size<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">14</span>, weight<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'bold'</span></span>
<span id="cb18-34">)</span>
<span id="cb18-35">plt.xlabel(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'Bill Length (mm)'</span>, size<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">12</span>)</span>
<span id="cb18-36">plt.ylabel(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'Bill Depth (mm)'</span>, size<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">12</span>)</span>
<span id="cb18-37"></span>
<span id="cb18-38">plt.legend(</span>
<span id="cb18-39">    title<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'Species'</span>,</span>
<span id="cb18-40">    bbox_to_anchor<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>(<span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.5</span>, <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span><span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.15</span>),</span>
<span id="cb18-41">    loc<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'upper center'</span>,</span>
<span id="cb18-42">    ncol<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span></span>
<span id="cb18-43">)</span>
<span id="cb18-44"></span>
<span id="cb18-45">plt.tight_layout()</span>
<span id="cb18-46">plt.show()</span></code></pre></div>
</details>
</div>
<p>The Python version uses Seaborn, which provides a statistical plotting interface on top of Matplotlib. The <code>regplot()</code> adds an overall trend line in gray, making Simpson’s paradox visible: the gray line slopes downward while species-specific patterns slope upward.</p>
</section>
<section id="visualization-in-julia-unicodeplots" class="level3">
<h3 class="anchored" data-anchor-id="visualization-in-julia-unicodeplots">Visualization in Julia (UnicodePlots)</h3>
<div class="sourceCode" id="cb19" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb19-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">using</span> UnicodePlots</span>
<span id="cb19-2"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">using</span> DataFrames</span>
<span id="cb19-3"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">using</span> CSV</span>
<span id="cb19-4"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">using</span> Statistics</span>
<span id="cb19-5"></span>
<span id="cb19-6"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">penguins</span> = CSV.read<span class="er" style="color: #AD0000;
background-color: null;
font-style: inherit;">(</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"penguins.csv"</span><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">,</span> DataFrame<span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">)</span></span>
<span id="cb19-7"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">dropmissing!</span><span class="er" style="color: #AD0000;
background-color: null;
font-style: inherit;">(</span><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">penguins</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">)</span></span>
<span id="cb19-8"></span>
<span id="cb19-9"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">plt</span> = scatterplot<span class="er" style="color: #AD0000;
background-color: null;
font-style: inherit;">(</span></span>
<span id="cb19-10">    <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">penguins.bill_length_mm,</span></span>
<span id="cb19-11">    <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">penguins.bill_depth_mm,</span></span>
<span id="cb19-12">    <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">name</span> = string.<span class="er" style="color: #AD0000;
background-color: null;
font-style: inherit;">(</span><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">penguins.species</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">)</span><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb19-13">    <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">title</span> = <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Bill Depth vs Length (Julia)"</span>,</span>
<span id="cb19-14">    <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">xlabel</span> = <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Bill Length (mm)"</span>,</span>
<span id="cb19-15">    <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">ylabel</span> = <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Bill Depth (mm)"</span>,</span>
<span id="cb19-16">    <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">canvas</span> = DotCanvas</span>
<span id="cb19-17"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">)</span></span>
<span id="cb19-18"></span>
<span id="cb19-19"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">plt</span></span></code></pre></div>
<p>Julia’s UnicodePlots creates text-based scatter plots that render reliably in any terminal or HTML environment. The trade-off is fewer aesthetic options compared to ggplot2 or Seaborn, but the performance advantage for large datasets can be substantial.</p>
</section>
<section id="visualization-in-observable-js" class="level3">
<h3 class="anchored" data-anchor-id="visualization-in-observable-js">Visualization in Observable JS</h3>
<div class="cell">
<details class="code-fold">
<summary>Code</summary>
<div class="sourceCode cell-code" id="cb20" data-startfrom="493" data-source-offset="-0" style="background: #f1f3f5;"><pre class="sourceCode js code-with-copy"><code class="sourceCode javascript" style="counter-reset: source-line 492;"><span id="cb20-493"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> { Plot } <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">from</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"@observablehq/plot"</span></span>
<span id="cb20-494"></span>
<span id="cb20-495">penguins <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">FileAttachment</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"penguins.csv"</span>)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">csv</span>()</span>
<span id="cb20-496"></span>
<span id="cb20-497">Plot<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">plot</span>({</span>
<span id="cb20-498">  <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">color</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> {<span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">scheme</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"category10"</span>}<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb20-499">  <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">marks</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> [</span>
<span id="cb20-500">    Plot<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">dot</span>(penguins<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> {</span>
<span id="cb20-501">      <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">x</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"bill_length_mm"</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb20-502">      <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">y</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"bill_depth_mm"</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb20-503">      <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">stroke</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"species"</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb20-504">      <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">fill</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"species"</span></span>
<span id="cb20-505">    })</span>
<span id="cb20-506">  ]<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb20-507">  <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">x</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> {<span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">label</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Bill Length (mm)"</span>}<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb20-508">  <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">y</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> {<span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">label</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Bill Depth (mm)"</span>}<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb20-509">  <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">title</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Bill Depth vs Length (Observable JS)"</span></span>
<span id="cb20-510">})</span></code></pre></div>
</details>
<div class="cell-output cell-output-display">
<div>
<div id="ojs-cell-2-1" data-nodetype="declaration">

</div>
</div>
</div>
<div class="cell-output cell-output-display">
<div>
<div id="ojs-cell-2-2" data-nodetype="declaration">

</div>
</div>
</div>
<div class="cell-output cell-output-display">
<div>
<div id="ojs-cell-2-3" data-nodetype="expression">

</div>
</div>
</div>
</div>
<p>Observable JS produces interactive plots that respond to mouse hover and pan events natively in the browser. No server is required once the HTML is rendered.</p>
</section>
<section id="language-comparison-summary" class="level3">
<h3 class="anchored" data-anchor-id="language-comparison-summary">Language Comparison Summary</h3>
<p>Each language brings distinct strengths to the table:</p>
<ul>
<li><strong>R (ggplot2)</strong>: Publication-quality static plots with deep customization and a grammar of graphics approach.</li>
<li><strong>Python (Seaborn)</strong>: Clean integration with statistical functions and strong support for categorical variables.</li>
<li><strong>Julia (UnicodePlots)</strong>: Fast performance and text-based output that works anywhere.</li>
<li><strong>Observable JS</strong>: Interactive web-native visualization with no server requirements.</li>
</ul>
</section>
</section>
<section id="checking-our-work" class="level1">
<h1>Checking Our Work</h1>
<p>After verifying the minimal example, check that all languages can access the same files and share data. Working directory consistency is the most common source of subtle bugs.</p>
<section id="working-directory-alignment" class="level2">
<h2 class="anchored" data-anchor-id="working-directory-alignment">Working Directory Alignment</h2>
<p>All languages need to access the same files. Check working directories in each:</p>
<div class="sourceCode" id="cb21" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb21-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">getwd</span>()</span>
<span id="cb21-2"></span>
<span id="cb21-3">import os</span>
<span id="cb21-4"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">os.getcwd</span>()</span>
<span id="cb21-5"></span>
<span id="cb21-6"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">pwd</span>()</span></code></pre></div>
<p>If any of these return different directories, the other will produce “file not found” errors when it attempts to read data written by the first.</p>
</section>
<section id="memory-monitoring" class="level2">
<h2 class="anchored" data-anchor-id="memory-monitoring">Memory Monitoring</h2>
<p>Running four language runtimes simultaneously is memory-intensive. Monitor usage if you experience slowdowns:</p>
<div class="sourceCode" id="cb22" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb22-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">gc</span>()</span>
<span id="cb22-2"></span>
<span id="cb22-3">import psutil</span>
<span id="cb22-4"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">psutil.Process</span>()<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">.memory_info</span>().rss <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1024</span> <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1024</span></span></code></pre></div>
</section>
<section id="things-to-watch-out-for" class="level2">
<h2 class="anchored" data-anchor-id="things-to-watch-out-for">Things to Watch Out For</h2>
<ol type="1">
<li><p><strong>Python path confusion.</strong> macOS ships with a system Python that is not the one you want. Always verify that reticulate points to your Anaconda or Miniconda installation, not <code>/usr/bin/python3</code>.</p></li>
<li><p><strong>Julia first-run compilation.</strong> The first time you call <code>julia_setup()</code>, Julia compiles its package cache. This can take several minutes and may look like the session has frozen. Be patient.</p></li>
<li><p><strong>Observable JS is HTML-only.</strong> Observable chunks do not execute when rendering to PDF or Word. If all four languages are needed in PDF output, a different approach for the JavaScript visualizations will be required.</p></li>
<li><p><strong>Working directory drift.</strong> Each language engine may start in a slightly different working directory. Use absolute paths or verify <code>getwd()</code> / <code>os.getcwd()</code> / <code>pwd()</code> in each language before reading shared files.</p></li>
<li><p><strong>Memory pressure.</strong> Running R, Python, and Julia simultaneously in a single Quarto render can consume 4+ GB of RAM. Close other applications and monitor Activity Monitor if you experience crashes.</p></li>
</ol>
</section>
</section>
<section id="daily-workflow" class="level1">
<h1>Daily Workflow</h1>
<p>Once all four languages are configured, these commands become routine:</p>
<table class="caption-top table">
<thead>
<tr class="header">
<th>Command</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><code>quarto render doc.qmd --to html</code></td>
<td>Render multi-language document</td>
</tr>
<tr class="even">
<td><code>quarto preview</code></td>
<td>Live preview with auto-reload</td>
</tr>
<tr class="odd">
<td><code>Rscript -e "reticulate::py_config()"</code></td>
<td>Verify Python binding</td>
</tr>
<tr class="even">
<td><code>Rscript -e "JuliaCall::julia_setup()"</code></td>
<td>Verify Julia binding</td>
</tr>
<tr class="odd">
<td><code>conda activate quarto-env</code></td>
<td>Activate the Python environment</td>
</tr>
</tbody>
</table>
</section>
<section id="uninstall-rollback" class="level1">
<h1>Uninstall / Rollback</h1>
<p>To remove individual language integrations without affecting the others:</p>
<div class="sourceCode" id="cb23" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb23-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Remove Julia integration</span></span>
<span id="cb23-2"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">Rscript</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-e</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"remove.packages('JuliaCall')"</span></span>
<span id="cb23-3"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">brew</span> uninstall julia        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># macOS</span></span>
<span id="cb23-4"></span>
<span id="cb23-5"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Remove Python integration</span></span>
<span id="cb23-6"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">Rscript</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-e</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"remove.packages('reticulate')"</span></span>
<span id="cb23-7"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">conda</span> remove <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--name</span> quarto-env <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--all</span></span>
<span id="cb23-8"></span>
<span id="cb23-9"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Remove Quarto itself</span></span>
<span id="cb23-10"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">brew</span> uninstall quarto        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># macOS</span></span>
<span id="cb23-11"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sudo</span> apt remove quarto       <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Ubuntu / Debian</span></span></code></pre></div>
<p>R and RStudio remain functional without any of the above.</p>
<p><img src="https://rgtlab.org/posts/pub-multi-language-quarto/media/images/ambiance3.png" class="img-fluid" alt="UCSD Geisel Library: a reminder that good documentation, like good architecture, requires careful structural planning."> {.img-fluid}</p>
<p><em>The best technical configurations, like the best libraries, are built on solid foundations.</em></p>
</section>
<section id="what-did-we-learn" class="level1">
<h1>What Did We Learn?</h1>
<section id="lessons-learnt" class="level2">
<h2 class="anchored" data-anchor-id="lessons-learnt">Lessons Learnt</h2>
<p><strong>Conceptual Understanding:</strong></p>
<ul>
<li>Multi-language Quarto documents are not just “running different languages”; they are an orchestration problem where R acts as the conductor.</li>
<li>Data sharing between languages requires a common serialization format (CSV works reliably; RDS and pickle do not cross language boundaries).</li>
<li>Each plotting library embodies a different philosophy: grammar of graphics (R), statistical defaults (Python), performance (Julia), interactivity (Observable).</li>
<li>Simpson’s paradox appeared naturally in the Palmer Penguins data, demonstrating why multi-view analysis across tools is valuable.</li>
</ul>
<p><strong>Technical Skills:</strong></p>
<ul>
<li>Configuring reticulate to use the correct Python installation requires explicit <code>use_python()</code> calls on most macOS systems.</li>
<li>JuliaCall’s <code>julia_setup()</code> performs first-run compilation that can take several minutes; this is normal, not an error.</li>
<li>Observable JS chunks require no installation but only work in HTML output format.</li>
<li>The <code>{verbatim}</code> chunk type in Quarto is useful for showing YAML and code examples without execution.</li>
</ul>
<p><strong>Gotchas and Pitfalls:</strong></p>
<ul>
<li>The macOS system Python and Anaconda Python will conflict if reticulate is not configured explicitly.</li>
<li>Julia package installation inside a Quarto render can time out; install packages separately beforehand.</li>
<li>Observable Plot’s API changes between versions; pin the version in your YAML dependencies.</li>
<li>Memory pressure from running four runtimes simultaneously can cause silent failures on machines with 8 GB RAM.</li>
</ul>
</section>
<section id="limitations" class="level2">
<h2 class="anchored" data-anchor-id="limitations">Limitations</h2>
<ul>
<li>This guide was tested on macOS only. Linux and Windows setups involve different PATH configurations and installer behaviours.</li>
<li>Julia integration through JuliaCall is less mature than Python integration through reticulate. Some Julia features may not work inside Quarto chunks.</li>
<li>Observable JS chunks cannot be rendered to PDF or Word output. Multi-format publishing requires fallback strategies for JavaScript content.</li>
<li>The guide assumes Anaconda for Python management. Users of pyenv, virtualenv, or system Python will need to adapt the reticulate configuration steps.</li>
<li>Memory requirements (8+ GB) may exclude older hardware from running all four languages simultaneously.</li>
<li>Package version compatibility across four language ecosystems is fragile. A major update to any one language can break the integration chain.</li>
</ul>
</section>
<section id="opportunities-for-improvement" class="level2">
<h2 class="anchored" data-anchor-id="opportunities-for-improvement">Opportunities for Improvement</h2>
<ol type="1">
<li><strong>Add Docker containerization</strong> to freeze all four language environments and eliminate host configuration variability.</li>
<li><strong>Create a setup validation script</strong> that checks all four language integrations and reports status in a single command.</li>
<li><strong>Explore Quarto’s native Julia engine</strong> (currently experimental) as an alternative to JuliaCall for more direct integration.</li>
<li><strong>Add Plotly or Altair</strong> as alternatives to Seaborn for Python, enabling interactive plots that match Observable JS’s capabilities.</li>
<li><strong>Build a project template</strong> with pre-configured <code>renv.lock</code>, <code>requirements.txt</code>, and Julia <code>Project.toml</code> files for one-command environment setup.</li>
<li><strong>Test on Linux and Windows</strong> to expand the guide beyond macOS-specific instructions.</li>
</ol>
</section>
</section>
<section id="wrapping-up" class="level1">
<h1>Wrapping Up</h1>
<p>Setting up a multi-language Quarto environment is less about any single installation and more about making four separate ecosystems aware of each other. R sits at the center, with reticulate and JuliaCall acting as bridges to Python and Julia, while Observable JS runs independently in the browser.</p>
<p>The process demonstrates that environment configuration is its own skill, distinct from programming in any one language. The time spent getting PATH variables, package versions, and working directories aligned pays off every time a document is rendered that would otherwise require four separate scripts and manual assembly.</p>
<p>For those starting from scratch, the strongest recommendation is to verify each language independently before combining them. Debugging a four-language pipeline is appreciably harder than debugging a single-language problem, and the extra verification step is worth the investment.</p>
<p>In conclusion, four points merit emphasis. First, R orchestrates the other languages through reticulate (Python) and JuliaCall (Julia), while Observable JS runs independently in the browser; understanding this topology is prerequisite to diagnosing any configuration failure. Second, each language integration should be verified independently before combining them in a single document. Third, CSV is the most reliable common data exchange format between languages; binary formats do not cross language boundaries. Fourth, one should budget extra time for first-run Julia compilations and PATH configuration for Python on macOS, both of which can appear to freeze the session but resolve with patience.</p>
</section>
<section id="see-also" class="level1">
<h1>See Also</h1>
<p><strong>Related posts:</strong></p>
<ul>
<li><a href="https://quarto.org/docs/guide/">Quarto Documentation</a>: the official guide covers multi-language support in depth.</li>
</ul>
<p><strong>Key resources:</strong></p>
<ul>
<li><a href="https://rstudio.github.io/reticulate/">Reticulate Documentation</a>: R-Python bridge package reference.</li>
<li><a href="https://non-contradiction.github.io/JuliaCall/index.html">JuliaCall Documentation</a>: R-Julia bridge package reference.</li>
<li><a href="https://observablehq.com/@observablehq/plot-api-reference">Observable Plot API Reference</a>: Observable JS plotting library.</li>
<li><a href="https://bookdown.org/yihui/rmarkdown-cookbook/">R Markdown Cookbook</a>: comprehensive reference for literate programming in R.</li>
</ul>
</section>
<section id="reproducibility" class="level1">
<h1>Reproducibility</h1>
<section id="environment-requirements" class="level2">
<h2 class="anchored" data-anchor-id="environment-requirements">Environment Requirements</h2>
<ul>
<li><strong>R</strong>: Version 4.4 or later</li>
<li><strong>Python</strong>: Anaconda distribution recommended</li>
<li><strong>Julia</strong>: Version 1.9 or later</li>
<li><strong>Quarto</strong>: Version 1.4 or later</li>
<li><strong>macOS</strong>: Monterey or later</li>
</ul>
</section>
<section id="version-checks" class="level2">
<h2 class="anchored" data-anchor-id="version-checks">Version Checks</h2>
<div class="sourceCode" id="cb24" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb24-1">R.version.string</span>
<span id="cb24-2"></span>
<span id="cb24-3"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">library</span>(reticulate)</span>
<span id="cb24-4"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">py_config</span>()</span>
<span id="cb24-5"></span>
<span id="cb24-6"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">library</span>(JuliaCall)</span>
<span id="cb24-7"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">julia_eval</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"VERSION"</span>)</span>
<span id="cb24-8"></span>
<span id="cb24-9">quarto<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">quarto_version</span>()</span></code></pre></div>
</section>
<section id="session-information" class="level2">
<h2 class="anchored" data-anchor-id="session-information">Session Information</h2>
<div class="cell">
<details class="code-fold">
<summary>Code</summary>
<div class="sourceCode cell-code" id="cb25" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb25-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sessionInfo</span>()</span></code></pre></div>
</details>
</div>
</section>
</section>
<section id="lets-connect" class="level1">
<h1>Let’s Connect</h1>
<p><em>Have questions, suggestions, or spot an error? Let me know.</em></p>
<ul>
<li><strong>GitHub:</strong> <a href="https://github.com/rgt47">rgt47</a></li>
<li><strong>Twitter/X:</strong> <a href="https://twitter.com/rgt47"><span class="citation" data-cites="rgt47">@rgt47</span></a></li>
<li><strong>LinkedIn:</strong> <a href="https://linkedin.com/in/rgthomaslab">Ronald Glenn Thomas</a></li>
<li><strong>Email:</strong> <a href="https://rgtlab.org/contact">Contact form</a></li>
</ul>
<p>I would enjoy hearing from you if:</p>
<ul>
<li>You spot an error or a better approach to any of the code in this post.</li>
<li>You have suggestions for topics you would like to see covered.</li>
<li>You want to discuss R programming, data science, or reproducible research.</li>
<li>You have questions about anything in this tutorial.</li>
<li>You just want to say hello and connect.</li>
</ul>
<section id="related-posts-in-this-cluster" class="level2">
<h2 class="anchored" data-anchor-id="related-posts-in-this-cluster">Related posts in this cluster</h2>
<p>This post is part of the <em>Quarto, R Markdown, and Publishing</em> series. Recommended reading order:</p>
<ol type="1">
<li><strong>Post 80: Multi-Language Quarto Documents on macOS</strong> (this post)</li>
<li>Post 81: <a href="../81-pub-r-script-to-rmd/">Rapid Conversion of Draft R Scripts to Formal Rmd</a></li>
<li>Post 83: <a href="../83-pub-statistical-computing-textbook/">Building a Statistical Computing Textbook</a></li>
<li>Post 84: <a href="../84-pub-obs-r-screencasts/">Setting up OBS for Live R Coding Screencasts</a></li>
</ol>


</section>
</section>

 ]]></description>
  <category>quarto</category>
  <category>r</category>
  <category>python</category>
  <category>julia</category>
  <guid>https://rgtlab.org/posts/pub-multi-language-quarto/</guid>
  <pubDate>Mon, 08 Jun 2026 07:00:00 GMT</pubDate>
  <media:content url="https://rgtlab.org/posts/pub-multi-language-quarto/media/images/hero.png" medium="image" type="image/png" height="80" width="144"/>
</item>
<item>
  <title>Rapid Conversion of Draft R Scripts to Formal Rmd Reports</title>
  <dc:creator>Ronald G. Thomas</dc:creator>
  <link>https://rgtlab.org/posts/pub-r-script-to-rmd/</link>
  <description><![CDATA[ 




<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/pub-r-script-to-rmd/media/images/hero.png" class="img-fluid figure-img" style="width:80.0%"></p>
<figcaption>A structured workflow transforms messy R scripts into polished analytical reports</figcaption>
</figure>
</div>
<section id="introduction" class="level1">
<h1>Introduction</h1>
<p>I did not really know how to quickly convert a working R script into a presentable report until I ran into a situation where a collaborator needed results the same afternoon. The analysis was complete, the code was tested, but the output was a tangled mix of console printouts and saved PNG files scattered across a directory. Assembling those fragments into a coherent document took longer than the analysis itself.</p>
<p>Three things many R developers are often reluctant to tackle: code documentation, testing, and report preparation. Of these, report preparation tends to be the most urgent. When a supervisor or reviewer asks for results, they rarely want a raw R script. They want tables, figures, narrative context, and a document they can read without opening RStudio.</p>
<p>We walk through several approaches to bridging the gap between a working R script and a finished R Markdown report. The goal is practical: reduce the friction between “my code works” and “here is a document someone can read.”</p>
<p>More formally, this post documents the script-to-document promotion path in the Document layer of the Workflow Construct described in <a href="../../posts/52-workflow-construct/">post 52</a>. The Document layer holds the artefact a reader sees (a rendered HTML or PDF report); the script-to-document promotion is the operation that lifts a working R script onto that layer without rewriting the analysis. This is the most frequent project-tier transition for an applied biostatistician and is the specific concern the post addresses.</p>
<section id="motivations" class="level2">
<h2 class="anchored" data-anchor-id="motivations">Motivations</h2>
<ul>
<li>I had a folder of working R scripts that produced correct results but existed only as console output and disconnected figure files.</li>
<li>Collaborators asked for formatted reports repeatedly, and each time I rebuilt the narrative from scratch rather than converting what already existed.</li>
<li>I wanted a systematic approach that would take minutes, not hours, to move from script to document.</li>
<li>I needed to preserve the exact code that generated the results, not rewrite it for presentation purposes.</li>
<li>I was curious whether <code>knitr::spin()</code> could genuinely replace manual chunk insertion for simple analytical scripts.</li>
</ul>
</section>
<section id="objectives" class="level2">
<h2 class="anchored" data-anchor-id="objectives">Objectives</h2>
<ol type="1">
<li>Understand the <code>knitr::spin()</code> function and how it converts annotated R scripts directly into R Markdown documents.</li>
<li>Compare the <code>spin()</code> approach against manual conversion (adding YAML headers and wrapping code in chunks by hand).</li>
<li>Use RStudio’s built-in “Compile Report” feature to generate quick reports without modifying the original script.</li>
<li>Develop a reusable template workflow for consistent formatting across multiple script-to-report conversions.</li>
</ol>
<p>I am documenting my learning process here. Errors and better approaches are welcome; see the contact section.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/pub-r-script-to-rmd/media/images/ambiance1.png" class="img-fluid figure-img" style="width:30.0%"></p>
<figcaption>Settling in for a focused coding session.</figcaption>
</figure>
</div>
</section>
</section>
<section id="prerequisites-and-setup" class="level1">
<h1>Prerequisites and Setup</h1>
<p>The techniques in this post require a standard R installation with the <code>knitr</code> and <code>rmarkdown</code> packages. RStudio is recommended for the “Compile Report” feature but is not strictly necessary for the command-line workflows.</p>
<p><strong>Background:</strong> This post assumes familiarity with writing R scripts and a basic understanding of what R Markdown documents look like. No prior experience with <code>knitr::spin()</code> is needed.</p>
<div class="cell">
<div class="sourceCode cell-code" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb1-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">install.packages</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">c</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"knitr"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"rmarkdown"</span>))</span></code></pre></div>
</div>
<div class="cell">
<div class="sourceCode cell-code" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb2-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">library</span>(knitr)</span>
<span id="cb2-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">library</span>(rmarkdown)</span></code></pre></div>
</div>
</section>
<section id="what-is-script-to-report-conversion" class="level1">
<h1>What is Script-to-Report Conversion?</h1>
<p>Script-to-report conversion is the process of transforming a plain R script (a <code>.R</code> file containing code, comments, and possibly some inline output) into a formatted document that weaves together code, results, and narrative text. The output is typically an HTML page, PDF, or Word document that a non-programmer can read without needing to run any code.</p>
<p>Think of it as the reverse of the usual R Markdown workflow. Instead of starting with a <code>.qmd</code> or <code>.Rmd</code> file and embedding code chunks, one starts with a working <code>.R</code> file and adds just enough structure to produce a readable report. The key insight is that existing comments can serve as the narrative, and existing code already produces the figures and tables needed.</p>
</section>
<section id="getting-started-the-problem-with-raw-scripts" class="level1">
<h1>Getting Started: The Problem with Raw Scripts</h1>
<p>Consider a typical analytical R script. It loads data, fits a model, prints a summary, and saves a plot. The code works perfectly, but the output is fragmented: summary statistics appear in the console, the plot is saved to disk, and there is no unified document tying them together.</p>
<div class="cell">
<div class="sourceCode cell-code" id="cb3" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb3-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">library</span>(ggplot2)</span>
<span id="cb3-2"></span>
<span id="cb3-3"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">data</span>(mtcars)</span>
<span id="cb3-4">mtcars<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">$</span>cyl <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">as.factor</span>(mtcars<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">$</span>cyl)</span>
<span id="cb3-5"></span>
<span id="cb3-6"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">summary</span>(mtcars[, <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">c</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"mpg"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"wt"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"hp"</span>)])</span>
<span id="cb3-7"></span>
<span id="cb3-8">model <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">lm</span>(mpg <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">~</span> wt <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> hp, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">data =</span> mtcars)</span>
<span id="cb3-9"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">summary</span>(model)</span>
<span id="cb3-10"></span>
<span id="cb3-11"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">ggplot</span>(mtcars, <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">aes</span>(<span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">x =</span> wt, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">y =</span> mpg, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">color =</span> cyl)) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span></span>
<span id="cb3-12">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">geom_point</span>(<span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">size =</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span></span>
<span id="cb3-13">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">geom_smooth</span>(<span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">method =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"lm"</span>, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">se =</span> <span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">FALSE</span>) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span></span>
<span id="cb3-14">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">theme_minimal</span>() <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span></span>
<span id="cb3-15">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">labs</span>(</span>
<span id="cb3-16">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">title =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Fuel Efficiency by Weight"</span>,</span>
<span id="cb3-17">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">x =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Weight (1000 lbs)"</span>,</span>
<span id="cb3-18">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">y =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Miles per Gallon"</span></span>
<span id="cb3-19">  )</span>
<span id="cb3-20"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">ggsave</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"mpg_by_weight.png"</span>, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">width =</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">8</span>, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">height =</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">5</span>)</span></code></pre></div>
</div>
<p>This script runs correctly, but sharing its results requires manually assembling the console output, the saved figure, and some written context into a separate document. The three approaches described below solve this problem at different levels of effort and control.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/pub-r-script-to-rmd/media/images/ambiance2.png" class="img-fluid figure-img"></p>
<figcaption>Transitioning from raw scripts to structured reports</figcaption>
</figure>
</div>
</section>
<section id="approach-1-knitrspin-the-fastest-path" class="level1">
<h1>Approach 1: <code>knitr::spin()</code> (The Fastest Path)</h1>
<p>The <code>knitr::spin()</code> function converts a specially annotated R script directly into an R Markdown document (and optionally renders it in one step). The key is a lightweight annotation syntax that uses R comments to embed narrative text and chunk options without restructuring the script.</p>
<section id="how-spin-annotation-works" class="level2">
<h2 class="anchored" data-anchor-id="how-spin-annotation-works">How spin() Annotation Works</h2>
<p>In a spin-compatible R script, three comment styles carry special meaning:</p>
<ul>
<li><code>#'</code> (hash-apostrophe) marks lines as narrative Markdown text.</li>
<li><code>#-</code> (hash-hyphen) starts a new unnamed code chunk.</li>
<li><code>#+ label, option=value</code> (hash-plus) starts a named chunk with explicit options.</li>
</ul>
<p>Everything else is treated as R code inside the current chunk. This means an existing script can be annotated with minimal changes.</p>
</section>
<section id="annotating-an-existing-script" class="level2">
<h2 class="anchored" data-anchor-id="annotating-an-existing-script">Annotating an Existing Script</h2>
<p>Here is the same analysis script rewritten with spin annotations. The original code is unchanged; only comments have been modified.</p>
<div class="cell">
<div class="sourceCode cell-code" id="cb4" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb4-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#' ---</span></span>
<span id="cb4-2"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#' title: "Fuel Efficiency Analysis"</span></span>
<span id="cb4-3"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#' author: "Ronald G. Thomas"</span></span>
<span id="cb4-4"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#' date: today</span></span>
<span id="cb4-5"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#' output: html_document</span></span>
<span id="cb4-6"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#' ---</span></span>
<span id="cb4-7"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#'</span></span>
<span id="cb4-8"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#' # Overview</span></span>
<span id="cb4-9"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#'</span></span>
<span id="cb4-10"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#' This report examines the relationship between</span></span>
<span id="cb4-11"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#' vehicle weight, horsepower, and fuel efficiency</span></span>
<span id="cb4-12"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#' using the mtcars dataset.</span></span>
<span id="cb4-13"></span>
<span id="cb4-14"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#+ setup, message=FALSE</span></span>
<span id="cb4-15"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">library</span>(ggplot2)</span>
<span id="cb4-16"></span>
<span id="cb4-17"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#' # Data Preparation</span></span>
<span id="cb4-18"></span>
<span id="cb4-19"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">data</span>(mtcars)</span>
<span id="cb4-20">mtcars<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">$</span>cyl <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">as.factor</span>(mtcars<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">$</span>cyl)</span>
<span id="cb4-21"></span>
<span id="cb4-22"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#' # Summary Statistics</span></span>
<span id="cb4-23"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#'</span></span>
<span id="cb4-24"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#' The dataset contains 32 observations across</span></span>
<span id="cb4-25"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#' three engine configurations.</span></span>
<span id="cb4-26"></span>
<span id="cb4-27"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">summary</span>(mtcars[, <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">c</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"mpg"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"wt"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"hp"</span>)])</span>
<span id="cb4-28"></span>
<span id="cb4-29"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#' # Linear Model</span></span>
<span id="cb4-30"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#'</span></span>
<span id="cb4-31"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#' We fit a multiple regression predicting miles</span></span>
<span id="cb4-32"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#' per gallon from weight and horsepower.</span></span>
<span id="cb4-33"></span>
<span id="cb4-34">model <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">lm</span>(mpg <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">~</span> wt <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> hp, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">data =</span> mtcars)</span>
<span id="cb4-35"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">summary</span>(model)</span>
<span id="cb4-36"></span>
<span id="cb4-37"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#' # Visualization</span></span>
<span id="cb4-38"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#'</span></span>
<span id="cb4-39"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#' The scatterplot below shows the relationship</span></span>
<span id="cb4-40"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#' between weight and fuel efficiency, colored</span></span>
<span id="cb4-41"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#' by cylinder count.</span></span>
<span id="cb4-42"></span>
<span id="cb4-43"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#+ fig-mpg, fig.width=8, fig.height=5</span></span>
<span id="cb4-44"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">ggplot</span>(mtcars, <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">aes</span>(<span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">x =</span> wt, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">y =</span> mpg, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">color =</span> cyl)) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span></span>
<span id="cb4-45">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">geom_point</span>(<span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">size =</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span></span>
<span id="cb4-46">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">geom_smooth</span>(<span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">method =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"lm"</span>, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">se =</span> <span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">FALSE</span>) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span></span>
<span id="cb4-47">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">theme_minimal</span>() <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span></span>
<span id="cb4-48">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">labs</span>(</span>
<span id="cb4-49">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">title =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Fuel Efficiency by Weight"</span>,</span>
<span id="cb4-50">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">x =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Weight (1000 lbs)"</span>,</span>
<span id="cb4-51">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">y =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Miles per Gallon"</span></span>
<span id="cb4-52">  )</span></code></pre></div>
</div>
</section>
<section id="running-spin" class="level2">
<h2 class="anchored" data-anchor-id="running-spin">Running spin()</h2>
<p>To convert and render in one step:</p>
<div class="cell">
<div class="sourceCode cell-code" id="cb5" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb5-1">knitr<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">spin</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"analysis_script.R"</span>, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">knit =</span> <span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">FALSE</span>)</span></code></pre></div>
</div>
<p>Setting <code>knit = FALSE</code> produces the <code>.Rmd</code> file without rendering it, which is useful for inspecting or editing the intermediate Markdown before producing the final document. To generate the report directly:</p>
<div class="cell">
<div class="sourceCode cell-code" id="cb6" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb6-1">knitr<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">spin</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"analysis_script.R"</span>, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">knit =</span> <span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">TRUE</span>)</span></code></pre></div>
</div>
<p>The function also accepts a <code>format</code> argument for producing different output types:</p>
<div class="cell">
<div class="sourceCode cell-code" id="cb7" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb7-1">rmarkdown<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">render</span>(</span>
<span id="cb7-2">  knitr<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">spin</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"analysis_script.R"</span>, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">knit =</span> <span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">FALSE</span>),</span>
<span id="cb7-3">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">output_format =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"pdf_document"</span></span>
<span id="cb7-4">)</span></code></pre></div>
</div>
</section>
</section>
<section id="approach-2-manual-conversion" class="level1">
<h1>Approach 2: Manual Conversion</h1>
<p>Manual conversion involves creating a new <code>.Rmd</code> file and migrating code from the R script into fenced code chunks. This approach provides the most control over document structure but requires the most effort.</p>
<section id="the-conversion-steps" class="level2">
<h2 class="anchored" data-anchor-id="the-conversion-steps">The Conversion Steps</h2>
<p>The process follows a predictable sequence:</p>
<ol type="1">
<li>Create a new <code>.Rmd</code> file with a YAML header.</li>
<li>Copy code from the <code>.R</code> file into fenced code chunks (<code>```{r} ... ```</code>).</li>
<li>Convert existing comments into narrative Markdown text outside the chunks.</li>
<li>Add chunk options for figure sizing, echo control, and caching.</li>
<li>Render the document with <code>rmarkdown::render()</code> or the RStudio Knit button.</li>
</ol>
</section>
<section id="a-minimal-template-for-manual-conversion" class="level2">
<h2 class="anchored" data-anchor-id="a-minimal-template-for-manual-conversion">A Minimal Template for Manual Conversion</h2>
<p>The following template provides a starting point that covers the most common needs:</p>
<div class="cell">
<details class="code-fold">
<summary>Rmd template structure</summary>
<div class="sourceCode cell-code" id="cb8" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb8-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># The YAML header goes at the top of the .Rmd file:</span></span>
<span id="cb8-2"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#</span></span>
<span id="cb8-3"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># ---</span></span>
<span id="cb8-4"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># title: "Analysis Report"</span></span>
<span id="cb8-5"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># author: "Your Name"</span></span>
<span id="cb8-6"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># date: "`r Sys.Date()`"</span></span>
<span id="cb8-7"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># output:</span></span>
<span id="cb8-8"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#   html_document:</span></span>
<span id="cb8-9"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#     toc: true</span></span>
<span id="cb8-10"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#     toc_float: true</span></span>
<span id="cb8-11"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#     code_folding: hide</span></span>
<span id="cb8-12"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># ---</span></span>
<span id="cb8-13"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#</span></span>
<span id="cb8-14"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Then structure content as:</span></span>
<span id="cb8-15"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#</span></span>
<span id="cb8-16"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># # Section Heading</span></span>
<span id="cb8-17"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#</span></span>
<span id="cb8-18"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Narrative text explaining what comes next.</span></span>
<span id="cb8-19"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#</span></span>
<span id="cb8-20"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># ```{r chunk-name, fig.width=8, fig.height=5}</span></span>
<span id="cb8-21"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># your_code_here()</span></span>
<span id="cb8-22"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># ```</span></span>
<span id="cb8-23"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#</span></span>
<span id="cb8-24"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Interpretation of the output above.</span></span></code></pre></div>
</details>
</div>
</section>
<section id="when-manual-conversion-makes-sense" class="level2">
<h2 class="anchored" data-anchor-id="when-manual-conversion-makes-sense">When Manual Conversion Makes Sense</h2>
<p>Manual conversion is preferable when the R script contains code that should not appear in the report (data cleaning steps, debugging statements, exploratory tangents). The conversion process naturally requires curation of what the reader sees, which often improves the document.</p>
</section>
</section>
<section id="approach-3-rstudios-compile-report" class="level1">
<h1>Approach 3: RStudio’s “Compile Report”</h1>
<p>RStudio includes a “Compile Report” feature that converts any R script into a report without modifying the file at all. This is the zero-effort option.</p>
<section id="how-to-use-it" class="level2">
<h2 class="anchored" data-anchor-id="how-to-use-it">How to Use It</h2>
<ol type="1">
<li>Open an <code>.R</code> file in RStudio.</li>
<li>Click the notebook icon in the editor toolbar (or use <code>Ctrl+Shift+K</code> / <code>Cmd+Shift+K</code>).</li>
<li>Select the output format (HTML, PDF, or Word).</li>
<li>RStudio runs <code>knitr::spin()</code> behind the scenes and renders the result.</li>
</ol>
<p>The feature interprets <code>#'</code> comments as Markdown, just like <code>spin()</code>. If the script has no special annotations, all code and standard comments appear in the output as a simple code listing with results.</p>
</section>
<section id="practical-considerations" class="level2">
<h2 class="anchored" data-anchor-id="practical-considerations">Practical Considerations</h2>
<p>The “Compile Report” approach works well for quick sharing within a team but has limitations. The output format options are restricted to what <code>rmarkdown</code> supports natively. There is no opportunity to edit the intermediate <code>.Rmd</code> before rendering. And the feature does not support Quarto-specific options like <code>code-fold</code> or cross-references.</p>
<p>For scripts that already use <code>#'</code> annotations, this feature provides the fastest possible path from code to document. For scripts without annotations, the output is functional but visually plain.</p>
</section>
</section>
<section id="verification" class="level1">
<h1>Verification</h1>
<p>After annotating a script and running <code>spin()</code>, confirm that the conversion and rendering pipeline works end to end.</p>
<div class="cell">
<div class="sourceCode cell-code" id="cb9" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb9-1">knitr<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">spin</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"analysis_script.R"</span>, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">knit =</span> <span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">FALSE</span>)</span>
<span id="cb9-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">file.exists</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"analysis_script.Rmd"</span>)</span></code></pre></div>
</div>
<div class="cell">
<div class="sourceCode cell-code" id="cb10" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb10-1">rmarkdown<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">render</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"analysis_script.Rmd"</span>)</span>
<span id="cb10-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">file.exists</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"analysis_script.html"</span>)</span></code></pre></div>
</div>
<div class="cell">
<div class="sourceCode cell-code" id="cb11" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb11-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">readLines</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"analysis_script.Rmd"</span>, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">n =</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">5</span>)</span></code></pre></div>
</div>
<p>If all three checks return <code>TRUE</code> or display the expected YAML header, the spin pipeline is working correctly.</p>
</section>
<section id="daily-workflow" class="level1">
<h1>Daily Workflow</h1>
<table class="caption-top table">
<colgroup>
<col style="width: 40%">
<col style="width: 60%">
</colgroup>
<thead>
<tr class="header">
<th style="text-align: left;">Task</th>
<th style="text-align: left;">Command</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td style="text-align: left;">Annotate script</td>
<td style="text-align: left;">Add <code>#'</code>, <code>#+</code>, <code>#-</code> comments to <code>.R</code> file</td>
</tr>
<tr class="even">
<td style="text-align: left;">Convert to Rmd</td>
<td style="text-align: left;"><code>knitr::spin("script.R", knit = FALSE)</code></td>
</tr>
<tr class="odd">
<td style="text-align: left;">Inspect intermediate</td>
<td style="text-align: left;"><code>file.edit("script.Rmd")</code></td>
</tr>
<tr class="even">
<td style="text-align: left;">Render to HTML</td>
<td style="text-align: left;"><code>rmarkdown::render("script.Rmd")</code></td>
</tr>
<tr class="odd">
<td style="text-align: left;">Render to PDF</td>
<td style="text-align: left;"><code>rmarkdown::render("script.Rmd", output_format = "pdf_document")</code></td>
</tr>
<tr class="even">
<td style="text-align: left;">Quick report (RStudio)</td>
<td style="text-align: left;"><code>Ctrl+Shift+K</code> on open <code>.R</code> file</td>
</tr>
<tr class="odd">
<td style="text-align: left;">Batch convert</td>
<td style="text-align: left;"><code>lapply(list.files(pattern = "\\.R$"), knitr::spin, knit = FALSE)</code></td>
</tr>
</tbody>
</table>
<section id="things-to-watch-out-for" class="level2">
<h2 class="anchored" data-anchor-id="things-to-watch-out-for">Things to Watch Out For</h2>
<ol type="1">
<li><p><strong>Chunk boundaries matter in spin().</strong> Every line of code between <code>#'</code> blocks becomes a single chunk. If separate chunks are needed for different options (such as hiding one block while showing another), inserting a <code>#+</code> line starts a new chunk explicitly.</p></li>
<li><p><strong>YAML must be exact.</strong> The <code>#' ---</code> lines in a spin-annotated script must follow YAML syntax precisely. A missing space after a colon or an incorrect indentation level will cause the render to fail with an uninformative error message.</p></li>
<li><p><strong>Figure paths can conflict.</strong> When <code>spin()</code> runs, it creates a figures directory based on the script name. If two scripts share a name in different directories, the figure directories may collide. Use explicit <code>fig.path</code> chunk options to avoid this.</p></li>
<li><p><strong>Encoding issues on Windows.</strong> Scripts with non-ASCII characters (accented names, special symbols) may fail during the spin conversion. Ensure the script is saved with UTF-8 encoding before running <code>spin()</code>.</p></li>
<li><p><strong>The “Compile Report” button uses the global R environment.</strong> Scripts that depend on objects created in a previous session will fail when rendered in a clean session. Testing with <code>rmarkdown::render()</code> in a fresh R session catches environment dependencies.</p></li>
</ol>
</section>
</section>
<section id="comparing-the-three-approaches" class="level1">
<h1>Comparing the Three Approaches</h1>
<p>The choice between <code>spin()</code>, manual conversion, and “Compile Report” depends on the complexity of the script and the quality expectations for the output.</p>
<table class="caption-top table">
<thead>
<tr class="header">
<th style="text-align: left;">Criterion</th>
<th style="text-align: left;"><code>spin()</code></th>
<th style="text-align: left;">Manual</th>
<th style="text-align: left;">Compile Report</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td style="text-align: left;">Setup time</td>
<td style="text-align: left;">5-15 min</td>
<td style="text-align: left;">20-60 min</td>
<td style="text-align: left;">0 min</td>
</tr>
<tr class="even">
<td style="text-align: left;">Output control</td>
<td style="text-align: left;">Moderate</td>
<td style="text-align: left;">Full</td>
<td style="text-align: left;">Limited</td>
</tr>
<tr class="odd">
<td style="text-align: left;">Preserves original script</td>
<td style="text-align: left;">Yes</td>
<td style="text-align: left;">No (new file)</td>
<td style="text-align: left;">Yes</td>
</tr>
<tr class="even">
<td style="text-align: left;">Supports Quarto</td>
<td style="text-align: left;">No</td>
<td style="text-align: left;">Yes</td>
<td style="text-align: left;">No</td>
</tr>
<tr class="odd">
<td style="text-align: left;">Chunk-level options</td>
<td style="text-align: left;">Yes (<code>#+</code>)</td>
<td style="text-align: left;">Yes</td>
<td style="text-align: left;">Yes (<code>#+</code>)</td>
</tr>
<tr class="even">
<td style="text-align: left;">Narrative quality</td>
<td style="text-align: left;">Good</td>
<td style="text-align: left;">Best</td>
<td style="text-align: left;">Basic</td>
</tr>
<tr class="odd">
<td style="text-align: left;">Suitable for publication</td>
<td style="text-align: left;">Sometimes</td>
<td style="text-align: left;">Yes</td>
<td style="text-align: left;">Rarely</td>
</tr>
<tr class="even">
<td style="text-align: left;">Learning curve</td>
<td style="text-align: left;">Low</td>
<td style="text-align: left;">Low</td>
<td style="text-align: left;">None</td>
</tr>
</tbody>
</table>
<p>For quick internal sharing, “Compile Report” or <code>spin()</code> with minimal annotations is sufficient. For external-facing documents, grant proposals, or publications, manual conversion into a <code>.qmd</code> or <code>.Rmd</code> file provides the control needed for professional formatting.</p>
</section>
<section id="uninstall-rollback" class="level1">
<h1>Uninstall / Rollback</h1>
<p>The spin workflow does not modify system configuration. To revert, delete the generated intermediate and output files.</p>
<div class="cell">
<div class="sourceCode cell-code" id="cb12" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb12-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">file.remove</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"analysis_script.Rmd"</span>)</span>
<span id="cb12-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">file.remove</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"analysis_script.html"</span>)</span>
<span id="cb12-3"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">unlink</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"analysis_script_files"</span>, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">recursive =</span> <span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">TRUE</span>)</span></code></pre></div>
</div>
<p>The original <code>.R</code> script remains unchanged because <code>spin()</code> reads from it without modifying it. No packages need to be uninstalled; <code>knitr</code> and <code>rmarkdown</code> are standard components of any R installation.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/pub-r-script-to-rmd/media/images/ambiance3.png" class="img-fluid figure-img"></p>
<figcaption>Refining the workflow from draft to polished report</figcaption>
</figure>
</div>
</section>
<section id="what-did-we-learn" class="level1">
<h1>What Did We Learn?</h1>
<section id="lessons-learnt" class="level2">
<h2 class="anchored" data-anchor-id="lessons-learnt">Lessons Learnt</h2>
<p><strong>Conceptual Understanding:</strong></p>
<ul>
<li>The distinction between a script and a report is primarily about narrative structure, not about code correctness. The same analysis can be either, depending on how comments and output are organized.</li>
<li><code>spin()</code> treats R comments as a lightweight markup language. Understanding the three annotation styles (<code>#'</code>, <code>#-</code>, <code>#+</code>) is sufficient to convert most analytical scripts.</li>
<li>Report generation works best when the original script is already well-commented. Poorly commented code requires substantial editing regardless of the conversion method.</li>
<li>The intermediate <code>.Rmd</code> produced by <code>spin()</code> is a standard R Markdown file that can be further edited, making the spin-then-edit workflow a practical middle ground.</li>
</ul>
<p><strong>Technical Skills:</strong></p>
<ul>
<li><code>knitr::spin()</code> with <code>knit = FALSE</code> is the most useful invocation because it produces an editable intermediate file.</li>
<li>RStudio’s “Compile Report” calls <code>spin()</code> internally, so learning spin annotation syntax benefits both workflows.</li>
<li>Wrapping <code>spin()</code> output in <code>rmarkdown::render()</code> enables programmatic control over output format, parameters, and rendering options.</li>
<li>For Quarto-based projects, manual conversion remains necessary because <code>spin()</code> produces <code>.Rmd</code> files, not <code>.qmd</code> files.</li>
</ul>
<p><strong>Gotchas and Pitfalls:</strong></p>
<ul>
<li>Forgetting the space after <code>#'</code> causes the line to be treated as a code comment rather than narrative text.</li>
<li>Scripts that modify the working directory with <code>setwd()</code> inside the file will break the spin rendering process because <code>knitr</code> manages the working directory itself.</li>
<li>Large scripts with many plots can produce extremely long reports. Consider splitting into focused analytical sections before converting.</li>
<li>The <code>spin()</code> YAML block must start and end with <code>#' ---</code> on its own line. Inline YAML after other content on the same line fails silently.</li>
</ul>
</section>
<section id="limitations" class="level2">
<h2 class="anchored" data-anchor-id="limitations">Limitations</h2>
<ul>
<li><code>knitr::spin()</code> produces R Markdown (<code>.Rmd</code>) output only. It does not generate Quarto (<code>.qmd</code>) files, which limits access to Quarto-specific features such as cross-references, callouts, and multi-format rendering.</li>
<li>The annotation syntax (<code>#'</code>, <code>#+</code>, <code>#-</code>) is specific to the <code>knitr</code> package. Scripts annotated for <code>spin()</code> will not render correctly with other literate programming tools.</li>
<li>“Compile Report” is an RStudio-specific feature. VS Code, Positron, and terminal-based workflows do not have an equivalent one-click option (though calling <code>spin()</code> from the command line achieves the same result).</li>
<li>None of these approaches handle multi-file analyses well. If the analysis spans several scripts that must run in sequence, a proper R Markdown or Quarto document with sourced scripts is more appropriate.</li>
<li>The formatting produced by <code>spin()</code> is functional but not publication-ready. Figures lack captions and cross-references unless manually added in a post-processing step.</li>
</ul>
</section>
<section id="opportunities-for-improvement" class="level2">
<h2 class="anchored" data-anchor-id="opportunities-for-improvement">Opportunities for Improvement</h2>
<ol type="1">
<li>Write a wrapper function that runs <code>spin()</code> and then performs automated post-processing: adding figure captions, inserting a table of contents, and converting the output to <code>.qmd</code> format.</li>
<li>Develop a standard annotation template for R scripts that includes placeholders for YAML, section headers, and figure captions, reducing the cognitive load of adding annotations.</li>
<li>Explore the <code>litr</code> package, which takes the opposite approach: generating R packages from R Markdown documents, providing a complementary perspective on the code-document relationship.</li>
<li>Build a Makefile or shell script that batch-converts all <code>.R</code> files in a project directory, producing a set of HTML reports with consistent styling.</li>
<li>Investigate whether a custom <code>knitr</code> output hook could translate <code>spin()</code> output directly into Quarto <code>.qmd</code> syntax, bypassing the <code>.Rmd</code> intermediate step.</li>
</ol>
</section>
</section>
<section id="wrapping-up" class="level1">
<h1>Wrapping Up</h1>
<p>Converting R scripts to formatted reports does not need to be a time-consuming process. The <code>knitr::spin()</code> function provides a lightweight path that preserves the original script while adding just enough structure to produce a readable document. For situations requiring more control, manual conversion into R Markdown or Quarto gives full access to formatting, cross-references, and publication-quality output.</p>
<p>What I found most useful in practice was the spin-then-edit workflow: run <code>spin()</code> with <code>knit = FALSE</code> to generate the <code>.Rmd</code> skeleton, then spend a few minutes refining the narrative and chunk options before rendering. This consistently took less than fifteen minutes for a typical analytical script, compared to forty-five minutes or more for a from- scratch manual conversion.</p>
<p>In conclusion, four points merit emphasis. First, <code>knitr::spin()</code> converts annotated <code>.R</code> scripts to <code>.Rmd</code> using three comment styles: <code>#'</code>, <code>#+</code>, and <code>#-</code>. Second, RStudio’s “Compile Report” runs <code>spin()</code> behind the scenes with zero setup. Third, manual conversion provides full control and remains necessary for Quarto-based projects. Fourth, the spin-then-edit workflow balances speed with document quality.</p>
<p>For R scripts sitting in a project folder producing results that no one else can easily read, running <code>knitr::spin("your_script.R", knit = FALSE)</code> and inspecting the output is worthwhile. The result is often closer to a finished report than anticipated.</p>
</section>
<section id="see-also" class="level1">
<h1>See Also</h1>
<p><strong>Related resources:</strong></p>
<ul>
<li><a href="https://yihui.org/knitr/demo/stitch/">knitr::spin() documentation</a>: Yihui Xie’s official documentation and examples for the spin function.</li>
<li><a href="https://bookdown.org/yihui/rmarkdown/">R Markdown: The Definitive Guide</a>: Comprehensive reference covering all aspects of R Markdown document creation.</li>
<li><a href="https://quarto.org/docs/get-started/">Quarto documentation</a>: Official guide for Quarto, the next-generation publishing system for R, Python, and Julia.</li>
<li><a href="https://kbroman.org/knitr_knutshell/">knitr in a knutshell</a>: Karl Broman’s concise tutorial on knitr, including spin usage patterns.</li>
<li><a href="https://r4ds.hadley.nz/">R for Data Science, 2nd edition</a>: Hadley Wickham and colleagues’ guide to modern R workflows, including communication and reporting chapters.</li>
</ul>
</section>
<section id="reproducibility" class="level1">
<h1>Reproducibility</h1>
<p>This post describes workflow patterns rather than a specific analysis pipeline. The code examples use base R, <code>knitr</code>, <code>rmarkdown</code>, and <code>ggplot2</code>. To test the spin workflow:</p>
<div class="cell">
<div class="sourceCode cell-code" id="cb13" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb13-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Save the annotated script as analysis_script.R,</span></span>
<span id="cb13-2"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># then run:</span></span>
<span id="cb13-3">knitr<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">spin</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"analysis_script.R"</span>, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">knit =</span> <span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">FALSE</span>)</span>
<span id="cb13-4"></span>
<span id="cb13-5"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Inspect the generated .Rmd file:</span></span>
<span id="cb13-6"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">file.edit</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"analysis_script.Rmd"</span>)</span>
<span id="cb13-7"></span>
<span id="cb13-8"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Render to HTML:</span></span>
<span id="cb13-9">rmarkdown<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">render</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"analysis_script.Rmd"</span>)</span></code></pre></div>
</div>
<p><strong>Session information:</strong></p>
<div class="cell">
<div class="sourceCode cell-code" id="cb14" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb14-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sessionInfo</span>()</span></code></pre></div>
</div>
<p><strong>Key package versions:</strong></p>
<ul>
<li>R: 4.4+</li>
<li>knitr: 1.45+</li>
<li>rmarkdown: 2.25+</li>
<li>ggplot2: 3.5+ (for figure examples)</li>
</ul>
</section>
<section id="lets-connect" class="level1">
<h1>Let’s Connect</h1>
<ul>
<li><strong>GitHub:</strong> <a href="https://github.com/rgt47">rgt47</a></li>
<li><strong>Twitter/X:</strong> <a href="https://twitter.com/rgt47"><span class="citation" data-cites="rgt47">@rgt47</span></a></li>
<li><strong>LinkedIn:</strong> <a href="https://linkedin.com/in/rgthomaslab">Ronald Glenn Thomas</a></li>
<li><strong>Email:</strong> <a href="https://rgtlab.org/contact">rgtlab.org/contact</a></li>
</ul>
<p>I would enjoy hearing from you if:</p>
<ul>
<li>An error or a better approach to any of the code in this post comes to mind.</li>
<li>There are suggestions for topics to see covered.</li>
<li>The interest is in discussing R programming, data science, or reproducible research.</li>
<li>There are questions about anything in this tutorial.</li>
<li>The goal is simply to say hello and connect.</li>
</ul>
<section id="related-posts-in-this-cluster" class="level2">
<h2 class="anchored" data-anchor-id="related-posts-in-this-cluster">Related posts in this cluster</h2>
<p>This post is part of the <em>Quarto, R Markdown, and Publishing</em> series. Recommended reading order:</p>
<ol type="1">
<li>Post 80: <a href="../80-pub-multi-language-quarto/">Multi-Language Quarto Documents on macOS</a></li>
<li><strong>Post 81: Rapid Conversion of Draft R Scripts to Formal Rmd</strong> (this post)</li>
<li>Post 83: <a href="../83-pub-statistical-computing-textbook/">Building a Statistical Computing Textbook</a></li>
<li>Post 84: <a href="../84-pub-obs-r-screencasts/">Setting up OBS for Live R Coding Screencasts</a></li>
</ol>


</section>
</section>

 ]]></description>
  <category>r</category>
  <category>rmarkdown</category>
  <category>reproducibility</category>
  <guid>https://rgtlab.org/posts/pub-r-script-to-rmd/</guid>
  <pubDate>Mon, 08 Jun 2026 07:00:00 GMT</pubDate>
  <media:content url="https://rgtlab.org/posts/pub-r-script-to-rmd/media/images/hero.png" medium="image" type="image/png" height="96" width="144"/>
</item>
<item>
  <title>Install Linux Mint on a MacBook Air</title>
  <dc:creator>Ronald G. Thomas</dc:creator>
  <link>https://rgtlab.org/posts/wf-linux-mint-on-macbook/</link>
  <description><![CDATA[ 




<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/wf-linux-mint-on-macbook/media/images/hero.png" class="img-fluid figure-img" style="width:80.0%"></p>
<figcaption>A 13-inch MacBook Air running Linux Mint, with an installer USB drive alongside</figcaption>
</figure>
</div>
<p><em>Linux Mint: a polished desktop experience on repurposed Apple hardware.</em></p>
<section id="introduction" class="level1">
<h1>Introduction</h1>
<p>Many users assume that installing Linux on Apple hardware requires fighting obscure driver issues: wifi cards that refuse to connect, trackpads that stutter, and suspend modes that brick the machine. This assumption, while once valid, has become largely outdated. Modern distributions like Linux Mint have matured to the point where installation on older MacBooks is surprisingly painless.</p>
<p>The specific goal of this project is to refurbish a 2016 13-inch MacBook Air with a modern Linux operating system and configure it as a functional data science workstation. The distribution we focus on is Linux Mint 22 (“Wilma”), chosen primarily because it ships with the hardware drivers needed for this particular MacBook, making the installation far more straightforward than most alternatives.</p>
<p>We walk through every step of the process: from downloading the ISO image and burning it onto a USB drive, through the installation itself, to post-install configuration of the shell, keyboard, display, and essential software. Common stumbling blocks are documented throughout so that readers can avoid them.</p>
<p>More formally, this post documents the Hardware and Operating System layers (Layers 1 and 2) of the Workflow Construct described in <a href="../../posts/52-workflow-construct/">post 52</a>. The ThinkPad-with-Mint pairing is the construct’s Linux side; it mirrors the macOS-with-MacBook side and provides the cross-platform substrate on which the higher layers (shell, editor, container runtime) are identical. The installation walked through here is the bootstrapping that brings a new ThinkPad to the point where the dotfiles repository’s <code>install.sh</code> (see <a href="../../posts/24-setupdotfilesongithub/">post 24</a>) can run.</p>
<section id="motivations" class="level2">
<h2 class="anchored" data-anchor-id="motivations">Motivations</h2>
<p>The following considerations motivated this project:</p>
<ul>
<li>A functional MacBook Air sitting unused because macOS updates had slowed it to a crawl represented wasted hardware that could be given a second life.</li>
<li>Data science workflows that rely on R, Python, and Docker benefit from a lightweight machine that can run all three without the overhead of a modern macOS installation.</li>
<li>Curiosity about whether Linux Mint could genuinely replace macOS for users accustomed to Apple’s trackpad gestures and keyboard shortcuts.</li>
<li>Setting up a reproducible development environment from scratch is a useful exercise in understanding what dependencies a workflow actually requires.</li>
<li>A portable secondary machine for teaching demonstrations, where the full software stack is visible and inspectable rather than hidden behind proprietary layers.</li>
</ul>
</section>
<section id="objectives" class="level2">
<h2 class="anchored" data-anchor-id="objectives">Objectives</h2>
<ol type="1">
<li>Download, verify, and burn the Linux Mint 22 ISO image onto a bootable USB drive.</li>
<li>Install Mint on a 2016 MacBook Air, including handling wifi, trackpad, and display drivers.</li>
<li>Configure the desktop environment, keyboard shortcuts, and shell (zsh) for a data science workflow.</li>
<li>Install and validate the core software stack: R, vim, Docker, Dropbox, and essential command-line utilities.</li>
</ol>
<p>Errors and alternative approaches are welcome; see the Let’s Connect section at the end.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/wf-linux-mint-on-macbook/media/images/ambiance1.png" class="img-fluid figure-img"></p>
<figcaption>A workspace ready for a fresh OS installation.</figcaption>
</figure>
</div>
</section>
</section>
<section id="prerequisites-and-setup" class="level1">
<h1>Prerequisites and Setup</h1>
<p>Before beginning, gather the following:</p>
<ul>
<li><strong>Target machine:</strong> A 2016 13-inch MacBook Air (Early 2015 hardware, <code>MacBookAir7,2</code>; one Thunderbolt 2 port, two USB-A ports, MagSafe 2 power). Other MacBook models may work but driver support varies.</li>
<li><strong>Working machine:</strong> Any computer with internet access and a USB port, used to download the ISO and burn the USB drive.</li>
<li><strong>USB flash drive:</strong> At least 4 GB capacity (the ISO is approximately 3 GB).</li>
<li><strong>External wifi adapter (if needed):</strong> A Panda Wireless USB adapter is a reliable fallback. Mint 22 ships with <code>Ralink RT5372</code> drivers, which support this adapter out of the box.</li>
<li><strong>Ethernet cable (optional but recommended):</strong> Wired connectivity during initial setup avoids wifi driver issues entirely.</li>
</ul>
</section>
<section id="what-is-linux-mint" class="level1">
<h1>What is Linux Mint?</h1>
<p>Linux Mint is a community-driven Linux distribution built on top of Ubuntu (which is itself built on Debian). It emphasises ease of use and ships with the Cinnamon desktop environment, which provides a traditional desktop metaphor: a taskbar, a start menu, and a system tray. For users migrating from macOS or Windows, Cinnamon feels immediately familiar.</p>
<p>The distinguishing feature of Mint, relative to Ubuntu or Fedora, is its focus on hardware compatibility and out-of-the-box usability. The Mint team bundles proprietary multimedia codecs, common drivers, and a curated set of default applications so that the system is functional immediately after installation. Since the beginning of the Linux era (circa 1991), the central challenge of installing a Linux distribution on consumer hardware has been wrestling with video, audio, trackpad, and power management drivers. Mint addresses this directly.</p>
</section>
<section id="getting-started-installation" class="level1 page-columns page-full">
<h1>Getting Started: Installation</h1>
<section id="download-and-verify-the-iso" class="level2">
<h2 class="anchored" data-anchor-id="download-and-verify-the-iso">Download and Verify the ISO</h2>
<p>Download the torrent file for Linux Mint 22 Wilma Cinnamon edition from the official Mint website:</p>
<p><code>linuxmint-22-cinnamon-64bit.iso.torrent</code></p>
<p>Install the macOS app <a href="https://transmissionbt.com/download">Transmission</a> and add the torrent file. Also download the associated <code>sha256sum.txt</code> file.</p>
<p>To verify the integrity of the downloaded ISO, generate its SHA256 checksum and compare it to the published hash:</p>
<div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb1-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sha256sum</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-b</span> linuxmint-22-cinnamon-64bit.iso</span></code></pre></div>
<p>Compare the output against the contents of <code>sha256sum.txt</code>. If the hashes match, the download is intact.</p>
</section>
<section id="burn-the-iso-to-usb" class="level2">
<h2 class="anchored" data-anchor-id="burn-the-iso-to-usb">Burn the ISO to USB</h2>
<p>Transfer the ISO file to a USB flash drive using <a href="https://etcher.balena.io/">balenaEtcher</a>, a cross-platform tool that writes disk images reliably. Insert the USB drive, select the ISO in balenaEtcher, and click “Flash.”</p>
</section>
<section id="boot-from-usb-and-install" class="level2 page-columns page-full">
<h2 class="anchored" data-anchor-id="boot-from-usb-and-install">Boot from USB and Install</h2>
<p>Insert the bootable USB flash drive into the target MacBook and reboot. Hold the <code>ALT</code> key during startup to access the boot drive selection screen. Select the icon for the USB drive.</p>
<p>A GRUB menu will appear.<sup>1</sup></p>
<div class="no-row-height column-margin column-container"><div id="fn1"><p><sup>1</sup>&nbsp;<strong>GNU GRand Unified Bootloader (GRUB):</strong> When a Linux operating system starts, GRUB is the first program that runs. It loads the kernel, which in turn loads the shell, the desktop environment, and other system services. <a href="https://www.codecademy.com/resources/blog/grub-linux/">codecademy.com</a></p></div></div><p>From the GRUB menu, choose <code>Start Linux Mint 22 Cinnamon 64-bit</code>. A Mint desktop will appear, allowing you to test-drive the system or proceed with installation by double-clicking the “Install Linux Mint” icon.</p>
</section>
<section id="installation-dialog" class="level2">
<h2 class="anchored" data-anchor-id="installation-dialog">Installation Dialog</h2>
<p>The setup dialog walks through the following screens in sequence:</p>
<ul>
<li><strong>Language:</strong> English (or your preference).</li>
<li><strong>Network:</strong> If ethernet is available, Mint connects automatically. Wifi can be configured later; skip this step if only wireless is available.</li>
<li><strong>Multimedia codecs:</strong> Check the box to install multimedia codecs.</li>
<li><strong>Installation type:</strong> Choose “Erase disk and install Linux Mint.”</li>
<li><strong>Location:</strong> Select your timezone (e.g., Los Angeles).</li>
<li><strong>User account:</strong> Create an administrator username, assign a password, and set a hostname.</li>
</ul>
<p>The installation proceeds without further input.</p>
</section>
<section id="post-install-connectivity" class="level2">
<h2 class="anchored" data-anchor-id="post-install-connectivity">Post-Install Connectivity</h2>
<p>When the installation completes, connect the target machine to the internet. If you have ethernet, plug the cable directly into the MacBook; Mint will connect automatically. For wireless access, Mint may or may not recognise the internal wifi hardware. If it does not, use a supported external adapter such as the Panda Wireless USB modem (supported via the <code>Ralink RT5372</code> drivers bundled with Mint 22).</p>
<p>Optionally, connect a second monitor through a Mini DisplayPort adapter into the MacBook’s Thunderbolt 2 port.</p>
<p>Reboot and log in with the administrator credentials you created during installation. The base system is ready.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/wf-linux-mint-on-macbook/media/images/ambiance2.png" class="img-fluid figure-img"></p>
<figcaption>A container-based workflow is one of the first things to configure on a fresh Linux installation</figcaption>
</figure>
</div>
<p><em>Building the software stack: from bare metal to a configured development environment.</em></p>
</section>
</section>
<section id="configuring-the-desktop" class="level1">
<h1>Configuring the Desktop</h1>
<section id="keyboard-and-trackpad" class="level2">
<h2 class="anchored" data-anchor-id="keyboard-and-trackpad">Keyboard and Trackpad</h2>
<p>Open <code>Mouse and Touchpad</code> in system settings and enable <code>Reverse scroll</code> (natural scrolling for macOS converts).</p>
<p>Open <code>Keyboard</code> &gt; <code>Layouts</code> &gt; <code>Options</code> &gt; <code>Caps Lock behavior</code> and select <code>Swap Esc and Caps-Lock</code>. This is an important setting for vim users.</p>
<p>Open <code>Shortcuts</code> &gt; <code>Windows</code> and configure:</p>
<ul>
<li><code>Maximize window</code>: <code>Super-f</code></li>
<li><code>Unmaximize window</code>: <code>Super-g</code></li>
<li><code>Close window</code>: <code>Super-q</code></li>
<li><code>Move window to other monitor</code>: <code>Shift-Super-UpArrow</code></li>
</ul>
</section>
<section id="display-settings" class="level2">
<h2 class="anchored" data-anchor-id="display-settings">Display Settings</h2>
<p>On a two-monitor system, open the Display settings (press the <code>Super</code> key and search for “display”). Set the external monitor as the primary display at its native resolution (e.g., <code>1920x1200</code> on a 24-inch Dell UltraSharp). Set <code>Monitor scale</code> to 150% to increase the default font size in applications. The MacBook’s internal panel is <code>1440x900</code> native and serves as the secondary display. A 4K external monitor at <code>3840x2160</code> can be substituted for the 24-inch primary.</p>
</section>
<section id="power-management" class="level2">
<h2 class="anchored" data-anchor-id="power-management">Power Management</h2>
<p>Switch the “suspend on timeout” behaviour to “shut down immediately” in power settings. This avoids system lock issues on lid close or idle timeout, which can be problematic on older MacBook hardware.</p>
</section>
</section>
<section id="installing-the-software-stack" class="level1">
<h1>Installing the Software Stack</h1>
<section id="copy-configuration-files-from-host" class="level2">
<h2 class="anchored" data-anchor-id="copy-configuration-files-from-host">Copy Configuration Files from Host</h2>
<p>Connect to the new Mint machine via SSH from an existing workstation:</p>
<div class="sourceCode" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb2-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># On the Mint machine</span></span>
<span id="cb2-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sudo</span> apt install openssh-server</span>
<span id="cb2-3"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">ip</span> route</span></code></pre></div>
<p>Note the IP address from the <code>ip route</code> output (e.g., <code>10.0.1.196</code>).</p>
<p>From the host machine, copy essential dotfiles:</p>
<div class="sourceCode" id="cb3" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb3-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># From the Mac</span></span>
<span id="cb3-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">scp</span> ~/Dropbox/dotfiles/kickstart/<span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">*</span> z@10.0.1.196:~</span></code></pre></div>
<p>Key configuration files to transfer:</p>
<ul>
<li><code>.vimrc</code>: vim configuration</li>
<li><code>.zshrc</code>: zsh shell configuration</li>
<li><code>.vim/</code> directory: contains <code>plug</code> and <code>ultisnips</code></li>
</ul>
<p>Optionally, copy a test workspace to verify the rendering pipeline later:</p>
<div class="sourceCode" id="cb4" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb4-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">scp</span> ~/work/teaching/fmph243b/project1 z@10.0.1.196:~</span></code></pre></div>
</section>
<section id="install-core-utilities" class="level2">
<h2 class="anchored" data-anchor-id="install-core-utilities">Install Core Utilities</h2>
<p>Update the package listings and install the essential tool chain:</p>
<div class="sourceCode" id="cb5" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb5-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sudo</span> apt update</span>
<span id="cb5-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sudo</span> apt upgrade <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-y</span></span>
<span id="cb5-3"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sudo</span> apt install <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb5-4">  r-base-core terminator eza tree zsh git vim <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb5-5">  fzf ripgrep autojump zsh-autosuggestions <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb5-6">  zathura <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-y</span></span></code></pre></div>
</section>
<section id="set-up-the-shell" class="level2">
<h2 class="anchored" data-anchor-id="set-up-the-shell">Set Up the Shell</h2>
<p>Make <code>zsh</code> the default shell:</p>
<div class="sourceCode" id="cb6" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb6-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">chsh</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-s</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$(</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">which</span> zsh<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">)</span></span></code></pre></div>
<p>Configure zsh following the companion post on shell setup (see the “See Also” section below for the link).</p>
</section>
<section id="set-up-symbolic-links" class="level2">
<h2 class="anchored" data-anchor-id="set-up-symbolic-links">Set Up Symbolic Links</h2>
<p>Run the dotfile linking script to connect the home directory to configurations stored in Dropbox:</p>
<div class="sourceCode" id="cb7" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb7-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">~/Dropbox/dotfiles/set_up_links.sh</span></span></code></pre></div>
<p>The script performs the following:</p>
<div class="sourceCode" id="cb8" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb8-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#!/bin/zsh</span></span>
<span id="cb8-2"></span>
<span id="cb8-3"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Move the install-created .config temporarily</span></span>
<span id="cb8-4"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">mv</span> ~/.config ~/.config.tmp</span>
<span id="cb8-5"></span>
<span id="cb8-6"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Link dotfiles from Dropbox to Home</span></span>
<span id="cb8-7"><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">ff</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">(</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">".zshrc"</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">".viminfo"</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">".vimrc"</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">".local"</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">".vim"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb8-8">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">".vimplugins"</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">".config"</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">".Rprofile"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">)</span></span>
<span id="cb8-9"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> P <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">${ff</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">[@]</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">}</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb8-10"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">do</span></span>
<span id="cb8-11"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Linking Dropbox version of </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$P</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;"> to Home"</span></span>
<span id="cb8-12">    <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">ln</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-v</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-s</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$HOME</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">/Dropbox/dotfiles/</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$P</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$HOME</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">/</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$P</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb8-13"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">done</span></span>
<span id="cb8-14"></span>
<span id="cb8-15"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Restore original .config contents into the linked dir</span></span>
<span id="cb8-16"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">cp</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-R</span> ~/.config.tmp/<span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">*</span> ~/.config</span>
<span id="cb8-17"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">rm</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-rf</span> ~/.config.tmp</span>
<span id="cb8-18"></span>
<span id="cb8-19"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Link working directories from Dropbox</span></span>
<span id="cb8-20"><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">dd</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">(</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"sandbox"</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"bin"</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"docs"</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"prj"</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"work"</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"ssh"</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shr"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">)</span></span>
<span id="cb8-21"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> P <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">${dd</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">[@]</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">}</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb8-22"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">do</span></span>
<span id="cb8-23">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Linking </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$P</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;"> from Dropbox to Home"</span></span>
<span id="cb8-24">    <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">ln</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-v</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-s</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$HOME</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">/Dropbox/</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$P</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$HOME</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">/</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$P</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb8-25"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">done</span></span></code></pre></div>
<p><strong>Alternative shortcut:</strong> Install Dropbox first (see below), then run <code>install_app.sh</code> and <code>set_up_links.sh</code> from <code>~/Dropbox/dotfiles/</code> while Dropbox syncs in the background.</p>
</section>
<section id="install-additional-applications" class="level2">
<h2 class="anchored" data-anchor-id="install-additional-applications">Install Additional Applications</h2>
<p>Install Zotero using the Software Manager. Set up Zotero syncing with the relevant account credentials.</p>
<p>Add the <code>vimium</code> extension to Firefox for keyboard-driven browsing.</p>
</section>
<section id="install-dropbox" class="level2">
<h2 class="anchored" data-anchor-id="install-dropbox">Install Dropbox</h2>
<div class="sourceCode" id="cb9" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb9-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sudo</span> apt install nautilus-dropbox</span>
<span id="cb9-2"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">dropbox</span> autostart y</span>
<span id="cb9-3"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">dropbox</span> start <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-i</span></span></code></pre></div>
<p>The Dropbox startup process launches a browser-based sign-in page. Log in with the appropriate Dropbox credentials.</p>
</section>
<section id="install-docker" class="level2">
<h2 class="anchored" data-anchor-id="install-docker">Install Docker</h2>
<p>Installing Docker on Mint requires adding Docker’s GPG key and repository to the Apt sources, because the Docker packages in the default Mint repositories are outdated:</p>
<div class="sourceCode" id="cb10" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb10-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sudo</span> apt update</span>
<span id="cb10-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sudo</span> apt install <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb10-3">  apt-transport-https ca-certificates curl gnupg</span>
<span id="cb10-4"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">curl</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-fsSL</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb10-5">  https://download.docker.com/linux/ubuntu/gpg <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb10-6">  <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">|</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sudo</span> gpg <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--dearmor</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb10-7">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-o</span> /usr/share/keyrings/docker.gpg</span>
<span id="cb10-8"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"deb [arch=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$(</span><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">dpkg</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--print-architecture</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">)</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;"> </span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb10-9"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  signed-by=/usr/share/keyrings/docker.gpg] </span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb10-10"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  https://download.docker.com/linux/ubuntu </span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb10-11"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  noble stable"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb10-12">  <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">|</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sudo</span> tee /etc/apt/sources.list.d/docker.list <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb10-13">  <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> /dev/null</span>
<span id="cb10-14"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sudo</span> apt update</span>
<span id="cb10-15"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sudo</span> apt install <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb10-16">  docker-ce docker-ce-cli containerd.io <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb10-17">  docker-buildx-plugin docker-compose-plugin</span>
<span id="cb10-18"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sudo</span> systemctl is-active docker</span></code></pre></div>
<p>Reference: <a href="https://linuxiac.com/how-to-install-docker-on-linux-mint-21/">How to Install Docker on Linux Mint 21 (linuxiac.com)</a>. The linked guide targets Mint 21 (Ubuntu 22.04 “jammy”); for Mint 22 on Ubuntu 24.04, substitute the <code>noble</code> codename in the repository line as shown above.</p>
</section>
</section>
<section id="checking-our-work" class="level1">
<h1>Checking Our Work</h1>
<p>The system should now be able to render both <code>.Rmd</code> and <code>.qmd</code> files. A good test is to render a known-working document:</p>
<div class="sourceCode" id="cb11" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb11-1"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">cd</span> ~/prj/setupmint</span>
<span id="cb11-2"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">quarto</span> render index.qmd <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--to</span> pdf</span></code></pre></div>
<p>If this fails, walk through the dependency chain systematically. The troubleshooting sequence below captures common errors encountered and how to resolve each one.</p>
<section id="troubleshooting-the-rendering-pipeline" class="level3">
<h3 class="anchored" data-anchor-id="troubleshooting-the-rendering-pipeline">Troubleshooting the Rendering Pipeline</h3>
<p>Starting with a sample <code>peng1.Rmd</code> file, the rendering was attempted from the command line. Each attempt surfaced the next missing dependency.</p>
<p><strong>Attempt 1:</strong> <code>R -e "render('peng1.Rmd')"</code> Error: <code>Command R not found</code>. Fix: <code>sudo apt install r-base-core</code></p>
<p><strong>Attempt 2:</strong> <code>R -e "render('peng1.Rmd')"</code> Error: <code>could not find function "render"</code>. Fix: <code>install.packages("rmarkdown")</code> in R.</p>
<p><strong>Attempt 3:</strong> <code>R -e "library(rmarkdown); render('peng1.Rmd')"</code> Error: <code>pandoc version 1.12.3 or higher required</code>. Fix: Install pandoc via apt or Quarto (which bundles its own pandoc).</p>
<p><strong>Attempt 4:</strong> <code>R -e "library(rmarkdown); render('peng1.Rmd')"</code> Error: <code>there is no package called 'pacman'</code>. Fix: <code>install.packages("pacman")</code> in R.</p>
<p><strong>Attempt 5:</strong> <code>R -e "library(rmarkdown); render('peng1.Rmd')"</code> Error: <code>could not find preamble.tex</code>. Fix: The file path used a macOS-style path (<code>/Users/zenn/shr/preamble.tex</code>). Transfer the file and update the path to the Linux equivalent.</p>
<p><strong>Attempt 6:</strong> <code>R -e "library(rmarkdown); render('peng1.Rmd')"</code> Error: <code>pdflatex not found</code>. Fix: Install TinyTeX from within R:</p>
<div class="sourceCode" id="cb12" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb12-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">install.packages</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"tinytex"</span>)</span>
<span id="cb12-2">tinytex<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">install_tinytex</span>()</span></code></pre></div>
<p><strong>Attempt 7:</strong> <code>R -e "library(rmarkdown); render('peng1.Rmd')"</code> Error: <code>file sudoku_apple.pdf not found</code>. Fix: Transfer the missing logo file from the host machine.</p>
<p><strong>Attempt 8:</strong> <code>R -e "library(rmarkdown); render('peng1.Rmd')"</code> Error: <code>no bibliography file found</code>. Fix: Transfer the <code>.bib</code> file and update the path in the YAML header.</p>
<p><strong>Attempt 9:</strong> <code>R -e "library(rmarkdown); render('peng1.Rmd')"</code> Minor errors from packages that failed to load via <code>pacman</code>. Removed <code>janitor</code>, <code>kableExtra</code>, <code>tidyverse</code>, <code>readxl</code> from the preamble and added <code>ggplot2</code> directly.</p>
<p><strong>Attempt 10:</strong> Success. The document rendered to PDF without errors.</p>
<p>We note that every rendering failure pointed to a specific missing dependency. Addressing them one at a time is tedious but educational; each fix clarifies what a workflow actually requires.</p>
</section>
<section id="things-to-watch-out-for" class="level2">
<h2 class="anchored" data-anchor-id="things-to-watch-out-for">Things to Watch Out For</h2>
<ol type="1">
<li><strong>Wifi drivers are the first hurdle.</strong> If the internal wifi card is not recognised, do not waste time debugging; plug in ethernet or use a Panda Wireless USB adapter and move on.</li>
<li><strong>macOS file paths break on Linux.</strong> Any configuration file, <code>.Rmd</code> header, or script that references <code>/Users/</code> will fail. Search and replace all paths after migrating files.</li>
<li><strong>Suspend-on-lid-close can lock the system.</strong> On older MacBooks, the safest power management setting is to shut down on lid close rather than suspend.</li>
<li><strong>Docker on Mint is not straightforward.</strong> The default Mint repositories do not include current Docker packages. Docker’s official GPG key and repository must be added manually.</li>
<li><strong>Package installation in R is slower on Linux.</strong> R packages compile from source on Linux (unlike macOS and Windows, which use pre-built binaries). Expect the first <code>install.packages()</code> call to take significantly longer.</li>
</ol>
</section>
</section>
<section id="daily-workflow" class="level1">
<h1>Daily Workflow</h1>
<p>Once the system is configured, these commands become routine:</p>
<table class="caption-top table">
<colgroup>
<col style="width: 52%">
<col style="width: 47%">
</colgroup>
<thead>
<tr class="header">
<th>Command</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><code>quarto render doc.qmd --to pdf</code></td>
<td>Render a Quarto document to PDF</td>
</tr>
<tr class="even">
<td><code>Rscript -e "rmarkdown::render('file.Rmd')"</code></td>
<td>Render an R Markdown report</td>
</tr>
<tr class="odd">
<td><code>docker compose up -d</code></td>
<td>Start a containerised analysis environment</td>
</tr>
<tr class="even">
<td><code>make r</code></td>
<td>Enter zzcollab Docker container</td>
</tr>
<tr class="odd">
<td><code>dropbox status</code></td>
<td>Check Dropbox sync progress</td>
</tr>
<tr class="even">
<td><code>sudo apt update &amp;&amp; sudo apt upgrade</code></td>
<td>System update</td>
</tr>
</tbody>
</table>
</section>
<section id="uninstall-rollback" class="level1">
<h1>Uninstall / Rollback</h1>
<p>To restore macOS on the MacBook:</p>
<ol type="1">
<li>Create a macOS bootable USB installer from another Mac using <code>createinstallmedia</code>.</li>
<li>Boot from the USB (hold <code>Option</code> at startup).</li>
<li>In Disk Utility, erase the entire disk (APFS format).</li>
<li>Install macOS from the USB installer.</li>
</ol>
<p>This is a full disk wipe. Back up any Linux-side files (e.g., <code>~/prj/</code>, configuration files) to external storage or Dropbox before proceeding.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/wf-linux-mint-on-macbook/media/images/ambiance3.png" class="img-fluid figure-img"></p>
<figcaption>A well-organised workspace makes the difference between a system you use and a system you abandon</figcaption>
</figure>
</div>
<p><em>From installation to a configured, productive environment.</em></p>
</section>
<section id="what-did-we-learn" class="level1">
<h1>What Did We Learn?</h1>
<section id="lessons-learnt" class="level2">
<h2 class="anchored" data-anchor-id="lessons-learnt">Lessons Learnt</h2>
<p><strong>Conceptual Understanding:</strong></p>
<ul>
<li>Linux Mint’s value proposition is not novelty; it is that the hard work of driver integration has already been done for common hardware configurations.</li>
<li>The Debian/Ubuntu package ecosystem (<code>apt</code>) is remarkably comprehensive. Nearly every tool required for data science workflows is a single <code>apt install</code> away.</li>
<li>A fresh Linux installation reveals every implicit dependency in your workflow that macOS had been hiding behind pre-installed frameworks.</li>
<li>Hardware compatibility remains the single most important factor when choosing a Linux distribution for physical (non-virtual) installation.</li>
</ul>
<p><strong>Technical Skills:</strong></p>
<ul>
<li>Verifying ISO integrity using SHA256 checksums before installation is a practice often skipped but worth adopting.</li>
<li>The <code>scp</code> and <code>ssh</code> workflow for bootstrapping a new machine from an existing one is efficient and repeatable.</li>
<li>Symbolic linking dotfiles from a cloud-synced directory (Dropbox) to the home directory creates a portable configuration that survives reinstallation.</li>
<li>Managing Docker on Mint requires understanding GPG keys and APT repository configuration at a level that macOS package managers abstract away.</li>
</ul>
<p><strong>Gotchas and Pitfalls:</strong></p>
<ul>
<li>The order of operations matters: install Dropbox early so that dotfile linking scripts can reference <code>~/Dropbox/</code> paths immediately.</li>
<li>R packages compile from source on Linux, which means system-level dependencies (<code>libcurl4-openssl-dev</code>, <code>libxml2-dev</code>) must be installed before certain R packages will build.</li>
<li>The <code>chsh</code> command requires a logout/login cycle to take effect; do not assume <code>zsh</code> is active immediately after running it.</li>
<li>TinyTeX installation from within R does not always add <code>pdflatex</code> to the system PATH. Verify with <code>which pdflatex</code> after installation.</li>
</ul>
</section>
<section id="limitations" class="level2">
<h2 class="anchored" data-anchor-id="limitations">Limitations</h2>
<ul>
<li>This guide is specific to the 2016 13-inch MacBook Air (Early 2015 hardware, Thunderbolt 2 port). Other MacBook models, especially those with the T2 security chip (2018 and later), may require additional steps or may not work at all.</li>
<li>Wifi driver support depends on the specific wireless chipset. The Panda Wireless workaround is reliable but adds an external dependency.</li>
<li>The guide assumes a complete disk erasure. Dual-boot configurations (macOS alongside Mint) are not covered and involve additional partitioning complexity.</li>
<li>Battery life on Linux is typically shorter than on macOS for the same hardware, because Apple’s power management optimisations are proprietary and not available to Linux kernels.</li>
<li>The Docker installation instructions reference the Ubuntu “noble” repository (Ubuntu 24.04), which matches the Mint 22.x base. Future Mint releases built on a newer Ubuntu base will require substituting the new codename.</li>
<li>Software Manager availability varies. Some applications (e.g., Zotero) may require manual installation from the vendor’s website rather than through the Mint Software Manager.</li>
</ul>
</section>
<section id="opportunities-for-improvement" class="level2">
<h2 class="anchored" data-anchor-id="opportunities-for-improvement">Opportunities for Improvement</h2>
<ol type="1">
<li><strong>Automate the entire post-install setup</strong> with a single shell script that installs all packages, configures the shell, sets up symbolic links, and installs Docker in one pass.</li>
<li><strong>Use Ansible or a similar configuration management tool</strong> to make the setup reproducible across multiple machines without manual intervention.</li>
<li><strong>Investigate the <code>ubuntu-drivers</code> utility</strong> for automated hardware driver detection and installation, which may simplify wifi and display driver setup.</li>
<li><strong>Benchmark battery life</strong> under Linux versus macOS on the same hardware and document power management tweaks (TLP, powertop) that close the gap.</li>
<li><strong>Create a minimal <code>renv</code>-based project</strong> as the verification test instead of relying on a colleague’s <code>.Rmd</code> file with unknown dependencies.</li>
<li><strong>Document the T2 chip workaround</strong> for newer MacBooks, which requires disabling Secure Boot via macOS Recovery before Linux can be installed.</li>
</ol>
</section>
</section>
<section id="wrapping-up" class="level1">
<h1>Wrapping Up</h1>
<p>Repurposing an aging MacBook Air with Linux Mint proves to be a practical project. A machine that had become too slow for macOS updates can run a complete data science environment (R, Python, Docker, vim, and Quarto) without noticeable difficulty.</p>
<p>The most valuable outcome is often not the end result but the process itself. Each rendering failure, each missing driver, each broken file path teaches something specific about what a workflow actually depends on. When everything runs without incident on macOS, that understanding never develops.</p>
<p>For those with an old MacBook collecting dust, this project is worth attempting. The worst case is an afternoon spent learning about one’s own toolchain; the best case is a fully functional secondary workstation.</p>
<p>In conclusion, four points merit emphasis. First, Linux Mint 22 installs cleanly on a 2016 MacBook Air with minimal driver friction, a result that would have been implausible on the same hardware a decade earlier. Second, the full software stack (R, Docker, vim, zsh, and Quarto) can be configured in a single sitting once the base system is running. Third, symbolic linking dotfiles from Dropbox creates a portable, reinstallation-proof configuration that transfers across machines with a single script. Fourth, the troubleshooting sequence for document rendering is genuinely educational: each error reveals a specific dependency that a working macOS environment had previously concealed.</p>
</section>
<section id="see-also" class="level1">
<h1>See Also</h1>
<p><strong>Related posts:</strong></p>
<ul>
<li><a href="../01-configtermzsh/configtermzsh/">Configure the Command Line for Data Science Development</a>: shell and terminal configuration guide</li>
<li><a href="../24-setupdotfilesongithub/setupdotfilesongithub/">Set Up a GitHub Dotfiles Repository</a>: managing configuration files with version control</li>
</ul>
<p><strong>Key resources:</strong></p>
<ul>
<li><a href="https://linuxmint.com/">Linux Mint Official Website</a>: ISO downloads, release notes, documentation</li>
<li><a href="https://linuxmint-installation-guide.readthedocs.io/">Linux Mint Installation Guide</a>: official step-by-step installation documentation</li>
<li><a href="https://etcher.balena.io/">balenaEtcher</a>: USB image writing tool</li>
<li><a href="https://linuxiac.com/how-to-install-docker-on-linux-mint-21/">How to Install Docker on Linux Mint</a>: Docker installation reference</li>
<li><a href="https://yihui.org/tinytex/">TinyTeX</a>: lightweight LaTeX distribution for R users</li>
</ul>
</section>
<section id="reproducibility" class="level1">
<h1>Reproducibility</h1>
<section id="hardware" class="level2">
<h2 class="anchored" data-anchor-id="hardware">Hardware</h2>
<ul>
<li><strong>Target machine:</strong> 2016 13-inch MacBook Air (Early 2015 hardware, model identifier <code>MacBookAir7,2</code>)</li>
<li><strong>Ports:</strong> 1 Thunderbolt 2, 2 USB-A 3.0, MagSafe 2, SDXC, 3.5 mm audio</li>
<li><strong>Native panel:</strong> 1440x900, 13.3-inch</li>
<li><strong>RAM:</strong> 8 GB</li>
<li><strong>Storage:</strong> 256 GB SSD</li>
</ul>
</section>
<section id="software-versions" class="level2">
<h2 class="anchored" data-anchor-id="software-versions">Software Versions</h2>
<ul>
<li><strong>Linux Mint:</strong> 22 “Wilma” (Cinnamon edition)</li>
<li><strong>Kernel:</strong> Based on Ubuntu 24.04 LTS (“Noble Numbat”)</li>
<li><strong>R:</strong> Version installed via <code>r-base-core</code> from Ubuntu repositories</li>
<li><strong>Docker:</strong> Latest stable from Docker’s official repository</li>
<li><strong>Quarto:</strong> Latest stable release</li>
</ul>
</section>
<section id="verification-commands" class="level2">
<h2 class="anchored" data-anchor-id="verification-commands">Verification Commands</h2>
<div class="sourceCode" id="cb13" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb13-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">cat</span> /etc/os-release</span>
<span id="cb13-2"></span>
<span id="cb13-3"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">uname</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-r</span></span>
<span id="cb13-4"></span>
<span id="cb13-5"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">R</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--version</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">|</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">head</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-1</span></span>
<span id="cb13-6"></span>
<span id="cb13-7"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sudo</span> systemctl is-active docker</span>
<span id="cb13-8"></span>
<span id="cb13-9"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$SHELL</span></span></code></pre></div>
</section>
</section>
<section id="lets-connect" class="level1">
<h1>Let’s Connect</h1>
<p><em>Have questions, suggestions, or spot an error? Let me know.</em></p>
<ul>
<li><strong>GitHub:</strong> <a href="https://github.com/rgt47">rgt47</a></li>
<li><strong>Twitter/X:</strong> <a href="https://twitter.com/rgt47"><span class="citation" data-cites="rgt47">@rgt47</span></a></li>
<li><strong>LinkedIn:</strong> <a href="https://linkedin.com/in/rgthomaslab">Ronald Glenn Thomas</a></li>
<li><strong>Email:</strong> <a href="https://rgtlab.org/contact">Contact form</a></li>
</ul>
<p>I would enjoy hearing from you if:</p>
<ul>
<li>You spot an error or a better approach to any of the code in this post.</li>
<li>You have suggestions for topics you would like to see covered.</li>
<li>You want to discuss R programming, data science, or reproducible research.</li>
<li>You have questions about anything in this tutorial.</li>
<li>You just want to say hello and connect.</li>
</ul>
<section id="related-posts-in-this-cluster" class="level2">
<h2 class="anchored" data-anchor-id="related-posts-in-this-cluster">Related posts in this cluster</h2>
<p>This post is part of the <em>Workflow Construct</em> series. Recommended reading order:</p>
<ol type="1">
<li>Post 15: <a href="../15-wf-construct-overview-anchor/">A Workflow Construct for the Modern Data Scientist</a></li>
<li>Post 16: <a href="../16-wf-unix-workspace-config/">Unix Command-Line Workspace Setup for Data Science</a></li>
<li>Post 17: <a href="../17-wf-multi-laptop-dotfiles-bootstrap/">Multi-Laptop macOS Bootstrap</a></li>
<li>Post 18: <a href="../18-wf-git-for-data-science/">Setting Up Git for Data Science Workflows</a></li>
<li>Post 19: <a href="../19-wf-neovim-data-science-ide/">Setting Up Neovim as a Data Science IDE</a></li>
<li>Post 20: <a href="../20-wf-r-vim-latex-workflow/">Extending the R-Vim Workflow with LaTeX</a></li>
<li>Post 21: <a href="../21-wf-modern-cli-tools/">Modern CLI Replacements for the Shell Layer</a></li>
<li>Post 22: <a href="../22-wf-claude-code-in-shell/">LLM-Augmented Editing for the Workflow Construct</a></li>
<li>Post 23: <a href="../23-wf-yabai-tiling-window-manager/">Configuring Yabai as a Tiling Window Manager</a></li>
<li>Post 24: <a href="../24-wf-pocket-terminal-ttyd-tailscale/">A pocket terminal with ttyd and Tailscale</a></li>
<li><strong>Post 25: Install Linux Mint on a MacBook Air</strong> (this post)</li>
</ol>


</section>
</section>


 ]]></description>
  <category>linux</category>
  <category>macos</category>
  <category>r</category>
  <category>python</category>
  <category>workflow-construct</category>
  <guid>https://rgtlab.org/posts/wf-linux-mint-on-macbook/</guid>
  <pubDate>Mon, 08 Jun 2026 07:00:00 GMT</pubDate>
  <media:content url="https://rgtlab.org/posts/wf-linux-mint-on-macbook/media/images/hero.png" medium="image" type="image/png" height="80" width="144"/>
</item>
<item>
  <title>Provisioning AWS EC2 Instances: Console and CLI Methods</title>
  <dc:creator>Ronald G. Thomas</dc:creator>
  <link>https://rgtlab.org/posts/cln-aws-ec2-provisioning/</link>
  <description><![CDATA[ 




<p><em>2026-05-17 17:08 PDT</em></p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/cln-aws-ec2-provisioning/media/images/hero.png" class="img-fluid quarto-figure quarto-figure-center figure-img" style="width:80.0%"></p>
</figure>
</div>
<p><em>Automating cloud infrastructure removes repetitive manual steps and frees attention for the work that matters.</em></p>
<section id="introduction" class="level1">
<h1>Introduction</h1>
<p>I did not really know how virtual servers worked until I had a Shiny app running locally and needed to share it with collaborators over the web. The app was finished, the code was clean, and everything worked on my laptop. ‘Works on my machine’ is not a deployment strategy.</p>
<p>The challenge had two parts: first, create and launch a virtual server in the cloud; second, configure that server to host the application securely. We cover the first part in full.</p>
<p>There are two well-defined entry points to AWS EC2 provisioning: the interactive web console and the <code>aws</code> CLI. The console is a good way to understand what each component does; the CLI is a better choice once the same configuration has been launched more than twice. We document both paths. They reach the same terminal state (a running Ubuntu instance with a security group, key pair, Elastic IP, and Docker installed). The choice is a matter of how often one expects to repeat the operation.</p>
<p>We cover Layer 11 (Cloud) of the workflow architecture described in <a href="../../posts/52-workflow-construct/">post 52</a>.</p>
<section id="motivations" class="level2">
<h2 class="anchored" data-anchor-id="motivations">Motivations</h2>
<ul>
<li>A working Shiny application had no deployment path for remote collaborators.</li>
<li>Managed platforms such as shinyapps.io impose constraints on the software stack; a self-managed server does not.</li>
<li>Clicking through the EC2 console is error-prone and slow after the first few repetitions; scripted provisioning is reproducible.</li>
<li>The four bash scripts presented here can be version controlled alongside application code, making the infrastructure part of the project record.</li>
</ul>
</section>
<section id="objectives" class="level2">
<h2 class="anchored" data-anchor-id="objectives">Objectives</h2>
<ol type="1">
<li>Understand the four pre-launch components required for any EC2 deployment (key pair, firewall, static IP, domain name).</li>
<li>Walk through the console path step by step, including Route 53 domain configuration.</li>
<li>Install and configure the AWS CLI, and document the eight environment variables that drive the automation scripts.</li>
<li>Write four bash scripts (security group, key pair, instance launch, Docker bootstrap) that replace the manual console workflow.</li>
<li>Document connecting to the server, verification, and teardown.</li>
</ol>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/cln-aws-ec2-provisioning/media/images/ambiance1.png" class="img-fluid figure-img"></p>
<figcaption>A clean workstation poised at the start of a focused provisioning session.</figcaption>
</figure>
</div>
</section>
</section>
<section id="what-is-aws-ec2" class="level1">
<h1>What is AWS EC2?</h1>
<p>Amazon Elastic Compute Cloud (EC2) is a service that provides resizable virtual servers in the cloud. Think of it as renting a computer that lives in an Amazon data centre. One chooses the operating system, the amount of memory and storage, and the network configuration. Once launched, one connects over SSH just as one would connect to any remote Linux machine.</p>
<p>The key distinction from a managed hosting platform is control: the operator manages everything on the server, from the operating system packages to the web server configuration. This requires more effort, but it provides complete flexibility over the software stack.</p>
<p>There are several cloud server hosting options: Microsoft Azure, Oracle, Google Cloud, Amazon AWS, Digital Ocean, and Hetzner. Each has its own approach to provisioning, and several offer free or low-cost tiers. AWS is a reasonable choice for a small custom server: the system is well documented and, in my experience, reliable. The AWS free tier includes 750 hours per month of <code>t2.micro</code> instance usage for the first 12 months.</p>
</section>
<section id="the-four-pre-launch-components" class="level1">
<h1>The Four Pre-Launch Components</h1>
<p>Regardless of whether the console or the CLI is used, four components must be in place before launching an instance:</p>
<ol type="1">
<li><strong>SSH key pair</strong>: allows encrypted remote login to the server.</li>
<li><strong>Security group (firewall)</strong>: restricts incoming traffic to named ports only.</li>
<li><strong>Static IP address (Elastic IP)</strong>: maintains the link between the domain name and the server across reboots. Without it, a new IP is assigned each time the instance restarts.</li>
<li><strong>Domain name</strong>: provides a human-readable URL rather than a raw IP address.</li>
</ol>
<p>These components are independent of any specific server instance. You can define multiple instances of each and reuse them across projects.</p>
<p>After launch, post-launch tasks include installing a web server, obtaining an SSL certificate, and configuring a reverse proxy to translate HTTPS (port 443) requests to Shiny (port 3838). Those steps are covered in the companion post on <a href="../../posts/33-shareshinycodeviadocker/">Dockerizing a Shiny application</a>.</p>
</section>
<section id="prerequisites" class="level1">
<h1>Prerequisites</h1>
<ul>
<li><strong>Operating system</strong>: macOS 13+ or a Linux distribution with bash 5+.</li>
<li><strong>AWS account</strong>: an active account with permission to create IAM users.</li>
<li><strong>Already installed (for CLI path)</strong>: Homebrew (macOS) or apt (Debian/Ubuntu), plus <code>jq</code> for JSON parsing.</li>
<li><strong>Background knowledge</strong>: comfort editing dotfiles and running shell commands; basic familiarity with the AWS console.</li>
<li><strong>Time required</strong>: approximately 30 minutes for either path on a first run.</li>
</ul>
</section>
<section id="method-a-console-walkthrough" class="level1">
<h1>Method A: Console Walkthrough</h1>
<p>This section walks through the EC2 web console. It is instructive for understanding the components and their relationships. After two or three manual launches, the CLI path below will be more efficient.</p>
<p>Open the EC2 console at <code>https://aws.amazon.com/console</code>, choose the regional service closest to the operator’s location (e.g., N. California), and navigate to the EC2 dashboard.</p>
<p>I recommend configuring the four pre-launch components before launching the instance, as it saves back-and-forth with the console.</p>
<section id="ssh-key-pair" class="level2">
<h2 class="anchored" data-anchor-id="ssh-key-pair">SSH Key Pair</h2>
<p>EC2 supports two approaches: generate the key pair locally and upload the public key, or have EC2 generate the pair and download the private key.</p>
<p><strong>Local generation approach:</strong></p>
<div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb1-1"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">cd</span> ~/.ssh</span>
<span id="cb1-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">ssh-keygen</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-m</span> PEM</span></code></pre></div>
<p>Name the key prefix <code>power1_app.pem</code> when prompted. The dialog asks for a passphrase; entering one adds additional security but is not required. <code>ssh-keygen</code> generates two files: <code>power1_app.pem</code> and <code>power1_app.pem.pub</code>.</p>
<p>Return to the EC2 dashboard and select <strong>Network &amp; Security &gt; Key Pairs &gt; Actions &gt; Import key pair</strong>. Enter the name <code>power1_app</code>, browse to <code>~/.ssh/power1_app.pem.pub</code>, and select <strong>Import key pair</strong>.</p>
<p><strong>EC2-generated approach:</strong></p>
<p>Select <strong>Create key pair</strong> in the upper right of the console. Enter a name (e.g., <code>power1_app</code>), select RSA key type and <code>.pem</code> file format. The private key <code>power1_app.pem</code> is offered for download. Place it in <code>~/.ssh/</code> and restrict permissions:</p>
<div class="sourceCode" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb2-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">chmod</span> 400 ~/.ssh/power1_app.pem</span></code></pre></div>
<div class="callout callout-style-default callout-warning callout-titled">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon"></i>
</div>
<div class="callout-title-container flex-fill">
Warning
</div>
</div>
<div class="callout-body-container callout-body">
<p>If the private key file has permissions broader than 400, SSH will refuse to use it with a ‘permissions too open’ error.</p>
</div>
</div>
</section>
<section id="firewall-security-group" class="level2">
<h2 class="anchored" data-anchor-id="firewall-security-group">Firewall (Security Group)</h2>
<p>Select <strong>Security Groups</strong> under <strong>Network &amp; Security</strong> in the left panel. Choose <strong>Create security group</strong> and name it <code>power1_app</code>. Under <strong>Inbound Rules</strong>, add SSH (port 22) and HTTPS (port 443), each with source <strong>Anywhere IPv4 0.0.0.0/0</strong>.</p>
<p>This creates a firewall that accepts only SSH and HTTPS inbound traffic.</p>
</section>
<section id="static-ip-address-elastic-ip" class="level2">
<h2 class="anchored" data-anchor-id="static-ip-address-elastic-ip">Static IP Address (Elastic IP)</h2>
<p>Navigate to <strong>Network and Security &gt; Elastic IPs &gt; Allocate Elastic IP address</strong>. An IP from the EC2 pool is assigned (e.g., <code>13.57.139.31</code>).</p>
<div class="callout callout-style-default callout-warning callout-titled">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon"></i>
</div>
<div class="callout-title-container flex-fill">
Warning
</div>
</div>
<div class="callout-body-container callout-body">
<p>Elastic IPs incur charges when allocated but not associated with a running instance. Release unused Elastic IPs immediately to avoid unexpected costs.</p>
</div>
</div>
</section>
<section id="domain-name-route-53" class="level2">
<h2 class="anchored" data-anchor-id="domain-name-route-53">Domain Name (Route 53)</h2>
<p>From the AWS console, navigate to the Route 53 dashboard (Services &gt; Route 53). Once a domain name is acquired (e.g., <code>rgtlab.org</code>), associate it with the static IP:</p>
<ol type="1">
<li>Click <strong>Hosted zones</strong> in the side panel.</li>
<li>Click on <code>rgtlab.org</code> in the centre panel.</li>
<li>Check the <code>rgtlab.org</code> Type A record.</li>
<li>Click <strong>Edit record</strong> in the right panel.</li>
<li>Change the IP address in the <strong>Value</strong> field to the allocated Elastic IP.</li>
</ol>
</section>
<section id="selecting-and-launching-the-instance" class="level2">
<h2 class="anchored" data-anchor-id="selecting-and-launching-the-instance">Selecting and Launching the Instance</h2>
<p>From <strong>Instances</strong> in the EC2 dashboard, click <strong>Launch Instances</strong> and follow these steps:</p>
<ol type="1">
<li><strong>Name the server</strong>: enter <code>power1_app</code>.</li>
<li><strong>Select the OS</strong>: choose Ubuntu (a mature Linux distribution).</li>
<li><strong>Choose an instance type</strong>: select <code>t2.micro</code> (1 CPU, 1 GiB memory).</li>
<li><strong>Choose a key pair</strong>: select <code>power1_app</code> from the dropdown.</li>
<li><strong>Select a security group</strong>: choose the <code>power1_app</code> group.</li>
<li><strong>Choose storage</strong>: enter 30 GB of EBS General Purpose SSD (GP2). Thirty gigabytes is the maximum allowed under the free tier. Smaller disk sizes can cause problems during Docker image builds.</li>
<li><strong>Advanced options</strong>: scroll to the bottom and upload the startup script from the Docker Bootstrap section below.</li>
<li>Click <strong>Launch Instance</strong>.</li>
</ol>
<p>After the instance launches, open the Elastic IP dialog under <strong>Network &amp; Security</strong>, select <strong>Actions &gt; Associate Elastic IP address</strong>, and associate the IP with the new instance.</p>
</section>
</section>
<section id="method-b-cli-scripts" class="level1">
<h1>Method B: CLI Scripts</h1>
<p>This section documents the CLI approach. Four bash scripts replace the manual console workflow entirely.</p>
<section id="installation" class="level2">
<h2 class="anchored" data-anchor-id="installation">Installation</h2>
<p>On macOS, Homebrew provides the simplest installation path:</p>
<div class="sourceCode" id="cb3" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb3-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">brew</span> install awscli jq</span>
<span id="cb3-2"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">aws</span> configure</span></code></pre></div>
<div class="callout callout-style-default callout-note callout-titled">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon"></i>
</div>
<div class="callout-title-container flex-fill">
Note
</div>
</div>
<div class="callout-body-container callout-body">
<p>Instructions for installing Homebrew can be found at <a href="https://brew.sh/">brew.sh</a>. IAM credential setup is covered in Appendix A below.</p>
</div>
</div>
<p>The <code>aws configure</code> command prompts for four values:</p>
<ul>
<li><strong>AWS Access Key ID</strong> and <strong>AWS Secret Access Key</strong>: from your IAM credentials CSV (see Appendix A).</li>
<li><strong>Default region</strong>: e.g., <code>us-west-1</code>.</li>
<li><strong>Default output format</strong>: <code>json</code> is recommended.</li>
</ul>
</section>
<section id="configuration" class="level2">
<h2 class="anchored" data-anchor-id="configuration">Configuration</h2>
<p>Nine parameters are required for automated instance generation. Eight are stored as environment variables in <code>~/.zshrc</code> (or equivalent) so every script can reference them without hardcoded values.</p>
<p><strong>VPC and Subnet</strong> (found on the EC2 dashboard under Your VPCs):</p>
<div class="sourceCode" id="cb4" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb4-1"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">export</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">vpc_id</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"vpc-14814b73"</span></span>
<span id="cb4-2"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">export</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">subnet_id</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"subnet-f02c90ab"</span></span></code></pre></div>
<p><strong>AMI, instance type, and storage</strong> (OS image, server capabilities):</p>
<div class="sourceCode" id="cb5" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb5-1"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">export</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">ami_id</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"ami-014d05e6b24240371"</span></span>
<span id="cb5-2"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">export</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">instance_type</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"t2.micro"</span></span>
<span id="cb5-3"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">export</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">storage_size</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"30"</span></span></code></pre></div>
<p><strong>Key pair name and security group</strong>:</p>
<div class="sourceCode" id="cb6" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb6-1"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">export</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">key_name</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"power1_app"</span></span>
<span id="cb6-2"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">export</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">security_grp</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"sg-0fef542d93849669c"</span></span></code></pre></div>
<p><strong>Static IP</strong> (the Elastic IP allocated above):</p>
<div class="sourceCode" id="cb7" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb7-1"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">export</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">static_ip</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"13.57.139.31"</span></span></code></pre></div>
<p>A ninth parameter, <code>proj_name</code>, can be supplied at call time via the <code>-p</code> flag on each script.</p>
<p>Verify after sourcing:</p>
<div class="sourceCode" id="cb8" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb8-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">aws</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--version</span></span>
<span id="cb8-2"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">aws</span> configure list</span>
<span id="cb8-3"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"vpc_id=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$vpc_id</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb8-4"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">aws</span> ec2 describe-instances <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb8-5">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--query</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'Reservations[].Instances[].InstanceId'</span></span></code></pre></div>
<p>If step 3 returns a JSON array (empty or populated), the credentials and region are configured correctly.</p>
</section>
<section id="script-1-create-security-group" class="level2">
<h2 class="anchored" data-anchor-id="script-1-create-security-group">Script 1: Create Security Group</h2>
<p>Creates a firewall and opens specified ports. The <code>-n</code> flag sets the group name; <code>-p</code> adds a port. Default behaviour opens ports 22 and 443 only.</p>
<div class="sourceCode" id="cb9" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb9-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">aws_create_security_group.sh</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-n</span> power1_app <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-p</span> 22 <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-p</span> 80 <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-p</span> 443</span></code></pre></div>
<div class="sourceCode" id="cb10" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb10-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#!/usr/bin/env bash</span></span>
<span id="cb10-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">Help()</span></span>
<span id="cb10-3"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">{</span></span>
<span id="cb10-4"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"The script generates a new security group."</span></span>
<span id="cb10-5"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"The group name is given with the -n flag."</span></span>
<span id="cb10-6"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Ports are specified with the -p flag."</span></span>
<span id="cb10-7"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Anticipated incoming ports: 22 ssh, 80 http,"</span></span>
<span id="cb10-8"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"  3838 shiny, 443 https."</span></span>
<span id="cb10-9"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Script will fail if group name already exists."</span></span>
<span id="cb10-10"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Reads vpc_id from environment variables."</span></span>
<span id="cb10-11"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Example:"</span></span>
<span id="cb10-12"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"  aws_create_security_group.sh </span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\\</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb10-13"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"    -n power1_app -p 22 -p 80 -p 443"</span></span>
<span id="cb10-14"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">}</span></span>
<span id="cb10-15"><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">sg_grp_name</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$(</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">basename</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$PWD</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">)</span></span>
<span id="cb10-16"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">while</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">getopts</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">":hp:n:"</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">opt</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">do</span></span>
<span id="cb10-17">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">case</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$opt</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span></span>
<span id="cb10-18">        <span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">p</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">)</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">ports</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">(</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$OPTARG</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">)</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;;</span></span>
<span id="cb10-19">        <span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">n</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">)</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">sg_grp_name</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$OPTARG</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;;</span></span>
<span id="cb10-20">        <span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">h</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">)</span> <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">Help</span></span>
<span id="cb10-21">            <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">exit</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;;</span></span>
<span id="cb10-22">        <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">*</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">)</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb10-23">            <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'error in command line parsing'</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;&amp;</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span></span>
<span id="cb10-24">            <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">exit</span> 1</span>
<span id="cb10-25">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">esac</span></span>
<span id="cb10-26"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">done</span></span>
<span id="cb10-27"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"sg group name = </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$sg_grp_name</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb10-28"></span>
<span id="cb10-29"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">aws</span> ec2 create-security-group <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb10-30">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--group-name</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$sg_grp_name</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb10-31">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--description</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"security group"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb10-32">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--tag-specifications</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb10-33">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"ResourceType=security-group,</span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb10-34"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">Tags=[{Key=Name,Value=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$sg_grp_name</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">}]"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb10-35">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--vpc-id</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$vpc_id</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> temp.txt</span>
<span id="cb10-36"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">wait</span></span>
<span id="cb10-37"><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">security_grp</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$(</span><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">jq</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-r</span> .GroupId temp.txt<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">)</span></span>
<span id="cb10-38"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">wait</span></span>
<span id="cb10-39"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"security group ID = </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$security_grp</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb10-40"></span>
<span id="cb10-41"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> i <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">${ports</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">[@]</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">}</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb10-42"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">do</span></span>
<span id="cb10-43">  <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">aws</span> ec2 authorize-security-group-ingress <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb10-44">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--group-id</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$security_grp</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb10-45">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--protocol</span> tcp <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb10-46">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--port</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">${i}</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb10-47">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--cidr</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"0.0.0.0/0"</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> /dev/null</span>
<span id="cb10-48"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">done</span></span></code></pre></div>
</section>
<section id="script-2-create-key-pair" class="level2">
<h2 class="anchored" data-anchor-id="script-2-create-key-pair">Script 2: Create Key Pair</h2>
<p>Generates an SSH key pair and stores the private key in <code>~/.ssh/</code>. The <code>-k</code> flag sets the key pair name; if omitted, the current directory name is used.</p>
<div class="sourceCode" id="cb11" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb11-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">aws_create_keypair.sh</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-k</span> power1_app</span></code></pre></div>
<div class="sourceCode" id="cb12" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb12-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#!/usr/bin/env bash</span></span>
<span id="cb12-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">Help()</span></span>
<span id="cb12-3"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">{</span></span>
<span id="cb12-4"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"The script generates a new key pair."</span></span>
<span id="cb12-5"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"The key pair name is given with the -k flag."</span></span>
<span id="cb12-6"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Script will fail if name already exists."</span></span>
<span id="cb12-7"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Example:"</span></span>
<span id="cb12-8"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"  aws_create_keypair.sh -k power1_app"</span></span>
<span id="cb12-9"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">}</span></span>
<span id="cb12-10"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">while</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">getopts</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'hk:'</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">flag</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">do</span></span>
<span id="cb12-11">  <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">case</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">${flag}</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span></span>
<span id="cb12-12">    <span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">h</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">)</span> <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">Help</span></span>
<span id="cb12-13">      <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">exit</span><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;;</span></span>
<span id="cb12-14">    <span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">k</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">)</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">key_pair_name</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">${OPTARG}</span><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;;</span></span>
<span id="cb12-15">  <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">esac</span></span>
<span id="cb12-16"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">done</span></span>
<span id="cb12-17"><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">base</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$(</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">basename</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$PWD</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">)</span></span>
<span id="cb12-18"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">[</span> <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">-z</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$key_pair_name</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">]</span></span>
<span id="cb12-19"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">then</span></span>
<span id="cb12-20">  <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">key_pair_name</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$base</span></span>
<span id="cb12-21"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fi</span></span>
<span id="cb12-22"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"key_pair_name is </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$key_pair_name</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb12-23"></span>
<span id="cb12-24"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">cd</span> ~/.ssh</span>
<span id="cb12-25"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">rm</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-f</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$HOME</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">/.ssh/</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$key_pair_name</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">.pem"</span></span>
<span id="cb12-26"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">aws</span> ec2 create-key-pair <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb12-27">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--key-name</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$key_pair_name</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb12-28">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--query</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'KeyMaterial'</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb12-29">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--output</span> text <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$HOME</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">/.ssh/</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$key_pair_name</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">.pem"</span></span>
<span id="cb12-30"></span>
<span id="cb12-31"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">wait</span></span>
<span id="cb12-32"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">chmod</span> 400 <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$HOME</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">/.ssh/</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$key_pair_name</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">.pem"</span></span></code></pre></div>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/cln-aws-ec2-provisioning/media/images/ambiance2.png" class="img-fluid figure-img"></p>
<figcaption>Server racks in a data centre conveying the physical infrastructure behind virtual cloud instances.</figcaption>
</figure>
</div>
</section>
<section id="script-3-launch-the-ec2-instance" class="level2">
<h2 class="anchored" data-anchor-id="script-3-launch-the-ec2-instance">Script 3: Launch the EC2 Instance</h2>
<p>This is the core script. It reads all eight environment variables, launches the instance, and associates the Elastic IP. The <code>-p</code> flag sets the project name used for tagging.</p>
<div class="sourceCode" id="cb13" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb13-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">aws_create_instance.sh</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-p</span> power1_app</span></code></pre></div>
<div class="sourceCode" id="cb14" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb14-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#!/usr/bin/env bash</span></span>
<span id="cb14-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">Help()</span></span>
<span id="cb14-3"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">{</span></span>
<span id="cb14-4"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Notes on current parameters:"</span></span>
<span id="cb14-5"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Security group should already exist."</span></span>
<span id="cb14-6"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"  If not, run aws_create_security_group.sh."</span></span>
<span id="cb14-7"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Key pair should already exist."</span></span>
<span id="cb14-8"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"  If not, run aws_create_keypair.sh."</span></span>
<span id="cb14-9"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"AMI ID is for Ubuntu Linux 22.04 LTS."</span></span>
<span id="cb14-10"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Check static IP: nslookup &lt;IP&gt;"</span></span>
<span id="cb14-11"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">""</span></span>
<span id="cb14-12"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Usage:"</span></span>
<span id="cb14-13"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"  aws_create_instance.sh -p power1_app"</span></span>
<span id="cb14-14"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">}</span></span>
<span id="cb14-15"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">while</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">getopts</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'hp:'</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">flag</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">do</span></span>
<span id="cb14-16">  <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">case</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">${flag}</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span></span>
<span id="cb14-17">    <span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">h</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">)</span> <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">Help</span></span>
<span id="cb14-18">      <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">exit</span><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;;</span></span>
<span id="cb14-19">    <span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">p</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">)</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">proj_name</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">${OPTARG}</span><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;;</span></span>
<span id="cb14-20">  <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">esac</span></span>
<span id="cb14-21"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">done</span></span>
<span id="cb14-22"><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">base</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$(</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">basename</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$PWD</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">)</span></span>
<span id="cb14-23"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">[</span> <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">-z</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$proj_name</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">]</span></span>
<span id="cb14-24"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">then</span></span>
<span id="cb14-25">  <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">proj_name</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$base</span></span>
<span id="cb14-26"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fi</span></span>
<span id="cb14-27"></span>
<span id="cb14-28"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">aws</span> ec2 run-instances <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb14-29">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--image-id</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$ami_id</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb14-30">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--count</span> 1 <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb14-31">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--instance-type</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$instance_type</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb14-32">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--key-name</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$keypair_name</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb14-33">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--security-group-ids</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$security_grp</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb14-34">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--subnet-id</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$subnet_id</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb14-35">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--block-device-mappings</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb14-36">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"[{</span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\"</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">DeviceName</span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\"</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">:</span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\"</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">/dev/sda1</span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\"</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">,</span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb14-37"><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\"</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">Ebs</span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\"</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">:{</span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\"</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">VolumeSize</span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\"</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">:</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$storage_size</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">}}]"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb14-38">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--tag-specifications</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb14-39">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"ResourceType=instance,</span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb14-40"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">Tags=[{Key=Name,Value=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$proj_name</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">}]"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb14-41">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--user-data</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb14-42">  file://~/Dropbox/prj/c060/aws_startup_code.sh</span>
<span id="cb14-43"></span>
<span id="cb14-44"><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">iid0</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$(</span><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">aws</span> ec2 describe-instances <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb14-45">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--filters</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Name=tag:Name,Values=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$proj_name</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">|</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb14-46">  <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">jq</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-r</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb14-47">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'.Reservations[].Instances[].InstanceId'</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">)</span></span>
<span id="cb14-48"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$iid0</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb14-49"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">read</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-p</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"enter instance id:"</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">iid</span></span>
<span id="cb14-50"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"instance id: </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$iid</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb14-51"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">aws</span> ec2 associate-address <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb14-52">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--public-ip</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$static_ip</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb14-53">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--instance-id</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$iid</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span></code></pre></div>
</section>
<section id="docker-bootstrap" class="level2">
<h2 class="anchored" data-anchor-id="docker-bootstrap">Script 4: Docker Bootstrap</h2>
<p>This user-data script runs automatically on first boot. It installs Docker and Docker Compose on the new Ubuntu instance, then adds the default <code>ubuntu</code> user to the <code>docker</code> group. Upload it in the <strong>Advanced options</strong> section of the console launch dialog, or pass it via <code>--user-data</code> in Script 3.</p>
<div class="sourceCode" id="cb15" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb15-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#!/bin/bash</span></span>
<span id="cb15-2"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">apt</span> update</span>
<span id="cb15-3"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">apt-get</span> install curl <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-y</span></span>
<span id="cb15-4"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">apt-get</span> install gnupg <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-y</span></span>
<span id="cb15-5"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">apt-get</span> install ca-certificates <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-y</span></span>
<span id="cb15-6"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">apt-get</span> install lsb-release <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-y</span></span>
<span id="cb15-7"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sudo</span> install <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-m</span> 0755 <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-d</span> /etc/apt/keyrings</span>
<span id="cb15-8"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">curl</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-fsSL</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb15-9">  https://download.docker.com/linux/ubuntu/gpg <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">|</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb15-10">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sudo</span> gpg <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--dearmor</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb15-11">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-o</span> /etc/apt/keyrings/docker.gpg</span>
<span id="cb15-12"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sudo</span> chmod a+r /etc/apt/keyrings/docker.gpg</span>
<span id="cb15-13"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb15-14">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"deb [arch="</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$(</span><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">dpkg</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--print-architecture</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">)</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">" </span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb15-15"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  signed-by=/etc/apt/keyrings/docker.gpg] </span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb15-16"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  https://download.docker.com/linux/ubuntu </span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb15-17"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  "</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$(</span><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">.</span> /etc/os-release <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb15-18">  <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">&amp;&amp;</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$VERSION_CODENAME</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">)</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">" stable"</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">|</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb15-19">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sudo</span> tee /etc/apt/sources.list.d/docker.list <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb15-20">  <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> /dev/null</span>
<span id="cb15-21"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">apt-get</span> update</span>
<span id="cb15-22"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">apt-get</span> install docker-ce docker-ce-cli <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb15-23">  containerd.io docker-compose-plugin <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-y</span></span>
<span id="cb15-24"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">su</span> ubuntu <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-</span></span>
<span id="cb15-25"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">usermod</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-aG</span> docker ubuntu</span></code></pre></div>
<div class="callout callout-style-default callout-note callout-titled">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon"></i>
</div>
<div class="callout-title-container flex-fill">
Note
</div>
</div>
<div class="callout-body-container callout-body">
<p>The startup script runs as root on first boot only. If it fails, check <code>/var/log/cloud-init-output.log</code> on the instance for debugging output.</p>
</div>
</div>
</section>
</section>
<section id="connecting-to-the-server" class="level1">
<h1>Connecting to the Server</h1>
<p>Construct a <code>config</code> file in <code>~/.ssh/</code> to connect with a simple hostname rather than remembering the IP address and key path:</p>
<div class="sourceCode" id="cb16" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb16-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">Host</span> rgtlab.org</span>
<span id="cb16-2">  <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">HostName</span> 13.57.139.31</span>
<span id="cb16-3">  <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">User</span> ubuntu</span>
<span id="cb16-4">  <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">Port</span> 22</span>
<span id="cb16-5">  <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">IdentityFile</span> ~/.ssh/power1_app.pem</span></code></pre></div>
<p>Then connect with:</p>
<div class="sourceCode" id="cb17" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb17-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">ssh</span> rgtlab.org</span></code></pre></div>
<div class="callout callout-style-default callout-tip callout-titled">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon"></i>
</div>
<div class="callout-title-container flex-fill">
Tip
</div>
</div>
<div class="callout-body-container callout-body">
<p>Set the private key permissions to 400 if not already done: <code>chmod 400 ~/.ssh/power1_app.pem</code></p>
</div>
</div>
</section>
<section id="verification" class="level1">
<h1>Verification</h1>
<p>After launching the instance and associating the Elastic IP, confirm the server is accessible and Docker is running.</p>
<p><strong>CLI path:</strong></p>
<div class="sourceCode" id="cb18" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb18-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">aws</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--version</span></span>
<span id="cb18-2"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">aws</span> configure list</span>
<span id="cb18-3"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"vpc_id=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$vpc_id</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb18-4"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">aws</span> ec2 describe-instances <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb18-5">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--query</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'Reservations[].Instances[].InstanceId'</span></span></code></pre></div>
<p><strong>Server access:</strong></p>
<div class="sourceCode" id="cb19" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb19-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">ssh</span> rgtlab.org <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"echo 'SSH connection successful'"</span></span>
<span id="cb19-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">ssh</span> rgtlab.org <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"docker --version"</span></span>
<span id="cb19-3"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">ssh</span> rgtlab.org <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"docker compose version"</span></span></code></pre></div>
<p>If all three server checks return expected output, the instance is correctly configured.</p>
</section>
<section id="daily-workflow" class="level1">
<h1>Daily Workflow</h1>
<table class="caption-top table">
<colgroup>
<col style="width: 52%">
<col style="width: 47%">
</colgroup>
<thead>
<tr class="header">
<th style="text-align: left;">Command</th>
<th style="text-align: left;">Action</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td style="text-align: left;"><code>aws_create_security_group.sh -n NAME -p P</code></td>
<td style="text-align: left;">Create firewall, open port <code>P</code></td>
</tr>
<tr class="even">
<td style="text-align: left;"><code>aws_create_keypair.sh -k NAME</code></td>
<td style="text-align: left;">Generate <code>.pem</code> key under <code>~/.ssh/</code></td>
</tr>
<tr class="odd">
<td style="text-align: left;"><code>aws_create_instance.sh -p NAME</code></td>
<td style="text-align: left;">Launch tagged EC2 instance</td>
</tr>
<tr class="even">
<td style="text-align: left;"><code>ssh rgtlab.org</code></td>
<td style="text-align: left;">Connect to the running instance</td>
</tr>
<tr class="odd">
<td style="text-align: left;"><code>ssh rgtlab.org "docker ps"</code></td>
<td style="text-align: left;">View running containers</td>
</tr>
<tr class="even">
<td style="text-align: left;"><code>ssh rgtlab.org "df -h"</code></td>
<td style="text-align: left;">Check disk usage</td>
</tr>
<tr class="odd">
<td style="text-align: left;"><code>aws ec2 describe-instances</code></td>
<td style="text-align: left;">List all instances in current region</td>
</tr>
<tr class="even">
<td style="text-align: left;"><code>aws ec2 terminate-instances --instance-ids ID</code></td>
<td style="text-align: left;">Terminate by instance ID</td>
</tr>
<tr class="odd">
<td style="text-align: left;">AWS Console &gt; EC2 &gt; Instances</td>
<td style="text-align: left;">Check instance status via GUI</td>
</tr>
</tbody>
</table>
</section>
<section id="things-to-watch-out-for" class="level1">
<h1>Things to Watch Out For</h1>
<ol type="1">
<li><p><strong>Security group names must be unique within a VPC.</strong> If a group with the same name already exists, the create script will fail. Check with <code>aws ec2 describe-security-groups</code> before running the script.</p></li>
<li><p><strong>Key pair names must also be unique.</strong> A duplicate name causes a CLI error. Delete the old pair first if regeneration is needed.</p></li>
<li><p><strong>The Elastic IP must be allocated before association.</strong> If a static IP has not yet been allocated, the associate command in Script 3 will fail silently.</p></li>
<li><p><strong>The startup script runs as root on first boot only.</strong> If the bootstrap script has an error, the instance must be terminated and a new one launched. There is no way to re-run user-data on an existing instance.</p></li>
<li><p><strong>Environment variables are session-scoped.</strong> Opening a new terminal without sourcing <code>.zshrc</code> means the scripts will fail because the variables are not set. Verify with <code>echo $vpc_id</code> before running any script.</p></li>
<li><p><strong>AWS CLI commands are region-specific.</strong> The <code>aws configure</code> region must match the region where the VPC and Elastic IP were allocated.</p></li>
<li><p><strong>The free tier storage limit is 30 GB.</strong> Choosing less than 30 GB may seem sufficient initially, but Docker images and system packages accumulate quickly.</p></li>
<li><p><strong>The <code>--cidr "0.0.0.0/0"</code> rule opens a port to the entire internet.</strong> Restrict this to the operator’s IP in production environments.</p></li>
</ol>
</section>
<section id="uninstall-rollback" class="level1">
<h1>Uninstall / Rollback</h1>
<p>To remove all AWS resources associated with a project, follow these steps in order:</p>
<ol type="1">
<li><strong>Terminate the instance.</strong> EC2 Console &gt; Instances &gt; select instance &gt; Actions &gt; Terminate instance.</li>
<li><strong>Release the Elastic IP.</strong> EC2 Console &gt; Elastic IPs &gt; select IP &gt; Actions &gt; Release Elastic IP address.</li>
<li><strong>Delete the security group.</strong> EC2 Console &gt; Security Groups &gt; select group &gt; Actions &gt; Delete security groups.</li>
<li><strong>Delete the SSH key pair.</strong> EC2 Console &gt; Key Pairs &gt; select pair &gt; Actions &gt; Delete.</li>
<li><strong>Remove local SSH config.</strong> Delete the entry from <code>~/.ssh/config</code> and remove the key file: <code>rm ~/.ssh/power1_app.pem</code></li>
</ol>
<p>To remove the AWS CLI from the workstation:</p>
<div class="sourceCode" id="cb20" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb20-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">rm</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-i</span> ~/.aws/credentials ~/.aws/config</span>
<span id="cb20-2"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">brew</span> uninstall awscli   <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># macOS</span></span></code></pre></div>
<div class="callout callout-style-default callout-warning callout-titled">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon"></i>
</div>
<div class="callout-title-container flex-fill">
Warning
</div>
</div>
<div class="callout-body-container callout-body">
<p>Orphaned Elastic IPs and unused EBS volumes continue to accrue charges. Always run the full teardown when a project is complete.</p>
</div>
</div>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/cln-aws-ec2-provisioning/media/images/ambiance3.png" class="img-fluid figure-img"></p>
<figcaption>A calm workspace with a laptop and notebook, representing the planning that goes into infrastructure automation.</figcaption>
</figure>
</div>
</section>
<section id="what-did-we-learn" class="level1">
<h1>What Did We Learn?</h1>
<section id="lessons-learnt" class="level2">
<h2 class="anchored" data-anchor-id="lessons-learnt">Lessons Learnt</h2>
<p><strong>Conceptual Understanding:</strong></p>
<ul>
<li>The EC2 console and the AWS CLI are two interfaces to the same API. Anything clickable in the browser has a corresponding <code>aws ec2</code> subcommand.</li>
<li>A security group is a stateful firewall at the instance level, not the subnet level. Each rule opens a single port to a specified CIDR range.</li>
<li>Elastic IPs are free while associated with a running instance but incur charges when allocated but unused.</li>
<li>User-data scripts provide a one-shot bootstrap mechanism. For ongoing configuration management, tools such as Ansible or cloud-init are more appropriate.</li>
<li>Cloud servers are ordinary Linux machines running in someone else’s data centre. The mental model of ‘remote laptop’ is surprisingly accurate.</li>
</ul>
<p><strong>Technical Skills:</strong></p>
<ul>
<li>Writing bash scripts with <code>getopts</code> for flag parsing produces reusable, self-documenting CLI tools.</li>
<li>The <code>jq</code> utility is essential for extracting fields from the JSON responses returned by AWS CLI commands.</li>
<li>Storing infrastructure parameters as environment variables decouples configuration from code, following the twelve-factor app methodology.</li>
<li>SSH <code>config</code> files simplify repeated connections and reduce typing errors.</li>
</ul>
<p><strong>Gotchas and Pitfalls:</strong></p>
<ul>
<li>Forgetting to <code>chmod 400</code> the PEM file will cause SSH to reject the connection with a permissions error.</li>
<li>Deleting a security group while an instance references it will cause the deletion to fail. Terminate the instance first.</li>
<li>Running out of disk space during Docker builds is a common issue on instances with less than 30 GB of storage.</li>
<li>The EC2 console interface changes periodically. Screenshots in tutorials may not match the current layout exactly.</li>
</ul>
</section>
<section id="limitations" class="level2">
<h2 class="anchored" data-anchor-id="limitations">Limitations</h2>
<ul>
<li>These scripts assume a single-instance deployment and do not address auto-scaling groups, load balancers, or multi-AZ redundancy.</li>
<li>The security group rules open ports to all IPv4 addresses, which is acceptable for development but inappropriate for production.</li>
<li>The bootstrap script installs Docker but does not configure TLS certificates, reverse proxies, or application-level security.</li>
<li>IAM credentials stored in <code>~/.aws/credentials</code> are long-lived. Rotating to short-lived credentials via IAM roles would be more secure.</li>
<li>The scripts do not include error recovery or rollback logic. A failure mid-sequence can leave orphaned resources.</li>
</ul>
</section>
<section id="opportunities-for-improvement" class="level2">
<h2 class="anchored" data-anchor-id="opportunities-for-improvement">Opportunities for Improvement</h2>
<ol type="1">
<li>Replace long-lived IAM credentials with IAM roles attached to the EC2 instance profile.</li>
<li>Add <code>set -euo pipefail</code> to each script for stricter error handling.</li>
<li>Migrate the bootstrap script to a cloud-init configuration file for better logging and idempotency.</li>
<li>Restrict security group ingress to the operator’s current public IP rather than <code>0.0.0.0/0</code>.</li>
<li>Wrap all four scripts in a single orchestration script with a <code>--teardown</code> flag to reverse the entire setup.</li>
<li>Explore AWS CloudFormation or Terraform for declarative infrastructure that can be version controlled and reviewed.</li>
<li>Use AWS Systems Manager Session Manager for SSH access without opening port 22, improving the security posture.</li>
</ol>
</section>
</section>
<section id="wrapping-up" class="level1">
<h1>Wrapping Up</h1>
<p>Provisioning an AWS EC2 instance requires the same four components regardless of method: a key pair, a security group, an Elastic IP, and an optional domain name. The console walkthrough reveals what each component does; the CLI scripts make the process repeatable in under two minutes.</p>
<p>The most transferable lesson is that the AWS console and the CLI are two windows into the same API. Once that relationship becomes clear, every console action suggests a corresponding scriptable command, and the scope for automation expands considerably.</p>
<p>In conclusion, four points merit emphasis. First, four bash scripts replace the entire EC2 console provisioning workflow, reducing a multi-step manual process to a single command sequence. Second, eight environment variables parameterise the scripts, making them reusable across projects without hardcoded values. Third, the teardown procedure is equally important: orphaned Elastic IPs and unused volumes accumulate charges that are easy to overlook. Fourth, the console path is valuable for learning the component relationships, while the CLI path is more efficient for any repeated provisioning.</p>
</section>
<section id="see-also" class="level1">
<h1>See Also</h1>
<p><strong>Related posts:</strong></p>
<ul>
<li><a href="../../posts/32-sharermdcodeviadocker/">Sharing R Code via Docker: R Markdown and Shiny</a>: post-launch server configuration and application deployment.</li>
<li><a href="../../posts/24-setupdotfilesongithub/">Creating a GitHub Dotfiles Repository</a>: managing configuration files across machines.</li>
<li><a href="../../posts/01-configtermzsh/">Configure the Command Line for Data Science</a>: terminal and shell setup.</li>
<li><a href="../../posts/52-workflow-construct/">A Workflow Construct for the Modern Data Scientist</a>: the 13-layer reference architecture that contextualises the Cloud layer.</li>
</ul>
<p><strong>Key resources:</strong></p>
<ul>
<li><a href="https://docs.aws.amazon.com/cli/latest/reference/ec2/">AWS CLI Command Reference: EC2</a></li>
<li><a href="https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html">AWS CLI Getting Started Guide</a></li>
<li><a href="https://docs.docker.com/engine/install/ubuntu/">Docker Installation on Ubuntu</a></li>
<li><a href="https://jqlang.github.io/jq/manual/">jq Manual</a></li>
<li><a href="https://aws.amazon.com/free/">AWS Free Tier Details</a></li>
</ul>
</section>
<section id="reproducibility" class="level1">
<h1>Reproducibility</h1>
<p><strong>Tested configuration:</strong></p>
<table class="caption-top table">
<thead>
<tr class="header">
<th style="text-align: left;">Component</th>
<th style="text-align: left;">Version</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td style="text-align: left;">Operating system</td>
<td style="text-align: left;">macOS 13.x</td>
</tr>
<tr class="even">
<td style="text-align: left;">AWS CLI</td>
<td style="text-align: left;">2.x</td>
</tr>
<tr class="odd">
<td style="text-align: left;">Shell</td>
<td style="text-align: left;">zsh 5.9</td>
</tr>
<tr class="even">
<td style="text-align: left;">jq</td>
<td style="text-align: left;">1.6+</td>
</tr>
<tr class="odd">
<td style="text-align: left;">Homebrew</td>
<td style="text-align: left;">4.x</td>
</tr>
<tr class="even">
<td style="text-align: left;">Last verified</td>
<td style="text-align: left;">2026-05-17</td>
</tr>
</tbody>
</table>
<p>The scripts in this post do not require R or Quarto. They require macOS or Linux with bash, Homebrew (macOS) for AWS CLI installation, an active AWS account with IAM credentials, and <code>jq</code> installed (<code>brew install jq</code>).</p>
<p>To reproduce the full provisioning workflow:</p>
<div class="sourceCode" id="cb21" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb21-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">aws</span> configure</span>
<span id="cb21-2"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">aws_create_security_group.sh</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-n</span> power1_app <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-p</span> 22 <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-p</span> 80 <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-p</span> 443</span>
<span id="cb21-3"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">aws_create_keypair.sh</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-k</span> power1_app</span>
<span id="cb21-4"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">aws_create_instance.sh</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-p</span> power1_app</span></code></pre></div>
</section>
<section id="appendix-a" class="level1">
<h1>Appendix A: IAM Credential Setup</h1>
<ol type="1">
<li>Log into the AWS console.</li>
<li>Search for the <strong>IAM</strong> service and navigate to the IAM dashboard.</li>
<li>Select <strong>User groups</strong> and create a group based on the Power User profile. Name it <code>admin</code> and include your IAM user in the group.</li>
<li>Select <strong>Users</strong> in the left panel, then click <strong>Create User</strong>.</li>
<li>Enter a username (e.g.&nbsp;<code>zenn</code>) and click <strong>Next</strong>, then <strong>Create User</strong>.</li>
<li>Click on the new username. Select the <strong>Security Credentials</strong> tab.</li>
<li>Under <strong>Access Keys</strong>, click <strong>Create access key</strong>.</li>
<li>Select <strong>Command Line Interface (CLI)</strong> and check the acknowledgement box.</li>
<li>Click <strong>Create access key</strong> and then <strong>Download .csv file</strong>.</li>
<li>Save the CSV to <code>~/.aws/</code>.</li>
</ol>
<p>Now configure the CLI:</p>
<div class="sourceCode" id="cb22" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb22-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">aws</span> configure</span></code></pre></div>
<p>Paste the Access Key ID and Secret Access Key from the CSV. Enter your region (e.g.&nbsp;<code>us-west-1</code>) and output format (<code>json</code>).</p>
<div class="callout callout-style-default callout-note callout-titled">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon"></i>
</div>
<div class="callout-title-container flex-fill">
Note
</div>
</div>
<div class="callout-body-container callout-body">
<p>Never commit AWS credentials to version control. Treat them as passwords.</p>
</div>
</div>
</section>
<section id="appendix-b" class="level1">
<h1>Appendix B: Sample Work Session</h1>
<p>A complete CLI provisioning session starting from scratch, assuming <code>aws configure</code> has been run and VPC/subnet IDs are set as environment variables.</p>
<p><strong>Step 1.</strong> Create a security group:</p>
<div class="sourceCode" id="cb23" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb23-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">aws_create_security_group.sh</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-n</span> power1_app <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-p</span> 22 <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-p</span> 80 <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-p</span> 443</span></code></pre></div>
<p><strong>Step 2.</strong> Create the key pair:</p>
<div class="sourceCode" id="cb24" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb24-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">aws_create_keypair.sh</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-k</span> power1_app</span></code></pre></div>
<p><strong>Step 3.</strong> Allocate a new Elastic IP via the console and update shell configuration. If the new IP is <code>204.236.167.50</code>:</p>
<p>Add to <code>~/.config/zsh/.zsh_export</code>:</p>
<div class="sourceCode" id="cb25" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb25-1"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">export</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">static_ip</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'204.236.167.50'</span></span>
<span id="cb25-2"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">export</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">security_grp</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'sg-0fda72c2879d6b2ad'</span></span></code></pre></div>
<p><strong>Step 4.</strong> Launch the instance:</p>
<div class="sourceCode" id="cb26" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb26-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">aws_create_instance.sh</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-p</span> power1_app</span></code></pre></div>
<p><strong>Step 5.</strong> Update SSH config with the new IP:</p>
<div class="sourceCode" id="cb27" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb27-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Edit ~/.ssh/config and update the HostName line</span></span>
<span id="cb27-2"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># to the new Elastic IP address.</span></span></code></pre></div>
</section>
<section id="lets-connect" class="level1">
<h1>Let’s Connect</h1>
<ul>
<li><strong>GitHub:</strong> <a href="https://github.com/rgt47">rgt47</a></li>
<li><strong>Twitter/X:</strong> <a href="https://twitter.com/rgt47"><span class="citation" data-cites="rgt47">@rgt47</span></a></li>
<li><strong>LinkedIn:</strong> <a href="https://linkedin.com/in/rgthomaslab">Ronald Glenn Thomas</a></li>
<li><strong>Email:</strong> <a href="https://rgtlab.org/contact">rgtlab.org/contact</a></li>
</ul>
<p>I would enjoy hearing from you if:</p>
<ul>
<li>You spot an error or a better approach to any of the code in this post.</li>
<li>You have suggestions for topics you would like to see covered.</li>
<li>You want to discuss R programming, data science, or reproducible research.</li>
<li>You have questions about anything in this tutorial.</li>
<li>You just want to say hello and connect.</li>
</ul>
<hr>
<p><em>Rendered on 2026-05-17 at 17:08 PDT.</em><br> <em>Source: ~/prj/qblog/posts/22-serversetupawscli/serversetupawscli/analysis/report/index.qmd</em></p>
<section id="related-posts-in-this-cluster" class="level2">
<h2 class="anchored" data-anchor-id="related-posts-in-this-cluster">Related posts in this cluster</h2>
<p>This post is part of the <em>Clinical Trials and Cloud Deployment</em> series. Recommended reading order:</p>
<ol type="1">
<li>Post 95: <a href="../95-cln-multilang-trial-validation/">Clinical Trial Data Validation Across Languages</a></li>
<li><strong>Post 96: Provisioning AWS EC2 Instances</strong> (this post)</li>
<li>Post 97: <a href="../97-cln-zzedc-investigator-independence/">Running ZZedc Independently for Clinical Trials</a></li>
</ol>


</section>
</section>

 ]]></description>
  <category>aws</category>
  <category>shell</category>
  <category>shiny</category>
  <category>docker</category>
  <guid>https://rgtlab.org/posts/cln-aws-ec2-provisioning/</guid>
  <pubDate>Sun, 17 May 2026 07:00:00 GMT</pubDate>
  <media:content url="https://rgtlab.org/posts/cln-aws-ec2-provisioning/media/images/hero.png" medium="image" type="image/png" height="80" width="144"/>
</item>
<item>
  <title>Research Backup Architecture: Ongoing System and GitHub Archival</title>
  <dc:creator>Ronald G. Thomas</dc:creator>
  <link>https://rgtlab.org/posts/sec-three-tier-backup-architecture/</link>
  <description><![CDATA[ 




<p><em>2026-05-17 16:55 PDT</em></p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/sec-three-tier-backup-architecture/media/images/hero.png" class="img-fluid quarto-figure quarto-figure-center figure-img" style="width:80.0%"></p>
</figure>
</div>
<p><em>Version control is the starting point; a backup architecture is the surrounding structure that makes it durable.</em></p>
<section id="introduction" class="level1">
<h1>Introduction</h1>
<p>GitHub alone is not a backup. That claim sounds counterintuitive when you have been committing and pushing faithfully for years, but it describes a real architectural gap. GitHub is a remote source-of-truth tier, not a backup tier: it holds one copy, operated by one company, under one pricing model, with no local fallback. A suspended account, a platform outage, or a credential compromise can sever access immediately.</p>
<p>The same gap appears at the local level. Manual <code>git push</code> commands get skipped during busy stretches. An external drive’s Time Machine backup silently stops when the drive is not connected. Cloud sync replicates files but does not preserve commit history. Each tool covers one slice of the risk surface; none covers it entirely.</p>
<p>This post brings together two complementary approaches to research backup:</p>
<ol type="1">
<li><strong>An ongoing three-tier system</strong> that runs automatically every 15 minutes, pushing all dirty Git repositories to GitHub, synchronising files to cloud storage, and relying on Time Machine for system-wide safety.</li>
<li><strong>A bulk GitHub archival procedure</strong> that creates verified local mirrors of 400+ private repositories, exports all GitHub-side metadata (issues, PRs, releases, wikis), and supports selective deletion after confirmed verification.</li>
</ol>
<p>Together they form a complete backup architecture: the ongoing system keeps the day-to-day workflow protected; the archival procedure handles the periodic task of mirroring GitHub itself, so that the source- of-truth tier survives the loss of the account or platform.</p>
<p>More formally, both components document the backup layer of the Workflow Construct described in <a href="../../posts/52-workflow-construct/">post 52</a>. Post 52 names backup as a load-bearing layer under the principle ‘two tiers or it is not backup’; the configuration here implements three active tiers for daily use, plus a fourth archival leaf that mirrors GitHub off-platform.</p>
<section id="motivations" class="level2">
<h2 class="anchored" data-anchor-id="motivations">Motivations</h2>
<p>The following considerations motivated this architecture:</p>
<ul>
<li>Having 400+ private repositories on GitHub with no local mirrors creates a single point of failure for years of accumulated work.</li>
<li>Running <code>git push</code> manually and forgetting for days at a time leaves important work vulnerable to local disk failure.</li>
<li>A plain <code>git clone</code> captures code history but misses issues, pull requests, releases, and wiki content that can be more valuable than the code itself.</li>
<li>A colleague’s hard-drive failure, which erased months of analytical work, demonstrated that a single backup tier is never sufficient.</li>
<li>Cloud synchronisation provides file-level replication but does not preserve Git commit history or GitHub metadata.</li>
<li>An automated solution must handle hundreds of repositories without manual intervention for each one.</li>
<li>The archival script requires a dry-run mode so that every action can be previewed before anything destructive is attempted.</li>
</ul>
</section>
<section id="objectives" class="level2">
<h2 class="anchored" data-anchor-id="objectives">Objectives</h2>
<ol type="1">
<li>Configure Time Machine as a system-wide safety net for files that live outside Git.</li>
<li>Write and schedule a production-grade script that commits and pushes all dirty Git repositories automatically every 15 minutes.</li>
<li>Build a bulk archival script that creates a full mirror, portable bundle, wiki clone, and metadata export for every private repository on a GitHub account.</li>
<li>Implement a verification phase that checks every bundle with <code>git bundle verify</code> before any deletion is permitted.</li>
<li>Add a selective preservation mechanism so active or shared repos remain on GitHub while dormant ones are archived and removed.</li>
</ol>
<p>Corrections and alternative approaches are welcome.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/sec-three-tier-backup-architecture/media/images/ambiance1.png" class="img-fluid figure-img"></p>
<figcaption>A workspace ready for a focused archival session.</figcaption>
</figure>
</div>
</section>
</section>
<section id="prerequisites-and-setup" class="level1">
<h1>Prerequisites and Setup</h1>
<section id="for-the-ongoing-backup-system" class="level2">
<h2 class="anchored" data-anchor-id="for-the-ongoing-backup-system">For the Ongoing Backup System</h2>
<p>This guide assumes a macOS environment with the following tools available:</p>
<ul>
<li><strong>macOS</strong> 12 (Monterey) or later</li>
<li><strong>Homebrew Bash</strong> (<code>/opt/homebrew/bin/bash</code>) – macOS ships with Bash 3.2, but the scripts here use Bash 4+ features</li>
<li><strong>Git</strong> 2.30 or later, configured with SSH keys for GitHub</li>
<li><strong>A USB external drive</strong> (1 TB recommended) for Time Machine</li>
<li><strong>A cloud sync service</strong> (Google Drive, Dropbox, or iCloud) for the third tier</li>
</ul>
<p>The research directory structure assumed throughout is <code>~/prj/</code>, containing all Git repositories. Adjust paths as needed for your own layout.</p>
</section>
<section id="for-the-github-archival-script" class="level2">
<h2 class="anchored" data-anchor-id="for-the-github-archival-script">For the GitHub Archival Script</h2>
<p>Before running the archival script, three tools must be installed and authenticated:</p>
<div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb1-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">gh</span> auth status</span>
<span id="cb1-2"></span>
<span id="cb1-3"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--version</span></span>
<span id="cb1-4"></span>
<span id="cb1-5"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">df</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-h</span> ~</span></code></pre></div>
<p>The disk space check is worth doing early: 400 repositories can require 20-40 GB depending on release asset history.</p>
<p>You also need a GitHub personal access token with <code>repo</code> and <code>delete_repo</code> scopes if you plan to use the deletion phase. The <code>gh auth login</code> flow can configure this interactively. For backup-only runs, the standard <code>repo</code> scope is sufficient.</p>
</section>
</section>
<section id="what-is-a-research-backup-architecture" class="level1">
<h1>What is a Research Backup Architecture?</h1>
<p>A research backup architecture layers multiple independent protection mechanisms so that no single point of failure can result in data loss. The architecture in this post has four components:</p>
<ol type="1">
<li><strong>Automated Git commits and pushes</strong> (every 15 minutes) – protects against uncommitted work and local corruption by pushing changes to GitHub.</li>
<li><strong>Cloud synchronisation</strong> (real-time via Google Drive or Dropbox) – provides continuous file-level replication across devices, useful for non-Git files and immediate access from other machines.</li>
<li><strong>Time Machine backups</strong> (hourly, system-wide) – captures the entire filesystem including system settings, application data, and files not covered by the other two tiers.</li>
<li><strong>Periodic GitHub archival</strong> (quarterly or before any account change) – creates verified local mirrors of every private repository, exporting all GitHub-side metadata that a plain <code>git clone</code> would miss: issue threads, pull request discussions, release binaries, labels, milestones, and wikis.</li>
</ol>
<p>Each tier compensates for a weakness in the others. Git does not capture large binary files well; cloud sync does not preserve commit history; Time Machine does not push data off-site; GitHub holds only one copy of the data it hosts. Together, the four components cover the full risk surface.</p>
</section>
<section id="section-1-the-three-tier-ongoing-backup-system" class="level1">
<h1>Section 1: The Three-Tier Ongoing Backup System</h1>
<section id="configuring-time-machine" class="level2">
<h2 class="anchored" data-anchor-id="configuring-time-machine">Configuring Time Machine</h2>
<p>Time Machine provides system-wide backup protection and serves as the safety net for everything beyond Git repositories.</p>
<section id="connect-and-format-the-usb-drive" class="level3">
<h3 class="anchored" data-anchor-id="connect-and-format-the-usb-drive">Connect and Format the USB Drive</h3>
<ol type="1">
<li>Connect your USB drive to the MacBook.</li>
<li>When prompted, do not use it for Time Machine yet; configure it properly first.</li>
<li>Open Disk Utility (Applications &gt; Utilities &gt; Disk Utility).</li>
<li>Select the USB drive from the sidebar.</li>
<li>Click Erase.</li>
<li>Choose format: APFS (recommended for modern Macs) or Mac OS Extended (Journaled).</li>
<li>Name it something recognisable, such as ‘Research Backup’.</li>
<li>Click Erase.</li>
</ol>
</section>
<section id="configure-time-machine" class="level3">
<h3 class="anchored" data-anchor-id="configure-time-machine">Configure Time Machine</h3>
<ol type="1">
<li>Open System Preferences &gt; Time Machine.</li>
<li>Click Select Backup Disk.</li>
<li>Choose your USB drive.</li>
<li>Click Use Disk.</li>
<li>If prompted about encryption, choose Encrypt Backup for security.</li>
</ol>
</section>
<section id="customise-exclusions" class="level3">
<h3 class="anchored" data-anchor-id="customise-exclusions">Customise Exclusions</h3>
<ol type="1">
<li>Click Options in Time Machine preferences.</li>
<li>Add folders to exclude: Downloads, Trash, virtual machines, and similar high-churn directories.</li>
<li>Do not exclude <code>~/prj</code> – that directory should be covered as a secondary layer behind Git.</li>
<li>Enable ‘Back up while on battery power’ if you work unplugged frequently.</li>
</ol>
<p>Time Machine will now back up the entire system (including <code>~/prj</code>) every hour when the USB drive is connected.</p>
</section>
</section>
<section id="the-minimal-backup-script" class="level2">
<h2 class="anchored" data-anchor-id="the-minimal-backup-script">The Minimal Backup Script</h2>
<p>Before building the full production script, it helps to see the core logic in its simplest form. This minimal version does three things: finds every Git repository under <code>~/prj</code>, checks whether it has uncommitted changes, and pushes those changes to the remote.</p>
<div class="sourceCode" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb2-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#!/opt/homebrew/bin/bash</span></span>
<span id="cb2-2"></span>
<span id="cb2-3"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">find</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$HOME</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">/prj"</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-name</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">".git"</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-type</span> d <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb2-4">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">|</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">while</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">read</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">git_dir</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">do</span></span>
<span id="cb2-5">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">cd</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$(</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">dirname</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$git_dir</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">)</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">||</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">continue</span></span>
<span id="cb2-6">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">[[</span> <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">-n</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$(</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> status <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--porcelain</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">)</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">]]</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">||</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">continue</span></span>
<span id="cb2-7">    <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> add <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-A</span></span>
<span id="cb2-8">    <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> commit <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-m</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb2-9">        <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Auto-backup: </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$(</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">date</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'+%Y-%m-%d %H:%M:%S'</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">)</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb2-10">    <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> push origin main <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span>/dev/null <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb2-11">        <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">||</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> push origin master <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span>/dev/null</span>
<span id="cb2-12"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">done</span></span></code></pre></div>
<p>This works, but it lacks error handling, logging, user filtering, and any mechanism for diagnosing failures. The full script below addresses each of these gaps.</p>
</section>
<section id="the-full-backup-script" class="level2">
<h2 class="anchored" data-anchor-id="the-full-backup-script">The Full Backup Script</h2>
<p>The production script extends the minimal version with comprehensive features. The sections below walk through it in logical segments.</p>
<section id="configuration-and-argument-parsing" class="level3">
<h3 class="anchored" data-anchor-id="configuration-and-argument-parsing">Configuration and Argument Parsing</h3>
<div class="sourceCode" id="cb3" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb3-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#!/opt/homebrew/bin/bash</span></span>
<span id="cb3-2"></span>
<span id="cb3-3"><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">RESEARCH_DIR</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$HOME</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">/prj/"</span></span>
<span id="cb3-4"><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">LOG_FILE</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$HOME</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">/Library/Logs/research_backup.log"</span></span>
<span id="cb3-5"><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">MAX_LOG_SIZE</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>10485760</span>
<span id="cb3-6"><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">VERBOSE</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>false</span>
<span id="cb3-7"></span>
<span id="cb3-8"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">while</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">[[</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$#</span> <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">-gt</span> 0 <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">]];</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">do</span></span>
<span id="cb3-9">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">case</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$1</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span></span>
<span id="cb3-10">        <span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">-v</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">|</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">--verbose</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">)</span></span>
<span id="cb3-11">            <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">VERBOSE</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>true</span>
<span id="cb3-12">            <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">shift</span></span>
<span id="cb3-13">            <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;;</span></span>
<span id="cb3-14">        <span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">-h</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">|</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">--help</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">)</span></span>
<span id="cb3-15">            <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Usage: </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$0</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;"> [-v|--verbose]"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb3-16">                 <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"[-h|--help]"</span></span>
<span id="cb3-17">            <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"  -v, --verbose"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb3-18">                 <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Enable verbose output"</span></span>
<span id="cb3-19">            <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"  -h, --help"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb3-20">                 <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Show this help message"</span></span>
<span id="cb3-21">            <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">exit</span> 0</span>
<span id="cb3-22">            <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;;</span></span>
<span id="cb3-23">        <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">*</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">)</span></span>
<span id="cb3-24">            <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Unknown option: </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$1</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb3-25">            <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Use -h or --help for usage"</span></span>
<span id="cb3-26">            <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">exit</span> 1</span>
<span id="cb3-27">            <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;;</span></span>
<span id="cb3-28">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">esac</span></span>
<span id="cb3-29"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">done</span></span></code></pre></div>
</section>
<section id="logging-infrastructure" class="level3">
<h3 class="anchored" data-anchor-id="logging-infrastructure">Logging Infrastructure</h3>
<p>Log rotation prevents the log file from growing without bound. The <code>log_message</code> function writes every event to disk and optionally echoes colour-coded output to the console when verbose mode is active.</p>
<div class="sourceCode" id="cb4" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb4-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">mkdir</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-p</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$(</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">dirname</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$LOG_FILE</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">)</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb4-2"></span>
<span id="cb4-3"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">[[</span> <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">-f</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$LOG_FILE</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb4-4">      <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">&amp;&amp;</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$(</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">stat</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-f%z</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$LOG_FILE</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">)</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb4-5">         -gt <span class="er" style="color: #AD0000;
background-color: null;
font-style: inherit;">$MAX_LOG_SIZE</span> <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">]]</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">then</span></span>
<span id="cb4-6">    <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">mv</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$LOG_FILE</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">${LOG_FILE}</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">.old"</span></span>
<span id="cb4-7">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">[[</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$VERBOSE</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">==</span> true <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">]];</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">then</span></span>
<span id="cb4-8">        <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"INFO: Rotated log file"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb4-9">             <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"(exceeded </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">${MAX_LOG_SIZE}</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;"> bytes)"</span></span>
<span id="cb4-10">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fi</span></span>
<span id="cb4-11"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fi</span></span>
<span id="cb4-12"></span>
<span id="cb4-13"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">log_message()</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">{</span></span>
<span id="cb4-14">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">local</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">level</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$1</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb4-15">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">local</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">message</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$2</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb4-16">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">local</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">timestamp</span></span>
<span id="cb4-17">    <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">timestamp</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$(</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">date</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'+%Y-%m-%d %H:%M:%S'</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">)</span></span>
<span id="cb4-18">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">local</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">log_entry</span></span>
<span id="cb4-19">    <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">log_entry</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$timestamp</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">: [</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$level</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">] </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$message</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb4-20"></span>
<span id="cb4-21">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$log_entry</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;&gt;</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$LOG_FILE</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb4-22"></span>
<span id="cb4-23">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">[[</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$VERBOSE</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">==</span> true <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">]];</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">then</span></span>
<span id="cb4-24">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">case</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$level</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span></span>
<span id="cb4-25">            <span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">ERROR</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">)</span></span>
<span id="cb4-26">                <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-e</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb4-27">                    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"\033[31m</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$log_entry</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">\033[0m"</span></span>
<span id="cb4-28">                <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;;</span></span>
<span id="cb4-29">            <span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">WARNING</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">)</span></span>
<span id="cb4-30">                <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-e</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb4-31">                    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"\033[33m</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$log_entry</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">\033[0m"</span></span>
<span id="cb4-32">                <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;;</span></span>
<span id="cb4-33">            <span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">SUCCESS</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">)</span></span>
<span id="cb4-34">                <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-e</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb4-35">                    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"\033[32m</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$log_entry</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">\033[0m"</span></span>
<span id="cb4-36">                <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;;</span></span>
<span id="cb4-37">            <span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">INFO</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">)</span></span>
<span id="cb4-38">                <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-e</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb4-39">                    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"\033[34m</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$log_entry</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">\033[0m"</span></span>
<span id="cb4-40">                <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;;</span></span>
<span id="cb4-41">            <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">*</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">)</span></span>
<span id="cb4-42">                <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$log_entry</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb4-43">                <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;;</span></span>
<span id="cb4-44">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">esac</span></span>
<span id="cb4-45">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fi</span></span>
<span id="cb4-46"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">}</span></span></code></pre></div>
</section>
<section id="repository-validation-functions" class="level3">
<h3 class="anchored" data-anchor-id="repository-validation-functions">Repository Validation Functions</h3>
<p>Four helper functions handle the filtering logic. <code>check_remote</code> verifies that the repository has an <code>origin</code> remote. <code>check_user_association</code> ensures that only repositories belonging to the ‘rgt47’ account are processed, preventing the script from pushing to collaborator remotes. <code>should_exclude_directory</code> skips archive and backup folders. <code>get_current_branch</code> and <code>branch_exists_on_remote</code> support intelligent push behaviour.</p>
<div class="sourceCode" id="cb5" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb5-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">check_remote()</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">{</span></span>
<span id="cb5-2">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">local</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">repo_dir</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$1</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb5-3">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">cd</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repo_dir</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">||</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span></span>
<span id="cb5-4">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">local</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">remote_url</span></span>
<span id="cb5-5">    <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">remote_url</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$(</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> remote get-url origin <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb5-6">        <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span>/dev/null<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">)</span></span>
<span id="cb5-7">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">[[</span> <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">-n</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$remote_url</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">]]</span></span>
<span id="cb5-8"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">}</span></span>
<span id="cb5-9"></span>
<span id="cb5-10"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">check_user_association()</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">{</span></span>
<span id="cb5-11">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">local</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">repo_dir</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$1</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb5-12">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">cd</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repo_dir</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">||</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span></span>
<span id="cb5-13"></span>
<span id="cb5-14">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">local</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">remote_url</span></span>
<span id="cb5-15">    <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">remote_url</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$(</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> remote get-url origin <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb5-16">        <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span>/dev/null<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">)</span></span>
<span id="cb5-17">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">[[</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$remote_url</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">==</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">*</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"rgt47"</span><span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">*</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">]];</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">then</span></span>
<span id="cb5-18">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span></span>
<span id="cb5-19">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fi</span></span>
<span id="cb5-20"></span>
<span id="cb5-21">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">local</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">git_user</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">git_email</span></span>
<span id="cb5-22">    <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">git_user</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$(</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> config user.name <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span>/dev/null<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">)</span></span>
<span id="cb5-23">    <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">git_email</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$(</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> config user.email <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span>/dev/null<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">)</span></span>
<span id="cb5-24"></span>
<span id="cb5-25">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">[[</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$git_user</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">==</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">*</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"rgt47"</span><span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">*</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">]]</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb5-26">       <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">||</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">[[</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$git_email</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">==</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">*</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"rgt47"</span><span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">*</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">]];</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">then</span></span>
<span id="cb5-27">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span></span>
<span id="cb5-28">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fi</span></span>
<span id="cb5-29"></span>
<span id="cb5-30">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">[[</span> <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">-z</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$git_user</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">]];</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">then</span></span>
<span id="cb5-31">        <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">git_user</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$(</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> config <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--global</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb5-32">            user.name <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span>/dev/null<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">)</span></span>
<span id="cb5-33">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fi</span></span>
<span id="cb5-34">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">[[</span> <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">-z</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$git_email</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">]];</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">then</span></span>
<span id="cb5-35">        <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">git_email</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$(</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> config <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--global</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb5-36">            user.email <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span>/dev/null<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">)</span></span>
<span id="cb5-37">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fi</span></span>
<span id="cb5-38"></span>
<span id="cb5-39">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">[[</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$git_user</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">==</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">*</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"rgt47"</span><span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">*</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">]]</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb5-40">       <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">||</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">[[</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$git_email</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">==</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">*</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"rgt47"</span><span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">*</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">]];</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">then</span></span>
<span id="cb5-41">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span></span>
<span id="cb5-42">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fi</span></span>
<span id="cb5-43"></span>
<span id="cb5-44">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span></span>
<span id="cb5-45"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">}</span></span>
<span id="cb5-46"></span>
<span id="cb5-47"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">should_exclude_directory()</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">{</span></span>
<span id="cb5-48">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">local</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">repo_name</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$1</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb5-49">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">local</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">repo_path</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$2</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb5-50"></span>
<span id="cb5-51">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">local</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">lower_name</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">lower_path</span></span>
<span id="cb5-52">    <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">lower_name</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$(</span><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repo_name</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb5-53">        <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">|</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">tr</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'[:upper:]'</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'[:lower:]'</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">)</span></span>
<span id="cb5-54">    <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">lower_path</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$(</span><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repo_path</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb5-55">        <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">|</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">tr</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'[:upper:]'</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'[:lower:]'</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">)</span></span>
<span id="cb5-56"></span>
<span id="cb5-57">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">[[</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$lower_name</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">==</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">*</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"archive"</span><span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">*</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">]]</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb5-58">       <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">||</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">[[</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$lower_name</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">==</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">*</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"backup"</span><span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">*</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">]];</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">then</span></span>
<span id="cb5-59">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span></span>
<span id="cb5-60">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fi</span></span>
<span id="cb5-61"></span>
<span id="cb5-62">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">[[</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$lower_path</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">==</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">*</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"archive"</span><span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">*</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">]]</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb5-63">       <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">||</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">[[</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$lower_path</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">==</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">*</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"backup"</span><span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">*</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">]];</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">then</span></span>
<span id="cb5-64">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span></span>
<span id="cb5-65">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fi</span></span>
<span id="cb5-66"></span>
<span id="cb5-67">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span></span>
<span id="cb5-68"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">}</span></span>
<span id="cb5-69"></span>
<span id="cb5-70"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">get_current_branch()</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">{</span></span>
<span id="cb5-71">    <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> symbolic-ref <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--short</span> HEAD <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span>/dev/null <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb5-72">        <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">||</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> rev-parse <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--short</span> HEAD <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span>/dev/null</span>
<span id="cb5-73"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">}</span></span>
<span id="cb5-74"></span>
<span id="cb5-75"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">branch_exists_on_remote()</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">{</span></span>
<span id="cb5-76">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">local</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">branch</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$1</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb5-77">    <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> ls-remote <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--heads</span> origin <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$branch</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb5-78">        <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span>/dev/null <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">|</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">grep</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-q</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$branch</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb5-79"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">}</span></span></code></pre></div>
</section>
<section id="main-loop-discovery-and-processing" class="level3">
<h3 class="anchored" data-anchor-id="main-loop-discovery-and-processing">Main Loop: Discovery and Processing</h3>
<p>The main loop uses <code>find</code> with null-delimited output to safely handle repository paths that contain spaces. Each repository passes through a series of checks before any Git operations are attempted.</p>
<div class="sourceCode" id="cb6" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb6-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">log_message</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"INFO"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-2">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Starting research backup scan"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-3">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">" with verbose=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$VERBOSE</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb6-4"></span>
<span id="cb6-5"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">[[</span> <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">!</span> <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">-d</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$RESEARCH_DIR</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">]];</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">then</span></span>
<span id="cb6-6">    <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">log_message</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"ERROR"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-7">        <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Research directory </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$RESEARCH_DIR</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-8">        <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">" does not exist"</span></span>
<span id="cb6-9">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">exit</span> 1</span>
<span id="cb6-10"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fi</span></span>
<span id="cb6-11"></span>
<span id="cb6-12"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">log_message</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"INFO"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-13">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Scanning: </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$RESEARCH_DIR</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb6-14"></span>
<span id="cb6-15"><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">repo_count</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>0</span>
<span id="cb6-16"><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">backup_count</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>0</span>
<span id="cb6-17"><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">error_count</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>0</span>
<span id="cb6-18"><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">warning_count</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>0</span>
<span id="cb6-19"><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">skipped_count</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>0</span>
<span id="cb6-20"><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">excluded_count</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>0</span>
<span id="cb6-21"></span>
<span id="cb6-22"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">while</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">IFS</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">read</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-r</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-d</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">''</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">git_dir</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">do</span></span>
<span id="cb6-23">    <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">repo_dir</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$(</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">dirname</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$git_dir</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">)</span></span>
<span id="cb6-24">    <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">repo_name</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$(</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">basename</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repo_dir</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">)</span></span>
<span id="cb6-25">    <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">relative_path</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">${repo_dir</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$RESEARCH_DIR}</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb6-26"></span>
<span id="cb6-27">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">should_exclude_directory</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-28">       <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repo_name</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$relative_path</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">then</span></span>
<span id="cb6-29">        <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">log_message</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"INFO"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-30">            <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Excluding (archive/backup):"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-31">            <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">" </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$relative_path</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb6-32">        <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">((</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">excluded_count</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">++</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">))</span></span>
<span id="cb6-33">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">continue</span></span>
<span id="cb6-34">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fi</span></span>
<span id="cb6-35"></span>
<span id="cb6-36">    <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">log_message</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"INFO"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-37">        <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Processing: </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$relative_path</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb6-38"></span>
<span id="cb6-39">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">! </span><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">cd</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repo_dir</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">then</span></span>
<span id="cb6-40">        <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">log_message</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"ERROR"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-41">            <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Cannot access: </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repo_dir</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb6-42">        <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">((</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">error_count</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">++</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">))</span></span>
<span id="cb6-43">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">continue</span></span>
<span id="cb6-44">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fi</span></span>
<span id="cb6-45"></span>
<span id="cb6-46">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">((</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">repo_count</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">++</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">))</span></span>
<span id="cb6-47"></span>
<span id="cb6-48">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">! </span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> rev-parse <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--git-dir</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-49">         <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span>/dev/null <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;&amp;</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">then</span></span>
<span id="cb6-50">        <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">log_message</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"ERROR"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-51">            <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Not a valid git repo:"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-52">            <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">" </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$relative_path</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb6-53">        <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">((</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">error_count</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">++</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">))</span></span>
<span id="cb6-54">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">continue</span></span>
<span id="cb6-55">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fi</span></span>
<span id="cb6-56"></span>
<span id="cb6-57">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">! </span><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">check_user_association</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repo_dir</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">then</span></span>
<span id="cb6-58">        <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">log_message</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"INFO"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-59">            <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Skipping (not rgt47):"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-60">            <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">" </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$relative_path</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb6-61">        <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">((</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">skipped_count</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">++</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">))</span></span>
<span id="cb6-62">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">continue</span></span>
<span id="cb6-63">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fi</span></span>
<span id="cb6-64"></span>
<span id="cb6-65">    <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">log_message</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"INFO"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-66">        <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$relative_path</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;"> associated with rgt47"</span></span>
<span id="cb6-67"></span>
<span id="cb6-68">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">! </span><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">check_remote</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repo_dir</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">then</span></span>
<span id="cb6-69">        <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">log_message</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"WARNING"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-70">            <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"No remote configured:"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-71">            <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">" </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$relative_path</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb6-72">        <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">((</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">warning_count</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">++</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">))</span></span>
<span id="cb6-73">        <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">((</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">skipped_count</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">++</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">))</span></span>
<span id="cb6-74">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">continue</span></span>
<span id="cb6-75">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fi</span></span>
<span id="cb6-76"></span>
<span id="cb6-77">    <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">current_branch</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$(</span><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">get_current_branch</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">)</span></span>
<span id="cb6-78">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">[[</span> <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">-z</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$current_branch</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">]];</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">then</span></span>
<span id="cb6-79">        <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">log_message</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"ERROR"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-80">            <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Cannot determine branch:"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-81">            <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">" </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$relative_path</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb6-82">        <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">((</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">error_count</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">++</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">))</span></span>
<span id="cb6-83">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">continue</span></span>
<span id="cb6-84">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fi</span></span>
<span id="cb6-85"></span>
<span id="cb6-86">    <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">log_message</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"INFO"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-87">        <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$relative_path</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;"> on branch:"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-88">        <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">" </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$current_branch</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb6-89"></span>
<span id="cb6-90">    <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">git_status</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$(</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> status <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--porcelain</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-91">        <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span>/dev/null<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">)</span></span>
<span id="cb6-92"></span>
<span id="cb6-93">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">[[</span> <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">-z</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$git_status</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">]];</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">then</span></span>
<span id="cb6-94">        <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">log_message</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"INFO"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-95">            <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$relative_path</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;"> is clean"</span></span>
<span id="cb6-96">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">continue</span></span>
<span id="cb6-97">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fi</span></span>
<span id="cb6-98"></span>
<span id="cb6-99">    <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">untracked</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$(</span><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$git_status</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-100">        <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">|</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">grep</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-c</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"^??"</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">||</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> 0<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">)</span></span>
<span id="cb6-101">    <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">modified</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$(</span><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$git_status</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-102">        <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">|</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">grep</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-c</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"^ M"</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">||</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> 0<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">)</span></span>
<span id="cb6-103">    <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">added</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$(</span><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$git_status</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-104">        <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">|</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">grep</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-c</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"^A "</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">||</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> 0<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">)</span></span>
<span id="cb6-105">    <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">deleted</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$(</span><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$git_status</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-106">        <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">|</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">grep</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-c</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"^D "</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">||</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> 0<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">)</span></span>
<span id="cb6-107"></span>
<span id="cb6-108">    <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">log_message</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"INFO"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-109">        <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$relative_path</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">: </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$untracked</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;"> new,"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-110">        <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">" </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$modified</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;"> modified, </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$added</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;"> added,"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-111">        <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">" </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$deleted</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;"> deleted"</span></span>
<span id="cb6-112"></span>
<span id="cb6-113">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">! </span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> add <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-A</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span>/dev/null<span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">then</span></span>
<span id="cb6-114">        <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">log_message</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"ERROR"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-115">            <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Failed to stage: </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$relative_path</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb6-116">        <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">((</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">error_count</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">++</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">))</span></span>
<span id="cb6-117">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">continue</span></span>
<span id="cb6-118">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fi</span></span>
<span id="cb6-119"></span>
<span id="cb6-120">    <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">log_message</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"INFO"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-121">        <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Staged changes: </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$relative_path</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb6-122"></span>
<span id="cb6-123">    <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">commit_message</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Auto-backup:"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-124">        <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">" </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$(</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">date</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'+%Y-%m-%d %H:%M:%S'</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">)</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb6-125"></span>
<span id="cb6-126">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> commit <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-m</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$commit_message</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-127">       <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span>/dev/null <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;&amp;</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">then</span></span>
<span id="cb6-128">        <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">log_message</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"SUCCESS"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-129">            <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Committed: </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$relative_path</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb6-130"></span>
<span id="cb6-131">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">! </span><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">branch_exists_on_remote</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-132">             <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$current_branch</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">then</span></span>
<span id="cb6-133">            <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">log_message</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"WARNING"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-134">                <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"'</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$current_branch</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">' not on"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-135">                <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">" remote: </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$relative_path</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb6-136"></span>
<span id="cb6-137">            <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> push <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--set-upstream</span> origin <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-138">               <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$current_branch</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span>/dev/null</span>
<span id="cb6-139">            <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">then</span></span>
<span id="cb6-140">                <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">log_message</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"SUCCESS"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-141">                    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Created and pushed"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-142">                    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">" '</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$current_branch</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">':"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-143">                    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">" </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$relative_path</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb6-144">                <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">((</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">backup_count</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">++</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">))</span></span>
<span id="cb6-145">            <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">else</span></span>
<span id="cb6-146">                <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">log_message</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"ERROR"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-147">                    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Failed to push new"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-148">                    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">" branch: </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$relative_path</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb6-149">                <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">((</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">error_count</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">++</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">))</span></span>
<span id="cb6-150">            <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fi</span></span>
<span id="cb6-151">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">else</span></span>
<span id="cb6-152">            <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> push origin <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-153">               <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$current_branch</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span>/dev/null</span>
<span id="cb6-154">            <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">then</span></span>
<span id="cb6-155">                <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">log_message</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"SUCCESS"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-156">                    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Pushed '</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$current_branch</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">':"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-157">                    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">" </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$relative_path</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb6-158">                <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">((</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">backup_count</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">++</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">))</span></span>
<span id="cb6-159">            <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">else</span></span>
<span id="cb6-160">                <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">log_message</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"ERROR"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-161">                    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Push failed:"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-162">                    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">" </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$relative_path</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-163">                    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">" (check network/auth)"</span></span>
<span id="cb6-164">                <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">((</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">error_count</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">++</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">))</span></span>
<span id="cb6-165">            <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fi</span></span>
<span id="cb6-166">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fi</span></span>
<span id="cb6-167">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">else</span></span>
<span id="cb6-168">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> diff <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--cached</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--quiet</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">then</span></span>
<span id="cb6-169">            <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">log_message</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"INFO"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-170">                <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"No changes to commit:"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-171">                <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">" </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$relative_path</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb6-172">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">else</span></span>
<span id="cb6-173">            <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">log_message</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"ERROR"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-174">                <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Commit failed:"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-175">                <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">" </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$relative_path</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb6-176">            <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">((</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">error_count</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">++</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">))</span></span>
<span id="cb6-177">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fi</span></span>
<span id="cb6-178">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fi</span></span>
<span id="cb6-179"></span>
<span id="cb6-180"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">done</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;(</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">find</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$RESEARCH_DIR</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-181">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-name</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">".git"</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-type</span> d <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-print0</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">)</span></span></code></pre></div>
</section>
<section id="summary-report" class="level3">
<h3 class="anchored" data-anchor-id="summary-report">Summary Report</h3>
<p>After processing every repository, the script logs aggregate statistics and, in verbose mode, prints a human-readable summary to the console.</p>
<div class="sourceCode" id="cb7" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb7-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">log_message</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"INFO"</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Backup scan complete"</span></span>
<span id="cb7-2"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">log_message</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"INFO"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb7-3">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Summary: </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repo_count</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;"> processed,"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb7-4">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">" </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$backup_count</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;"> backed up"</span></span>
<span id="cb7-5"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">log_message</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"INFO"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb7-6">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Excluded: </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$excluded_count</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">,"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb7-7">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">" Skipped: </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$skipped_count</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">,"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb7-8">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">" Errors: </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$error_count</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">,"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb7-9">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">" Warnings: </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$warning_count</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb7-10"></span>
<span id="cb7-11"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">[[</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$VERBOSE</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">==</span> true <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">]];</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">then</span></span>
<span id="cb7-12">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">""</span></span>
<span id="cb7-13">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"=== BACKUP SUMMARY ==="</span></span>
<span id="cb7-14">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Repositories found:"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb7-15">         <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$(</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">(</span><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">repo_count</span> + excluded_count<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">" </span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb7-16"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">         "</span> + skipped_count<span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">)</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">)</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb7-17">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Excluded: </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$excluded_count</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb7-18">         <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"(archive/backup)"</span></span>
<span id="cb7-19">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Skipped: </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$skipped_count</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;"> (not rgt47)"</span></span>
<span id="cb7-20">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Processed: </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repo_count</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb7-21">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Backed up: </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$backup_count</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb7-22">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Errors: </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$error_count</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb7-23">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Warnings: </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$warning_count</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb7-24">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">""</span></span>
<span id="cb7-25">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Log file: </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$LOG_FILE</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb7-26"></span>
<span id="cb7-27">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">[[</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$error_count</span> <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">-gt</span> 0 <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">]];</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">then</span></span>
<span id="cb7-28">        <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">""</span></span>
<span id="cb7-29">        <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"WARNING: There were errors"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb7-30">             <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"during backup. Check the log"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb7-31">             <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"file for details."</span></span>
<span id="cb7-32">        <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">exit</span> 1</span>
<span id="cb7-33">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">elif</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">[[</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$warning_count</span> <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">-gt</span> 0 <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">]];</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">then</span></span>
<span id="cb7-34">        <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">""</span></span>
<span id="cb7-35">        <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"NOTE: Backup completed with"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb7-36">             <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"warnings. Check the log file"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb7-37">             <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"for details."</span></span>
<span id="cb7-38">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">else</span></span>
<span id="cb7-39">        <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">""</span></span>
<span id="cb7-40">        <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Backup completed successfully."</span></span>
<span id="cb7-41">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fi</span></span>
<span id="cb7-42"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fi</span></span>
<span id="cb7-43"></span>
<span id="cb7-44"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">exit</span> 0</span></code></pre></div>
</section>
</section>
<section id="scheduling-with-cron" class="level2">
<h2 class="anchored" data-anchor-id="scheduling-with-cron">Scheduling with Cron</h2>
<p>The final step is to make the script run automatically. A cron job at 15-minute intervals provides a good balance between backup frequency and system resource usage.</p>
<div class="sourceCode" id="cb8" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb8-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">crontab</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-e</span></span></code></pre></div>
<p>Add the following entry:</p>
<div class="sourceCode" id="cb9" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb9-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">*/15</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">*</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">*</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">*</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">*</span> /Users/<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$(</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">whoami</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">)</span>/scripts/backup-research.sh</span></code></pre></div>
<p>Save and exit (<code>Ctrl+X</code>, then <code>Y</code>, then <code>Enter</code> in nano; or <code>Esc</code>, <code>:wq</code>, <code>Enter</code> in vim). Verify:</p>
<div class="sourceCode" id="cb10" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb10-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">crontab</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-l</span></span></code></pre></div>
<p>Wait 15 minutes, then confirm execution by inspecting the log:</p>
<div class="sourceCode" id="cb11" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb11-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">tail</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-20</span> ~/Library/Logs/research_backup.log</span></code></pre></div>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/sec-three-tier-backup-architecture/media/images/ambiance2.png" class="img-fluid figure-img"></p>
<figcaption>Terminal windows and backup logs – the machinery behind automated research protection.</figcaption>
</figure>
</div>
</section>
</section>
<section id="section-2-bulk-github-account-archival" class="level1">
<h1>Section 2: Bulk GitHub Account Archival</h1>
<p>The ongoing backup system protects daily work, but it does not address the accumulation of private repositories that exist only on GitHub. A GitHub archive, in this context, is a local copy of everything GitHub stores for a repository: not just the code and commit history, but also the metadata that lives only on GitHub’s servers (issue threads, pull request discussions, release binaries, labels, milestones, and wiki pages).</p>
<p>A regular <code>git clone</code> captures the commit graph but misses all of that surrounding context. A mirror clone (<code>git clone --mirror</code>) captures every ref, including remote-tracking branches and tags. A git bundle packages that mirror into a single portable file. And the GitHub API exports capture the metadata that git itself does not track.</p>
<p>The archival script combines all three approaches: mirror clone for completeness, bundle for portability, and API exports for metadata. The result is a self-contained backup directory per repository that can survive even if GitHub itself becomes unavailable.</p>
<section id="the-three-phase-approach" class="level2">
<h2 class="anchored" data-anchor-id="the-three-phase-approach">The Three-Phase Approach</h2>
<p>The archive script follows a strict three-phase process. Understanding this structure makes the full script easier to follow.</p>
<p><strong>Phase 1: Backup Everything.</strong> For each repository, the script creates:</p>
<ul>
<li>A full git mirror with every branch and tag</li>
<li>A portable bundle file for easy transfer</li>
<li>Wiki content (if the repo has one)</li>
<li>Metadata exports via the GitHub API (issues, PRs, releases, labels, milestones, workflows)</li>
<li>Downloaded release assets (binaries, artifacts)</li>
</ul>
<p><strong>Phase 2: Verify Backups.</strong> Before any deletion, the script runs <code>git bundle verify</code> on every bundle. If any verification fails, the entire deletion phase is aborted. This step is essential: without it, the deletion phase cannot be trusted.</p>
<p><strong>Phase 3: Selective Deletion.</strong> Only repos not in the keep-list get deleted, and only after an explicit typed confirmation (<code>DELETE</code>). Repos in <code>KEEP_ON_GITHUB</code> are still backed up but are not removed from GitHub.</p>
</section>
<section id="the-complete-working-script" class="level2">
<h2 class="anchored" data-anchor-id="the-complete-working-script">The Complete Working Script</h2>
<p>Save the full script as <code>github-archive.sh</code> and make it executable with <code>chmod +x github-archive.sh</code>.</p>
<section id="script-header-and-configuration" class="level3">
<h3 class="anchored" data-anchor-id="script-header-and-configuration">Script Header and Configuration</h3>
<div class="sourceCode" id="cb12" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb12-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#!/bin/bash</span></span>
<span id="cb12-2"></span>
<span id="cb12-3"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">set</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-e</span></span>
<span id="cb12-4"></span>
<span id="cb12-5"><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">OWNER</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"your-username"</span></span>
<span id="cb12-6"><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">BACKUP_DIR</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$HOME</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">/github-archive"</span></span>
<span id="cb12-7"><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">DATE</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$(</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">date</span> +%Y%m%d<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">)</span></span>
<span id="cb12-8"><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">LOG_FILE</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$BACKUP_DIR</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">/archive_</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$DATE</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">.log"</span></span>
<span id="cb12-9"><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">DRY_RUN</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>false</span>
<span id="cb12-10"></span>
<span id="cb12-11"><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">KEEP_ON_GITHUB</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">(</span></span>
<span id="cb12-12">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"important-project"</span></span>
<span id="cb12-13">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"active-work"</span></span>
<span id="cb12-14">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shared-with-team"</span></span>
<span id="cb12-15"><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">)</span></span>
<span id="cb12-16"></span>
<span id="cb12-17"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">usage()</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">{</span></span>
<span id="cb12-18">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Usage: </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$0</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;"> [OPTIONS]"</span></span>
<span id="cb12-19">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">""</span></span>
<span id="cb12-20">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Options:"</span></span>
<span id="cb12-21">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"  -n, --dry-run     Show what would happen"</span></span>
<span id="cb12-22">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"  -o, --owner NAME  GitHub username/org"</span></span>
<span id="cb12-23">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"  -d, --dir PATH    Backup directory"</span></span>
<span id="cb12-24">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"  -h, --help        Show this help message"</span></span>
<span id="cb12-25">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">exit</span> 0</span>
<span id="cb12-26"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">}</span></span>
<span id="cb12-27"></span>
<span id="cb12-28"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">while</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">[[</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$#</span> <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">-gt</span> 0 <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">]];</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">do</span></span>
<span id="cb12-29">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">case</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$1</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span></span>
<span id="cb12-30">        <span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">-n</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">|</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">--dry-run</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">)</span></span>
<span id="cb12-31">            <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">DRY_RUN</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>true</span>
<span id="cb12-32">            <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">shift</span></span>
<span id="cb12-33">            <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;;</span></span>
<span id="cb12-34">        <span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">-o</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">|</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">--owner</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">)</span></span>
<span id="cb12-35">            <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">OWNER</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$2</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb12-36">            <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">shift</span> 2</span>
<span id="cb12-37">            <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;;</span></span>
<span id="cb12-38">        <span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">-d</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">|</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">--dir</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">)</span></span>
<span id="cb12-39">            <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">BACKUP_DIR</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$2</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb12-40">            <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">shift</span> 2</span>
<span id="cb12-41">            <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;;</span></span>
<span id="cb12-42">        <span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">-h</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">|</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">--help</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">)</span></span>
<span id="cb12-43">            <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">usage</span></span>
<span id="cb12-44">            <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;;</span></span>
<span id="cb12-45">        <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">*</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">)</span></span>
<span id="cb12-46">            <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Unknown option: </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$1</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb12-47">            <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">usage</span></span>
<span id="cb12-48">            <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;;</span></span>
<span id="cb12-49">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">esac</span></span>
<span id="cb12-50"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">done</span></span>
<span id="cb12-51"></span>
<span id="cb12-52"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">mkdir</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-p</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$BACKUP_DIR</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span></code></pre></div>
<p>The <code>KEEP_ON_GITHUB</code> array is the most important configuration. Reviewing the repository list before running the script ensures the right ones are included.</p>
</section>
<section id="utility-functions" class="level3">
<h3 class="anchored" data-anchor-id="utility-functions">Utility Functions</h3>
<div class="sourceCode" id="cb13" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb13-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">log()</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">{</span></span>
<span id="cb13-2">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">local</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">prefix</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">""</span></span>
<span id="cb13-3">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">[</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$DRY_RUN</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">=</span> true <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">]</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">then</span></span>
<span id="cb13-4">        <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">prefix</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"[DRY-RUN] "</span></span>
<span id="cb13-5">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fi</span></span>
<span id="cb13-6">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"[</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$(</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">date</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'+%Y-%m-%d %H:%M:%S'</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">)</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">] </span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb13-7"><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">${prefix}$1</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">|</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">tee</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-a</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$LOG_FILE</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb13-8"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">}</span></span>
<span id="cb13-9"></span>
<span id="cb13-10"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">is_kept()</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">{</span></span>
<span id="cb13-11">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">local</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">repo</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$1</span></span>
<span id="cb13-12">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> kept <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">${KEEP_ON_GITHUB</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">[@]</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">}</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">do</span></span>
<span id="cb13-13">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">[</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repo</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">=</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$kept</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">]</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">then</span></span>
<span id="cb13-14">            <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span></span>
<span id="cb13-15">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fi</span></span>
<span id="cb13-16">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">done</span></span>
<span id="cb13-17">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span></span>
<span id="cb13-18"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">}</span></span></code></pre></div>
</section>
<section id="phase-1-the-backup-function" class="level3">
<h3 class="anchored" data-anchor-id="phase-1-the-backup-function">Phase 1: The Backup Function</h3>
<div class="sourceCode" id="cb14" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb14-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">backup_repo()</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">{</span></span>
<span id="cb14-2">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">local</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">repo</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$1</span></span>
<span id="cb14-3">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">local</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">repo_dir</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$BACKUP_DIR</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">/</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repo</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb14-4"></span>
<span id="cb14-5">    <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">log</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"=== Backing up </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repo</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;"> ==="</span></span>
<span id="cb14-6"></span>
<span id="cb14-7">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">[</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$DRY_RUN</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">=</span> true <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">]</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">then</span></span>
<span id="cb14-8">        <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">log</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Would create: </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repo_dir</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb14-9">        <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">log</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Would clone: </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$OWNER</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">/</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repo</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb14-10">        <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">log</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Would export: issues, PRs, releases"</span></span>
<span id="cb14-11">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span></span>
<span id="cb14-12">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fi</span></span>
<span id="cb14-13"></span>
<span id="cb14-14">    <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">mkdir</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-p</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repo_dir</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb14-15">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">cd</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repo_dir</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb14-16"></span>
<span id="cb14-17">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">[</span> <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">!</span> <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">-d</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"repo.git"</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">]</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">then</span></span>
<span id="cb14-18">        <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">log</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Cloning repository..."</span></span>
<span id="cb14-19">        <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">gh</span> repo clone <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$OWNER</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">/</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repo</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb14-20">            repo.git <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--mirror</span></span>
<span id="cb14-21">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">else</span></span>
<span id="cb14-22">        <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">log</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Updating existing clone..."</span></span>
<span id="cb14-23">        <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">cd</span> repo.git <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">&amp;&amp;</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> fetch <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--all</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">&amp;&amp;</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">cd</span> ..</span>
<span id="cb14-24">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fi</span></span>
<span id="cb14-25"></span>
<span id="cb14-26">    <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">log</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Creating git bundle..."</span></span>
<span id="cb14-27">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">cd</span> repo.git</span>
<span id="cb14-28">    <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> bundle create ../repo.bundle <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--all</span></span>
<span id="cb14-29">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">cd</span> ..</span>
<span id="cb14-30"></span>
<span id="cb14-31">    <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">log</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Checking for wiki..."</span></span>
<span id="cb14-32">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">gh</span> api <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"repos/</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$OWNER</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">/</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repo</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb14-33">        <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--jq</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'.has_wiki'</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span>/dev/null <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb14-34">        <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">|</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">grep</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-q</span> true<span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">then</span></span>
<span id="cb14-35">        <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> clone <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb14-36">            <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"https://github.com/</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$OWNER</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">/</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repo</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">.wiki.git"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb14-37">            wiki.git <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span>/dev/null <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb14-38">            <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">||</span> <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">log</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"No wiki content"</span></span>
<span id="cb14-39">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fi</span></span>
<span id="cb14-40"></span>
<span id="cb14-41">    <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">log</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Exporting metadata..."</span></span>
<span id="cb14-42">    <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">gh</span> api <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"repos/</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$OWNER</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">/</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repo</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb14-43">        <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> repo-info.json <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span>/dev/null <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">||</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">true</span></span>
<span id="cb14-44">    <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">gh</span> api <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"repos/</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$OWNER</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">/</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repo</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">/issues?state=all"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb14-45">        <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--paginate</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> issues.json <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span>/dev/null <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">||</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">true</span></span>
<span id="cb14-46">    <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">gh</span> api <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"repos/</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$OWNER</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">/</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repo</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">/pulls?state=all"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb14-47">        <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--paginate</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb14-48">        <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> pull-requests.json <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span>/dev/null <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">||</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">true</span></span>
<span id="cb14-49">    <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">gh</span> api <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"repos/</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$OWNER</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">/</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repo</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">/releases"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb14-50">        <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--paginate</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> releases.json <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span>/dev/null <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">||</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">true</span></span>
<span id="cb14-51">    <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">gh</span> api <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"repos/</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$OWNER</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">/</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repo</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">/labels"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb14-52">        <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--paginate</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> labels.json <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span>/dev/null <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">||</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">true</span></span>
<span id="cb14-53">    <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">gh</span> api <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"repos/</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$OWNER</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">/</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repo</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">/milestones?state=all"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb14-54">        <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--paginate</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb14-55">        <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> milestones.json <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span>/dev/null <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">||</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">true</span></span>
<span id="cb14-56"></span>
<span id="cb14-57">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">[</span> <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">-s</span> releases.json <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">]</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb14-58">        <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">&amp;&amp;</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">[</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$(</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">cat</span> releases.json<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">)</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">!=</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"[]"</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">]</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">then</span></span>
<span id="cb14-59">        <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">log</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Downloading release assets..."</span></span>
<span id="cb14-60">        <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">mkdir</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-p</span> release-assets</span>
<span id="cb14-61">        <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">gh</span> release list <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-R</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$OWNER</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">/</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repo</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb14-62">            <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--limit</span> 100 <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span>/dev/null <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb14-63">            <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">|</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">while</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">read</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-r</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">tag</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">rest</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">do</span></span>
<span id="cb14-64">            <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">gh</span> release download <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$tag</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb14-65">                <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-R</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$OWNER</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">/</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repo</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb14-66">                <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-D</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"release-assets/</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$tag</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb14-67">                <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span>/dev/null <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">||</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">true</span></span>
<span id="cb14-68">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">done</span></span>
<span id="cb14-69">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fi</span></span>
<span id="cb14-70"></span>
<span id="cb14-71">    <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">log</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Completed backup of </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repo</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb14-72">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">cd</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$BACKUP_DIR</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb14-73"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">}</span></span></code></pre></div>
<p>The <code>--paginate</code> flag on <code>gh api</code> calls is essential. Without it, GitHub’s API returns only the first 30 items per endpoint, which means issues and PRs on larger repos are silently lost.</p>
</section>
<section id="phase-2-the-verification-function" class="level3">
<h3 class="anchored" data-anchor-id="phase-2-the-verification-function">Phase 2: The Verification Function</h3>
<div class="sourceCode" id="cb15" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb15-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">verify_backup()</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">{</span></span>
<span id="cb15-2">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">local</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">repo</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$1</span></span>
<span id="cb15-3">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">local</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">repo_dir</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$BACKUP_DIR</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">/</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repo</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb15-4"></span>
<span id="cb15-5">    <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">log</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Verifying backup of </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repo</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">..."</span></span>
<span id="cb15-6"></span>
<span id="cb15-7">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">[</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$DRY_RUN</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">=</span> true <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">]</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">then</span></span>
<span id="cb15-8">        <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">log</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Would verify: </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repo_dir</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">/repo.bundle"</span></span>
<span id="cb15-9">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span></span>
<span id="cb15-10">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fi</span></span>
<span id="cb15-11"></span>
<span id="cb15-12">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">[</span> <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">-f</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repo_dir</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">/repo.bundle"</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">]</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">then</span></span>
<span id="cb15-13">        <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">cd</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repo_dir</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb15-14">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> bundle verify repo.bundle <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb15-15">            <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> /dev/null <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;&amp;</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">then</span></span>
<span id="cb15-16">            <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">log</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"PASS: Bundle verified"</span></span>
<span id="cb15-17">            <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span></span>
<span id="cb15-18">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">else</span></span>
<span id="cb15-19">            <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">log</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"FAIL: Bundle verification failed!"</span></span>
<span id="cb15-20">            <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span></span>
<span id="cb15-21">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fi</span></span>
<span id="cb15-22">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">else</span></span>
<span id="cb15-23">        <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">log</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"FAIL: Bundle not found!"</span></span>
<span id="cb15-24">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span></span>
<span id="cb15-25">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fi</span></span>
<span id="cb15-26"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">}</span></span></code></pre></div>
</section>
<section id="phase-3-the-deletion-function" class="level3">
<h3 class="anchored" data-anchor-id="phase-3-the-deletion-function">Phase 3: The Deletion Function</h3>
<div class="sourceCode" id="cb16" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb16-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">delete_repo()</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">{</span></span>
<span id="cb16-2">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">local</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">repo</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$1</span></span>
<span id="cb16-3"></span>
<span id="cb16-4">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">[</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$DRY_RUN</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">=</span> true <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">]</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">then</span></span>
<span id="cb16-5">        <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">log</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Would delete: </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$OWNER</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">/</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repo</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb16-6">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span></span>
<span id="cb16-7">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fi</span></span>
<span id="cb16-8"></span>
<span id="cb16-9">    <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">log</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Deleting </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repo</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;"> from GitHub..."</span></span>
<span id="cb16-10">    <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">gh</span> repo delete <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$OWNER</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">/</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repo</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--yes</span></span>
<span id="cb16-11">    <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">log</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Deleted </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repo</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb16-12"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">}</span></span></code></pre></div>
</section>
<section id="main-execution-flow" class="level3">
<h3 class="anchored" data-anchor-id="main-execution-flow">Main Execution Flow</h3>
<div class="sourceCode" id="cb17" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb17-1"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">[</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$DRY_RUN</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">=</span> true <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">]</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">then</span></span>
<span id="cb17-2">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"======================================="</span></span>
<span id="cb17-3">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">" DRY-RUN MODE - No changes will be made"</span></span>
<span id="cb17-4">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"======================================="</span></span>
<span id="cb17-5">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">""</span></span>
<span id="cb17-6"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fi</span></span>
<span id="cb17-7"></span>
<span id="cb17-8"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">log</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Starting GitHub archive process"</span></span>
<span id="cb17-9"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">log</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Owner: </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$OWNER</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb17-10"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">log</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Backup directory: </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$BACKUP_DIR</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb17-11"></span>
<span id="cb17-12"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">log</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Fetching list of private repositories..."</span></span>
<span id="cb17-13"><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">repos</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$(</span><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">gh</span> repo list <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$OWNER</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb17-14">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--limit</span> 500 <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--private</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb17-15">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--json</span> name <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-q</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'.[].name'</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">)</span></span>
<span id="cb17-16"><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">repo_count</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$(</span><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repos</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">|</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">wc</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-l</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">|</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">tr</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-d</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">' '</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">)</span></span>
<span id="cb17-17"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">log</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Found </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repo_count</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;"> private repositories"</span></span>
<span id="cb17-18"></span>
<span id="cb17-19"><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">repos_to_delete</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">""</span></span>
<span id="cb17-20"><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">repos_to_keep</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">""</span></span>
<span id="cb17-21"><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">delete_count</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>0</span>
<span id="cb17-22"><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">keep_count</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>0</span>
<span id="cb17-23"></span>
<span id="cb17-24"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> repo <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repos</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">do</span></span>
<span id="cb17-25">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">is_kept</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repo</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">then</span></span>
<span id="cb17-26">        <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">repos_to_keep</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repos_to_keep</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;"> </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repo</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb17-27">        <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">((</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">keep_count</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">++</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">))</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">||</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">true</span></span>
<span id="cb17-28">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">else</span></span>
<span id="cb17-29">        <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">repos_to_delete</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repos_to_delete</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;"> </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repo</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb17-30">        <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">((</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">delete_count</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">++</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">))</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">||</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">true</span></span>
<span id="cb17-31">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fi</span></span>
<span id="cb17-32"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">done</span></span>
<span id="cb17-33"></span>
<span id="cb17-34"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">log</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Repos to archive and DELETE: </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$delete_count</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb17-35"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">log</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Repos to archive and KEEP: </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$keep_count</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb17-36"></span>
<span id="cb17-37"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">""</span></span>
<span id="cb17-38"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"=== REPO CATEGORIZATION ==="</span></span>
<span id="cb17-39"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">""</span></span>
<span id="cb17-40"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Will be DELETED after backup (</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$delete_count</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">):"</span></span>
<span id="cb17-41"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> repo <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repos_to_delete</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">do</span></span>
<span id="cb17-42">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"  x </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repo</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb17-43"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">done</span></span>
<span id="cb17-44"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">""</span></span>
<span id="cb17-45"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Will be KEPT on GitHub (</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$keep_count</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">):"</span></span>
<span id="cb17-46"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> repo <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repos_to_keep</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">do</span></span>
<span id="cb17-47">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"  + </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repo</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb17-48"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">done</span></span>
<span id="cb17-49"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">""</span></span>
<span id="cb17-50"></span>
<span id="cb17-51"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">[</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$DRY_RUN</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">=</span> true <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">]</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">then</span></span>
<span id="cb17-52">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"=== DRY-RUN: PHASE 1 (BACKUP) ==="</span></span>
<span id="cb17-53">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> repo <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repos</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">do</span></span>
<span id="cb17-54">        <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">backup_repo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repo</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb17-55">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">done</span></span>
<span id="cb17-56"></span>
<span id="cb17-57">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">""</span></span>
<span id="cb17-58">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"=== DRY-RUN: PHASE 2 (VERIFICATION) ==="</span></span>
<span id="cb17-59">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> repo <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repos</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">do</span></span>
<span id="cb17-60">        <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">verify_backup</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repo</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb17-61">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">done</span></span>
<span id="cb17-62"></span>
<span id="cb17-63">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">""</span></span>
<span id="cb17-64">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"=== DRY-RUN: PHASE 3 (DELETION) ==="</span></span>
<span id="cb17-65">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> repo <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repos_to_delete</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">do</span></span>
<span id="cb17-66">        <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">delete_repo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repo</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb17-67">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">done</span></span>
<span id="cb17-68"></span>
<span id="cb17-69">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">""</span></span>
<span id="cb17-70">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"======================================="</span></span>
<span id="cb17-71">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"   DRY-RUN COMPLETE"</span></span>
<span id="cb17-72">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"======================================="</span></span>
<span id="cb17-73">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">""</span></span>
<span id="cb17-74">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Run without --dry-run to execute."</span></span>
<span id="cb17-75">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">exit</span> 0</span>
<span id="cb17-76"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fi</span></span>
<span id="cb17-77"></span>
<span id="cb17-78"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">log</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"=== PHASE 1: BACKUP ==="</span></span>
<span id="cb17-79"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> repo <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repos</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">do</span></span>
<span id="cb17-80">    <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">backup_repo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repo</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb17-81"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">done</span></span>
<span id="cb17-82"></span>
<span id="cb17-83"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">log</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"=== PHASE 2: VERIFICATION ==="</span></span>
<span id="cb17-84"><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">failed_repos</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">""</span></span>
<span id="cb17-85"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> repo <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repos</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">do</span></span>
<span id="cb17-86">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">! </span><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">verify_backup</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repo</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">then</span></span>
<span id="cb17-87">        <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">failed_repos</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$failed_repos</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;"> </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repo</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb17-88">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fi</span></span>
<span id="cb17-89"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">done</span></span>
<span id="cb17-90"></span>
<span id="cb17-91"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">[</span> <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">-n</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$failed_repos</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">]</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">then</span></span>
<span id="cb17-92">    <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">log</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"WARNING: Verification failed:</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$failed_repos</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb17-93">    <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">log</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Aborting deletion phase"</span></span>
<span id="cb17-94">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">exit</span> 1</span>
<span id="cb17-95"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fi</span></span>
<span id="cb17-96"></span>
<span id="cb17-97"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">log</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"All backups verified successfully!"</span></span>
<span id="cb17-98"></span>
<span id="cb17-99"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">log</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"=== PHASE 3: DELETION ==="</span></span>
<span id="cb17-100"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">""</span></span>
<span id="cb17-101"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Backup complete and verified!"</span></span>
<span id="cb17-102"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">""</span></span>
<span id="cb17-103"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">read</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-p</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Delete </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$delete_count</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;"> repos? </span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb17-104"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">(type 'DELETE'): "</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">confirm</span></span>
<span id="cb17-105"></span>
<span id="cb17-106"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">[</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$confirm</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">=</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"DELETE"</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">]</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">then</span></span>
<span id="cb17-107">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> repo <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repos_to_delete</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">do</span></span>
<span id="cb17-108">        <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">delete_repo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$repo</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb17-109">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">done</span></span>
<span id="cb17-110">    <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">log</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Deleted </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$delete_count</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;"> repositories"</span></span>
<span id="cb17-111"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">else</span></span>
<span id="cb17-112">    <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">log</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Deletion cancelled"</span></span>
<span id="cb17-113"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fi</span></span>
<span id="cb17-114"></span>
<span id="cb17-115"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">log</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Archive process complete"</span></span></code></pre></div>
</section>
</section>
<section id="using-the-archival-script" class="level2">
<h2 class="anchored" data-anchor-id="using-the-archival-script">Using the Archival Script</h2>
<div class="sourceCode" id="cb18" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb18-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">./github-archive.sh</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--dry-run</span></span>
<span id="cb18-2"></span>
<span id="cb18-3"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">./github-archive.sh</span></span>
<span id="cb18-4"></span>
<span id="cb18-5"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">./github-archive.sh</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb18-6">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--owner</span> myorg <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--dir</span> /external/drive/backup</span></code></pre></div>
<p>Running the dry-run first is not merely useful; it is essential. The categorisation output can catch repos that were forgotten in the keep-list.</p>
<p>Configure the <code>KEEP_ON_GITHUB</code> array to match your situation:</p>
<div class="sourceCode" id="cb19" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb19-1"><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">KEEP_ON_GITHUB</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">(</span></span>
<span id="cb19-2">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"active-projects"</span></span>
<span id="cb19-3">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shared-with-team"</span></span>
<span id="cb19-4">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"client-work"</span></span>
<span id="cb19-5">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"portfolio"</span></span>
<span id="cb19-6"><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">)</span></span></code></pre></div>
</section>
<section id="backup-directory-structure" class="level2">
<h2 class="anchored" data-anchor-id="backup-directory-structure">Backup Directory Structure</h2>
<p>After running the script, the backup directory looks like this:</p>
<pre><code>~/github-archive/
+-- important-project/      # KEPT on GitHub
|   +-- repo.git/
|   +-- repo.bundle
|   +-- repo-info.json
|
+-- old-project-1/          # DELETED from GitHub
|   +-- repo.git/
|   +-- repo.bundle
|   +-- wiki.git/
|   +-- repo-info.json
|   +-- issues.json
|   +-- pull-requests.json
|   +-- releases.json
|   +-- labels.json
|   +-- milestones.json
|   +-- release-assets/
|
+-- archive_20260517.log</code></pre>
<table class="caption-top table">
<thead>
<tr class="header">
<th>Content</th>
<th>Format</th>
<th>Use Case</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>Git history</td>
<td><code>repo.git/</code> + bundle</td>
<td>Full reproducibility</td>
</tr>
<tr class="even">
<td>Wiki</td>
<td><code>wiki.git/</code></td>
<td>Documentation</td>
</tr>
<tr class="odd">
<td>Issues</td>
<td><code>issues.json</code></td>
<td>Discussion archive</td>
</tr>
<tr class="even">
<td>Pull requests</td>
<td><code>pull-requests.json</code></td>
<td>Code review history</td>
</tr>
<tr class="odd">
<td>Releases</td>
<td><code>releases.json</code></td>
<td>Version history</td>
</tr>
<tr class="even">
<td>Release assets</td>
<td><code>release-assets/</code></td>
<td>Binaries, artifacts</td>
</tr>
<tr class="odd">
<td>Metadata</td>
<td><code>repo-info.json</code></td>
<td>Repository config</td>
</tr>
<tr class="even">
<td>Labels</td>
<td><code>labels.json</code></td>
<td>Issue classification</td>
</tr>
<tr class="odd">
<td>Milestones</td>
<td><code>milestones.json</code></td>
<td>Project tracking</td>
</tr>
</tbody>
</table>
</section>
<section id="restoring-from-archive" class="level2">
<h2 class="anchored" data-anchor-id="restoring-from-archive">Restoring from Archive</h2>
<div class="sourceCode" id="cb21" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb21-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> clone <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb21-2">    ~/github-archive/repo-name/repo.bundle <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb21-3">    restored-repo</span>
<span id="cb21-4"></span>
<span id="cb21-5"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">gh</span> repo create new-repo-name <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--private</span></span>
<span id="cb21-6"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">cd</span> ~/github-archive/repo-name/repo.git</span>
<span id="cb21-7"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> push <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--mirror</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb21-8">    git@github.com:you/new-repo-name.git</span></code></pre></div>
<p>The bundle approach is more convenient for quick local inspection; the mirror push is better for actually recreating a repository on GitHub.</p>
</section>
</section>
<section id="section-3-verification-and-testing" class="level1">
<h1>Section 3: Verification and Testing</h1>
<section id="ongoing-backup-verification" class="level2">
<h2 class="anchored" data-anchor-id="ongoing-backup-verification">Ongoing Backup Verification</h2>
<p>To confirm that the daily backup script is running correctly:</p>
<div class="sourceCode" id="cb22" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb22-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">tail</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-20</span> ~/Library/Logs/research_backup.log</span>
<span id="cb22-2"></span>
<span id="cb22-3"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">~/scripts/backup-research.sh</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--verbose</span></span></code></pre></div>
<p>Check that recent log entries show <code>[SUCCESS]</code> for pushed repositories and that the timestamp reflects the expected cron interval.</p>
</section>
<section id="github-archive-verification" class="level2">
<h2 class="anchored" data-anchor-id="github-archive-verification">GitHub Archive Verification</h2>
<p>Before trusting any archival backup, run these checks:</p>
<div class="sourceCode" id="cb23" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb23-1"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">cd</span> ~/github-archive/repo-name</span>
<span id="cb23-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> bundle verify repo.bundle</span>
<span id="cb23-3"></span>
<span id="cb23-4"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">ls</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-la</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">*</span>.json</span>
<span id="cb23-5"></span>
<span id="cb23-6"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> bundle list-heads repo.bundle</span>
<span id="cb23-7"></span>
<span id="cb23-8"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">python3</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-m</span> json.tool issues.json <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">|</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">head</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-50</span></span></code></pre></div>
<p>These checks confirm that the bundle is structurally valid, that metadata files are non-empty, that all branches are present, and that the issue export is well-formed JSON.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/sec-three-tier-backup-architecture/media/images/ambiance3.jpg" class="img-fluid figure-img"></p>
<figcaption>Organised workspace: the goal after archiving 400 repos.</figcaption>
</figure>
</div>
<p><em>After the archive, what remains on GitHub is clean and intentional.</em></p>
</section>
</section>
<section id="daily-workflow" class="level1">
<h1>Daily Workflow</h1>
<table class="caption-top table">
<colgroup>
<col style="width: 52%">
<col style="width: 47%">
</colgroup>
<thead>
<tr class="header">
<th>Command</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><code>~/scripts/backup-research.sh --verbose</code></td>
<td>Run ongoing backup with console output</td>
</tr>
<tr class="even">
<td><code>tail -20 ~/Library/Logs/research_backup.log</code></td>
<td>Review recent backup log entries</td>
</tr>
<tr class="odd">
<td><code>bash github-archive.sh --dry-run</code></td>
<td>Preview what would be backed up or deleted</td>
</tr>
<tr class="even">
<td><code>bash github-archive.sh</code></td>
<td>Run full archival: backup, verify, confirm deletion</td>
</tr>
<tr class="odd">
<td><code>bash github-archive.sh --owner ORG</code></td>
<td>Archive a different owner’s repositories</td>
</tr>
<tr class="even">
<td><code>git bundle verify repo.bundle</code></td>
<td>Spot-check a specific archival bundle</td>
</tr>
<tr class="odd">
<td><code>gh repo list --json name</code></td>
<td>Confirm which repositories remain on GitHub</td>
</tr>
<tr class="even">
<td><code>crontab -l</code></td>
<td>Verify that the 15-minute cron job is registered</td>
</tr>
</tbody>
</table>
<p>Run the archival script quarterly or before any GitHub plan or account change. The ongoing backup script runs automatically once scheduled.</p>
</section>
<section id="things-to-watch-out-for" class="level1">
<h1>Things to Watch Out For</h1>
<ol type="1">
<li><p><strong>Homebrew Bash path.</strong> macOS ships with Bash 3.2, which lacks features the backup script depends on. Ensure the shebang points to <code>/opt/homebrew/bin/bash</code> (Apple Silicon) or <code>/usr/local/bin/bash</code> (Intel). Version mismatches can cause silent failures that are difficult to diagnose.</p></li>
<li><p><strong>SSH key agent in cron.</strong> Cron jobs do not inherit your shell environment. If Git remotes use SSH, the cron job may fail silently because <code>ssh-agent</code> is not available. Add <code>eval "$(ssh-agent -s)"</code> to the script or use macOS Keychain integration.</p></li>
<li><p><strong>The <code>--limit 500</code> cap on <code>gh repo list</code>.</strong> The command defaults to 30 results. The archival script sets it to 500, but if you have more repositories than that, you need to increase the limit or paginate manually.</p></li>
<li><p><strong>Disk space surprises.</strong> An initial estimate of 20 GB for 400 repos may prove insufficient; closer to 35 GB may be needed if several repos have large binary assets in their release history. Check with <code>df -h</code> before starting.</p></li>
<li><p><strong>Network interruptions during archival.</strong> If a clone fails midway, the <code>repo.git</code> directory exists but is incomplete. The verification phase catches this, but you must delete the partial clone and rerun.</p></li>
<li><p><strong>API rate limits.</strong> GitHub’s API allows 5,000 requests per hour for authenticated users. With 400 repos and 6 API calls each, that is 2,400 requests – within the limit but close. If the limit is reached, the script pauses without automatic retry.</p></li>
<li><p><strong>Time Machine drive connection.</strong> Time Machine requires the USB drive to be physically connected. When travelling without the drive, this tier is inactive. The Git push tier continues to operate but cloud sync becomes the only off-site copy.</p></li>
</ol>
</section>
<section id="uninstall-rollback" class="level1">
<h1>Uninstall / Rollback</h1>
<section id="removing-the-ongoing-backup-system" class="level2">
<h2 class="anchored" data-anchor-id="removing-the-ongoing-backup-system">Removing the Ongoing Backup System</h2>
<div class="sourceCode" id="cb24" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb24-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">crontab</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-e</span></span></code></pre></div>
<p>Delete the line referencing <code>backup-research.sh</code>. Then remove the script:</p>
<div class="sourceCode" id="cb25" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb25-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">rm</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-i</span> ~/scripts/backup-research.sh</span></code></pre></div>
<p>The log file can be removed independently:</p>
<div class="sourceCode" id="cb26" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb26-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">rm</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-i</span> ~/Library/Logs/research_backup.log</span></code></pre></div>
</section>
<section id="restoring-from-the-github-archive" class="level2">
<h2 class="anchored" data-anchor-id="restoring-from-the-github-archive">Restoring from the GitHub Archive</h2>
<div class="sourceCode" id="cb27" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb27-1"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">cd</span> ~/github-archive/repo-name</span>
<span id="cb27-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> clone repo.bundle restored-repo</span>
<span id="cb27-3"></span>
<span id="cb27-4"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">gh</span> repo create owner/repo-name <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--private</span></span>
<span id="cb27-5"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">cd</span> restored-repo</span>
<span id="cb27-6"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> remote add origin git@github.com:owner/repo-name.git</span>
<span id="cb27-7"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> push <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--mirror</span> origin</span></code></pre></div>
</section>
<section id="removing-the-archive-script-and-local-copies" class="level2">
<h2 class="anchored" data-anchor-id="removing-the-archive-script-and-local-copies">Removing the Archive Script and Local Copies</h2>
<div class="sourceCode" id="cb28" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb28-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">rm</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-i</span> github-archive.sh</span>
<span id="cb28-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">rm</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-ri</span> ~/github-archive/</span></code></pre></div>
<p>Confirm before removing backups: if GitHub has already been cleaned up, the local archive is the only copy.</p>
</section>
</section>
<section id="what-did-we-learn" class="level1">
<h1>What Did We Learn?</h1>
<section id="lessons-learnt" class="level2">
<h2 class="anchored" data-anchor-id="lessons-learnt">Lessons Learnt</h2>
<p><strong>Conceptual Understanding:</strong></p>
<ul>
<li>GitHub is a source-of-truth tier, not a backup tier. A suspended account, a pricing change, or a credential compromise can sever access immediately, regardless of how many commits are in the history.</li>
<li>A single backup mechanism covers only one slice of the risk surface. The combination of automated Git pushes, cloud sync, Time Machine, and periodic archival covers the full surface more reliably than any one mechanism alone.</li>
<li>A plain <code>git clone</code> misses metadata that can be more valuable than the code itself: issue discussions, PR review threads, and release notes.</li>
<li>The three-phase archival pattern (backup, verify, delete) is a general discipline applicable to any destructive batch operation, not just GitHub archiving.</li>
</ul>
<p><strong>Technical Skills:</strong></p>
<ul>
<li>Bash <code>find</code> with <code>-print0</code> and <code>read -d ''</code> safely handles directory names containing spaces and special characters, which is common in research project naming.</li>
<li>The <code>gh</code> CLI is more powerful than often expected. Combining <code>gh repo list</code>, <code>gh api --paginate</code>, and <code>gh release download</code> covers nearly every GitHub operation without touching the web interface.</li>
<li><code>git bundle verify</code> is a built-in safety net that may be unfamiliar. It confirms the bundle is a valid, complete repository.</li>
<li>Log rotation using file size checks (<code>stat -f%z</code>) prevents unbounded log growth in long-running automated scripts.</li>
</ul>
<p><strong>Gotchas and Pitfalls:</strong></p>
<ul>
<li>Cron does not source <code>.bashrc</code> or <code>.zshrc</code>, so environment variables, SSH keys, and PATH modifications are not available unless explicitly set within the script or the crontab.</li>
<li>The <code>--paginate</code> flag on <code>gh api</code> is critical. Forgetting it silently loses issues beyond the first 30 on larger repos.</li>
<li>GitHub wikis are technically separate git repositories. They must be cloned independently, and they silently fail if the wiki was enabled but never populated.</li>
<li><code>set -e</code> causes the archival script to exit on the first error, which is good for safety but requires <code>|| true</code> on commands that are expected to fail (such as cloning an empty wiki).</li>
</ul>
</section>
<section id="limitations" class="level2">
<h2 class="anchored" data-anchor-id="limitations">Limitations</h2>
<ul>
<li>The ongoing backup script only pushes to the <code>origin</code> remote. Repositories using multiple remotes receive backup to only one of them.</li>
<li>Auto-generated commit messages (‘Auto-backup: timestamp’) lack descriptive content. This serves the backup purpose but pollutes Git history for active development branches.</li>
<li>The archival script does not handle GitHub Actions workflow run history or Codespaces configurations, which are not available through the standard API.</li>
<li>Repository settings (branch protection rules, webhook configurations, deploy keys) are not exported. Recreating those requires manual setup or additional API calls.</li>
<li>Git LFS objects are not included in the mirror clone by default. Repositories using LFS need <code>git lfs fetch --all</code> added to the backup function.</li>
<li>The archival script processes repositories sequentially. For 400+ repos, this can take several hours. Parallelisation would speed things up but adds complexity.</li>
<li>Time Machine requires the USB drive to be physically connected. When travelling without the drive, this tier is inactive.</li>
</ul>
</section>
<section id="opportunities-for-improvement" class="level2">
<h2 class="anchored" data-anchor-id="opportunities-for-improvement">Opportunities for Improvement</h2>
<ol type="1">
<li>Replace fixed commit messages in the ongoing backup script with a brief summary of changed file names, providing more informative Git history.</li>
<li>Add Git LFS support to the archival script by inserting <code>git lfs fetch --all</code> into the backup function for repositories that use large file storage.</li>
<li>Parallelise the archival backup phase using <code>xargs -P 4</code> or GNU <code>parallel</code> to clone multiple repositories concurrently, reducing total runtime.</li>
<li>Export repository settings (branch protection rules, webhooks, deploy keys) so that restoration is truly complete.</li>
<li>Migrate from cron to <code>launchd</code> for better macOS integration, including wake-from-sleep triggers and retry logic.</li>
<li>Add a companion <code>github-restore.sh</code> script that reads a backup directory and recreates repositories on GitHub, including metadata re-import.</li>
<li>Add compression: after archival verification, compress each repository directory with <code>tar -czf</code> to reduce storage requirements by roughly 50-70%.</li>
</ol>
</section>
</section>
<section id="wrapping-up" class="level1">
<h1>Wrapping Up</h1>
<p>The two components in this post address different timescales of the same underlying concern: ensuring that research work is recoverable regardless of what fails. The ongoing three-tier system handles the daily rhythm, committing and pushing every 15 minutes while Time Machine and cloud sync provide complementary coverage. The archival procedure handles the periodic task of verifying that GitHub itself – the source-of-truth tier – has a local mirror that would survive the loss of the account.</p>
<p>The most important lesson is not about any specific tool but about the architecture: no single backup mechanism is sufficient, verification must be part of the process rather than assumed, and automation is the only reliable way to maintain discipline across hundreds of repositories over months and years.</p>
<p>Main takeaways:</p>
<ul>
<li>GitHub is not a backup. It is one copy, held by one company. The backup architecture is everything surrounding it.</li>
<li>Three independent ongoing tiers (Git push, cloud sync, Time Machine) cover the full spectrum of daily failure modes.</li>
<li>The three-phase archival pattern (backup, verify, delete) prevents data loss from incomplete archives and ensures deletions are irreversible only after confirmation.</li>
<li>A 400-repo archive takes roughly 2-4 hours and 20-40 GB of disk space depending on repo sizes.</li>
</ul>
</section>
<section id="see-also" class="level1">
<h1>See Also</h1>
<p><strong>Related posts on this blog:</strong></p>
<ul>
<li><a href="../../posts/01-configtermzsh/configtermzsh/analysis/report/index.qmd">Configure the Command Line for Data Science Development</a> – the terminal and shell setup that complements this backup architecture.</li>
<li><a href="../../posts/03-setupgit/setupgit/analysis/report/">Setting Up Git for Data Science Development</a></li>
<li><a href="../../posts/24-setupdotfilesongithub/setupdotfilesongithub/analysis/report/">Creating a GitHub Dotfiles Repository</a></li>
</ul>
<p><strong>Key resources:</strong></p>
<ul>
<li><a href="https://cli.github.com/manual/">GitHub CLI Documentation</a></li>
<li><a href="https://git-scm.com/docs/git-bundle">Git Bundle Documentation</a></li>
<li><a href="https://docs.github.com/en/rest/repos">GitHub REST API: Repositories</a></li>
<li><a href="https://git-scm.com/book/en/v2">Pro Git Book (free)</a> – authoritative reference for Git concepts</li>
<li><a href="https://support.apple.com/en-us/104984">Apple Time Machine Documentation</a> – official setup and troubleshooting guide</li>
<li><a href="https://crontab.guru/">Crontab Guru</a> – interactive cron schedule expression editor</li>
<li><a href="https://brew.sh/">Homebrew</a> – macOS package manager for installing Bash 5 and other tools</li>
<li><a href="https://git-lfs.github.com/">Git LFS Documentation</a></li>
</ul>
</section>
<section id="reproducibility" class="level1">
<h1>Reproducibility</h1>
<p><strong>Environment requirements:</strong></p>
<ul>
<li>macOS 12 (Monterey) or later; tested on macOS 14 Sonoma</li>
<li>Bash 5.x via Homebrew (macOS ships 3.2; install with <code>brew install bash</code>)</li>
<li>Git 2.39 or later</li>
<li><code>gh</code> CLI 2.40 or later</li>
</ul>
<p><strong>Version checks:</strong></p>
<div class="sourceCode" id="cb29" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb29-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--version</span></span>
<span id="cb29-2"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">gh</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--version</span></span>
<span id="cb29-3"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">bash</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--version</span></span></code></pre></div>
<p><strong>Script files:</strong></p>
<table class="caption-top table">
<thead>
<tr class="header">
<th>File</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><code>backup-research.sh</code></td>
<td>Ongoing 15-minute backup</td>
</tr>
<tr class="even">
<td><code>github-archive.sh</code></td>
<td>Bulk archival script</td>
</tr>
<tr class="odd">
<td><code>archive_YYYYMMDD.log</code></td>
<td>Archival execution log</td>
</tr>
<tr class="even">
<td><code>research_backup.log</code></td>
<td>Ongoing backup log</td>
</tr>
</tbody>
</table>
<p>This post does not use R or Docker. The entire workflow is pure Bash with the GitHub CLI and macOS system tools.</p>
</section>
<section id="lets-connect" class="level1">
<h1>Let’s Connect</h1>
<p><em>Have questions, suggestions, or spot an error? Let me know.</em></p>
<ul>
<li><strong>GitHub:</strong> <a href="https://github.com/rgt47">rgt47</a></li>
<li><strong>Twitter/X:</strong> <a href="https://twitter.com/rgt47"><span class="citation" data-cites="rgt47">@rgt47</span></a></li>
<li><strong>LinkedIn:</strong> <a href="https://linkedin.com/in/rgthomaslab">Ronald Glenn Thomas</a></li>
<li><strong>Email:</strong> <a href="https://rgtlab.org/contact">Contact form</a></li>
</ul>
<p>I would enjoy hearing from you if:</p>
<ul>
<li>You spot an error or a better approach to any of the code in this post.</li>
<li>You have suggestions for topics you would like to see covered.</li>
<li>You want to discuss R programming, data science, or reproducible research.</li>
<li>You have questions about anything in this tutorial.</li>
<li>You just want to say hello and connect.</li>
</ul>
<hr>
<p><em>Rendered on 2026-05-17 at 17:08 PDT.</em><br> <em>Source: ~/prj/qblog/posts/02-githubarchive/githubarchive/analysis/report/index.qmd</em></p>


</section>

 ]]></description>
  <category>git</category>
  <category>shell</category>
  <category>macos</category>
  <category>reproducibility</category>
  <guid>https://rgtlab.org/posts/sec-three-tier-backup-architecture/</guid>
  <pubDate>Sun, 17 May 2026 07:00:00 GMT</pubDate>
  <media:content url="https://rgtlab.org/posts/sec-three-tier-backup-architecture/media/images/hero.png" medium="image" type="image/png"/>
</item>
<item>
  <title>Extending the R-Vim Workflow: LaTeX Integration and Dynamic Snippets</title>
  <dc:creator>Ronald G. Thomas</dc:creator>
  <link>https://rgtlab.org/posts/wf-r-vim-latex-workflow/</link>
  <description><![CDATA[ 




<p><em>2026-05-17 16:55 PDT</em></p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/wf-r-vim-latex-workflow/media/images/hero.png" class="img-fluid quarto-figure quarto-figure-center figure-img" style="width:80.0%"></p>
</figure>
</div>
<p><em>Setting the stage for a unified editing environment.</em></p>
<section id="introduction" class="level1">
<h1>Introduction</h1>
<p>I did not really know how to connect Vim, R, and LaTeX into a single productive workflow until I spent a weekend configuring vimtex and UltiSnips from scratch. Before that weekend, my workflow was fragmented: I wrote R code in one editor, switched to another for LaTeX, and lost time every time I moved between tools.</p>
<p>The frustration peaked when I realised I was spending more time switching contexts than actually writing code or prose. Every tool had its own shortcuts, its own conventions, and its own mental model. I needed a single environment that could handle R scripts, R Markdown documents, and LaTeX files without forcing me to leave Vim.</p>
<p>We document the complete configuration arrived at after several rounds of trial and error, then extend it with a technique discovered later: embedding Python code directly inside UltiSnips snippets. Static snippets cover predictable boilerplate. Python-interpolated snippets cover cases where the expansion must be computed at runtime (for instance, generating a variable number of repeated elements based on a value typed into the first tabstop).</p>
<p>More formally, we document the Vim-plugins layer (Layer 6) of the Workflow Construct described in <a href="../../posts/52-workflow-construct/">post 52</a>, specifically the R-plus-LaTeX subset. The plugins documented here (Nvim-R or zzvim-r, vimtex, UltiSnips) compose to give Vim the same edit-run-inspect cycle that RStudio provides for R and Skim provides for LaTeX, without leaving the editor. They are the operative artefacts that make the Editor layer (<a href="../../posts/26-setupneovim/">post 26</a>) usable for the applied biostatistician’s daily R-and-LaTeX work.</p>
<section id="motivations" class="level2">
<h2 class="anchored" data-anchor-id="motivations">Motivations</h2>
<ul>
<li>I was tired of context-switching between RStudio, a LaTeX editor, and the terminal every time I worked on a statistical report.</li>
<li>I wanted a single editor that could send R code to a live REPL, compile LaTeX documents, and expand code snippets without leaving the keyboard.</li>
<li>My existing Vim setup had no awareness of R or LaTeX filetypes, so I had to configure everything from scratch.</li>
<li>I needed ALE linting and auto-fixing for R code style consistency across projects.</li>
<li>I wanted UltiSnips templates for R Markdown YAML headers so I could scaffold new analysis files in seconds.</li>
<li>I had never configured ftplugin files before and wanted to understand how Vim dispatches settings by filetype.</li>
<li>Static snippets proved too rigid for cases that require a variable number of output elements; I needed a way to compute expansion content at the moment of triggering.</li>
</ul>
</section>
<section id="objectives" class="level2">
<h2 class="anchored" data-anchor-id="objectives">Objectives</h2>
<ol type="1">
<li>Install and configure vimtex, UltiSnips, ALE, and supporting plugins in a single .vimrc file.</li>
<li>Create filetype-specific configurations for R and R Markdown that open an R terminal and define useful keybindings.</li>
<li>Set up UltiSnips snippet expansion and popup menu navigation without conflicts between Tab mappings.</li>
<li>Walk through a practical example of editing an R Markdown file with the Palmer Penguins dataset inside this configured Vim environment.</li>
<li>Extend UltiSnips with Python interpolation to generate dynamic, parametric snippet expansions.</li>
</ol>
<p>I am documenting my learning process here. If you spot errors or have better approaches, please let me know.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/wf-r-vim-latex-workflow/media/images/vim-logo.png" class="img-fluid quarto-figure quarto-figure-center figure-img" style="width:30.0%"></p>
</figure>
</div>
<p><em>The kind of focused environment where configuration work happens best.</em></p>
</section>
</section>
<section id="prerequisites-and-setup" class="level1">
<h1>Prerequisites and Setup</h1>
<p>Before proceeding, the following software must be installed on the macOS system:</p>
<ul>
<li><strong>Vim 8.1+ compiled with Python 3 support</strong> or <strong>Neovim 0.5+</strong> with terminal support. To verify Python 3 support, run <code>:echo has('python3')</code> inside Vim; a return value of <code>1</code> confirms it is available.</li>
<li><strong>R 4.4+</strong> from <a href="https://cran.r-project.org/bin/macosx/">CRAN</a></li>
<li><strong>MacTeX</strong> from <a href="https://www.tug.org/mactex/mactex-download.html">TUG</a></li>
<li><strong>vim-plug</strong> plugin manager, installed with:</li>
</ul>
<div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb1-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">curl</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-fLo</span> ~/.vim/autoload/plug.vim <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb1-2">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--create-dirs</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb1-3">  https://raw.githubusercontent.com/<span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb1-4">junegunn/vim-plug/master/plug.vim</span></code></pre></div>
<p>A working terminal emulator (iTerm2 or Kitty) and basic familiarity with Vim normal and insert modes are also assumed. See the companion post ‘Setting up a minimal neovim…’ for details on installing plugins with Neovim. Python 3 support is required for UltiSnips to function at all; without it, the plugin loads but snippet expansion silently fails.</p>
</section>
<section id="what-is-a-vim-based-r-and-latex-workflow" class="level1">
<h1>What is a Vim-Based R and LaTeX Workflow?</h1>
<p>A Vim-based R and LaTeX workflow is a configuration that supports writing R code, R Markdown documents, and LaTeX files inside a single Vim session. Instead of switching between RStudio for data analysis and a dedicated LaTeX editor for typesetting, Vim serves as the unified interface.</p>
<p>Think of it as turning Vim into a lightweight IDE. The .vimrc file loads plugins for LaTeX compilation (vimtex), code snippet expansion (UltiSnips), and syntax linting (ALE). Filetype plugins detect whether the open file is an R script or an R Markdown document and automatically open an R terminal in a split pane. Keybindings allow sending code to the REPL, inspecting data objects, and rendering documents without leaving the editor.</p>
<p>The practical benefit is speed. Once configured, one can move from opening a file to running a complete analysis and compiling a PDF report without touching the mouse or switching applications.</p>
</section>
<section id="getting-started" class="level1">
<h1>Getting Started</h1>
<p>The first step is to create or edit the <code>~/.vimrc</code> file with the full plugin configuration. The configuration below is a complete, working .vimrc that I use daily. It includes plugin declarations, global settings, and keybindings.</p>
<p>After saving the .vimrc, open Vim and run <code>:PlugInstall</code> to download and install all declared plugins.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/wf-r-vim-latex-workflow/media/images/ambiance2.png" class="img-fluid figure-img" alt="A close-up photograph of a keyboard with a terminal showing Vim configuration in progress."></p>
<figcaption>A close-up of a keyboard and terminal screen showing configuration files being edited.</figcaption>
</figure>
</div>
<p><em>The iterative process of refining a configuration file one setting at a time.</em></p>
</section>
<section id="the-complete-.vimrc-configuration" class="level1">
<h1>The Complete .vimrc Configuration</h1>
<p>The following is the full <code>~/.vimrc</code> file. It is organised into sections: plugin declarations with vim-plug, plugin-specific settings inline with each <code>Plug</code> call, and global editor settings at the end.</p>
<pre class="vim"><code>set runtimepath^=~/vimplugins
syntax enable
filetype plugin indent on
let mapleader = ","
let maplocalleader = " "

call plug#begin('~/vimplugins')

" -- Colour schemes --
Plug 'rakr/vim-one'
Plug 'mhartington/oceanic-next'
Plug 'rafi/awesome-vim-colorschemes'
Plug 'tpope/vim-vividchalk'

" -- Clipboard --
Plug 'jasonccox/vim-wayland-clipboard'

" -- Alignment --
Plug 'junegunn/vim-easy-align'

" -- Undo tree --
Plug 'mbbill/undotree'

" -- Terminal REPL integration --
Plug 'jpalardy/vim-slime'

" -- LaTeX --
Plug 'lervag/vimtex'
let g:vimtex_complete_close_braces=1
let g:vimtex_quickfix_mode=0

" -- Snippets --
Plug 'sirver/ultisnips'
let g:UltiSnipsExpandTrigger="&lt;C-tab&gt;"
let g:UltiSnipsJumpForwardTrigger="&lt;tab&gt;"
let g:UltiSnipsJumpBackwardTrigger="&lt;S-tab&gt;"
nnoremap &lt;leader&gt;U \
  &lt;Cmd&gt;call UltiSnips#RefreshSnippets()&lt;CR&gt;
nnoremap &lt;leader&gt;u :UltiSnipsEdit&lt;cr&gt;

" -- Linting and fixing --
Plug 'dense-analysis/ale'
let g:ale_virtualtext_cursor = 'disabled'
let g:ale_set_balloons = 1
highlight clear ALEErrorSign
highlight clear ALEWarningSign
let g:ale_sign_error = '●'
let g:ale_sign_warning = '.'
let g:ale_linters = {
\   'python': ['pylsp']
\ }
let g:ale_fix_on_save = 1
let g:ale_fixers = {
\   '*': ['remove_trailing_lines',
\         'trim_whitespace'],
\   'r': ['styler'],
\   'rmd': ['styler'],
\   'quarto': ['styler'],
\   'python': ['black','isort'],
\   'javascript': ['eslint'],
\ }
nnoremap &lt;leader&gt;n :ALENext&lt;CR&gt;

" -- Fuzzy finder --
Plug 'junegunn/fzf',
  \ { 'do': { -&gt; fzf#install() } }
Plug 'junegunn/fzf.vim'
nnoremap &lt;leader&gt;z :Files&lt;CR&gt;
nnoremap &lt;Leader&gt;' :Marks&lt;CR&gt;
nnoremap &lt;Leader&gt;/ :BLines&lt;CR&gt;
nnoremap &lt;Leader&gt;b :Buffers&lt;CR&gt;
nnoremap &lt;Leader&gt;r :Rg&lt;CR&gt;
nnoremap &lt;Leader&gt;s :Snippets&lt;CR&gt;
tnoremap &lt;leader&gt;b &lt;C-w&gt;:Buffers&lt;cr&gt;
tnoremap &lt;leader&gt;r &lt;C-w&gt;:Rg&lt;cr&gt;
tnoremap &lt;leader&gt;z &lt;C-w&gt;:Files&lt;cr&gt;

" -- Navigation and editing --
Plug 'junegunn/vim-peekaboo'
Plug 'tpope/vim-unimpaired'
Plug 'tpope/vim-obsession'
Plug 'tpope/vim-repeat'
Plug 'tpope/vim-commentary'
autocmd FileType quarto
  \ setlocal commentstring=#\ %s
Plug 'tpope/vim-surround'
Plug 'justinmk/vim-sneak'
let g:sneak#label = 1
let g:sneak#s_next = 1
let g:sneak#use_ic_scs = 1
Plug 'machakann/vim-highlightedyank'

" -- Status line --
Plug 'vim-airline/vim-airline'
let g:airline#extensions#ale#enabled = 1
let g:airline#extensions#tabline#enabled = 1
let g:airline#extensions#fzf#enabled = 1

" -- R integration --
Plug 'rgt47/rgt-R'

" -- Completion --
Plug 'girishji/vimcomplete'

call plug#end()

" -- Editor settings --
if $COLORTERM == 'truecolor'
  set termguicolors
endif
set scrolloff=7
set iskeyword-=.
set completeopt=menu,menuone,popup,
  \noinsert,noselect
set complete+=k
set dictionary=/usr/share/dict/words
highlight Pmenu guifg=Black guibg=cyan
  \ gui=bold
highlight PmenuSel gui=bold guifg=White
  \ guibg=blue
set gfn=Monaco:h14
set encoding=utf-8
set lazyredraw
set autochdir
set number relativenumber
set clipboard=unnamed
set textwidth=80
set colorcolumn=80
set cursorline
set hlsearch
set incsearch
set ignorecase
set smartcase
set showmatch
set noswapfile
set hidden
set gdefault
set splitright
set wildmenu
set wildignorecase
set wildmode=list:full

" -- Popup menu navigation with Tab --
inoremap &lt;expr&gt; &lt;tab&gt;
  \ pumvisible() ? "\&lt;C-n&gt;" : "\&lt;tab&gt;"
inoremap &lt;expr&gt; &lt;S-tab&gt;
  \ pumvisible() ? "\&lt;C-p&gt;" : "\&lt;S-tab&gt;"

" -- Buffer and window navigation --
nnoremap &lt;leader&gt;o &lt;C-w&gt;:b1&lt;CR&gt;
nnoremap &lt;leader&gt;t &lt;C-w&gt;:b2&lt;CR&gt;
nnoremap &lt;leader&gt;h &lt;C-w&gt;:b3&lt;CR&gt;
nnoremap &lt;leader&gt;&lt;leader&gt; &lt;C-w&gt;w
nnoremap &lt;leader&gt;a ggVG
nnoremap &lt;leader&gt;m vipgq
nnoremap &lt;leader&gt;f :tab split&lt;cr&gt;
nnoremap &lt;leader&gt;v :edit ~/.vimrc&lt;cr&gt;
nnoremap &lt;localleader&gt;&lt;leader&gt; &lt;C-u&gt;
nnoremap &lt;localleader&gt;&lt;localleader&gt; &lt;C-d&gt;
noremap - $
noremap : ;
noremap ; :
inoremap &lt;F10&gt; &lt;C-x&gt;&lt;C-k&gt;
inoremap &lt;F12&gt; &lt;C-x&gt;&lt;C-o&gt;
inoremap &lt;silent&gt; &lt;Esc&gt; &lt;Esc&gt;`^

" -- Terminal mode navigation --
tnoremap &lt;F1&gt; &lt;C-\&gt;&lt;C-n&gt;
tnoremap &lt;leader&gt;o &lt;C-w&gt;:b1&lt;CR&gt;
tnoremap &lt;leader&gt;t &lt;C-w&gt;:b2&lt;CR&gt;
tnoremap &lt;leader&gt;h &lt;C-w&gt;:b3&lt;CR&gt;
tnoremap &lt;leader&gt;&lt;leader&gt; &lt;C-w&gt;w
tnoremap lf ls()&lt;CR&gt;

" -- Auto-save and formatting --
au FocusGained * :let @z=@*
set updatetime=1000
autocmd CursorHold,CursorHoldI * update
autocmd FileType rmd
  \ setlocal commentstring=#\ %s
let $FZF_DEFAULT_OPTS =
  \ '--bind "ctrl-j:down,ctrl-k:up,'
  \ . 'j:preview-down,k:preview-up"'
set formatoptions-=c
  \ formatoptions-=r formatoptions-=o

" -- EasyAlign --
xmap ga &lt;Plug&gt;(EasyAlign)
nmap ga &lt;Plug&gt;(EasyAlign)

colorscheme one
set background=dark</code></pre>
<p>After saving this file, open Vim and run:</p>
<pre class="vim"><code>:PlugInstall</code></pre>
<p>Vim-plug will download and install every plugin declared between <code>plug#begin()</code> and <code>plug#end()</code>.</p>
</section>
<section id="filetype-plugin-for-r-markdown" class="level1">
<h1>Filetype Plugin for R Markdown</h1>
<p>Create the file <code>~/.vim/ftplugin/rmd.vim</code>. This file is loaded automatically whenever Vim opens a file with the <code>.Rmd</code> or <code>.rmd</code> extension. It defines terminal-mode shortcuts for rendering and sourcing, and normal-mode shortcuts for inspecting R objects under the cursor.</p>
<pre class="vim"><code>" ~/.vim/ftplugin/rmd.vim

" -- Terminal shortcuts for R Markdown --
tnoremap ZD quarto::quarto_render(
  \output_format = "pdf")&lt;CR&gt;
tnoremap ZO source("&lt;C-W&gt;"%")
tnoremap ZQ q('no')&lt;C-\&gt;&lt;C-n&gt;:q!&lt;CR&gt;
tnoremap ZR render("&lt;C-W&gt;"%")&lt;CR&gt;
tnoremap ZT :!R -e 'render("&lt;C-r&gt;%",
  \ output_format="pdf_document")'&lt;CR&gt;
tnoremap ZS style_dir()&lt;CR&gt;
tnoremap ZX exit&lt;CR&gt;
tnoremap ZZ q('no')&lt;C-\&gt;&lt;C-n&gt;:q!&lt;CR&gt;

" -- Object inspection under cursor --
nnoremap &lt;localleader&gt;d
  \ :let @c=expand("&lt;cword&gt;") \|
  \ :let @d="dim(".@c.")"."\n" \|
  \ :call term_sendkeys(
  \   term_list()[0], @d)&lt;CR&gt;
nnoremap &lt;localleader&gt;h
  \ :let @c=expand("&lt;cword&gt;") \|
  \ :let @d="head(".@c.")"."\n" \|
  \ :call term_sendkeys(
  \   term_list()[0], @d)&lt;CR&gt;
nnoremap &lt;localleader&gt;s
  \ :let @c=expand("&lt;cword&gt;") \|
  \ :let @d="str(".@c.")"."\n" \|
  \ :call term_sendkeys(
  \   term_list()[0], @d)&lt;CR&gt;
nnoremap &lt;localleader&gt;p
  \ :let @c=expand("&lt;cword&gt;") \|
  \ :let @d="print(".@c.")"."\n" \|
  \ :call term_sendkeys(
  \   term_list()[0], @d)&lt;CR&gt;
nnoremap &lt;localleader&gt;n
  \ :let @c=expand("&lt;cword&gt;") \|
  \ :let @d="names(".@c.")"."\n" \|
  \ :call term_sendkeys(
  \   term_list()[0], @d)&lt;CR&gt;

" -- Auto-open R terminal --
autocmd BufEnter *
  \ if &amp;ft ==# 'rmd' &amp;&amp; !exists('b:entered')
  \ | execute(
  \   'let b:entered = 1 | :ter ++rows=5 R')
  \ | endif</code></pre>
<p>The object inspection mappings deserve explanation. When the cursor is on a variable name, pressing <code>&lt;localleader&gt;d</code> extracts the word under the cursor, wraps it in <code>dim()</code>, and sends the command to the R terminal. This allows checking dimensions, printing heads, inspecting structure, and viewing names of any R object without leaving normal mode.</p>
</section>
<section id="filetype-plugin-for-r-scripts" class="level1">
<h1>Filetype Plugin for R Scripts</h1>
<p>Create the file <code>~/.vim/ftplugin/r.vim</code>. This file is nearly identical to the R Markdown ftplugin but uses the alternate file register (<code>#</code> instead of <code>%</code>) for sourcing commands, which is appropriate when the R script is not the current buffer.</p>
<pre class="vim"><code>" ~/.vim/ftplugin/r.vim

" -- Terminal shortcuts for R scripts --
tnoremap ZD quarto::quarto_render(
  \output_format = "pdf")&lt;CR&gt;
tnoremap ZO source("&lt;C-W&gt;"#")
tnoremap ZQ q('no')&lt;C-\&gt;&lt;C-n&gt;:q!&lt;CR&gt;
tnoremap ZR render("&lt;C-W&gt;"#")
tnoremap ZS style_dir()&lt;CR&gt;
tnoremap ZX exit&lt;CR&gt;
tnoremap ZZ q('no')&lt;C-\&gt;&lt;C-n&gt;:q!&lt;CR&gt;

" -- Object inspection under cursor --
nnoremap &lt;localleader&gt;d
  \ :let @c=expand("&lt;cword&gt;") \|
  \ :let @d="dim(".@c.")"."\n" \|
  \ :call term_sendkeys(
  \   term_list()[0], @d)&lt;CR&gt;
nnoremap &lt;localleader&gt;h
  \ :let @c=expand("&lt;cword&gt;") \|
  \ :let @d="head(".@c.")"."\n" \|
  \ :call term_sendkeys(
  \   term_list()[0], @d)&lt;CR&gt;
nnoremap &lt;localleader&gt;s
  \ :let @c=expand("&lt;cword&gt;") \|
  \ :let @d="str(".@c.")"."\n" \|
  \ :call term_sendkeys(
  \   term_list()[0], @d)&lt;CR&gt;
nnoremap &lt;localleader&gt;p
  \ :let @c=expand("&lt;cword&gt;") \|
  \ :let @d="print(".@c.")"."\n" \|
  \ :call term_sendkeys(
  \   term_list()[0], @d)&lt;CR&gt;
nnoremap &lt;localleader&gt;n
  \ :let @c=expand("&lt;cword&gt;") \|
  \ :let @d="names(".@c.")"."\n" \|
  \ :call term_sendkeys(
  \   term_list()[0], @d)&lt;CR&gt;

" -- Auto-open R terminal --
autocmd BufEnter *
  \ if &amp;ft ==# 'r' &amp;&amp; !exists('b:entered')
  \ | execute(
  \   'let b:entered = 1 | :ter ++rows=5 R')
  \ | endif</code></pre>
</section>
<section id="ultisnips-static-snippets" class="level1">
<h1>UltiSnips: Static Snippets</h1>
<p>UltiSnips is a snippet engine for Vim that expands trigger words into multi-line text templates. Each snippet definition lives in a filetype-specific file under <code>~/.vim/UltiSnips/</code>, for example <code>~/.vim/UltiSnips/rmd.snippets</code> for R Markdown files. A snippet consists of a trigger keyword, one or more tabstops (marked <code>$1</code>, <code>$2</code>, …) where the cursor will land after expansion, and the body text placed between those tabstops.</p>
<p>The critical configuration detail for this setup is avoiding conflicts between UltiSnips and popup menu navigation, since both want to use the Tab key. The solution is to assign <code>Ctrl-Tab</code> as the expand trigger and reserve plain <code>Tab</code> and <code>Shift-Tab</code> for jumping between tabstops. Separately, the popup menu mappings in <code>.vimrc</code> use <code>Tab</code> and <code>Shift-Tab</code> only when a popup is visible.</p>
<pre class="vim"><code>let g:UltiSnipsExpandTrigger="&lt;C-tab&gt;"
let g:UltiSnipsJumpForwardTrigger="&lt;tab&gt;"
let g:UltiSnipsJumpBackwardTrigger="&lt;S-tab&gt;"

inoremap &lt;expr&gt; &lt;tab&gt;
  \ pumvisible() ? "\&lt;C-n&gt;" : "\&lt;tab&gt;"
inoremap &lt;expr&gt; &lt;S-tab&gt;
  \ pumvisible() ? "\&lt;C-p&gt;" : "\&lt;S-tab&gt;"</code></pre>
<p>With this configuration, the workflow is:</p>
<ol type="1">
<li>Type a snippet trigger word (e.g., <code>rheader</code>).</li>
<li>Press <code>Ctrl-Tab</code> to expand the snippet.</li>
<li>Press <code>Tab</code> to jump to the next tabstop.</li>
<li>When a popup menu appears during typing, <code>Tab</code> navigates forward through completions instead.</li>
</ol>
<p>A minimal R Markdown YAML header snippet looks like this in the snippets file:</p>
<pre><code>snippet rheader "R Markdown YAML header"
---
title: "$1"
author: "${2:R.G. Thomas}"
date: today
output:
  pdf_document:
    keep_tex: true
header-includes:
  - \usepackage{lipsum, fancyhdr,
      titling, currfile}
  - \usepackage[export]{adjustbox}
  - \pagestyle{fancy}
---
$0
endsnippet</code></pre>
<p>When <code>rheader</code> is typed and <code>Ctrl-Tab</code> is pressed, the template expands and the cursor lands at <code>$1</code> (the title field). Pressing <code>Tab</code> moves to <code>$2</code> (the author field, which has a default value), and then to <code>$0</code> (the final resting position after all tabstops are filled).</p>
</section>
<section id="ultisnips-python-interpolated-dynamic-snippets" class="level1">
<h1>UltiSnips: Python-Interpolated Dynamic Snippets</h1>
<p>Static snippets cover predictable boilerplate. There is a class of problem, however, where the expansion content must be computed at the moment of triggering based on something the user has typed. UltiSnips supports three interpolation mechanisms beyond static text: shell execution (backtick), Vimscript (<code>!v</code>), and Python (<code>!p</code>). Python interpolation is the most capable: it executes an arbitrary Python expression inside the snippet body and inserts the result into the expanded text.</p>
<p>The <code>!p</code> marker tells UltiSnips to treat the enclosed block as Python. Inside that block, the special variable <code>snip.rv</code> (rv standing for ‘return value’) holds whatever the snippet should insert at that position. Any valid Python expression can compute <code>snip.rv</code>, including imports, loops, and string formatting.</p>
<section id="a-concrete-motivating-problem" class="level2">
<h2 class="anchored" data-anchor-id="a-concrete-motivating-problem">A Concrete Motivating Problem</h2>
<p>A question on Stack Overflow (https://stackoverflow.com/questions/78636197) asked how to write a snippet that generates a variable number of chord placeholders. The number of placeholders depends on a value the user types into the first tabstop. This is exactly the kind of problem that static snippets cannot solve: the body cannot be known at the time the snippet file is written because it depends on runtime input.</p>
<p>The Python function that solves this problem is straightforward:</p>
<div class="sourceCode" id="cb8" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb8-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> make_chords(num_reps):</span>
<span id="cb8-2">    text <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">""</span></span>
<span id="cb8-3">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> i <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">range</span>(num_reps):</span>
<span id="cb8-4">        text <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+=</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"&lt; &gt; "</span></span>
<span id="cb8-5">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> text</span></code></pre></div>
<p>To embed this inside an UltiSnips snippet definition:</p>
<pre><code>snippet chords "Generate chord placeholders"
`!p
def make_chords(num_reps):
    text = ""
    for i in range(num_reps):
        text += "&lt; &gt; "
    return text

snip.rv = make_chords(int(t[1]))
`
endsnippet</code></pre>
<p>The backtick delimiters with <code>!p</code> open and close the Python block. Inside the block, <code>t[1]</code> refers to the current value of tabstop 1. When the user types a number into the first tabstop and moves focus, UltiSnips re-evaluates the Python block and replaces the output accordingly. The <code>int()</code> conversion is required because <code>t[1]</code> is always a string.</p>
</section>
<section id="inserting-the-current-date" class="level2">
<h2 class="anchored" data-anchor-id="inserting-the-current-date">Inserting the Current Date</h2>
<p>A simpler but commonly useful dynamic snippet inserts the current date at expansion time:</p>
<pre><code>snippet today "Insert today's date"
`!p
import datetime
snip.rv = datetime.date.today().isoformat()
`
endsnippet</code></pre>
<p>This snippet takes no tabstops. When triggered, it imports the <code>datetime</code> module and assigns the ISO-formatted date string to <code>snip.rv</code>. The result is a plain text date that does not change after insertion, unlike a live template that re-evaluates on every keystroke.</p>
</section>
<section id="a-numbered-list-generator" class="level2">
<h2 class="anchored" data-anchor-id="a-numbered-list-generator">A Numbered List Generator</h2>
<p>A more elaborate pattern uses Python to generate a numbered list of a length specified by the user:</p>
<pre><code>snippet numlist "Numbered list of N items"
${1:5}
`!p
n = int(t[1])
snip.rv = "\n".join(
    f"{i+1}. " for i in range(n)
)
`
endsnippet</code></pre>
<p>When <code>5</code> is the value of tabstop 1, the expansion becomes five numbered lines. Changing the number before moving off the tabstop regenerates the list.</p>
</section>
<section id="debugging-python-snippets" class="level2">
<h2 class="anchored" data-anchor-id="debugging-python-snippets">Debugging Python Snippets</h2>
<p>When a Python interpolation block fails silently, the most reliable diagnostic is to open Vim’s message log with <code>:messages</code> immediately after a failed expansion. Python exceptions inside UltiSnips are caught and displayed as Vim error messages. A second approach is to test the Python logic independently in a Python interpreter before embedding it, since the UltiSnips execution environment is standard CPython with no special restrictions beyond access to the <code>snip</code> object.</p>
<pre class="vim"><code>" Force UltiSnips to re-read all snippet files
:call UltiSnips#RefreshSnippets()

" Open the snippet file for the current filetype
:UltiSnipsEdit</code></pre>
<p>If the expansion trigger fires but produces no output, confirm Python 3 support with <code>:echo has('python3')</code>. A return value of <code>0</code> means the Vim binary was compiled without Python and the entire <code>!p</code> mechanism is unavailable.</p>
</section>
</section>
<section id="practical-application" class="level1">
<h1>Practical Application</h1>
<p>This section walks through a concrete example of using the configured environment. The goal is to perform a logistic regression on the Palmer Penguins dataset, predicting gender, using only Vim and the R terminal.</p>
<p><strong>Step 1:</strong> Create a working directory and open an empty R Markdown file.</p>
<div class="sourceCode" id="cb13" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb13-1"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">cd</span> ~/prj/penguins</span>
<span id="cb13-2"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">vim</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-u</span> ~/.vimrc p.Rmd</span></code></pre></div>
<p><strong>Step 2:</strong> Enter insert mode and type the first R command to load the data.</p>
<pre class="vim"><code>i
library(palmerpenguins)</code></pre>
<p><strong>Step 3:</strong> Exit insert mode with <code>Ctrl-C</code>, then yank the line into the unnamed register.</p>
<pre class="vim"><code>&lt;C-c&gt;
yy</code></pre>
<p><strong>Step 4:</strong> The ftplugin for R Markdown automatically opens an R terminal in a split pane. If it has not opened yet, use <code>:ter R</code> to start one manually.</p>
<p><strong>Step 5:</strong> In the terminal pane, paste the yanked line from the register:</p>
<pre class="vim"><code>&lt;C-w&gt;""</code></pre>
<p>This sends <code>library(palmerpenguins)</code> to the R REPL and executes it.</p>
<p><strong>Step 6:</strong> Use UltiSnips to scaffold the R Markdown header. Type <code>rheader</code> on the first line and press <code>Ctrl-Tab</code>. The snippet expands into a YAML header template with tabstops for the project name, title, author, and bibliography path. Press <code>Tab</code> to navigate between tabstops. Do not leave insert mode while navigating or the snippet session will end.</p>
</section>
<section id="r-markdown-template-structure" class="level1">
<h1>R Markdown Template Structure</h1>
<p>For reference, a minimal R Markdown YAML header for PDF output with LaTeX customisation looks like this:</p>
<div class="sourceCode" id="cb17" style="background: #f1f3f5;"><pre class="sourceCode yaml code-with-copy"><code class="sourceCode yaml"><span id="cb17-1"><span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">---</span></span>
<span id="cb17-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">title</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> </span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Penguins data analysis"</span></span>
<span id="cb17-3"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">author</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> </span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"R.G. Thomas"</span></span>
<span id="cb17-4"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">date</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> today</span></span>
<span id="cb17-5"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">output</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span></span>
<span id="cb17-6"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">  </span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">pdf_document</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span></span>
<span id="cb17-7"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">    </span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">keep_tex</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> </span><span class="ch" style="color: #20794D;
background-color: null;
font-style: inherit;">true</span></span>
<span id="cb17-8"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">header-includes</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span></span>
<span id="cb17-9"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">  </span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">-</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> \usepackage{lipsum, fancyhdr,</span></span>
<span id="cb17-10"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">      titling, currfile}</span></span>
<span id="cb17-11"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">  </span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">-</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> \usepackage[export]{adjustbox}</span></span>
<span id="cb17-12"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">  </span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">-</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> \pagestyle{fancy}</span></span>
<span id="cb17-13"><span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">---</span></span></code></pre></div>
<p>A useful tip for file completion in Vim when editing R Markdown or Quarto files: temporarily set the filetype to <code>tex</code> with <code>:set filetype=tex</code>, then type <code>\includegraphics{</code> or <code>\input{</code> followed by <code>Ctrl-X Ctrl-O</code> for omni-completion of file paths.</p>
<section id="things-to-watch-out-for" class="level2">
<h2 class="anchored" data-anchor-id="things-to-watch-out-for">Things to Watch Out For</h2>
<ol type="1">
<li><p><strong>Tab key conflicts.</strong> UltiSnips and popup menu completion both use Tab. If snippets stop expanding, check that <code>g:UltiSnipsExpandTrigger</code> is set to <code>&lt;C-tab&gt;</code> rather than <code>&lt;tab&gt;</code>.</p></li>
<li><p><strong>ftplugin not loading.</strong> If the R terminal does not open automatically, verify that <code>filetype plugin indent on</code> appears in the <code>.vimrc</code> before any <code>Plug</code> calls, and that the ftplugin files are in <code>~/.vim/ftplugin/</code> (not <code>~/.vim/plugin/</code>).</p></li>
<li><p><strong>ALE styler dependency.</strong> The ALE fixer for R uses the <code>styler</code> package. If ALE reports errors on save, install styler in R first: <code>install.packages("styler")</code>.</p></li>
<li><p><strong>Colour scheme not found.</strong> If Vim reports <code>E185: Cannot find color scheme</code>, run <code>:PlugInstall</code> to ensure the colour scheme plugin has been downloaded.</p></li>
<li><p><strong>Terminal register paste.</strong> The <code>&lt;C-w&gt;""</code> paste in terminal mode pastes from the unnamed register. If the text was yanked into a named register, use <code>&lt;C-w&gt;"a</code> (for register <code>a</code>).</p></li>
<li><p><strong>Python interpolation requires Python 3 support.</strong> Vim must be compiled with <code>+python3</code>. The default macOS system Vim often lacks this. If <code>:echo has('python3')</code> returns <code>0</code>, install Vim via Homebrew (<code>brew install vim</code>) or use Neovim with a Lua snippet engine such as LuaSnip.</p></li>
<li><p><strong>Leaving insert mode terminates the snippet session.</strong> During tabstop navigation, pressing <code>Escape</code> ends the session and leaves the remaining tabstop markers as literal text. If this happens, undo the expansion with <code>u</code> and re-trigger the snippet.</p></li>
</ol>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/wf-r-vim-latex-workflow/media/images/ambiance3.png" class="img-fluid figure-img" alt="A quiet library reading room with natural light, representing the reflective phase of configuration work."></p>
<figcaption>A calm library reading room with natural light filtering through tall windows.</figcaption>
</figure>
</div>
<p><em>Stepping back to reflect on what was learnt.</em></p>
</section>
<section id="lessons-learnt" class="level2">
<h2 class="anchored" data-anchor-id="lessons-learnt">Lessons Learnt</h2>
<section id="conceptual-understanding" class="level3">
<h3 class="anchored" data-anchor-id="conceptual-understanding">Conceptual Understanding</h3>
<ul>
<li>A Vim-based R and LaTeX workflow replaces application switching with buffer switching, which is measurably faster once muscle memory develops.</li>
<li>Filetype plugins are the correct mechanism for language-specific settings in Vim; global .vimrc settings should remain language-agnostic.</li>
<li>The separation between snippet expansion (Ctrl-Tab) and popup navigation (Tab) is essential for avoiding conflicts in a multi-plugin environment.</li>
<li>UltiSnips tabstops provide a structured way to scaffold repetitive code patterns, reducing boilerplate errors in YAML headers.</li>
<li>Static snippets and Python-interpolated snippets occupy different parts of the solution space: use static snippets for fixed structure and Python snippets when the expansion depends on runtime input.</li>
</ul>
</section>
<section id="technical-skills" class="level3">
<h3 class="anchored" data-anchor-id="technical-skills">Technical Skills</h3>
<ul>
<li>Configuring vim-plug with inline <code>let</code> statements keeps plugin settings co-located with their declarations, improving maintainability.</li>
<li>The <code>term_sendkeys()</code> function in Vim 8+ enables programmatic communication with terminal buffers, which is the foundation for REPL integration.</li>
<li>ALE fixers configured per filetype (<code>r</code>, <code>rmd</code>, <code>quarto</code>) apply consistent code style without manual intervention.</li>
<li>The <code>autocmd BufEnter</code> pattern with a guard variable (<code>b:entered</code>) prevents multiple R terminals from spawning when switching buffers.</li>
<li>The <code>!p</code> marker in UltiSnips gives access to a full Python environment; <code>snip.rv</code> is the single output variable that carries the computed text back into the expansion.</li>
<li><code>t[1]</code> inside a Python block reads the current value of tabstop 1, enabling parametric expansions where the output depends on what the user has typed.</li>
</ul>
</section>
<section id="gotchas-and-pitfalls" class="level3">
<h3 class="anchored" data-anchor-id="gotchas-and-pitfalls">Gotchas and Pitfalls</h3>
<ul>
<li>Leaving insert mode during UltiSnips tabstop navigation silently terminates the snippet session, leaving partially expanded text.</li>
<li>The <code>autochdir</code> setting can interfere with relative file paths in R Markdown documents if the working directory changes unexpectedly.</li>
<li>ALE’s <code>ale_fix_on_save</code> may conflict with files that are intentionally unformatted (e.g., raw data files opened in Vim).</li>
<li>The <code>set iskeyword-=.</code> setting breaks word boundaries at dots, which is helpful for R (where <code>data.frame</code> is two words) but can confuse navigation in other filetypes.</li>
<li>Python exceptions inside UltiSnips are swallowed silently unless <code>:messages</code> is checked; a snippet that produces no output is often a Python runtime error rather than a configuration problem.</li>
</ul>
</section>
</section>
<section id="limitations" class="level2">
<h2 class="anchored" data-anchor-id="limitations">Limitations</h2>
<ul>
<li>This configuration is specific to Vim 8+ with terminal support; Neovim requires different terminal API calls (<code>jobsend</code> instead of <code>term_sendkeys</code>).</li>
<li>The ftplugin auto-opens R in a 5-row terminal split, which may be too small on low-resolution displays.</li>
<li>ALE’s <code>styler</code> fixer requires the <code>styler</code> R package to be installed in the active R library; if missing, ALE silently fails to fix on save.</li>
<li>UltiSnips requires Vim compiled with Python 3 support; the default macOS system Vim may lack this. Neovim users should use LuaSnip, which provides equivalent Python-like interpolation through Lua functions.</li>
<li>Python code inside UltiSnips snippets runs synchronously on the main thread. Expensive computations (file reads, network requests) will block the editor for the duration.</li>
<li>The configuration does not include debugging support; stepping through R code requires a separate tool or plugin.</li>
<li>Vimtex’s forward and inverse search with a PDF viewer (e.g., Zathura or Skim) requires additional configuration not covered here.</li>
<li>Python snippet code runs in a shared interpreter state; variables defined in one snippet block persist into subsequent expansions in the same session, which can produce unexpected results if variable names collide.</li>
</ul>
</section>
<section id="opportunities-for-improvement" class="level2">
<h2 class="anchored" data-anchor-id="opportunities-for-improvement">Opportunities for Improvement</h2>
<ol type="1">
<li>Add Neovim-compatible terminal integration using <code>vim.fn.jobsend()</code> for cross-editor portability.</li>
<li>Configure vimtex forward search with Skim or Zathura for real-time PDF preview during LaTeX editing.</li>
<li>Create project-specific <code>.vimrc</code> files that override the terminal split size based on the display resolution.</li>
<li>Develop additional UltiSnips templates for common R analysis patterns (ggplot scaffolds, model fitting boilerplate, knitr chunk options).</li>
<li>Port the Python interpolation patterns to LuaSnip for Neovim users, using Lua functions in place of Python <code>!p</code> blocks.</li>
<li>Integrate vim-slime as an alternative to the built-in terminal for sending code to tmux sessions.</li>
<li>Add a Quarto-specific ftplugin file for <code>.qmd</code> files with render commands that target both HTML and PDF output.</li>
</ol>
</section>
</section>
<section id="wrapping-up" class="level1">
<h1>Wrapping Up</h1>
<p>The configuration described in this post transforms Vim into a unified environment for R programming and LaTeX document preparation, then extends it with programmable snippet expansion through Python interpolation. The core insight is that filetype plugins and a carefully managed Tab key mapping are the two pillars of the static setup, while <code>snip.rv</code> and the <code>t[]</code> tabstop array are the two primitives needed for dynamic expansions.</p>
<p>What I learnt most from this process is that configuration is iterative. The .vimrc I use today looks nothing like the one I started with. Each problem encountered (a Tab conflict, a missing colour scheme, a terminal that would not open) taught me something about how Vim dispatches events and how plugins interact. The Python interpolation extension followed the same pattern: a concrete problem (a Stack Overflow question about chord placeholders) revealed a capability I had not known existed.</p>
<p>For anyone attempting a similar setup, start with just vimtex and one ftplugin file. Get the R terminal auto-opening reliably before adding UltiSnips or ALE. Add static snippets next. Reach for Python interpolation only after encountering a problem that static snippets genuinely cannot solve. Layering complexity gradually makes debugging much simpler.</p>
<p>In conclusion, five points merit emphasis. First, use <code>Ctrl-Tab</code> for snippet expansion to avoid Tab key conflicts with popup menu navigation. Second, place filetype-specific settings in <code>~/.vim/ftplugin/</code> rather than cluttering the global .vimrc. Third, ALE with <code>styler</code> provides automatic R code formatting on save with no manual intervention. Fourth, the <code>term_sendkeys()</code> function enables sending arbitrary R commands from normal mode to a running R REPL. Fifth, the <code>!p</code> marker in UltiSnips enables Python interpolation: assign the result to <code>snip.rv</code> and read tabstop values from <code>t[1]</code>, <code>t[2]</code>, etc.</p>
</section>
<section id="see-also" class="level1">
<h1>See Also</h1>
<p><strong>Related posts:</strong></p>
<ul>
<li>‘Setting up a minimal Neovim configuration for data science’ (companion post on Neovim plugin installation)</li>
<li>‘Configure the Command Line for Data Science Development’ (terminal and shell configuration)</li>
<li><a href="../../posts/26-setupneovim/">Post 26: Setting up Neovim</a></li>
<li><a href="../../posts/52-workflow-construct/">Post 52: The Workflow Construct</a></li>
</ul>
<p><strong>Key resources:</strong></p>
<ul>
<li><a href="https://github.com/lervag/vimtex">vimtex documentation</a></li>
<li><a href="https://github.com/SirVer/ultisnips">UltiSnips repository</a></li>
<li><a href="https://github.com/SirVer/ultisnips/blob/master/doc/UltiSnips.txt">UltiSnips full documentation</a></li>
<li><a href="https://github.com/dense-analysis/ale">ALE (Asynchronous Lint Engine)</a></li>
<li><a href="https://github.com/junegunn/vim-plug">vim-plug plugin manager</a></li>
<li><a href="https://wraihan.com/posts/vimtex-and-zathura/">Vimtex and Zathura setup guide</a></li>
<li><a href="http://vimcasts.org/episodes/ultisnips-python-interpolation/">UltiSnips Python interpolation (Vimcasts)</a></li>
<li><a href="https://github.com/L3MON4D3/LuaSnip">LuaSnip (Neovim alternative)</a></li>
<li><a href="https://stackoverflow.com/questions/78636197">Stack Overflow: chord placeholder snippet</a></li>
<li><a href="https://mg.readthedocs.io/latexmk.html">Latexmk documentation</a></li>
</ul>
</section>
<section id="reproducibility" class="level1">
<h1>Reproducibility</h1>
<p>This configuration was developed and tested on macOS with Vim 9.0, Python 3.11, and R 4.4. The following files constitute the complete configuration:</p>
<table class="caption-top table">
<thead>
<tr class="header">
<th>File</th>
<th>Purpose</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><code>~/.vimrc</code></td>
<td>Global plugin and editor settings</td>
</tr>
<tr class="even">
<td><code>~/.vim/ftplugin/rmd.vim</code></td>
<td>R Markdown keybindings</td>
</tr>
<tr class="odd">
<td><code>~/.vim/ftplugin/r.vim</code></td>
<td>R script keybindings</td>
</tr>
<tr class="even">
<td><code>~/.vim/UltiSnips/rmd.snippets</code></td>
<td>R Markdown snippet library</td>
</tr>
<tr class="odd">
<td><code>~/.vim/UltiSnips/all.snippets</code></td>
<td>Filetype-agnostic snippets</td>
</tr>
</tbody>
</table>
<p>To reproduce the environment:</p>
<div class="sourceCode" id="cb18" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb18-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">curl</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-fLo</span> ~/.vim/autoload/plug.vim <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb18-2">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--create-dirs</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb18-3">  https://raw.githubusercontent.com/<span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb18-4">junegunn/vim-plug/master/plug.vim</span>
<span id="cb18-5"></span>
<span id="cb18-6"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">cp</span> vimrc ~/.vimrc</span>
<span id="cb18-7"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">cp</span> ftplugin/rmd.vim ~/.vim/ftplugin/rmd.vim</span>
<span id="cb18-8"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">cp</span> ftplugin/r.vim ~/.vim/ftplugin/r.vim</span>
<span id="cb18-9"></span>
<span id="cb18-10"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">vim</span> +PlugInstall +qall</span>
<span id="cb18-11"></span>
<span id="cb18-12"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">Rscript</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-e</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"install.packages('styler')"</span></span></code></pre></div>
<p>To verify Python 3 support after installation:</p>
<pre class="vim"><code>:echo has('python3')
:echo g:UltiSnipsExpandTrigger</code></pre>
</section>
<section id="lets-connect" class="level1">
<h1>Let’s Connect</h1>
<ul>
<li><strong>GitHub:</strong> <a href="https://github.com/rgt47">rgt47</a></li>
<li><strong>Twitter/X:</strong> <a href="https://twitter.com/rgt47"><span class="citation" data-cites="rgt47">@rgt47</span></a></li>
<li><strong>LinkedIn:</strong> <a href="https://linkedin.com/in/rgthomaslab">Ronald Glenn Thomas</a></li>
<li><strong>Email:</strong> <a href="https://rgtlab.org/contact">rgtlab.org/contact</a></li>
</ul>
<p>I would enjoy hearing from you if:</p>
<ul>
<li>You spot an error or a better approach to any of the code in this post.</li>
<li>You have suggestions for topics you would like to see covered.</li>
<li>You want to discuss R programming, data science, or reproducible research.</li>
<li>You have questions about anything in this tutorial.</li>
<li>You just want to say hello and connect.</li>
</ul>
<hr>
<p><em>Rendered on 2026-05-17 at 17:08 PDT.</em><br> <em>Source: ~/prj/qblog/posts/30-setupRvimtex/setupRvimtex/analysis/report/index.qmd</em></p>
<section id="related-posts-in-this-cluster" class="level2">
<h2 class="anchored" data-anchor-id="related-posts-in-this-cluster">Related posts in this cluster</h2>
<p>This post is part of the <em>Workflow Construct</em> series. Recommended reading order:</p>
<ol type="1">
<li>Post 15: <a href="../15-wf-construct-overview-anchor/">A Workflow Construct for the Modern Data Scientist</a></li>
<li>Post 16: <a href="../16-wf-unix-workspace-config/">Unix Command-Line Workspace Setup for Data Science</a></li>
<li>Post 17: <a href="../17-wf-multi-laptop-dotfiles-bootstrap/">Multi-Laptop macOS Bootstrap</a></li>
<li>Post 18: <a href="../18-wf-git-for-data-science/">Setting Up Git for Data Science Workflows</a></li>
<li>Post 19: <a href="../19-wf-neovim-data-science-ide/">Setting Up Neovim as a Data Science IDE</a></li>
<li><strong>Post 20: Extending the R-Vim Workflow with LaTeX</strong> (this post)</li>
<li>Post 21: <a href="../21-wf-modern-cli-tools/">Modern CLI Replacements for the Shell Layer</a></li>
<li>Post 22: <a href="../22-wf-claude-code-in-shell/">LLM-Augmented Editing for the Workflow Construct</a></li>
<li>Post 23: <a href="../23-wf-yabai-tiling-window-manager/">Configuring Yabai as a Tiling Window Manager</a></li>
<li>Post 24: <a href="../24-wf-pocket-terminal-ttyd-tailscale/">A pocket terminal with ttyd and Tailscale</a></li>
<li>Post 25: <a href="../25-wf-linux-mint-on-macbook/">Install Linux Mint on a MacBook Air</a></li>
</ol>


</section>
</section>

 ]]></description>
  <category>vim</category>
  <category>r</category>
  <category>python</category>
  <category>workflow-construct</category>
  <guid>https://rgtlab.org/posts/wf-r-vim-latex-workflow/</guid>
  <pubDate>Sun, 17 May 2026 07:00:00 GMT</pubDate>
  <media:content url="https://rgtlab.org/posts/wf-r-vim-latex-workflow/media/images/hero.png" medium="image" type="image/png" height="96" width="144"/>
</item>
<item>
  <title>Sharing R Code via Docker: R Markdown Reports and Shiny Applications</title>
  <dc:creator>Ronald G. Thomas</dc:creator>
  <link>https://rgtlab.org/posts/zc-share-rmd-via-docker/</link>
  <description><![CDATA[ 




<p><em>2026-05-17 16:55 PDT</em></p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/zc-share-rmd-via-docker/media/images/hero.png" class="img-fluid quarto-figure quarto-figure-center figure-img" style="width:80.0%"></p>
</figure>
</div>
<p><em>Just as shipping containers standardised global freight, Docker containers standardise software delivery: for static analyses and interactive applications alike.</em></p>
<section id="introduction" class="level1">
<h1>Introduction</h1>
<p>I did not really know how fragile sharing R code could be until my colleague (let us call him Joe) spent an entire afternoon debugging missing packages, incompatible R versions, and absent files, all because I sent him a single <code>.Rmd</code> file by email. The code ran perfectly on my machine. Joe’s Linux Mint workstation disagreed on every count.</p>
<p>The Shiny case is worse. A static R Markdown report requires R, packages, and possibly LaTeX. A Shiny application requires all of that plus a running web server, reactive dependencies, and often JavaScript libraries or database connections. Each additional dependency is another opportunity for something to break on a collaborator’s machine, and the failure mode is a blank browser tab with no useful error message.</p>
<p>We cover both cases. The core Docker machinery (Dockerfile authoring, base image selection, dependency installation) is explained once and applies to both. The post then presents two parallel case studies:</p>
<ul>
<li><strong>Case A</strong>: Dockerizing an R Markdown report for static PDF output.</li>
<li><strong>Case B</strong>: Dockerizing a Shiny application for interactive browser use.</li>
</ul>
<p>More formally, we document the Container layer of the Workflow Construct described in <a href="../../posts/52-workflow-construct/">post 52</a>. The Container layer is replaceable across runtimes (Docker on the laptop, Apptainer on HPC, Podman as a daemonless alternative); its core machinery is the same regardless of what the container ultimately serves.</p>
<section id="motivations" class="level2">
<h2 class="anchored" data-anchor-id="motivations">Motivations</h2>
<ul>
<li>I had sent an R Markdown analysis to a colleague and watched him spend hours debugging environment issues that had nothing to do with the analysis itself.</li>
<li>I had also sent a Shiny application as a zip file and observed the same pattern: hours resolving package version conflicts and missing system libraries before the app would launch.</li>
<li>Every error my colleagues encountered was entirely preventable with proper packaging.</li>
<li>I wanted a reproducible method for sharing R analyses that would work regardless of the recipient’s operating system, R version, or installed packages.</li>
<li>The experience convinced me that ‘works on my machine’ is not a valid standard for collaborative data science, and that Docker addresses both the static and interactive output cases.</li>
</ul>
</section>
<section id="objectives" class="level2">
<h2 class="anchored" data-anchor-id="objectives">Objectives</h2>
<ol type="1">
<li>Explain the core Docker concepts (image, container, Dockerfile, layer caching, volume mounts, port mapping) that apply to all R containerisation workflows.</li>
<li>Demonstrate the failure modes of naive sharing for both R Markdown and Shiny by walking through the sequence of errors a collaborator encounters.</li>
<li>Write complete Dockerfiles for each case: one for an R Markdown report, one for a Shiny application.</li>
<li>Document the complete workflow from authoring to collaboration, including image sharing options and Shiny-specific concerns (process supervision, port binding, host parameter).</li>
</ol>
<p>I am documenting my learning process here. If you spot errors or have better approaches, please let me know.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/zc-share-rmd-via-docker/media/images/docker-logo.png" class="img-fluid quarto-figure quarto-figure-center figure-img" style="width:30.0%"></p>
</figure>
</div>
<p><em>The analysis begins on one machine, but it needs to run on many.</em></p>
</section>
</section>
<section id="prerequisites-and-setup" class="level1">
<h1>Prerequisites and Setup</h1>
<p>Before proceeding, the following are required:</p>
<ul>
<li><strong>Docker Desktop</strong> installed on the workstation (macOS, Windows, or Linux)</li>
<li><strong>R 4.0+</strong> with <code>rmarkdown</code> and/or <code>shiny</code> installed locally (for authoring)</li>
<li><strong>A Docker Hub account</strong> (free) for pushing and sharing images</li>
<li>Basic familiarity with the terminal and R Markdown or Shiny application structure</li>
</ul>
</section>
<section id="what-is-docker-for-r-users" class="level1">
<h1>What is Docker for R Users?</h1>
<p>Docker is a tool that packages an application and its entire computing environment (operating system libraries, programming language, installed packages, configuration files, data) into a single portable unit called an image. When someone runs that image, they get a container that behaves identically to the environment in which the analysis was developed.</p>
<p>Think of it like shipping a complete laboratory instead of just a lab notebook. The naive approach to sharing code is like sending someone a lab notebook and hoping they have the same equipment, reagents, and room temperature. Docker is like shipping the entire lab bench, pre-loaded with everything needed to replicate the experiment.</p>
<p>For R users specifically, Docker solves the problem of environment divergence. The R version, installed packages, LaTeX distribution, auxiliary files, and data are all captured in the image. The recipient does not need to install anything except Docker itself.</p>
<section id="core-concepts" class="level2">
<h2 class="anchored" data-anchor-id="core-concepts">Core Concepts</h2>
<p><strong>Image</strong>: A snapshot of a complete computing environment. Images are built from a Dockerfile and stored as a series of read-only layers.</p>
<p><strong>Container</strong>: A running instance of an image. Containers are ephemeral by default; data written inside a container is lost when it stops unless a volume mount is used.</p>
<p><strong>Dockerfile</strong>: A text file that defines how to build an image. Each instruction (<code>FROM</code>, <code>RUN</code>, <code>COPY</code>, <code>CMD</code>) creates a new layer. Docker caches layers, so unchanged instructions do not re-execute on subsequent builds.</p>
<p><strong>Volume mount</strong>: A link between a directory on the host machine and a path inside the container. Volume mounts allow the container to read host files and write output that persists after the container stops.</p>
<p><strong>Port mapping</strong>: For network services (Shiny applications), the container’s internal port must be mapped to a host port with the <code>-p</code> flag, or the host machine cannot connect to the running service.</p>
</section>
<section id="the-rocker-project" class="level2">
<h2 class="anchored" data-anchor-id="the-rocker-project">The Rocker Project</h2>
<p>The <a href="https://rocker-project.org/">Rocker Project</a> maintains a family of Docker images for R. The relevant images for this post are:</p>
<table class="caption-top table">
<thead>
<tr class="header">
<th>Image</th>
<th>Contents</th>
<th>Use case</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><code>rocker/r-ver</code></td>
<td>Base R only</td>
<td>Lightweight scripts</td>
</tr>
<tr class="even">
<td><code>rocker/verse</code></td>
<td>R + tidyverse + LaTeX</td>
<td>R Markdown PDF</td>
</tr>
<tr class="odd">
<td><code>rocker/shiny</code></td>
<td>R + Shiny Server</td>
<td>Shiny applications</td>
</tr>
</tbody>
</table>
<p>All images accept a version tag (e.g., <code>rocker/verse:4</code>) to pin the R version.</p>
</section>
</section>
<section id="case-a-dockerizing-an-r-markdown-report" class="level1">
<h1>Case A: Dockerizing an R Markdown Report</h1>
<section id="the-analysis" class="level2">
<h2 class="anchored" data-anchor-id="the-analysis">The Analysis</h2>
<p>Assume you have a simple R Markdown file called <code>peng.Rmd</code> that analyses the Palmer Penguins dataset. The file uses <code>pacman</code> for package management, <code>tidyverse</code> for data manipulation and plotting, and <code>knitr</code> for output formatting. It also references a custom LaTeX preamble file (<code>preamble.tex</code>) and a logo image (<code>sudoku.png</code>) for the PDF header.</p>
<div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode default code-with-copy"><code class="sourceCode default"><span id="cb1-1"></span>
<span id="cb1-2">---</span>
<span id="cb1-3">title: "Penguins analysis"</span>
<span id="cb1-4">author: "R.G. Thomas"</span>
<span id="cb1-5">date: "`r format(Sys.time(), '%B %d, %Y')`"</span>
<span id="cb1-6">fontsize: 11pt</span>
<span id="cb1-7">geometry: "left=3cm,right=5cm,top=2cm,bottom=2cm"</span>
<span id="cb1-8">output:</span>
<span id="cb1-9">  pdf_document:</span>
<span id="cb1-10">    keep_tex: true</span>
<span id="cb1-11">    includes:</span>
<span id="cb1-12">      in_header: "preamble.tex"</span>
<span id="cb1-13">---</span>
<span id="cb1-14"></span>
<span id="cb1-15">```{r include=F, echo=F}</span>
<span id="cb1-16">library(pacman)</span>
<span id="cb1-17">p_load(palmerpenguins, tidyverse, knitr)</span>
<span id="cb1-18"></span>
<span id="cb1-19">opts_chunk$set(</span>
<span id="cb1-20">  warning = FALSE,</span>
<span id="cb1-21">  message = FALSE,</span>
<span id="cb1-22">  echo = FALSE,</span>
<span id="cb1-23">  results = "asis",</span>
<span id="cb1-24">  dev = "pdf"</span>
<span id="cb1-25">)</span>
<span id="cb1-26">```</span>
<span id="cb1-27"></span>
<span id="cb1-28"># Introduction</span>
<span id="cb1-29"></span>
<span id="cb1-30">We can work with the dataset `penguins`</span>
<span id="cb1-31">included in the package `palmerpenguins`.</span>
<span id="cb1-32"></span>
<span id="cb1-33">```{r }</span>
<span id="cb1-34">library(palmerpenguins)</span>
<span id="cb1-35">```</span>
<span id="cb1-36"></span>
<span id="cb1-37">One naive approach is to split the dataset</span>
<span id="cb1-38">and do three separate analyses:</span>
<span id="cb1-39"></span>
<span id="cb1-40">```{r }</span>
<span id="cb1-41">df1 &lt;- split(penguins, penguins$species)</span>
<span id="cb1-42"></span>
<span id="cb1-43">foo &lt;- function(df, z) {</span>
<span id="cb1-44">  df |&gt;</span>
<span id="cb1-45">    ggplot(</span>
<span id="cb1-46">      aes(</span>
<span id="cb1-47">        x = bill_length_mm,</span>
<span id="cb1-48">        y = flipper_length_mm</span>
<span id="cb1-49">      )</span>
<span id="cb1-50">    ) +</span>
<span id="cb1-51">    geom_point(</span>
<span id="cb1-52">      aes(color = island), alpha = .5</span>
<span id="cb1-53">    ) +</span>
<span id="cb1-54">    geom_smooth() +</span>
<span id="cb1-55">    scale_color_manual(</span>
<span id="cb1-56">      values = c("purple", "green", "red")</span>
<span id="cb1-57">    ) +</span>
<span id="cb1-58">    theme_bw() +</span>
<span id="cb1-59">    labs(</span>
<span id="cb1-60">      title = paste(</span>
<span id="cb1-61">        z, " Penguin Anatomy Comparison"</span>
<span id="cb1-62">      ),</span>
<span id="cb1-63">      x = "Flipper length",</span>
<span id="cb1-64">      y = "Bill length",</span>
<span id="cb1-65">      color = "Island"</span>
<span id="cb1-66">    )</span>
<span id="cb1-67">  plotfile_name &lt;- paste0(z, ".pdf")</span>
<span id="cb1-68">  ggsave(plotfile_name)</span>
<span id="cb1-69">  cat(</span>
<span id="cb1-70">    paste0(</span>
<span id="cb1-71">      "\\includegraphics[height=3cm]{",</span>
<span id="cb1-72">      plotfile_name, "}"</span>
<span id="cb1-73">    ),</span>
<span id="cb1-74">    "\n"</span>
<span id="cb1-75">  )</span>
<span id="cb1-76">  cat("\\vspace{1cm}", "\n")</span>
<span id="cb1-77">}</span>
<span id="cb1-78"></span>
<span id="cb1-79">bar &lt;- df1 |&gt; map2(names(df1), foo)</span>
<span id="cb1-80">```</span></code></pre></div>
<p>The file runs cleanly on the author’s machine and produces a PDF report with three species-specific scatter plots comparing bill length and flipper length.</p>
</section>
<section id="the-naive-approach-and-its-errors" class="level2">
<h2 class="anchored" data-anchor-id="the-naive-approach-and-its-errors">The Naive Approach and Its Errors</h2>
<p>The simplest approach is to email <code>peng.Rmd</code> to Joe and ask him to render it. The following sequence of errors occurred in the order presented.</p>
<p><strong>Error 1: R Not Found.</strong> Linux cannot find <code>R</code>. Joe installs <code>r-base-core</code> via <code>apt</code>.</p>
<p><strong>Error 2: Function render Not Found.</strong> R loads but <code>rmarkdown</code> is missing. The package installation fails due to missing system libraries (<code>libssl-dev</code>, <code>libcurl4-openssl-dev</code>, <code>libxml2-dev</code>, and others).</p>
<p><strong>Error 3: Pandoc Version Too Old.</strong> The system pandoc is below 1.12.3. Joe installs a newer version.</p>
<p><strong>Error 4: Package pacman Not Found.</strong> After resolving pandoc, <code>pacman</code> is missing.</p>
<p><strong>Error 5: Missing preamble.tex.</strong> The author forgot to send the LaTeX preamble file. The path convention also differs between macOS and Linux.</p>
<p><strong>Error 6: pdflatex Not Found.</strong> LaTeX is absent. Joe installs <code>tinytex</code>.</p>
<p><strong>Error 7: Missing Logo File.</strong> <code>pandoc error: file sudoku.png not found</code>. The author forgot to send the logo image referenced in the preamble.</p>
<p><strong>Error 8: Missing Bibliography File.</strong> Another file the author forgot to include.</p>
<p>After eight errors and several hours, Joe renders the report. Every error was environmental.</p>
</section>
<section id="the-dockerfile" class="level2">
<h2 class="anchored" data-anchor-id="the-dockerfile">The Dockerfile</h2>
<p>The following Dockerfile starts from <code>rocker/verse:4</code> (which includes R, tidyverse, and LaTeX), installs additional R packages, updates the LaTeX distribution, creates a non-root user, and copies all necessary files into the image.</p>
<div class="sourceCode" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode dockerfile code-with-copy"><code class="sourceCode dockerfile"><span id="cb2-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">FROM</span> rocker/verse:4</span>
<span id="cb2-2"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">RUN</span> <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">apt</span> update</span>
<span id="cb2-3"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">RUN</span> <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">apt</span> install vim <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-y</span></span>
<span id="cb2-4"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">RUN</span> <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">R</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-e</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"install.packages('pacman')"</span></span>
<span id="cb2-5"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">RUN</span> <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">R</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-e</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"install.packages('palmerpenguins')"</span></span>
<span id="cb2-6"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">RUN</span> <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">R</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-e</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"install.packages('tidyverse')"</span></span>
<span id="cb2-7"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">RUN</span> <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">R</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-e</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"install.packages('knitr')"</span></span>
<span id="cb2-8"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">RUN</span> <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">R</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-e</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"install.packages('rmarkdown')"</span></span>
<span id="cb2-9"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">RUN</span> <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">tlmgr</span> init-usertree</span>
<span id="cb2-10"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">RUN</span> <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">tlmgr</span> update <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--self</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--all</span></span>
<span id="cb2-11"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">RUN</span> <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">tlmgr</span> install fancyhdr adjustbox <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb2-12">  geometry titling</span>
<span id="cb2-13"></span>
<span id="cb2-14"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">RUN</span> <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">addgroup</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--system</span> joe <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">&amp;&amp;</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb2-15">  <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">adduser</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--system</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--ingroup</span> joe joe</span>
<span id="cb2-16"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">RUN</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">chmod</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-R</span> 0777 <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb2-17">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'/usr/local/lib/R/site-library'</span></span>
<span id="cb2-18"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">RUN</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">chown</span> joe:joe <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-R</span> /home/joe</span>
<span id="cb2-19"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">USER</span> joe</span>
<span id="cb2-20"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">WORKDIR</span> /home/joe</span>
<span id="cb2-21"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">RUN</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">mkdir</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-p</span> /home/joe/shr</span>
<span id="cb2-22"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">RUN</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">mkdir</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-p</span> /home/joe/output</span>
<span id="cb2-23"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">COPY</span> /preamble.tex /home/joe/shr</span>
<span id="cb2-24"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">COPY</span> sudoku.png /home/joe/shr</span>
<span id="cb2-25"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">COPY</span> peng.Rmd /home/joe/shr</span>
<span id="cb2-26"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">CMD</span> [<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"/bin/bash"</span>]</span></code></pre></div>
<p>Key decisions:</p>
<ul>
<li><strong><code>FROM rocker/verse:4</code></strong> provides R 4.x with tidyverse and LaTeX pre-installed.</li>
<li><strong>Each <code>RUN</code> installs a specific package</strong>, making layer caching effective. If only the Rmd file changes, only the <code>COPY</code> layer rebuilds.</li>
<li><strong><code>tlmgr install</code></strong> adds the exact LaTeX packages referenced by <code>preamble.tex</code>.</li>
<li><strong>A non-root user (<code>joe</code>)</strong> runs the analysis as a security best practice.</li>
<li><strong><code>COPY</code> commands</strong> place the preamble, logo, and Rmd file inside the image, so nothing is missing.</li>
</ul>
</section>
<section id="building-and-sharing-the-image" class="level2">
<h2 class="anchored" data-anchor-id="building-and-sharing-the-image">Building and Sharing the Image</h2>
<p>Build with a tag and platform flag for cross-architecture compatibility:</p>
<div class="sourceCode" id="cb3" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb3-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">docker</span> build <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-t</span> rgt47/penguin_review <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb3-2">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--platform</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>linux/amd64 .</span></code></pre></div>
<p><strong>Option 1: Docker Hub</strong> (recommended for remote collaboration)</p>
<div class="sourceCode" id="cb4" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb4-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">docker</span> push rgt47/penguin_review</span></code></pre></div>
<p>Joe pulls the image on his machine:</p>
<div class="sourceCode" id="cb5" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb5-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">docker</span> pull rgt47/penguin_review</span></code></pre></div>
<p><strong>Option 2: File transfer</strong> (useful for air-gapped networks)</p>
<div class="sourceCode" id="cb6" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb6-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">docker</span> save rgt47/penguin_review <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-2">  <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">|</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">gzip</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> penguin_review.tgz</span></code></pre></div>
<p>Joe loads the image from the archive:</p>
<div class="sourceCode" id="cb7" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb7-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">docker</span> load <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-i</span> penguin_review.tgz</span></code></pre></div>
</section>
<section id="running-the-container" class="level2">
<h2 class="anchored" data-anchor-id="running-the-container">Running the Container</h2>
<p>Joe runs the container with a volume mount that connects a local output directory to the container’s output directory. This is how rendered files are extracted from the container.</p>
<div class="sourceCode" id="cb8" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb8-1"><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">droot</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$PWD</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">/output"</span></span>
<span id="cb8-2"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">docker</span> run <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-it</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--rm</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb8-3">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--platform</span> linux/x86_64 <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb8-4">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-v</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$droot</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span>:/home/joe/output <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb8-5">  rgt47/penguin_review</span></code></pre></div>
<p>Inside the container, Joe renders the report:</p>
<div class="sourceCode" id="cb9" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb9-1"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">cd</span> output</span>
<span id="cb9-2"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">R</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-e</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"library(rmarkdown); </span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb9-3"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  render('../shr/peng.Rmd')"</span></span></code></pre></div>
<p>The rendered PDF appears in the local <code>output/</code> directory. No errors. No missing packages. No missing files.</p>
</section>
<section id="the-latex-preamble" class="level2">
<h2 class="anchored" data-anchor-id="the-latex-preamble">The LaTeX Preamble</h2>
<p>For reference, the <code>preamble.tex</code> file that caused Error 5 in the naive approach:</p>
<div class="sourceCode" id="cb10" style="background: #f1f3f5;"><pre class="sourceCode latex code-with-copy"><code class="sourceCode latex"><span id="cb10-1"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">\usepackage</span>[export]{<span class="ex" style="color: null;
background-color: null;
font-style: inherit;">adjustbox</span>}</span>
<span id="cb10-2"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">\usepackage</span>{<span class="ex" style="color: null;
background-color: null;
font-style: inherit;">fancyhdr</span>}</span>
<span id="cb10-3"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">\usepackage</span>{<span class="ex" style="color: null;
background-color: null;
font-style: inherit;">titling</span>}</span>
<span id="cb10-4"></span>
<span id="cb10-5"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">\pagestyle</span>{fancy}</span>
<span id="cb10-6"></span>
<span id="cb10-7"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">\pretitle</span>{</span>
<span id="cb10-8"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">\begin</span>{<span class="ex" style="color: null;
background-color: null;
font-style: inherit;">flushright</span>}</span>
<span id="cb10-9"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">\includegraphics</span>[width=3cm,valign=c]{</span>
<span id="cb10-10"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">  sudoku.png</span>}<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">\\</span></span>
<span id="cb10-11"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">\end</span>{<span class="ex" style="color: null;
background-color: null;
font-style: inherit;">flushright</span>}</span>
<span id="cb10-12"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">\begin</span>{<span class="ex" style="color: null;
background-color: null;
font-style: inherit;">flushleft</span>} <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">\LARGE</span> }</span>
<span id="cb10-13"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">\posttitle</span>{</span>
<span id="cb10-14">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">\par</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">\end</span>{<span class="ex" style="color: null;
background-color: null;
font-style: inherit;">flushleft</span>}<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">\vskip</span> 0.5em}</span>
<span id="cb10-15"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">\predate</span>{</span>
<span id="cb10-16">  <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">\begin</span>{<span class="ex" style="color: null;
background-color: null;
font-style: inherit;">flushleft</span>}<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">\large</span>}</span>
<span id="cb10-17"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">\postdate</span>{</span>
<span id="cb10-18">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">\par</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">\end</span>{<span class="ex" style="color: null;
background-color: null;
font-style: inherit;">flushleft</span>}}</span>
<span id="cb10-19"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">\preauthor</span>{</span>
<span id="cb10-20">  <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">\begin</span>{<span class="ex" style="color: null;
background-color: null;
font-style: inherit;">flushleft</span>}<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">\large</span>}</span>
<span id="cb10-21"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">\postauthor</span>{</span>
<span id="cb10-22">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">\par</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">\end</span>{<span class="ex" style="color: null;
background-color: null;
font-style: inherit;">flushleft</span>}}</span>
<span id="cb10-23"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">\fancyfoot</span>[L]{<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">\currfilename</span>}</span>
<span id="cb10-24"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">\fancyfoot</span>[R]{</span>
<span id="cb10-25">  <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">\includegraphics</span>[width=.8cm]{<span class="ex" style="color: null;
background-color: null;
font-style: inherit;">sudoku.png</span>}}</span>
<span id="cb10-26"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">\fancyhead</span>[L]{<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">\today</span>}</span></code></pre></div>
<p>In the Docker image, this file is already in place at <code>/home/joe/shr/preamble.tex</code>. Joe never needs to know it exists.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/zc-share-rmd-via-docker/media/images/ambiance2.png" class="img-fluid figure-img" alt="Hands carefully placing items into a container, symbolising the methodical process of packaging an R analysis into a Docker image."></p>
<figcaption>A pair of hands assembling components into a container, representing the process of packaging an analysis for reproducibility.</figcaption>
</figure>
</div>
<p><em>Packaging everything together so no piece is left behind.</em></p>
</section>
</section>
<section id="case-b-dockerizing-a-shiny-application" class="level1">
<h1>Case B: Dockerizing a Shiny Application</h1>
<p>The Shiny case introduces concerns absent from the R Markdown case: the container must run a persistent web server, bind to a network port accessible from the host, and handle multiple concurrent connections. The Dockerfile structure is similar, but the base image, the <code>CMD</code>, and the <code>docker run</code> flags differ.</p>
<section id="the-application" class="level2">
<h2 class="anchored" data-anchor-id="the-application">The Application</h2>
<p>Assume you have a single-file Shiny app (<code>app.R</code>) that displays an interactive scatter plot of the Palmer Penguins dataset.</p>
<div class="cell">
<div class="sourceCode cell-code" id="cb11" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb11-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">library</span>(shiny)</span>
<span id="cb11-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">library</span>(ggplot2)</span>
<span id="cb11-3"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">library</span>(DT)</span>
<span id="cb11-4"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">library</span>(palmerpenguins)</span>
<span id="cb11-5"></span>
<span id="cb11-6">ui <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">fluidPage</span>(</span>
<span id="cb11-7">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">titlePanel</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Palmer Penguins Explorer"</span>),</span>
<span id="cb11-8">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sidebarLayout</span>(</span>
<span id="cb11-9">    <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sidebarPanel</span>(</span>
<span id="cb11-10">      <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">selectInput</span>(</span>
<span id="cb11-11">        <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"species"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Species:"</span>,</span>
<span id="cb11-12">        <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">choices =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">c</span>(</span>
<span id="cb11-13">          <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"All"</span>,</span>
<span id="cb11-14">          <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">unique</span>(</span>
<span id="cb11-15">            <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">as.character</span>(penguins<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">$</span>species)</span>
<span id="cb11-16">          )</span>
<span id="cb11-17">        )</span>
<span id="cb11-18">      ),</span>
<span id="cb11-19">      <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">selectInput</span>(</span>
<span id="cb11-20">        <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"x_var"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"X Variable:"</span>,</span>
<span id="cb11-21">        <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">choices =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">c</span>(</span>
<span id="cb11-22">          <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"bill_length_mm"</span>,</span>
<span id="cb11-23">          <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"bill_depth_mm"</span>,</span>
<span id="cb11-24">          <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"flipper_length_mm"</span>,</span>
<span id="cb11-25">          <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"body_mass_g"</span></span>
<span id="cb11-26">        )</span>
<span id="cb11-27">      ),</span>
<span id="cb11-28">      <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">selectInput</span>(</span>
<span id="cb11-29">        <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"y_var"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Y Variable:"</span>,</span>
<span id="cb11-30">        <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">choices =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">c</span>(</span>
<span id="cb11-31">          <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"flipper_length_mm"</span>,</span>
<span id="cb11-32">          <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"bill_length_mm"</span>,</span>
<span id="cb11-33">          <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"bill_depth_mm"</span>,</span>
<span id="cb11-34">          <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"body_mass_g"</span></span>
<span id="cb11-35">        )</span>
<span id="cb11-36">      )</span>
<span id="cb11-37">    ),</span>
<span id="cb11-38">    <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">mainPanel</span>(</span>
<span id="cb11-39">      <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">plotOutput</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"scatter"</span>),</span>
<span id="cb11-40">      <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">DTOutput</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"table"</span>)</span>
<span id="cb11-41">    )</span>
<span id="cb11-42">  )</span>
<span id="cb11-43">)</span>
<span id="cb11-44"></span>
<span id="cb11-45">server <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">function</span>(input, output, session) {</span>
<span id="cb11-46">  filtered_data <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">reactive</span>({</span>
<span id="cb11-47">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> (input<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">$</span>species <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"All"</span>) {</span>
<span id="cb11-48">      penguins</span>
<span id="cb11-49">    } <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">else</span> {</span>
<span id="cb11-50">      penguins[</span>
<span id="cb11-51">        penguins<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">$</span>species <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> input<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">$</span>species,</span>
<span id="cb11-52">      ]</span>
<span id="cb11-53">    }</span>
<span id="cb11-54">  })</span>
<span id="cb11-55"></span>
<span id="cb11-56">  output<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">$</span>scatter <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">renderPlot</span>({</span>
<span id="cb11-57">    <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">ggplot</span>(</span>
<span id="cb11-58">      <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">filtered_data</span>(),</span>
<span id="cb11-59">      <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">aes</span>(</span>
<span id="cb11-60">        <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">x =</span> .data[[input<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">$</span>x_var]],</span>
<span id="cb11-61">        <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">y =</span> .data[[input<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">$</span>y_var]],</span>
<span id="cb11-62">        <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">colour =</span> species</span>
<span id="cb11-63">      )</span>
<span id="cb11-64">    ) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span></span>
<span id="cb11-65">      <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">geom_point</span>(<span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">alpha =</span> <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.7</span>, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">size =</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span></span>
<span id="cb11-66">      <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">theme_minimal</span>() <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span></span>
<span id="cb11-67">      <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">labs</span>(</span>
<span id="cb11-68">        <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">x =</span> input<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">$</span>x_var,</span>
<span id="cb11-69">        <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">y =</span> input<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">$</span>y_var,</span>
<span id="cb11-70">        <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">colour =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Species"</span></span>
<span id="cb11-71">      )</span>
<span id="cb11-72">  })</span>
<span id="cb11-73"></span>
<span id="cb11-74">  output<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">$</span>table <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">renderDT</span>({</span>
<span id="cb11-75">    <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">filtered_data</span>()</span>
<span id="cb11-76">  })</span>
<span id="cb11-77">}</span>
<span id="cb11-78"></span>
<span id="cb11-79"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">shinyApp</span>(ui, server)</span></code></pre></div>
</div>
<p>The app runs flawlessly on the development machine.</p>
</section>
<section id="the-naive-approach-and-its-errors-1" class="level2">
<h2 class="anchored" data-anchor-id="the-naive-approach-and-its-errors-1">The Naive Approach and Its Errors</h2>
<p>You zip the directory and email it to Joe. The following sequence of errors occurred.</p>
<p><strong>Error 1: Shiny Not Installed.</strong> <code>there is no package called 'shiny'</code>. Installing it triggers compilation of <code>httpuv</code> and its system dependencies.</p>
<p><strong>Error 2: System Libraries Missing.</strong> The <code>httpuv</code> package fails to compile because <code>libssl-dev</code> and <code>libuv1-dev</code> are absent.</p>
<p><strong>Error 3: Package DT Not Found.</strong> After installing Shiny, the app fails because <code>DT</code> is missing.</p>
<p><strong>Error 4: palmerpenguins Not Found.</strong> The data package is missing.</p>
<p><strong>Error 5: Port Already in Use.</strong> Another service occupies port 3838. Joe must identify and stop the conflicting process.</p>
<p><strong>Error 6: Version Incompatibility.</strong> Joe’s R version is 4.1; the app uses <code>.data[[]]</code> syntax from <code>rlang</code> 1.0+, which requires a newer <code>ggplot2</code> than what installed against R 4.1.</p>
<p>After six errors and several hours, Joe has a running application. Every error was environmental.</p>
</section>
<section id="the-dockerfile-1" class="level2">
<h2 class="anchored" data-anchor-id="the-dockerfile-1">The Dockerfile</h2>
<p>The following Dockerfile starts from <code>rocker/shiny:4</code>, which includes R and Shiny Server pre-installed.</p>
<div class="sourceCode" id="cb12" style="background: #f1f3f5;"><pre class="sourceCode dockerfile code-with-copy"><code class="sourceCode dockerfile"><span id="cb12-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">FROM</span> rocker/shiny:4</span>
<span id="cb12-2"></span>
<span id="cb12-3"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">RUN</span> <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">apt-get</span> update <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">&amp;&amp;</span> <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">apt-get</span> install <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-y</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb12-4">  libssl-dev <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb12-5">  libcurl4-openssl-dev <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb12-6">  libuv1-dev <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb12-7">  <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">&amp;&amp;</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">rm</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-rf</span> /var/lib/apt/lists/<span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">*</span></span>
<span id="cb12-8"></span>
<span id="cb12-9"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">RUN</span> <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">R</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-e</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"install.packages(c( </span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb12-10"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  'ggplot2', </span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb12-11"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  'DT', </span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb12-12"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  'palmerpenguins', </span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb12-13"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  'rlang' </span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb12-14"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  ), repos = 'https://cran.r-project.org')"</span></span>
<span id="cb12-15"></span>
<span id="cb12-16"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">RUN</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">rm</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-rf</span> /srv/shiny-server/<span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">*</span></span>
<span id="cb12-17"></span>
<span id="cb12-18"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">COPY</span> app.R /srv/shiny-server/app.R</span>
<span id="cb12-19"></span>
<span id="cb12-20"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">EXPOSE</span> 3838</span>
<span id="cb12-21"></span>
<span id="cb12-22"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">CMD</span> [<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"/usr/bin/shiny-server"</span>]</span></code></pre></div>
<p>Key decisions:</p>
<ul>
<li><strong><code>FROM rocker/shiny:4</code></strong> provides R 4.x with Shiny Server pre-installed, eliminating manual web server configuration.</li>
<li><strong>System libraries</strong> are installed before R packages to ensure compilation of binary dependencies (<code>httpuv</code>, <code>openssl</code>).</li>
<li><strong><code>rm -rf /srv/shiny-server/*</code></strong> removes default sample apps, leaving only the target application.</li>
<li><strong><code>EXPOSE 3838</code></strong> documents the port that Shiny Server listens on (it does not publish the port; that requires <code>-p</code> at runtime).</li>
<li><strong><code>CMD</code></strong> starts Shiny Server when the container launches, so Joe does not need to type any commands inside the container.</li>
</ul>
</section>
<section id="alternative-single-file-approach" class="level2">
<h2 class="anchored" data-anchor-id="alternative-single-file-approach">Alternative: Single-File Approach</h2>
<p>For simpler applications, it is possible to bypass Shiny Server and run the app directly with <code>Rscript</code>. This uses the smaller <code>rocker/r-ver</code> base image.</p>
<div class="sourceCode" id="cb13" style="background: #f1f3f5;"><pre class="sourceCode dockerfile code-with-copy"><code class="sourceCode dockerfile"><span id="cb13-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">FROM</span> rocker/r-ver:4</span>
<span id="cb13-2"></span>
<span id="cb13-3"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">RUN</span> <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">R</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-e</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"install.packages(c( </span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb13-4"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  'shiny', </span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb13-5"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  'ggplot2', </span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb13-6"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  'DT', </span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb13-7"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  'palmerpenguins' </span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb13-8"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  ), repos = 'https://cran.r-project.org')"</span></span>
<span id="cb13-9"></span>
<span id="cb13-10"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">COPY</span> app.R /app/app.R</span>
<span id="cb13-11"></span>
<span id="cb13-12"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">EXPOSE</span> 3838</span>
<span id="cb13-13"></span>
<span id="cb13-14"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">CMD</span> [<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Rscript"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"-e"</span>, <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb13-15">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shiny::runApp('/app/app.R', </span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb13-16"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  host='0.0.0.0', port=3838)"</span>]</span></code></pre></div>
<p>The trade-off is the loss of Shiny Server features (logging, process management, multi-app serving), but for single-app containers it is often sufficient and produces a smaller image.</p>
</section>
<section id="building-and-sharing-the-image-1" class="level2">
<h2 class="anchored" data-anchor-id="building-and-sharing-the-image-1">Building and Sharing the Image</h2>
<p>Build with a tag:</p>
<div class="sourceCode" id="cb14" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb14-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">docker</span> build <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-t</span> rgt47/penguins-shiny <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb14-2">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--platform</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>linux/amd64 .</span></code></pre></div>
<p><strong>Option 1: Docker Hub</strong></p>
<div class="sourceCode" id="cb15" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb15-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">docker</span> push rgt47/penguins-shiny</span></code></pre></div>
<p>Joe pulls the image:</p>
<div class="sourceCode" id="cb16" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb16-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">docker</span> pull rgt47/penguins-shiny</span></code></pre></div>
<p><strong>Option 2: File transfer</strong></p>
<div class="sourceCode" id="cb17" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb17-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">docker</span> save rgt47/penguins-shiny <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb17-2">  <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">|</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">gzip</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> penguins-shiny.tgz</span></code></pre></div>
<p>Joe loads the image:</p>
<div class="sourceCode" id="cb18" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb18-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">docker</span> load <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-i</span> penguins-shiny.tgz</span></code></pre></div>
</section>
<section id="running-the-container-1" class="level2">
<h2 class="anchored" data-anchor-id="running-the-container-1">Running the Container</h2>
<p>Joe runs the container with port mapping:</p>
<div class="sourceCode" id="cb19" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb19-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">docker</span> run <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-d</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--rm</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb19-2">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-p</span> 3838:3838 <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb19-3">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--name</span> penguins-app <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb19-4">  rgt47/penguins-shiny</span></code></pre></div>
<p>He opens a browser to <code>http://localhost:3838</code> and the application is running. No errors. No missing packages. No port conflicts.</p>
<p>To stop the application:</p>
<div class="sourceCode" id="cb20" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb20-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">docker</span> stop penguins-app</span></code></pre></div>
</section>
<section id="shiny-specific-process-supervision-and" class="level2">
<h2 class="anchored" data-anchor-id="shiny-specific-process-supervision-and">Shiny-Specific: Process Supervision and</h2>
</section>
<section id="multi-user-deployment" class="level2">
<h2 class="anchored" data-anchor-id="multi-user-deployment">Multi-User Deployment</h2>
<p>The <code>rocker/shiny</code> image runs the open-source edition of Shiny Server, which does not efficiently handle many concurrent users. For production deployments with authentication and per-user container isolation, consider <a href="https://www.shinyproxy.io/">ShinyProxy</a>. ShinyProxy acts as a reverse proxy and launches a fresh container for each authenticated user, providing isolation without requiring Shiny Server Pro.</p>
<p>For applications requiring persistent user uploads or database writes, mount a host directory to the container:</p>
<div class="sourceCode" id="cb21" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb21-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">docker</span> run <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-d</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--rm</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb21-2">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-p</span> 3838:3838 <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb21-3">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-v</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$PWD</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">/uploads"</span>:/srv/shiny-server/uploads <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb21-4">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--name</span> penguins-app <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb21-5">  rgt47/penguins-shiny</span></code></pre></div>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/zc-share-rmd-via-docker/media/images/ambiance3.png" class="img-fluid figure-img" alt="A closed laptop on a tidy desk with a cup of coffee, symbolising the satisfaction of a reproducible workflow completed."></p>
<figcaption>A quiet desk with a closed laptop and a cup of coffee, representing the conclusion of a completed workflow.</figcaption>
</figure>
</div>
<p><em>When the analysis runs on the first try, on any machine, the work is done.</em></p>
</section>
</section>
<section id="things-to-watch-out-for" class="level1">
<h1>Things to Watch Out For</h1>
<ol type="1">
<li><p><strong>Volume mount paths must be absolute.</strong> The <code>-v</code> flag requires an absolute path on the host side. Use <code>"$PWD/output"</code> or provide the full path explicitly.</p></li>
<li><p><strong>Platform flag matters for Apple Silicon.</strong> When building on an M1/M2/M3 Mac, the default architecture is <code>arm64</code>. Use <code>--platform=linux/amd64</code> to ensure the image runs on Intel-based Linux machines.</p></li>
<li><p><strong>The <code>host</code> parameter must be <code>'0.0.0.0'</code> for Shiny.</strong> By default, <code>shiny::runApp()</code> binds to <code>127.0.0.1</code>, which is only accessible inside the container. This is the most common reason a Dockerized Shiny app appears to start but shows a blank page.</p></li>
<li><p><strong><code>EXPOSE</code> does not publish the port.</strong> The <code>EXPOSE</code> instruction in a Dockerfile is documentation only. The <code>-p</code> flag at runtime is what makes the port reachable from the host.</p></li>
<li><p><strong>Image size can be large.</strong> The <code>rocker/verse</code> base image is approximately 2 GB; <code>rocker/shiny</code> is approximately 1.5 GB. Adding packages increases this further. Use <code>rocker/r-ver</code> for a smaller base when the full tidyverse or LaTeX is not required.</p></li>
<li><p><strong>R package versions are not pinned.</strong> The Dockerfiles above install the latest version of each package at build time. For strict reproducibility, use <code>renv</code> to pin exact package versions and restore from <code>renv.lock</code> inside the Dockerfile.</p></li>
<li><p><strong>Non-root user permissions.</strong> If the container writes output files as a non-root user, the host directory may need appropriate permissions. The <code>chmod -R 0777</code> in the Case A Dockerfile is permissive; in production, use more restrictive permissions.</p></li>
</ol>
</section>
<section id="daily-workflow" class="level1">
<h1>Daily Workflow</h1>
<table class="caption-top table">
<colgroup>
<col style="width: 40%">
<col style="width: 60%">
</colgroup>
<thead>
<tr class="header">
<th>Task</th>
<th>Command</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>Build image (R Markdown)</td>
<td><code>docker build -t rgt47/penguin_review --platform=linux/amd64 .</code></td>
</tr>
<tr class="even">
<td>Build image (Shiny)</td>
<td><code>docker build -t rgt47/penguins-shiny --platform=linux/amd64 .</code></td>
</tr>
<tr class="odd">
<td>Push to Docker Hub</td>
<td><code>docker push rgt47/&lt;image-name&gt;</code></td>
</tr>
<tr class="even">
<td>Save image to file</td>
<td><code>docker save rgt47/&lt;image-name&gt; \| gzip &gt; image.tgz</code></td>
</tr>
<tr class="odd">
<td>Load image from file</td>
<td><code>docker load -i image.tgz</code></td>
</tr>
<tr class="even">
<td>Run R Markdown container</td>
<td><code>docker run -it --rm -v "$PWD/output":/home/joe/output rgt47/penguin_review</code></td>
</tr>
<tr class="odd">
<td>Run Shiny container</td>
<td><code>docker run -d --rm -p 3838:3838 --name penguins-app rgt47/penguins-shiny</code></td>
</tr>
<tr class="even">
<td>View Shiny logs</td>
<td><code>docker logs penguins-app</code></td>
</tr>
<tr class="odd">
<td>Stop Shiny container</td>
<td><code>docker stop penguins-app</code></td>
</tr>
<tr class="even">
<td>List running containers</td>
<td><code>docker ps</code></td>
</tr>
<tr class="odd">
<td>Remove stopped containers</td>
<td><code>docker container prune</code></td>
</tr>
</tbody>
</table>
</section>
<section id="uninstall-rollback" class="level1">
<h1>Uninstall / Rollback</h1>
<p>To remove a specific image:</p>
<div class="sourceCode" id="cb22" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb22-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">docker</span> rmi rgt47/penguin_review</span>
<span id="cb22-2"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">docker</span> rmi rgt47/penguins-shiny</span></code></pre></div>
<p>To remove all stopped containers and dangling images (reclaim disk space):</p>
<div class="sourceCode" id="cb23" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb23-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">docker</span> system prune</span></code></pre></div>
<p>To remove all unused images (not just dangling):</p>
<div class="sourceCode" id="cb24" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb24-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">docker</span> system prune <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-a</span></span></code></pre></div>
<p>Docker Desktop can be uninstalled via the application’s settings menu on macOS and Windows. On Linux, remove the <code>docker-ce</code> package via the system package manager. Uninstalling Docker does not affect files outside the Docker storage directory (typically <code>/var/lib/docker</code>).</p>
</section>
<section id="lessons-learnt" class="level1">
<h1>Lessons Learnt</h1>
<section id="conceptual-understanding" class="level2">
<h2 class="anchored" data-anchor-id="conceptual-understanding">Conceptual Understanding</h2>
<ul>
<li>The naive approach to sharing R code fails because it assumes the recipient’s environment matches the author’s environment. In practice, it never does.</li>
<li>Docker eliminates environment divergence by packaging the complete computing context alongside the analysis code, whether that code produces a static file or a live web application.</li>
<li>The <code>rocker</code> project provides pre-built Docker images for R that include common dependencies, significantly reducing Dockerfile complexity.</li>
<li>Volume mounts are the mechanism for extracting output from containers; port mapping is the mechanism for connecting a container’s network service to the host machine’s browser.</li>
</ul>
</section>
<section id="technical-skills" class="level2">
<h2 class="anchored" data-anchor-id="technical-skills">Technical Skills</h2>
<ul>
<li>Building a Dockerfile for R requires installing both R packages (<code>install.packages()</code>) and system libraries (<code>apt install</code>) for packages with compiled dependencies.</li>
<li>The <code>rocker/verse</code> base image covers most R Markdown rendering requirements; <code>rocker/shiny</code> covers Shiny Server deployments; <code>rocker/r-ver</code> is the smallest base for scripts that do not need the full tidyverse or LaTeX.</li>
<li><code>docker save</code> and <code>docker load</code> provide an alternative to Docker Hub for sharing images on restricted networks.</li>
<li><code>docker run -d</code> starts a container in detached mode, which is appropriate for long-running server applications like Shiny.</li>
</ul>
</section>
<section id="gotchas-and-pitfalls" class="level2">
<h2 class="anchored" data-anchor-id="gotchas-and-pitfalls">Gotchas and Pitfalls</h2>
<ul>
<li>Forgetting to include auxiliary files (preamble, images, data) in the Dockerfile <code>COPY</code> commands is the most common source of R Markdown build failures.</li>
<li>Forgetting to set <code>host='0.0.0.0'</code> is the most common reason a Dockerized Shiny app appears to start but shows a blank page.</li>
<li>The <code>--platform</code> flag is essential for cross-architecture compatibility between Apple Silicon and Intel machines.</li>
<li>Running <code>docker build</code> without <code>--no-cache</code> may use stale cached layers if upstream packages have been updated; add <code>--no-cache</code> when a fully fresh build is required.</li>
</ul>
</section>
</section>
<section id="limitations" class="level1">
<h1>Limitations</h1>
<ul>
<li>Docker Desktop is required on the recipient’s machine, which may not be permitted in all organisational environments.</li>
<li>Base images are large (1.5 to 2 GB before adding packages), making image sharing impractical on low-bandwidth connections.</li>
<li>R package versions are not pinned in these Dockerfiles; future builds may install different versions, breaking reproducibility over time. Integrating <code>renv</code> addresses this but adds complexity.</li>
<li>The Case B Dockerfile does not address authentication or access control; the Shiny app is accessible to anyone who can reach the mapped port.</li>
<li>Shiny Server (open-source edition) does not efficiently handle many concurrent users. For production multi-user deployments, ShinyProxy or Shiny Server Pro is required.</li>
<li>Neither Dockerfile includes a health check; orchestration tools (Kubernetes, Docker Compose) cannot verify the container is actually responding without one.</li>
<li>Docker ensures environment consistency, not analytical validity. Code correctness is a separate concern.</li>
</ul>
</section>
<section id="opportunities-for-improvement" class="level1">
<h1>Opportunities for Improvement</h1>
<ol type="1">
<li>Integrate <code>renv</code> into both Dockerfiles to pin exact package versions and ensure long-term reproducibility.</li>
<li>Add a <code>Makefile</code> to the R Markdown image that automates the rendering step, allowing Joe to run <code>make render</code> instead of typing the full R command.</li>
<li>Use multi-stage Docker builds to separate the build environment (with compilation tools) from the runtime environment, reducing the final image size.</li>
<li>Add a <code>docker-compose.yml</code> for analyses that require additional services (e.g., a PostgreSQL database alongside R or Shiny).</li>
<li>Implement ShinyProxy for multi-user Shiny deployments with authentication and per-user container isolation.</li>
<li>Add a <code>HEALTHCHECK</code> instruction to the Shiny Dockerfile (<code>HEALTHCHECK CMD curl -f    http://localhost:3838 || exit 1</code>) for container orchestration.</li>
<li>Explore GitHub Actions to automatically build and push Docker images whenever the analysis or application code is updated.</li>
</ol>
</section>
<section id="wrapping-up" class="level1">
<h1>Wrapping Up</h1>
<p>The contrast between the naive approach and the Docker approach illustrates a fundamental principle of reproducible research: the computing environment is part of the analysis. Sending an Rmd file or a Shiny application without its environment is like publishing a paper without its methodology section.</p>
<p>The naive approach produced eight separate errors for the R Markdown case and six for the Shiny case, each requiring Joe to diagnose and resolve a dependency issue. The Docker approach produced zero errors in either case. Joe pulled the image, ran a single command, and either opened a rendered PDF in his <code>output/</code> directory or a working application in his browser.</p>
<p>The Shiny case makes the stakes clearer: a missing dependency in a static analysis produces a clear error message. A missing dependency in a Shiny app produces a blank browser tab. Docker addresses both failure modes by fixing the entire stack at build time.</p>
<p>In conclusion, five points merit emphasis. First, the naive approach produced eight distinct environment errors for R Markdown and six for Shiny before the output could be obtained. Second, Docker eliminates all such errors by packaging the complete environment into a single image. Third, Case A uses <code>rocker/verse</code> (R + tidyverse + LaTeX) with volume mounts for PDF output. Fourth, Case B uses <code>rocker/shiny</code> (R + Shiny Server) with port mapping for browser access. Fifth, the <code>host='0.0.0.0'</code> parameter and the <code>-p</code> flag are the two settings most commonly missed when Dockerizing Shiny applications.</p>
</section>
<section id="see-also" class="level1">
<h1>See Also</h1>
<p><strong>Related posts:</strong></p>
<ul>
<li>“Configure the Command Line for Data Science Development” (terminal and Docker setup)</li>
<li>“Setting Up R, Vimtex, and UltiSnips in Vim” (the Vim editing environment used to author the original analysis)</li>
</ul>
<p><strong>Key resources:</strong></p>
<ul>
<li><a href="https://www.statworx.com/en/content-hub/blog/%20running-your-r-script-in-docker/">Running your R script in Docker (Statworx)</a></li>
<li><a href="https://rocker-project.org/">Rocker Project: Docker containers for R</a></li>
<li><a href="https://hub.docker.com/r/rocker/shiny">rocker/shiny Docker image</a></li>
<li><a href="https://docs.posit.co/shiny-server/">Shiny Server documentation</a></li>
<li><a href="https://www.shinyproxy.io/">ShinyProxy: open-source Shiny deployment</a></li>
<li><a href="https://docs.docker.com/">Docker documentation</a></li>
<li><a href="https://rstudio.github.io/renv/">renv: Project Environments for R</a></li>
</ul>
</section>
<section id="reproducibility" class="level1">
<h1>Reproducibility</h1>
<p>This workflow was developed on macOS with Docker Desktop 4.x, R 4.4, and Shiny 1.8.</p>
<p><strong>Case A files:</strong></p>
<table class="caption-top table">
<thead>
<tr class="header">
<th>File</th>
<th>Purpose</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><code>peng.Rmd</code></td>
<td>R Markdown analysis file</td>
</tr>
<tr class="even">
<td><code>Dockerfile</code></td>
<td>Environment definition</td>
</tr>
<tr class="odd">
<td><code>preamble.tex</code></td>
<td>LaTeX header customisation</td>
</tr>
<tr class="even">
<td><code>sudoku.png</code></td>
<td>Logo image for PDF header</td>
</tr>
</tbody>
</table>
<p><strong>Case B files:</strong></p>
<table class="caption-top table">
<thead>
<tr class="header">
<th>File</th>
<th>Purpose</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><code>app.R</code></td>
<td>Shiny application code</td>
</tr>
<tr class="even">
<td><code>Dockerfile</code></td>
<td>Environment definition</td>
</tr>
</tbody>
</table>
<p>To reproduce Case A:</p>
<div class="sourceCode" id="cb25" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb25-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">docker</span> build <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-t</span> rgt47/penguin_review <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb25-2">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--platform</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>linux/amd64 .</span>
<span id="cb25-3"></span>
<span id="cb25-4"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">mkdir</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-p</span> output</span>
<span id="cb25-5"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">docker</span> run <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-it</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--rm</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb25-6">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--platform</span> linux/x86_64 <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb25-7">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-v</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$PWD</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">/output"</span>:/home/joe/output <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb25-8">  rgt47/penguin_review</span>
<span id="cb25-9"></span>
<span id="cb25-10"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Inside the container:</span></span>
<span id="cb25-11"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># cd output</span></span>
<span id="cb25-12"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># R -e "library(rmarkdown); render('../shr/peng.Rmd')"</span></span></code></pre></div>
<p>To reproduce Case B:</p>
<div class="sourceCode" id="cb26" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb26-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">docker</span> build <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-t</span> rgt47/penguins-shiny <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb26-2">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--platform</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>linux/amd64 .</span>
<span id="cb26-3"></span>
<span id="cb26-4"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">docker</span> run <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-d</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--rm</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb26-5">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-p</span> 3838:3838 <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb26-6">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--name</span> penguins-app <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb26-7">  rgt47/penguins-shiny</span>
<span id="cb26-8"></span>
<span id="cb26-9"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Open browser to http://localhost:3838</span></span>
<span id="cb26-10"></span>
<span id="cb26-11"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">docker</span> stop penguins-app</span></code></pre></div>
</section>
<section id="lets-connect" class="level1">
<h1>Let’s Connect</h1>
<ul>
<li><strong>GitHub:</strong> <a href="https://github.com/rgt47">rgt47</a></li>
<li><strong>Twitter/X:</strong> <a href="https://twitter.com/rgt47"><span class="citation" data-cites="rgt47">@rgt47</span></a></li>
<li><strong>LinkedIn:</strong> <a href="https://linkedin.com/in/rgthomaslab">Ronald Glenn Thomas</a></li>
<li><strong>Email:</strong> <a href="https://rgtlab.org/contact">rgtlab.org/contact</a></li>
</ul>
<p>I would enjoy hearing from you if:</p>
<ul>
<li>You spot an error or a better approach to any of the code in this post.</li>
<li>You have suggestions for topics you would like to see covered.</li>
<li>You want to discuss R programming, data science, or reproducible research.</li>
<li>You have questions about anything in this tutorial.</li>
<li>You just want to say hello and connect.</li>
</ul>
<hr>
<p><em>Rendered on 2026-05-17 at 17:08 PDT.</em><br> <em>Source: ~/prj/qblog/posts/32-sharermdcodeviadocker/sharermdcodeviadocker/analysis/report/index.qmd</em></p>
<section id="related-posts-in-this-cluster" class="level2">
<h2 class="anchored" data-anchor-id="related-posts-in-this-cluster">Related posts in this cluster</h2>
<p>This post is part of the <em>ZZCOLLAB Reproducible Compendia</em> series. Recommended reading order:</p>
<ol type="1">
<li>Post 01: <a href="../01-zc-quarto-compendium-intro/">Reproducible Blog Posts with ZZCOLLAB</a></li>
<li>Post 02: <a href="../02-zc-blog-post-template/">Constructing a reproducible blog post using zzcollab tools</a></li>
<li>Post 03: <a href="../03-zc-markdown-to-blog-workflow/">From Markdown to Blog Post: A ZZCOLLAB workflow</a></li>
<li><strong>Post 04: Sharing R Code via Docker: R Markdown Reports</strong> (this post)</li>
<li>Post 05: <a href="../05-zc-analysis-initiation-checklist/">A 55-Item Initiation Checklist for zzcollab Data Analyses</a></li>
<li>Post 06: <a href="../06-zc-manuscript-report-elements/">Seven Required Elements for a zzc Manuscript report.Rmd</a></li>
<li>Post 07: <a href="../07-zc-tiered-ci-strategy/">A tiered CI strategy for zzcollab research compendia</a></li>
<li>Post 08: <a href="../08-zc-github-actions-workflows/">GitHub Actions workflows for zzcollab research compendia</a></li>
</ol>


</section>
</section>

 ]]></description>
  <category>r</category>
  <category>docker</category>
  <category>rmarkdown</category>
  <category>shiny</category>
  <category>reproducibility</category>
  <guid>https://rgtlab.org/posts/zc-share-rmd-via-docker/</guid>
  <pubDate>Sun, 17 May 2026 07:00:00 GMT</pubDate>
  <media:content url="https://rgtlab.org/posts/zc-share-rmd-via-docker/media/images/hero.png" medium="image" type="image/png" height="96" width="144"/>
</item>
<item>
  <title>Migrating Off Dropbox: Beyond Dotfiles</title>
  <dc:creator>Ronald G. Thomas</dc:creator>
  <link>https://rgtlab.org/posts/sec-dropbox-to-portable-sync/</link>
  <description><![CDATA[ 




<!-- ============================================================================
AUTHOR PROVIDES — concrete inputs the blogger must supply

Every item below maps to a placeholder in the body. Fill in this checklist
FIRST, then paste your answers into the matching `[bracketed]` slots. Do not
publish until every box is ticked.

YAML FRONT MATTER (lines 1-20)
  [x] title           — 'Migrating Off Dropbox: Beyond Dotfiles'
  [x] subtitle        — decision framework for the rest of the workflow
  [x] date            — 2026-05-07
  [x] categories      — workflow, sync, migration, dropbox
  [x] description     — set
  [x] image           — media/images/hero.png (Imagen 3, 2026-05-07)
  [x] draft: false    — flip when imagery in place and render verified

NARRATIVE INPUTS
  [x] Hook sentence   — 'I did not really appreciate how much of my
        workflow depended on Dropbox until I tried to leave it.'
  [x] Pain point      — half-portable dotfiles + Dropbox-bound everything
                        else; new laptop without Dropbox is broken
  [x] Motivations     — five bullets
  [x] Objectives      — four verifiable end states
  [x] What is [the problem]? — three-layer model
  [x] Daily-workflow paragraph — how the post-migration setup feels
  [x] Things to Watch Out For — six gotchas
  [x] Lessons Learnt  — conceptual / technical / gotchas
  [x] Limitations     — six bullets
  [x] Opportunities for Improvement — five next steps
  [x] Wrapping Up     — conclusion + main takeaways
  [x] See Also        — post 24, post 20, plus key external links

CONFIGURATION DELIVERABLES (real, working artifacts, not snippets)
  [x] Prerequisites list — listed in body
  [ ] Installation block — N/A (this post is a framework, not an install)
  [x] Complete deliverable — docs/migration-decision-worksheet.qmd
  [ ] Verification commands — N/A (post-specific commands inline)
  [ ] Optional keybinding/command reference table — not applicable
  [x] Uninstall / rollback steps — covered as 'staying with Dropbox' path
  [ ] Optional appendices — see-also section serves this role

VERSION MATRIX (Reproducibility section)
  [x] OS + version tested        — macOS 15.4 (Sequoia)
  [x] Tool versions              — git 2.45+, rsync 3.2+, Syncthing 1.27+
  [x] Date of last verification  — 2026-05-07

IMAGES (4 total: hero + 3 ambiance, all in media/images/)
  [ ] Hero image (80% width)        — placeholders in place; prompts ready
  [ ] Ambiance image 1 (100% width)
  [ ] Ambiance image 2 (100% width)
  [ ] Ambiance image 3 (100% width)
  [ ] media/images/README.md        — update with new prompts and dates

CONTACT & METADATA
  [x] Author name in YAML matches site author
  [x] Social links in 'Let's Connect' updated (or removed)
  [x] Giscus comments enabled (inherited from _quarto.yml)

============================================================================ -->
<!-- ============================================================================
HERO IMAGE
============================================================================ -->
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/sec-dropbox-to-portable-sync/media/images/hero.png" class="img-fluid figure-img" style="width:80.0%"></p>
<figcaption>A wooden tray on a plain workbench, with one large compartment on the left holding a single round glass weight and three narrower compartments on the right holding three different small objects, suggesting a single category being separated into three purpose-fit ones.</figcaption>
</figure>
</div>
<p><em>A portable dotfiles repository is only the first half of leaving Dropbox. The other half is the rest of the workflow.</em></p>
<div class="callout callout-style-default callout-note callout-titled">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon"></i>
</div>
<div class="callout-title-container flex-fill">
Note
</div>
</div>
<div class="callout-body-container callout-body">
<p><strong>Multi-Machine Setup Series</strong> (recommended reading order): <a href="../../posts/64-migrating-off-dropbox/">Part 1: Migrating off Dropbox</a> | <a href="../../posts/65-multi-laptop-bootstrap/">Part 2: Multi-Laptop Bootstrap</a> | <a href="../../posts/67-multi-laptop-security/">Part 3: Security Audit</a></p>
</div>
</div>
<section id="introduction" class="level1">
<h1>Introduction</h1>
<p>I did not really appreciate how much of my daily workflow depended on Dropbox until I tried to leave it. The dotfiles migration described in <a href="https://focusonr.org/posts/24-setupdotfilesongithub/">post 24</a> is the obvious half: pull the configuration files out of a cloud-mounted directory, put them under git, write an installer that creates symlinks. Once that work is committed, every dotfile in <code>$HOME</code> becomes version-controlled and reproducible on a new laptop with two commands.</p>
<p>The non-obvious half is everything else. The dotfiles deploy cleanly on a fresh machine, then the new shell tries to <code>cd ~/prj/some-project</code> and fails because the project tree still lives at <code>~/Library/CloudStorage/Dropbox/prj/</code>. Backup scripts hardcode the same Dropbox path. AI-tool history files live under a Dropbox-synced symlink and accumulate conflict copies on every overlapping write between machines. The configuration is portable; the workflow is not.</p>
<p>This post is the second half of post 24. It frames the problem as three independent layers (dotfiles, project content, append-only history), surveys the sync mechanisms each layer can use, and walks through a migration sequence that does not break a running pipeline mid-move. The companion deliverable is a decision worksheet that takes a reader from current state to a concrete migration plan. The choice of whether to leave Dropbox at all is left to the reader; this post is about doing it deliberately rather than partially.</p>
<div class="callout callout-style-default callout-note callout-titled">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon"></i>
</div>
<div class="callout-title-container flex-fill">
Status of this migration
</div>
</div>
<div class="callout-body-container callout-body">
<p>This post documents an ongoing migration on the author’s own setup. At time of writing, Layer 1 (dotfiles) is complete: the repository exists at <code>~/dotfiles/</code>, the installer is written and dry-run-tested, and the architecture below has been exercised end-to-end on this laptop. Layer 2 (project content) and Layer 3 (append-only history) are in progress. The framework is presented as it stands today; the ‘After completing this migration’ results section near the end is labelled as expected outcomes rather than claimed ones, and will be updated with measured numbers once the migration reaches a fresh laptop. The companion plan tracking this work is available in the dotfiles repository as <code>MULTI_LAPTOP_PLAN.md</code>.</p>
</div>
</div>
<section id="motivations" class="level2">
<h2 class="anchored" data-anchor-id="motivations">Motivations</h2>
<ul>
<li>A new laptop bootstrap should be a one-command operation. Today, after running the dotfiles installer, half the workflow still requires installing Dropbox, signing in, and waiting for several gigabytes to materialise before anything functional happens.</li>
<li>Dropbox sync of append-only files (shell history, AI-tool history, editor undo files) routinely produces conflict copies on multi-machine setups. The 12 conflict copies of <code>history.jsonl</code> discovered in this account on 2026-05-02 are one symptom of an architectural mismatch.</li>
<li>Project content directories under Dropbox commingle with cloud-only artefacts that should not be synced (model output, large derived data, machine-specific caches). Excluding them via <code>.dropboxignore</code> is fragile and error-prone.</li>
<li>The set of services that compete for <code>$HOME</code> (Dropbox, iCloud Drive, Google Drive, OneDrive) keeps growing, and choosing one default for everything is increasingly suboptimal as each service specialises.</li>
<li>The work to reach ‘workflow independence’ is largely the same work as ‘workflow modernisation’: separating concerns by layer, choosing sync mechanisms that match each layer’s semantics, and documenting each move so it can be audited later.</li>
</ul>
</section>
<section id="objectives" class="level2">
<h2 class="anchored" data-anchor-id="objectives">Objectives</h2>
<ol type="1">
<li>Inventory the workflow against three layers (dotfiles, project content, append-only history) and classify every sync-relevant artefact into exactly one layer.</li>
<li>Produce a decision worksheet that, for each layer, names the candidate sync mechanisms and the trade-offs that distinguish them.</li>
<li>Sequence the migration so that the backup pipeline never points at a stale source path and the launchd jobs stay valid throughout the move.</li>
<li>Establish a fresh-machine bootstrap that does not require Dropbox and verifies the entire workflow end to end.</li>
</ol>
<p>This post documents the author’s own migration. If errors are spotted or better approaches are known, the comment thread below is the right place to note them.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/sec-dropbox-to-portable-sync/media/images/ambiance1.png" class="img-fluid figure-img"></p>
<figcaption>A wooden desk seen from above with three small index cards arranged in a horizontal row, each card showing a hand-drawn symbol, suggesting classification before action.</figcaption>
</figure>
</div>
</section>
</section>
<section id="what-is-the-problem" class="level1">
<h1>What is the Problem?</h1>
<p>The problem is that ‘sync everything via one cloud provider’ was a reasonable default in 2014, when Dropbox was the only practical option for arbitrary file sync, but it now bundles three semantically distinct concerns into a single mechanism. The three concerns:</p>
<ul>
<li><strong>Configuration distribution</strong> wants version history, atomic deploys, and the ability to roll back. Git solves this; cloud sync only approximates it.</li>
<li><strong>Project content sync</strong> wants block-level diffs for large files, conflict semantics that match the file format, and the option to exclude derived artefacts. Different sync mechanisms handle these very differently.</li>
<li><strong>Append-only history</strong> wants either single-machine writes or a sync mechanism that understands append semantics. Cloud sync writes the whole file on every change and produces a conflict copy on every overlap.</li>
</ul>
<p>A useful analogy: it is the same reason a research compendium separates <code>data/</code>, <code>R/</code>, and <code>analysis/</code>. Each directory has different rules about what changes, how it changes, and who is allowed to change it. Lumping them all into one sync mechanism enforces no useful constraint and produces friction at the boundary.</p>
<p>A concrete example from this migration: the file <code>~/.claude/history.jsonl</code> is an append-only log of prompts entered into the Claude Code CLI. Five machines (athena, Joe, julia.local, MacBook-Air.local, and the author’s primary laptop) write to it through a Dropbox-synced symlink. Most days, only one machine is active and the file syncs cleanly. The day two machines write within the same minute, Dropbox cannot merge the two append streams and creates a conflict copy. Across April 2026 alone, this produced four new conflict copies on this account, each holding several hundred KB of unique prompts; the running total since January is eleven. The file format is not the problem; the choice to sync an append log via a whole-file cloud provider is.</p>
</section>
<section id="prerequisites" class="level1">
<h1>Prerequisites</h1>
<p>This post assumes:</p>
<ul>
<li><strong>Operating system:</strong> macOS 13+ or a recent Linux distribution. The sync-mechanism table is applicable to both, but specific path conventions reference macOS.</li>
<li><strong>Already in place:</strong> a working dotfiles repository per <a href="https://focusonr.org/posts/24-setupdotfilesongithub/">post 24</a>. The architecture below builds on that foundation; without it, half the moves cannot be made cleanly.</li>
<li><strong>Background knowledge:</strong> comfort with git remotes, basic launchd plist structure, rclone or rsync at the level of a single sync command. The post does not require expertise in any of these.</li>
<li><strong>Hardware:</strong> a single primary laptop is sufficient. A second machine (or VM) is useful for the migration’s verification step but is not required.</li>
<li><strong>Time required:</strong> about a day’s careful work to inventory and decide; about a half-day to execute the moves once the decisions are made.</li>
</ul>
<p>On a single-machine setup where conflict copies have never appeared, the migration’s payoff is smaller and may not warrant the undertaking. The framework still applies, but Dropbox is unlikely to be causing friction in that scenario.</p>
</section>
<section id="the-three-layers" class="level1">
<h1>The Three Layers</h1>
<p>Treat the workflow as three layers. Each layer has its own sync semantics, its own candidate mechanisms, and its own failure modes.</p>
<section id="layer-1-dotfiles-distribution" class="level2">
<h2 class="anchored" data-anchor-id="layer-1-dotfiles-distribution">Layer 1: Dotfiles distribution</h2>
<p>Solved by post 24. The summary, for reference: configuration files live in a git repository at <code>~/dotfiles/</code> (outside any cloud-mounted path), an <code>install.sh</code> creates symlinks into <code>$HOME</code> per machine, secrets stay out of the repository entirely. New-machine bootstrap is <code>git clone … ~/dotfiles &amp;&amp; cd ~/dotfiles &amp;&amp; ./install.sh</code>.</p>
<p>This layer is the easiest to migrate because the files are small, change infrequently, and are nearly always edited from a single machine. Git handles the multi-machine merge cleanly when overlap does occur.</p>
<p>What post 24 explicitly does not address: project content (the next layer), backup-pipeline source paths (also next layer), and append-only history (the third layer). Those failures show up only after the dotfiles layer is migrated, which is why this post exists.</p>
</section>
<section id="layer-2-project-content-sync" class="level2">
<h2 class="anchored" data-anchor-id="layer-2-project-content-sync">Layer 2: Project content sync</h2>
<p>Project content is the directory tree that holds research repositories, data, drafts, and archives. On a Dropbox-bound workflow this is <code>~/Dropbox/prj/</code> and similar siblings (<code>docs/</code>, <code>sbx/</code>, <code>work/</code>, <code>shr/</code>).</p>
<p>Project content has different sync characteristics from dotfiles:</p>
<ul>
<li>It is much larger (tens of GB is typical).</li>
<li>It contains both small text (R, Quarto) and large binary (data, figures, PDFs).</li>
<li>It changes frequently, often on a single machine within a session.</li>
<li>It includes derived artefacts that should not be synced at all (renv libraries, model output, caches).</li>
</ul>
<p>The candidate sync mechanisms, with the dimensions that matter:</p>
<table class="caption-top table">
<colgroup>
<col style="width: 33%">
<col style="width: 33%">
<col style="width: 33%">
</colgroup>
<thead>
<tr class="header">
<th>Mechanism</th>
<th>Strength</th>
<th>Weakness</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><strong>Per-project git remotes (GitHub, GitLab, Codeberg)</strong></td>
<td>Diff-aware history, atomic deploys, free for small repos</td>
<td>Bad with large binary files (Git LFS quotas), each project must be a repo</td>
</tr>
<tr class="even">
<td><strong>Syncthing (peer-to-peer, no cloud)</strong></td>
<td>No middleman, no quotas, sync-to-LAN is fast, files are local-first</td>
<td>Two machines must be online together to sync; conflict semantics are simpler than Dropbox’s but still file-level</td>
</tr>
<tr class="odd">
<td><strong>rsync over SSH to a NAS or VPS</strong></td>
<td>Bandwidth-efficient, deterministic, scriptable</td>
<td>One-way push (or hand-coded bidirectional); requires the destination to exist; no version history without extra work</td>
</tr>
<tr class="even">
<td><strong>iCloud Drive (native macOS)</strong></td>
<td>Tight macOS integration, included with Apple ID</td>
<td>macOS-only; opaque sync timing; cannot exclude subdirectories cleanly</td>
</tr>
<tr class="odd">
<td><strong>Google Drive (rclone or native)</strong></td>
<td>Effectively unlimited storage on enterprise plans</td>
<td>Same whole-file model as Dropbox; conflicts behave the same way</td>
</tr>
</tbody>
</table>
<p>Most readers benefit from a hybrid: per-project git remotes for code (the bulk of the value) and Syncthing or rsync for the data that does not belong in a public or even private repo. The hybrid wins because it lets each artefact go to the mechanism that matches its semantics.</p>
<p>The single most consequential observation in this migration: ‘project content’ is not a homogeneous category. Splitting it into ‘code’ and ‘data’ before choosing a mechanism makes every subsequent decision easier.</p>
<section id="reconstructable-local-caches-the-maildir-case" class="level3">
<h3 class="anchored" data-anchor-id="reconstructable-local-caches-the-maildir-case">Reconstructable local caches: the Maildir case</h3>
<p>A subset of what looks like Layer 2 data is in fact a local cache of a canonical source that already lives elsewhere. For these, the right move is not to choose a sync mechanism at all; it is to let each machine rebuild its copy from the authoritative source on demand.</p>
<p>The worked example: a Maildir-based mail setup with mutt, mbsync, and notmuch. The IMAP server holds the canonical mailbox. <code>mbsync</code> mirrors a subset of folders into a local Maildir at <code>~/.local/share/mail/</code> (or any other non-synced path; the XDG <code>~/.local/share/&lt;app&gt;/</code> location is a sensible default). <code>notmuch new</code> indexes the Maildir into a local Xapian database. <code>mutt</code> reads the Maildir directly. On a second laptop, the same configs deploy via Layer 1, the same <code>mbsync</code> runs from a launchd job, the local Maildir gets repopulated from IMAP, and <code>notmuch new</code> rebuilds the index. There is nothing to sync between the two laptops; the IMAP server is doing the synchronisation by being the canonical source.</p>
<p>Putting the Maildir in Dropbox is wrong on four counts. It duplicates the work IMAP already does. It triples the disk footprint (IMAP + Dropbox + local). Maildir’s one-message-per-file format produces hundreds of thousands of small files that strain cloud-sync engines. And the notmuch Xapian database is a B-tree-on-disk format that is not safe to share between processes on different machines via file sync.</p>
<p>The same pattern applies to a broader class of local artefacts: Homebrew’s package cache (<code>~/Library/Caches/Homebrew/</code>), language-server indexes, browser caches, ccache, AI-model caches that hash to the same content given the same prompts. None of these need to be synced between machines; each is a derivative of an authoritative source and is rebuilt cheaply on demand. Treat them as per-machine state, exclude them from any sync mechanism, and add the rebuild step to the new-machine bootstrap if it does not happen automatically.</p>
<p>The new-laptop bootstrap for the Maildir case, after <code>install.sh</code> has deployed the configs:</p>
<div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb1-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Restore IMAP/SMTP credentials (manual; from pass / keychain / 1Password).</span></span>
<span id="cb1-2"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Then:</span></span>
<span id="cb1-3"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">mbsync</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-a</span>                        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># populate ~/.local/share/mail/</span></span>
<span id="cb1-4"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">notmuch</span> new                      <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># build the Xapian index</span></span>
<span id="cb1-5"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">launchctl</span> bootstrap <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"gui/</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$UID</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb1-6">  ~/Library/LaunchAgents/local.mbsync.plist</span>
<span id="cb1-7"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">launchctl</span> bootstrap <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"gui/</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$UID</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb1-8">  ~/Library/LaunchAgents/local.notmuch.new.plist</span></code></pre></div>
<p>Time depends on mailbox size; a 3 GB mailbox over residential broadband is roughly 30-60 minutes. The two scheduled jobs keep the Maildir current thereafter.</p>
<p>The one cost of this pattern is that mail is not available offline until the first <code>mbsync -a</code> completes. For mutt users this is rarely a meaningful constraint; for users who need immediate offline access on a fresh laptop, an <code>rsync</code> of a recent Maildir snapshot from the previous laptop into <code>~/.local/share/mail/</code> skips the IMAP fetch step.</p>
</section>
<section id="worked-example-relocating-an-existing-maildir-to-.localsharemail" class="level3">
<h3 class="anchored" data-anchor-id="worked-example-relocating-an-existing-maildir-to-.localsharemail">Worked example: relocating an existing Maildir to ~/.local/share/mail/</h3>
<p>A common starting point for long-time mutt users is a Maildir at <code>~/Mail/</code> (the convention from before the XDG Base Directory specification existed). The relocation to <code>~/.local/share/mail/</code> is mechanical, and the cache-and-index can be moved in place rather than re-downloaded. The steps below assume mutt + mbsync + notmuch and a single Gmail account at <code>~/Mail/gmail/</code>; generalisation to other layouts is direct.</p>
<div class="sourceCode" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb2-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 1. Stop any running mbsync. If a launchd job runs mbsync on a</span></span>
<span id="cb2-2"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#    schedule, bootout it temporarily.</span></span>
<span id="cb2-3"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">pkill</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-f</span> mbsync <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">||</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">true</span></span>
<span id="cb2-4"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">launchctl</span> bootout <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"gui/</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$UID</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">/local.mbsync"</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span>/dev/null <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">||</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">true</span></span>
<span id="cb2-5"></span>
<span id="cb2-6"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 2. Cheap insurance: dump the notmuch tag database. Tags are user</span></span>
<span id="cb2-7"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#    state; the message index can be rebuilt, the tags cannot.</span></span>
<span id="cb2-8"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">notmuch</span> dump <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--output</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$HOME</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">/notmuch-dump-</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$(</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">date</span> +%Y%m%d<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">)</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">.txt"</span></span>
<span id="cb2-9"></span>
<span id="cb2-10"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 3. Move the Maildir AND .notmuch together. They must move</span></span>
<span id="cb2-11"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#    atomically to keep the Xapian database's relative paths valid.</span></span>
<span id="cb2-12"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">mkdir</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-p</span> ~/.local/share/mail</span>
<span id="cb2-13"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">mv</span> ~/Mail/gmail   ~/.local/share/mail/gmail</span>
<span id="cb2-14"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">mv</span> ~/Mail/.notmuch ~/.local/share/mail/.notmuch</span>
<span id="cb2-15"></span>
<span id="cb2-16"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 4. Update the dotfiles repo (Layer 1, single source of truth)</span></span>
<span id="cb2-17"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#    BEFORE editing any live config under $HOME. Edit:</span></span>
<span id="cb2-18"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#       ~/dotfiles/editors/notmuch-config         (path = $HOME/.local/share/mail)</span></span>
<span id="cb2-19"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#       ~/dotfiles/config/mutt/muttrc             (folder, spoolfile, postponed, record)</span></span>
<span id="cb2-20"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#       ~/dotfiles/mbsyncrc                        (Path, Inbox under each MaildirStore)</span></span>
<span id="cb2-21"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#    Commit and push. (The dotfiles repo is OUTSIDE cloud-mounted</span></span>
<span id="cb2-22"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#    paths per Layer 1, so in-place editors are safe to use here.)</span></span>
<span id="cb2-23"></span>
<span id="cb2-24"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 5. Reflect the dotfiles changes into $HOME (idempotent).</span></span>
<span id="cb2-25"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">cd</span> ~/dotfiles <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">&amp;&amp;</span> <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">./install.sh</span></span>
<span id="cb2-26"></span>
<span id="cb2-27"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 6. Verify.</span></span>
<span id="cb2-28"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">mbsync</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-a</span>                  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Resumes incrementally; should NOT re-download.</span></span>
<span id="cb2-29"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">notmuch</span> search <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'*'</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">|</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">head</span>  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Confirms the index resolves message paths.</span></span>
<span id="cb2-30"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">mutt</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-e</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'exit'</span>             <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Opens and exits cleanly with the new paths.</span></span>
<span id="cb2-31"></span>
<span id="cb2-32"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 7. Re-enable the scheduled mbsync once verification passes.</span></span>
<span id="cb2-33"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">launchctl</span> bootstrap <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"gui/</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$UID</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb2-34">    ~/Library/LaunchAgents/local.mbsync.plist</span></code></pre></div>
<p>Two points worth flagging:</p>
<ul>
<li>The notmuch Xapian database stores message paths relative to <code>database.path</code>. Moving the Maildir and <code>.notmuch</code> together preserves those relative paths, so <code>notmuch new</code> is not required. If the two directories are moved separately for any reason, the index becomes invalid and <code>notmuch new</code> is needed to rebuild it.</li>
<li>Step 4 (update the dotfiles repo first) is the step the obvious version of this recipe omits. Editing the live <code>~/.config/mutt/muttrc</code> directly before updating the repo copy makes the two diverge. A second laptop bootstrapped from the repo today would deploy a muttrc that points at a Maildir that does not exist. Always edit the repo file, then <code>install.sh</code>.</li>
</ul>
<p>The pre-XDG location (<code>~/Mail/</code>) can be deleted after a few days of confirmed working state; keep it around briefly as a rollback target.</p>
</section>
</section>
<section id="layer-3-append-only-history-files" class="level2">
<h2 class="anchored" data-anchor-id="layer-3-append-only-history-files">Layer 3: Append-only history files</h2>
<p>This layer is small in bytes but disproportionate in friction. The relevant files:</p>
<ul>
<li><code>~/.zsh_history</code> (shell command history)</li>
<li><code>~/.viminfo</code> (vim’s last-used files, registers, marks)</li>
<li><code>~/.claude/history.jsonl</code> (Claude Code prompt history)</li>
<li>Editor undo files (<code>~/.local/state/vim/undo/</code>)</li>
<li>AI-tool histories from any other tools (<code>~/.gemini/</code>, etc.)</li>
</ul>
<p>Each is append-only at the application level and whole-file at the sync layer. That mismatch is what produces conflict copies. There are three architecturally honest choices:</p>
<ol type="1">
<li><strong>Per-machine state.</strong> Do not sync these files at all. Accept that arrow-key history on one laptop does not include commands typed on another. Simplest; works for most users; what most non-Dropbox dotfile setups assume.</li>
<li><strong>Single-machine writes.</strong> Designate one machine as the canonical owner; the others write to local files that are never synced. Practical only if the user genuinely uses one machine 95% of the time.</li>
<li><strong>Append-aware sync.</strong> A few sync mechanisms (atuin for shell history, custom JSONL mergers for AI logs) understand append semantics and can merge two divergent streams. More moving parts, but eliminates the conflict-copy class entirely.</li>
</ol>
<p>The right choice depends on the user. Most readers will be happiest with option 1 plus, optionally, atuin for shell history because it is genuinely useful across machines and well-engineered for the multi-machine case.</p>
</section>
</section>
<section id="the-decision-worksheet" class="level1">
<h1>The Decision Worksheet</h1>
<p>The companion deliverable for this post is a Quarto document at <code>docs/migration-decision-worksheet.qmd</code>. It walks the reader through:</p>
<ol type="1">
<li><strong>Inventory.</strong> List every sync-relevant artefact in <code>$HOME</code> and <code>~/Dropbox/</code>. Classify each into one of the three layers.</li>
<li><strong>Mechanism selection.</strong> For each layer, pick a sync mechanism (or ‘no sync, per-machine’) from the table above.</li>
<li><strong>Migration order.</strong> Sequence the moves so the backup pipeline never points at a stale path. The general rule is: dotfiles first (already done if post 24 was followed), then a single project as a pilot, then the rest of project content, then history files.</li>
<li><strong>Verification.</strong> Bootstrap a clean user account or VM and confirm the full workflow comes up without Dropbox.</li>
</ol>
<p>The worksheet is generic; it does not assume any particular project structure or sync mechanism. Completing it for a given setup converts the migration into a series of small, audited moves rather than a single large rewrite.</p>
</section>
<section id="migration-sequence" class="level1">
<h1>Migration Sequence</h1>
<p>The order of operations matters because the backup pipeline must remain valid at every intermediate step. The sequence below comes from running this migration on a real workflow.</p>
<ol type="1">
<li><strong>Confirm dotfiles foundation.</strong> Verify post 24’s repository works. Run <code>install.sh --dry-run</code> on a clean account or VM. Resolve any failures before proceeding.</li>
<li><strong>Introduce a path-abstraction env var.</strong> Add <code>PRJ_ROOT</code> to <code>~/.zshenv</code> with a fallback that resolves to <code>~/Dropbox/prj</code> for now. Replace literal <code>~/Dropbox/prj</code> references in scripts and configs with <code>$PRJ_ROOT</code>. Nothing visible changes, but the next move is now a single-line edit.</li>
<li><strong>Pilot one project.</strong> Pick a single small repo. Move it from <code>~/Dropbox/prj/&lt;project&gt;/</code> to <code>~/prj/&lt;project&gt;/</code> (real path, outside cloud). Push to a private git remote if it is not already. Confirm the workflow on it.</li>
<li><strong>Migrate the rest of project content in batches.</strong> A batch a day prevents the backup pipeline from going stale. For each batch: move, repoint any in-script paths, push to remotes, verify.</li>
<li><strong>Repoint backup sources.</strong> Once <code>$PRJ_ROOT</code> resolves to the new path, edit <code>backup-gdrive</code> and <code>backup-icloud</code> (or their equivalents) to source from <code>$PRJ_ROOT</code> instead of the literal Dropbox path. The launchd plists themselves do not change; only the scripts they call.</li>
<li><strong>Resolve append-only history.</strong> Pick option 1, 2, or 3 from the previous section. Apply per-file. Document the choice in the dotfiles repo’s README for future reference.</li>
<li><strong>Verify on a clean account.</strong> Boot a fresh user, clone dotfiles, run installer, set up project remotes, confirm the workflow. The first surprise reveals the assumption that was hiding.</li>
<li><strong>Optional: uninstall Dropbox.</strong> Most users do not need to actually remove Dropbox. Once the workflow is independent, Dropbox can stay installed as a backup or for sharing files with colleagues, without being load-bearing for the workflow itself.</li>
</ol>
<p>The sequence is robust to interruption. Stopping after step 3 leaves a working pilot with the backup pipeline intact. Stopping after step 5 leaves the workflow independent; the history-file decision can be deferred.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/sec-dropbox-to-portable-sync/media/images/ambiance2.png" class="img-fluid figure-img"></p>
<figcaption>Two open-top boxes side by side on a workbench, the left box full of mixed objects and the right box divided into three sorted compartments, with a single object mid-transfer between them.</figcaption>
</figure>
</div>
</section>
<section id="things-to-watch-out-for" class="level1">
<h1>Things to Watch Out For</h1>
<p>Seven gotchas to anticipate during the migration. Each lists the symptom and the fix.</p>
<ol type="1">
<li><strong>The backup pipeline goes stale silently.</strong> Symptom: <code>backup-gdrive</code> runs every night, exits 0, but the syncthing folder it backs up has not changed in a week because the source path no longer exists. Fix: <code>rclone sync</code> exits 0 even when the source is empty, so add an explicit <code>[[ -d $PRJ_ROOT ]]</code> guard at the top of every backup script and fail loud on missing source.</li>
<li><strong>Cloud-mounted git working trees corrupt their indexes.</strong> Symptom: <code>git status</code> reports phantom changes; <code>git fsck</code> finds dangling refs; the working tree is inconsistent. Fix: never put <code>~/dotfiles/</code> inside <code>~/Dropbox/</code>, <code>~/Library/CloudStorage/...</code>, or <code>~/Library/Mobile Documents/</code>. Git’s frequent writes to <code>.git/index</code> race with the cloud provider’s sync engine. The repo MUST be on a non-synced filesystem path.</li>
<li><strong>Symlinks to Dropbox break asymmetrically across machines.</strong> Symptom: <code>~/.config/git -&gt; ~/Library/CloudStorage/Dropbox/dotfiles/config/git</code> on machine A, but on machine B the same symlink is dangling because Dropbox is at a different path or not installed. Fix: symlinks created by <code>install.sh</code> should target <code>$DOTFILES</code> (an env var resolved at install time), not literal Dropbox paths.</li>
<li><strong>Append-only history conflict copies do not announce themselves.</strong> Symptom: a file named <code>history (machine's conflicted copy YYYY-MM-DD).jsonl</code> appears next to the canonical file, with no notification, no error, no merge prompt. Fix: a weekly grep for ‘conflicted copy’ across the Dropbox tree, or an explicit migration of the affected files out of cloud sync.</li>
<li><strong>launchd plist substitutions overwrite themselves.</strong> Symptom: an edit to <code>~/dotfiles/launchd/local.backup.research.plist</code> does not change the running job, because <code>install.sh</code> sed-substitutes <code>__USER__</code> to <code>$USER</code> when writing to <code>~/Library/LaunchAgents/</code>, and the active plist is the substituted copy. Fix: do not edit the active plist directly; edit the source in <code>~/dotfiles/launchd/</code>, then re-run <code>install.sh</code> plus <code>launchctl bootout</code> and <code>launchctl bootstrap</code>.</li>
<li><strong>Project content includes runtime artefacts that should not move.</strong> Symptom: a 200 MB <code>renv/library/</code> directory comes along with a project move and now lives at <code>~/prj/&lt;project&gt;/renv/library/</code>, where renv expects to manage it. Fix: confirm <code>.gitignore</code> excludes the right paths before pushing; add <code>**/renv/library/</code> to the dotfiles repo’s <code>.gitignore</code> if one is accidentally tracked.</li>
<li><strong>In-place editors race with cloud sync and truncate files to zero bytes.</strong> Symptom: <code>sed -i.bak file</code>, <code>awk -i inplace</code>, or <code>perl -i</code> against a path under <code>~/Library/CloudStorage/...</code> returns 0 but leaves the file empty. The <code>.bak</code> saved by <code>-i.bak</code> is also placed in cloud storage and is subject to the same race; on the incident that produced this gotcha, both the file and its backup were lost locally and required Dropbox version-history restore. Fix: never run <code>-i</code>-style editors on cloud-mounted paths. Either use the Edit tool / a real editor, or copy to <code>/tmp</code>, edit, and <code>mv</code> back. (If the dotfiles repo is on a non-synced path per gotcha 2, this hazard is confined to scripts that touch <code>~/Library/CloudStorage/...</code> from elsewhere.)</li>
</ol>
</section>
<section id="uninstall-rollback" class="level1">
<h1>Uninstall / Rollback</h1>
<p>The most common ‘undo’ is not ‘reinstall Dropbox’ but ‘put project content back under Dropbox while keeping dotfiles independent’. The architecture supports this directly because the layers are independent.</p>
<p>To roll project content back into Dropbox:</p>
<ol type="1">
<li>Move <code>~/prj/&lt;project&gt;/</code> back to <code>~/Dropbox/prj/&lt;project&gt;/</code>.</li>
<li>Update <code>~/.zshenv</code> so <code>PRJ_ROOT</code> resolves to the Dropbox path.</li>
<li>The dotfiles repo, backup scripts, and launchd plists do not need any change because they reference <code>$PRJ_ROOT</code>.</li>
</ol>
<p>This is also the recommended path for a reader who completed the dotfiles work but is not yet ready to migrate project content. Set up the env var, leave the path pointing at Dropbox, defer the project move until later.</p>
<p>To roll back the entire migration: the dotfiles installer’s <code>--dry-run</code> should never have shipped destructively, so the rollback is just <code>rm -rf ~/dotfiles</code> plus restoring <code>$HOME</code> from a Time Machine backup. Most readers will not need this.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/sec-dropbox-to-portable-sync/media/images/ambiance3.png" class="img-fluid figure-img"></p>
<figcaption>A closed wooden chest with three small brass clasps on a stone surface, an open notebook and a capped fountain pen beside it, suggesting a deliberate and completed reorganisation.</figcaption>
</figure>
</div>
</section>
<section id="what-did-we-learn" class="level1">
<h1>What Did We Learn?</h1>
<section id="lessons-learnt" class="level2">
<h2 class="anchored" data-anchor-id="lessons-learnt">Lessons Learnt</h2>
<p><strong>Conceptual:</strong></p>
<ul>
<li>The ‘sync everything via one cloud provider’ default conflates three independent concerns. Separating them makes each easier to reason about.</li>
<li>Migration sequencing matters more than mechanism choice. The wrong mechanism is correctable; a half-migrated workflow is hard to leave for a year.</li>
<li>Per-machine state is a feature, not a bug, for many file types. The right answer for shell history is often ‘do not sync’, not ‘sync better’.</li>
</ul>
<p><strong>Technical:</strong></p>
<ul>
<li>A single environment variable (<code>$PRJ_ROOT</code>) provides enough indirection to migrate path references in two passes: first introduce the variable while it still resolves to Dropbox, then change what it resolves to.</li>
<li>launchd plists with <code>__USER__</code> placeholders deploy cleanly across machines via <code>install.sh</code> sed-substitution; bootstrapping the substituted copy avoids machine-specific files in the dotfiles repo.</li>
<li>Atomic git operations on a non-synced filesystem are reliable; the same operations on a cloud-synced filesystem race with the sync engine and produce subtle corruption.</li>
</ul>
<p><strong>Gotchas:</strong></p>
<ul>
<li>‘rclone sync exits 0’ does not mean the sync succeeded; it can mean the source had nothing to send because the path is wrong.</li>
<li>A symlink created on one machine resolves correctly on that machine but may dangle on another. Always test the new-machine case.</li>
<li>Conflict copies of append-only files accumulate silently. Periodic auditing is the only way to notice.</li>
</ul>
</section>
<section id="limitations" class="level2">
<h2 class="anchored" data-anchor-id="limitations">Limitations</h2>
<ul>
<li>The migration described here covers a single-user workflow. Team setups (where the same Dropbox folder is shared with collaborators) need additional considerations not addressed in this post.</li>
<li>Project content moved from Dropbox to per-project git remotes is now subject to GitHub or GitLab quotas, including LFS limits for binary files. Plan accordingly.</li>
<li>The decision worksheet is a generic framework; it does not generate scripts for any particular sync mechanism. Each reader still has to write the rclone or rsync commands for their setup.</li>
<li>Append-aware history sync (option 3) requires either an existing tool (atuin works for shell history) or hand-rolled mergers for proprietary log formats. The post does not provide those mergers.</li>
<li>Migrating off Dropbox does not, by itself, improve the security posture of credentials. Files like <code>~/.aws/credentials</code> should be addressed separately; cloud sync was not their only exposure.</li>
<li>The post assumes macOS or Linux. Windows-specific path conventions and sync semantics are out of scope.</li>
</ul>
</section>
<section id="opportunities-for-improvement" class="level2">
<h2 class="anchored" data-anchor-id="opportunities-for-improvement">Opportunities for Improvement</h2>
<ol type="1">
<li>Extend the decision worksheet into a small CLI tool that audits a current setup and generates a draft migration plan.</li>
<li>Build an atuin (or atuin-equivalent) integration template for the dotfiles repo, so option 3 from layer 3 becomes a one-line opt-in.</li>
<li>Add a <code>verify.sh</code> to the dotfiles repo that runs the end-to-end check (dotfiles installed, <code>$PRJ_ROOT</code> valid, backup scripts pointing at it, launchd jobs loaded) on demand and reports pass/fail.</li>
<li>Document a canonical ‘two-laptop’ setup with Syncthing replacing the Dropbox role for shared project content.</li>
<li>Generalise the worksheet’s mechanism table into a maintained list (a small repo or a wiki page) that adds new tools as the ecosystem evolves (Filen, S3-backed, etc.).</li>
</ol>
</section>
</section>
<section id="wrapping-up" class="level1">
<h1>Wrapping Up</h1>
<p>A portable dotfiles repository is the first half of a Dropbox-free workflow; this post is the framework for the second half. The key idea: stop treating sync as a single dimension and start treating it as three layers (configuration, project content, append-only history) with different semantics and different right answers.</p>
<p>Most of the work is in the inventory and decision step, not in the moves themselves. Once each artefact is classified and each layer has a chosen mechanism, the actual migration is a sequence of small file moves and a few text edits. The pieces that take time are testing the new-machine bootstrap, deciding what to do about history files, and resisting the temptation to migrate everything in one weekend.</p>
<section id="expected-outcomes" class="level2">
<h2 class="anchored" data-anchor-id="expected-outcomes">Expected outcomes</h2>
<p>The architecture aims to deliver three measurable end states. The first is verifiable now (Layer 1 is complete on the author’s primary laptop); the other two will be confirmed and updated with measured numbers when Layer 2 and Layer 3 land.</p>
<ul>
<li><strong>New-laptop setup</strong> is <code>git clone … ~/dotfiles &amp;&amp; ./install.sh &amp;&amp; (per-project git clones)</code>. The plan budget is roughly 30 minutes from clean machine to working environment; the actual figure will be added after a fresh-VM bootstrap is run.</li>
<li><strong>Conflict copies of <code>history.jsonl</code> and similar files</strong> should stop recurring once the affected files are moved out of Dropbox sync. The post will be updated with the post-migration count once Layer 3 is applied.</li>
<li><strong>Backup pipelines</strong> will source from <code>$PRJ_ROOT</code>, which can point anywhere; once Layer 2 lands, Dropbox is no longer load-bearing for the workflow.</li>
</ul>
<p>In conclusion, four points merit emphasis. First, the dotfiles layer is the wedge, not the goal; once it is in place, the rest of the migration is mechanical. Second, ‘project content’ is two layers in disguise (code, which wants git, and data, which wants something else); the two should be separated before a mechanism is chosen. Third, some of what looks like ‘data’ is actually a local cache of an authoritative source elsewhere (Maildir from IMAP, Homebrew cache, language-server indexes); for these the right answer is not to sync at all, but to rebuild on each machine from the canonical source. Fourth, append-only files are the only category that benefits from sophisticated sync; for everything else, simpler is better.</p>
</section>
</section>
<section id="see-also" class="level1">
<h1>See Also</h1>
<ul>
<li>Post 24, <a href="https://focusonr.org/posts/24-setupdotfilesongithub/">Creating a GitHub Dotfiles Repository</a> — the prerequisite. This post is its sequel.</li>
<li>Post 65, <a href="https://focusonr.org/posts/65-multi-laptop-bootstrap/">Multi-Laptop Bootstrap: Dotfiles Repository</a> — the next post in this series; implements the dotfiles layer with <code>install.sh</code>, <code>Makefile</code>, and a sensitive-files inventory.</li>
<li>Post 66, <a href="https://focusonr.org/posts/66-unix-pass-setup/">Setting Up pass, the Unix Password Manager</a> — covers GPG key generation, pass initialisation, and migrating credentials identified in this post’s Layer 1 inventory.</li>
<li>Post 67, <a href="https://focusonr.org/posts/67-multi-laptop-security/">Multi-Laptop Security: Hardening the Bootstrap</a> — a security audit of the full infrastructure established across posts 64-66.</li>
<li>Post 20, <a href="https://focusonr.org/posts/20-researchbackupsystem/">Setting Up a Comprehensive Research Backup System on macOS</a> — three-tier backup architecture; most of the relevant launchd-plist patterns originate there.</li>
<li>Post 22, <a href="https://focusonr.org/posts/22-serversetupawscli/">Launching AWS EC2 Instances with Bash Scripts and the AWS CLI</a> — the worked example of <code>__USER__</code> substitution and CLI-driven deployment.</li>
<li><code>MULTI_LAPTOP_PLAN.md</code> (in the dotfiles repository) — the live action plan tracking this migration, with done-marks and effort estimates for each step. <code>MIGRATION_NOTES.md</code> in the same repository captures what was copied and what was deliberately excluded during the Layer 1 move.</li>
<li><a href="https://docs.syncthing.net/">Syncthing documentation</a> — peer-to-peer sync, the most-recommended Dropbox replacement for project content.</li>
<li><a href="https://atuin.sh/">Atuin</a> — the cleanest answer to ‘I want my shell history across machines’; an example of append-aware sync done right.</li>
<li><a href="https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html">The XDG Base Directory Specification</a> — the underlying convention that determines where each XDG-aware tool actually puts its files; relevant when choosing between <code>~/.cache</code>, <code>~/.local/share</code>, and <code>~/.local/state</code>.</li>
</ul>
</section>
<section id="reproducibility" class="level1">
<h1>Reproducibility</h1>
<p>Tested on:</p>
<table class="caption-top table">
<thead>
<tr class="header">
<th>Component</th>
<th>Version</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>OS</td>
<td>macOS 15.4 (Sequoia)</td>
</tr>
<tr class="even">
<td>Shell</td>
<td>zsh 5.9</td>
</tr>
<tr class="odd">
<td>git</td>
<td>2.45</td>
</tr>
<tr class="even">
<td>rclone</td>
<td>1.68</td>
</tr>
<tr class="odd">
<td>Syncthing</td>
<td>1.27</td>
</tr>
<tr class="even">
<td>flock (homebrew)</td>
<td>2.40</td>
</tr>
<tr class="odd">
<td>Date verified</td>
<td>2026-05-07</td>
</tr>
</tbody>
</table>
<p>The decision worksheet is a generic Quarto document and does not depend on any particular tool versions; the table above captures the environment in which the framework was developed and validated.</p>
</section>
<section id="lets-connect" class="level1">
<h1>Let’s Connect</h1>
<p>I would enjoy hearing from you if:</p>
<ul>
<li>You have made the same migration with different sync-mechanism choices and want to compare notes.</li>
<li>You see a gotcha I missed, especially in the append-only-history layer.</li>
<li>You use Linux or Windows and want to extend the worksheet to your environment.</li>
<li>You just want to say hello and connect.</li>
</ul>
<hr>
<p><em>Rendered on 2026-05-08 at 06:10 PDT.</em><br> <em>Source: ~/Dropbox/prj/qblog/posts/64-migrating-off-dropbox/migrating-off-dropbox/analysis/report/index.qmd</em></p>
<!-- ============================================================================
PRE-PUBLISH QA CHECKLIST: verify each item BEFORE setting draft: false

This is the verification pass that mirrors the AUTHOR PROVIDES list at the
top of this file. If anything below is unchecked, the post is not ready.

[ ] YAML
    [ ] title, subtitle, date, categories, description, image filled in
    [x] document-type: 'blog'
    [x] draft: false

[ ] Narrative complete (no remaining `[bracketed]` placeholders)
    [ ] grep '\[' index.qmd returns only legitimate code/links

[ ] Configuration artifacts present and tested
    [ ] Full config file shown verbatim, not snippets
    [ ] Install commands tested on a clean machine (or VM)
    [ ] Verification commands produce the documented output
    [ ] Uninstall / rollback steps documented

[ ] Things to Watch Out For
    [ ] At least 5 gotchas listed (setup posts require this)
    [ ] Each gotcha has both a symptom AND a fix

[ ] Visual design
    [ ] 1 hero image (width=80%) immediately after AUTHOR PROVIDES block
    [ ] Exactly 3 ambiance images ({.img-fluid} only, no width=NN%,
        no fig-align): after Objectives, after Configuration, before
        Lessons Learnt
    [ ] Hero and ambiance captions describe the actual image (not a
        placeholder description)
    [ ] No hand-coded "Download PDF" link in body. The site auto-injects
        one via _includes/after-body.html for any post with format: pdf
        in YAML.
    [ ] media/images/README.md attributes every image

[ ] Content quality
    [ ] Learner voice: author positioned as peer, not expert
    [ ] Zero emoji anywhere (narrative, comments, captions)
    [ ] Zero em dashes (forbidden per house style; use parens or commas)
    [ ] Single quotes preferred over double quotes in prose
    [ ] Each command and config setting interpreted in plain language

[ ] Reproducibility
    [ ] Version matrix table filled in (OS, tool, dependencies, date)
    [ ] Config files committed under analysis/configs/
    [ ] Install script (if provided) is runnable on a clean machine

[ ] Render verification
    [x] quarto render index.qmd produces clean HTML with no warnings
    [x] Hero image displays in /blog/ listing card
    [ ] Internal links resolve
============================================================================ -->
<section id="related-posts-in-this-cluster" class="level2">
<h2 class="anchored" data-anchor-id="related-posts-in-this-cluster">Related posts in this cluster</h2>
<p>This post is part of the <em>Security, Backup, and Sync</em> series. Recommended reading order:</p>
<ol type="1">
<li>Post 31: <a href="../31-sec-three-tier-backup-architecture/">Research Backup Architecture</a></li>
<li><strong>Post 32: Migrating Off Dropbox: Beyond Dotfiles</strong> (this post)</li>
<li>Post 33: <a href="../33-sec-pass-password-manager/">Setting Up pass: a Unix Password Manager</a></li>
<li>Post 34: <a href="../34-sec-aws-and-pass-secrets/">Secrets Management for the Workflow Construct</a></li>
<li>Post 35: <a href="../35-sec-multi-laptop-threat-model/">Security Foundations for a Multi-Laptop Research Cluster</a></li>
</ol>


</section>
</section>

 ]]></description>
  <category>workflow</category>
  <category>sync</category>
  <category>migration</category>
  <category>dropbox</category>
  <guid>https://rgtlab.org/posts/sec-dropbox-to-portable-sync/</guid>
  <pubDate>Thu, 07 May 2026 07:00:00 GMT</pubDate>
  <media:content url="https://rgtlab.org/posts/sec-dropbox-to-portable-sync/media/images/hero.png" medium="image" type="image/png" height="80" width="144"/>
</item>
<item>
  <title>A tiered CI strategy for zzcollab research compendia</title>
  <dc:creator>Ronald G. Thomas</dc:creator>
  <link>https://rgtlab.org/posts/zc-tiered-ci-strategy/</link>
  <description><![CDATA[ 




<!-- ============================================================================
AUTHOR PROVIDES — concrete inputs the blogger must supply

Every item below maps to a placeholder in the body. Fill in this checklist
FIRST, then paste your answers into the matching `[bracketed]` slots. Do not
publish until every box is ticked.

YAML FRONT MATTER (lines 1-20)
  [x] title           — '[Tool]' replaced with actual tool name, < 70 chars
  [x] subtitle        — one-line elaboration of what the post sets up
  [x] date            — YYYY-MM-DD of intended publication
  [x] categories      — include `setup`, the tool name, and host OS
  [x] description     — 1-2 sentences for blog listing card
  [x] image           — path to hero image (relative to this .qmd)
  [x] draft: false    — flip when ready

NARRATIVE INPUTS
  [x] Hook sentence   — 'I didn't know how to configure [tool] until [trigger]'
  [x] Pain point      — the specific friction the tool removes
  [x] Motivations     — exactly 4-5 bullets (workflow gap, frustration, curiosity, learning)
  [x] Objectives      — exactly 4 numbered, verifiable end states
  [x] What is [Tool]? — 1-sentence definition + 1 analogy + 1 concrete example
  [x] Daily-workflow paragraph — how this tool fits into your routine after setup
  [x] Things to Watch Out For — 5-7 gotchas (required; setup posts need this)
  [x] Lessons Learnt  — 4 conceptual + 4 technical + 4 gotcha bullets
  [x] Limitations     — 4-6 bullets (what this setup does NOT solve)
  [x] Opportunities for Improvement — 5-6 numbered next-step extensions
  [x] Wrapping Up     — 2-3 paragraph conclusion + 'Main takeaways'
  [x] See Also        — 3-5 links (official docs + 1-2 related blog posts)

CONFIGURATION DELIVERABLES (real, working artifacts, not snippets)
  [x] Prerequisites list — OS version, hardware, prior knowledge, dependent tools
  [x] Installation block — exact bash commands, tested on a clean machine
  [x] Complete config file(s) — verbatim, copy-pasteable, with inline comments
        Place under: analysis/configs/<filename>
  [x] Verification commands — `<tool> --version` plus 1-2 functional smoke tests
  [ ] Optional keybinding/command reference table — markdown table for daily use
  [x] Uninstall / rollback steps — how the reader undoes what you just did
  [ ] Optional appendices — credential setup, sample session, teardown procedure

VERSION MATRIX (Reproducibility section)
  [x] OS + version tested        (e.g., macOS 15.4, Ubuntu 24.04)
  [x] Tool version pinned        (e.g., aws-cli 2.15)
  [x] Dependency versions        (e.g., homebrew 4.2, jq 1.7)
  [x] Date of last verification

IMAGES (4 total: hero + 3 ambiance, all in media/images/)
  [x] Hero image (80% width)        — workspace/tool logo, sets tone
  [x] Ambiance image 1 (100% width) — after Objectives (workspace shot)
  [x] Ambiance image 2 (100% width) — after Configuration (terminal/editor)
  [x] Ambiance image 3 (100% width) — before Lessons Learnt (workflow scene)
  [x] media/images/README.md        — sources + attribution for every image

CONTACT & METADATA
  [x] Author name in YAML matches site author
  [ ] Social links in 'Let's Connect' updated (or removed)
  [x] Giscus comments enabled (inherited from _quarto.yml)

============================================================================ -->
<!-- ============================================================================
HERO IMAGE
============================================================================ -->
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/zc-tiered-ci-strategy/media/images/hero.png" class="img-fluid figure-img" style="width:80.0%"></p>
<figcaption>A layered set of frosted glass panels stacked at increasing depth, each carrying a fainter ink mark, evoking the verification tiers a CI workflow stacks between a commit and a deploy.</figcaption>
</figure>
</div>
<p><em>Three independent verification tiers, each with its own failure mode and its own definition of ‘passing’.</em></p>
<div class="callout callout-style-default callout-warning callout-titled" title="Correction -- May 2026">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon"></i>
</div>
<div class="callout-title-container flex-fill">
Correction – May 2026
</div>
</div>
<div class="callout-body-container callout-body">
<p>Two recommendations in this post were superseded by findings from multi-repo deployment in May 2026. Both are annotated inline below at the relevant sections.</p>
<p><strong><code>r-lib/actions/setup-renv@v2</code> fails on zzcollab research compendia.</strong> All zzcollab compendia include an <code>.Rprofile</code> gate that suppresses renv activation outside a Docker container (<code>ZZCOLLAB_CONTAINER=true</code> must be set). When <code>setup-renv@v2</code> calls <code>renv::restore()</code> on a host runner, the gate fires, renv declines to activate, and no packages are installed. The CI job either silently passes (nothing installed, nothing verified) or fails with misleading errors. The corrected approach for <code>r-package.yml</code> is <code>r-lib/actions/setup-r-dependencies@v2</code>, which reads <code>DESCRIPTION</code> directly and never invokes renv.</p>
<p><strong><code>renv::status()$synchronized</code> is an unstable internal field.</strong> After a renv version downgrade during <code>renv::restore()</code> (for example, from 1.2.3 to 1.2.2), the field returns <code>FALSE</code> regardless of actual synchronisation state, producing spurious CI failures. The <code>quit(status = 1)</code> guard built on this field should be removed. Relying on <code>renv::restore()</code>’s exit code is sufficient.</p>
<p>The current working architecture (<code>r-package.yml</code> v2.7.0) is documented in <a href="../../69-zzc-github-workflows/zzc-github-workflows/index.html">Post 69: zzcollab GitHub Actions workflows</a>.</p>
</div>
</div>
<section id="introduction" class="level1">
<h1>Introduction</h1>
<p>I did not realise how much my CI was lying to me until I added a single explicit failure check and watched five ‘passing’ projects turn honestly red. The framework’s default workflow for the zzcollab ecosystem (a Docker-plus-renv pattern) had been showing green checkmarks across the projects I cared about, and I had been trusting those checkmarks the way one trusts the brake light on a car. The revelation was that some of the brake lights were on green because the bulb was burnt out, not because anything had been verified.</p>
<p>The discovery happened almost by accident. I was migrating one research compendium to a tighter CI pattern (<code>r-lib/actions/setup-renv@v2</code> plus an explicit <code>quit(status = 1)</code> when <code>renv::status()</code> reports inconsistency) and the first run failed. Reading the log I saw a table titled <code>The following package(s) are in an inconsistent state:</code> listing dozens of packages installed and used by source code but never recorded in the lockfile. The previous CI had not been catching this; the new check did. When I extended the migration to four more repositories the same shape of finding appeared in three of them, plus two distinct categories of project-side regression that had been similarly invisible.</p>
<p>This post walks through the migration: a tiered CI model with one template per zzcollab workspace type, an explicit synchronisation gate for lockfiles, and a strategy for getting the framework out of the business of reporting fictional health. The companion <a href="../docs/ci-strategy-tiered-model.pdf">white paper</a> (mirrored in the zzcollab repository) records the analytic detail; this post is the procedural distillation.</p>
</section>
<section id="motivations" class="level1">
<h1>Motivations</h1>
<ul>
<li><strong>Green checkmarks were structurally misleading.</strong> A workflow that prints findings without erroring on them looks identical, in GitHub’s UI, to a workflow that found nothing wrong.</li>
<li><strong>The framework’s single CI default conflated four workspace types.</strong> Tool packages, LaTeX manuscript compendia, Quarto compendia, and Quarto blog posts have substantively different CI needs. Applying the same template to all four meant some projects were over-checked (blog posts forced through <code>R CMD   check</code>) and some were under-checked (compendia whose render was never tested).</li>
<li><strong>Lockfile drift was the dominant silent regression.</strong> Three of five projects in the multi-repo verification had <code>renv.lock</code> pinning many fewer packages than their source actually used. The previous CI did not detect this; researchers were running in environments their lockfiles could not reproduce.</li>
<li><strong>Posit binary identifiers age out of the registry.</strong> Lockfiles that pin specific Posit Package Manager binary builds (the <code>-N</code> suffix) eventually fail when those builds are retired from the snapshot. This pattern recurred across two unrelated repos and two different packages (Rcpp 1.1.1-1, S7 0.2.1-1).</li>
<li><strong>The default R version is not stable.</strong> <code>setup-r@v2</code>’s <code>r-version: 'release'</code> resolves to whatever is current; lockfiles pin a specific older version; the mismatch surfaces as opaque compilation errors against the wrong header set.</li>
</ul>
</section>
<section id="objectives" class="level1">
<h1>Objectives</h1>
<p>By the end of this migration, the zzcollab project will have:</p>
<ol type="1">
<li><strong>A workspace-type-appropriate CI template</strong> chosen from the four-row typology. Tool packages get one shape; the three compendium variants get tiered models with the right Tier 3 renderer.</li>
<li><strong>A Tier 1 lockfile-validation job that fails informatively</strong> when <code>renv.lock</code> is out of sync with declared dependencies or actually-used source-code references.</li>
<li><strong>R-version pinning sourced from the lockfile</strong> (<code>r-version: 'renv'</code>) so the CI environment matches what the project’s developers run locally.</li>
<li><strong>Posit Package Manager URLs that resolve to current binaries</strong>, either via date-pinned URLs in <code>renv.lock</code> or via the <code>RENV_CONFIG_REPOS_OVERRIDE</code> env var that <code>setup-renv@v2</code> sets automatically.</li>
</ol>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/zc-tiered-ci-strategy/media/images/ambiance1.png" class="img-fluid figure-img"></p>
<figcaption>A coffee setting in a quiet workspace, the morning’s first mug poured before the day’s verification work begins. Placeholder imagery; topic-specific replacement pending.</figcaption>
</figure>
</div>
</section>
<section id="what-is-the-tiered-ci-model" class="level1">
<h1>What is the tiered CI model?</h1>
<p>A workflow design in which CI is split into independent jobs by verification purpose, each gated on a different cadence and each catching a different class of regression. The architectural analogy is a building’s life-safety inspections: the structural inspection fires once at construction, the fire-alarm test fires periodically, and the daily lock-up check fires at every closing. Each is a separate inspection with a separate sign-off.</p>
<p>In zzcollab CI specifically, three tiers cover most needs. Tier 1 (<code>validate</code>) verifies that the lockfile, the declared dependencies in <code>DESCRIPTION</code>, and the source code’s actual imports agree. Tier 2 (<code>check</code>) is the conventional <code>R CMD check</code>. Tier 3 (<code>render</code>) renders the project’s primary deliverable document (the manuscript or blog post). Workspace type determines which tiers apply: tool packages skip Tier 3, blog posts skip Tier 2, compendia run all three.</p>
</section>
<section id="prerequisites" class="level1">
<h1>Prerequisites</h1>
<ul>
<li>A zzcollab project (compendium or tool package) with a populated <code>renv.lock</code>, <code>DESCRIPTION</code>, and either an <code>analysis/report/</code> directory or a top-level <code>index.qmd</code>.</li>
<li>A GitHub repository with Actions enabled.</li>
<li>The <code>gh</code> CLI installed locally for triggering and reading runs.</li>
<li>R 4.4 or later locally for <code>renv::snapshot()</code> and <code>renv::status()</code> invocations.</li>
<li>About one hour of attention per repository for the first migration; subsequent migrations take 10-15 minutes once the patterns are in hand.</li>
</ul>
</section>
<section id="migrating-an-existing-zzcollab-project" class="level1">
<h1>Migrating an existing zzcollab project</h1>
<p>The migration is the same five steps for every workspace type; only the workflow contents differ. Pick one repository to start with and walk it end to end before applying the pattern to others.</p>
<section id="step-1-identify-the-workspace-type" class="level2">
<h2 class="anchored" data-anchor-id="step-1-identify-the-workspace-type">Step 1: Identify the workspace type</h2>
<p>Inspect the project’s deliverable to choose one of four templates:</p>
<ul>
<li><code>analysis/report/report.Rmd</code> exists with <code>output: pdf_document</code> in the YAML header: <strong>LaTeX manuscript compendium</strong>.</li>
<li><code>analysis/report/index.qmd</code> (or top-level <code>index.qmd</code>) has <code>document-type: "blog"</code> in the YAML header, or the project lives inside a Quarto blog tree: <strong>Quarto blog post</strong>.</li>
<li><code>analysis/report/index.qmd</code> (or <code>report.qmd</code>) without the blog marker, targeting HTML or PDF via Quarto: <strong>Quarto analysis compendium</strong>.</li>
<li>None of the above; only <code>R/</code>, <code>tests/</code>, <code>man/</code> populated: <strong>tool package</strong>.</li>
</ul>
<p>File presence alone is unreliable for this decision. zzcollab scaffolds the same R-package skeleton (DESCRIPTION, NAMESPACE, R/, tests/) under every paradigm, so the discriminating signal is the YAML header of the primary document, not the directory structure.</p>
</section>
<section id="step-2-replace-the-workflow" class="level2">
<h2 class="anchored" data-anchor-id="step-2-replace-the-workflow">Step 2: Replace the workflow</h2>
<p>The four templates are recorded in the <a href="../docs/ci-strategy-tiered-model.pdf">white paper</a> and reproduced in <code>analysis/configs/</code> in this companion compendium. The shape common to all four is:</p>
<div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode yaml code-with-copy"><code class="sourceCode yaml"><span id="cb1-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">name</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> &lt;Workflow name per workspace&gt;</span></span>
<span id="cb1-2"></span>
<span id="cb1-3"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">on</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span></span>
<span id="cb1-4"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">  </span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">push</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span></span>
<span id="cb1-5"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">    </span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">branches</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> </span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">[</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> main</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">,</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> master </span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">]</span></span>
<span id="cb1-6"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">  </span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">pull_request</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span></span>
<span id="cb1-7"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">    </span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">branches</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> </span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">[</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> main</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">,</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> master </span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">]</span></span>
<span id="cb1-8"></span>
<span id="cb1-9"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">jobs</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span></span>
<span id="cb1-10"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">  </span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">validate</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span></span>
<span id="cb1-11"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">    </span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">runs-on</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> ubuntu-latest</span></span>
<span id="cb1-12"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">    </span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">env</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span></span>
<span id="cb1-13"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">      </span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">GITHUB_PAT</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> ${{ secrets.GITHUB_TOKEN }}</span></span>
<span id="cb1-14"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">    </span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">steps</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span></span>
<span id="cb1-15"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">      </span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">-</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> </span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">uses</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> actions/checkout@v4</span></span>
<span id="cb1-16"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">      </span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">-</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> </span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">uses</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> r-lib/actions/setup-r@v2</span></span>
<span id="cb1-17"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">        </span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">with</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span></span>
<span id="cb1-18"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">          </span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">use-public-rspm</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> </span><span class="ch" style="color: #20794D;
background-color: null;
font-style: inherit;">true</span></span>
<span id="cb1-19"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">          </span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">r-version</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> </span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'renv'</span></span>
<span id="cb1-20"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">      </span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">-</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> </span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">uses</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> r-lib/actions/setup-renv@v2</span></span>
<span id="cb1-21"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">      </span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">-</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> </span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">name</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> Report renv status (fail on inconsistency)</span></span>
<span id="cb1-22"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">        </span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">shell</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> Rscript {0}</span></span>
<span id="cb1-23"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">        run</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">: </span><span class="ch" style="color: #20794D;
background-color: null;
font-style: inherit;">|</span></span>
<span id="cb1-24">          status &lt;- renv::status()</span>
<span id="cb1-25">          if (!isTRUE(status$synchronized)) {</span>
<span id="cb1-26">            cat("renv reports the project is not synchronized.\n",</span>
<span id="cb1-27">                "Run renv::snapshot() locally and commit ",</span>
<span id="cb1-28">                "the updated renv.lock.\n",</span>
<span id="cb1-29">                sep = "")</span>
<span id="cb1-30">            quit(status = 1)</span>
<span id="cb1-31">          }</span>
<span id="cb1-32">          cat("renv: lockfile, library, and source are synchronized.\n")</span></code></pre></div>
<div class="callout callout-style-default callout-warning callout-titled" title="Correction (May 2026)">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon"></i>
</div>
<div class="callout-title-container flex-fill">
Correction (May 2026)
</div>
</div>
<div class="callout-body-container callout-body">
<p><code>r-lib/actions/setup-renv@v2</code> does not work on zzcollab research compendia. The zzcollab <code>.Rprofile</code> gate suppresses renv outside a Docker container (<code>ZZCOLLAB_CONTAINER=true</code>), so <code>setup-renv@v2</code> cannot install the lockfile on a host runner. The corrected <code>r-package.yml</code> template uses <code>r-lib/actions/setup-r-dependencies@v2</code> (reads <code>DESCRIPTION</code>, no renv involvement) together with <code>r-lib/actions/check-r-package@v2</code>. See <a href="../../69-zzc-github-workflows/zzc-github-workflows/index.html">Post 69</a> for the complete v2.7.0 template.</p>
</div>
</div>
<p>Three points warrant emphasis. First, <code>r-version: 'renv'</code> reads the R version from the lockfile rather than defaulting to whatever <code>release</code> happens to be. Second, the explicit <code>quit(status = 1)</code> on <code>!isTRUE(status$synchronized)</code> is what turns the silent print into a visible failure. Third, the workflow does not run a Docker container; <code>setup-renv@v2</code> handles renv installation, lockfile restore, and GitHub Actions caching itself. The Docker container remains useful for local development reproducibility but is not needed in CI.</p>
</section>
<section id="step-3-push-and-observe-failures" class="level2">
<h2 class="anchored" data-anchor-id="step-3-push-and-observe-failures">Step 3: Push and observe failures</h2>
<p>Commit and push the new workflow. The first run almost certainly fails. This is expected and informative; the failure mode indicates which class of project-side regression is present.</p>
<div class="sourceCode" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb2-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> add .github/workflows/</span>
<span id="cb2-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> commit <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-m</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Migrate to new-generation tiered CI"</span></span>
<span id="cb2-3"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> push origin main</span>
<span id="cb2-4"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">gh</span> run list <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--limit</span> 5</span></code></pre></div>
<p>Examine the failed run with <code>gh run view &lt;id&gt; --log-failed</code>. The failures fall into a small number of categories.</p>
</section>
<section id="step-4-address-project-side-findings" class="level2">
<h2 class="anchored" data-anchor-id="step-4-address-project-side-findings">Step 4: Address project-side findings</h2>
<p>For each failure category, the fix is project-side, not CI-side. Section ‘Things to Watch Out For’ below catalogues the patterns observed across the multi-repo verification and the action each implies. Once the project-side issues are repaired, the same workflow will pass cleanly.</p>
</section>
<section id="step-5-verify-and-propagate" class="level2">
<h2 class="anchored" data-anchor-id="step-5-verify-and-propagate">Step 5: Verify and propagate</h2>
<p>Once one repository is green, apply the same workflow template (adjusted for workspace type) to the other repositories in the same project family. The mechanical work shrinks dramatically: copy the YAML, push, observe, fix project-side issues that surface.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/zc-tiered-ci-strategy/media/images/ambiance2.png" class="img-fluid figure-img"></p>
<figcaption>A coffee scene mid-pour, the kind of small precision the migration above is asking of CI. Placeholder imagery; topic-specific replacement pending.</figcaption>
</figure>
</div>
</section>
</section>
<section id="things-to-watch-out-for" class="level1">
<h1>Things to Watch Out For</h1>
<p>Six recurring patterns to expect during this migration, each with a specific symptom and a specific fix. The first three were observed directly in the five-repository verification; the remaining three are documented in the companion white paper from earlier diagnostic work. None of these are CI-infrastructure noise; all are project-side regressions that the previous CI pattern was masking.</p>
<p><strong>Symptom 1: <code>renv::status()</code> lists packages in ‘inconsistent state’ but the workflow shows green.</strong> This was the founding discovery. <code>renv::status()</code> prints findings and returns a list; it does not raise an error. Without an explicit <code>quit(status = 1)</code> on <code>!isTRUE(status$synchronized)</code>, the workflow exits 0 and GitHub marks the run successful regardless of how much drift the log records. <strong>Fix:</strong> the explicit guard shown in Step 2 above.</p>
<div class="callout callout-style-default callout-warning callout-titled" title="Correction (May 2026)">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon"></i>
</div>
<div class="callout-title-container flex-fill">
Correction (May 2026)
</div>
</div>
<div class="callout-body-container callout-body">
<p>The <code>renv::status()$synchronized</code> field is not a stable API. After a renv version downgrade during <code>renv::restore()</code> (observed when the lockfile pins renv 1.2.2 but the host runner installs 1.2.3 first, then downgrades), the field returns <code>FALSE</code> regardless of actual synchronisation state. The <code>quit(status = 1)</code> guard therefore produces spurious CI failures on an otherwise clean project. Relying on <code>renv::restore()</code>’s own exit code, and omitting the <code>renv::status()</code> check entirely, is the more robust pattern.</p>
</div>
</div>
<p><strong>Symptom 2: ‘package(s) are in an inconsistent state, installed: y, recorded: n, used: y’.</strong> The lockfile is incomplete. Source code references packages that the lockfile does not record; those packages are present in the project library because something (usually <code>setup-renv@v2</code>’s aggressive transitive resolution, or a prior <code>renv::install()</code>) put them there. <strong>Fix:</strong> locally, <code>renv::snapshot()</code> to capture the actual library, then commit the updated <code>renv.lock</code>. The CI should then go green honestly.</p>
<p><strong>Symptom 3: <code>Error: failed to install '&lt;package&gt;'</code>, where the version has a <code>-N</code> suffix (<code>Rcpp 1.1.1-1</code>, <code>S7 0.2.1-1</code>).</strong> Posit Package Manager has retired the specific binary build the lockfile pins. <strong>Fix:</strong> locally, refresh the lockfile against the current Posit URL via <code>renv::snapshot()</code>, or pin <code>R$Repositories[0].URL</code> in <code>renv.lock</code> to a date-stamped Posit URL with the OS segment (<code>__linux__/noble/&lt;date&gt;</code>) so binaries are reproducible from a fixed snapshot.</p>
<p><strong>Symptom 4: Compilation errors against R headers, e.g. <code>R_NamespaceRegistry was not declared</code> or <code>R_ext/PrtUtil.h: No such file or directory</code>.</strong> The CI is running a different R version than the lockfile pins. The default <code>r-version: 'release'</code> resolves to the current release at run time, which drifts forward as new R versions ship. <strong>Fix:</strong> add <code>r-version: 'renv'</code> to <code>setup-r@v2</code>’s <code>with:</code> block. This reads the lockfile and matches the pinned version exactly.</p>
<p><strong>Symptom 5: <code>R CMD check found WARNINGs</code> for things like ‘Undocumented code objects’ or ‘Consider adding importFrom’.</strong> The previous CI used <code>error_on = 'error'</code> in its hand-rolled <code>rcmdcheck</code> call, which let WARNINGs pass. <code>r-lib/actions/check-r-package@v2</code> errors on WARNINGs by default. <strong>Fix:</strong> address the warnings (documentation gaps, NAMESPACE imports, examples). These are real package-quality regressions; the fact that CI surfaced them is the point.</p>
<p><strong>Symptom 6: A workflow with the right structure but fires on every push, including pushes that change only the manuscript.</strong> The render tier is expensive (5-15 minutes) and should not run on every push. <strong>Fix:</strong> path-filter the render workflow to trigger only on changes under <code>analysis/</code>, <code>R/</code>, <code>DESCRIPTION</code>, or <code>renv.lock</code>. The split between <code>r-package.yml</code> (always-on validation) and <code>render-report.yml</code> (path-filtered render) is the standard zzcollab pattern.</p>
<p><strong>Symptom 7 (Docker-anchored compendia only): <code>renv.lock</code> after <code>renv::snapshot()</code> records macOS binary fingerprints instead of Linux binaries, and CI or a new collaborator’s <code>renv::restore()</code> installs different package builds than the developer tested.</strong> The lockfile was snapshotted on the developer’s macOS host. <code>renv::snapshot()</code> captures the installed library of the running session; if that session is macOS R, the lockfile records macOS binary hashes that a Linux runner cannot reproduce. The lockfile then describes an environment that only the original developer on that specific machine can fully reproduce, which violates the core zzcollab reproducibility guarantee.</p>
<p>For Docker-anchored compendia the answer is unambiguous: snapshot inside the Docker container. The reasoning rests on a five-pillar coherence check:</p>
<ul>
<li><strong>Dockerfile</strong> – defines the computational environment (R version, OS, system libraries).</li>
<li><strong><code>renv.lock</code></strong> – must record packages that install cleanly inside that environment, meaning Linux binaries against the container’s system libraries.</li>
<li><strong><code>.Rprofile</code></strong> – bootstraps renv on every session start.</li>
<li><strong><code>R/</code> and <code>analysis/</code></strong> – the source code.</li>
<li><strong>Raw data</strong> – the fixed input.</li>
</ul>
<p>All five must cohere around the single environment the Dockerfile defines. The Dockerfile is the anchor; the lockfile follows it. A lockfile snapshotted outside the container has slipped its anchor.</p>
<p><strong>Fix:</strong> enter the container with <code>make r</code>, ensure all required packages are installed (<code>renv::restore()</code> if the library is not yet populated), then run <code>renv::snapshot()</code>. The lockfile will then record Linux binary fingerprints that match both the container environment and the Ubuntu CI runner.</p>
<p><strong>Consequence for CI design:</strong> for a Docker-anchored compendium, running CI inside the container (as the old-generation <code>rocker/tidyverse</code>-based workflow does) is the more principled choice, not a legacy pattern to be discarded. The new-generation <code>setup-renv@v2</code> pattern trades the container’s reproducibility guarantee for CI simplicity – a trade worth making for tool packages and pure-renv projects, but one to evaluate carefully for a compendium whose five pillars are anchored to a Dockerfile. The recommended next step when migrating a Docker-anchored compendium is therefore not to discard the container-based CI but to tighten it: add the explicit <code>renv::status()</code> synchronisation guard (Symptom 1) inside the container job rather than switching to a containerless workflow.</p>
<p><strong>Symptom 8 (Docker-anchored compendia with mounted TinyTeX): <code>sh: 1: xelatex: Permission denied</code> even after <code>chmod -R a+rx $HOME/.TinyTeX</code> on the host runner.</strong> This one has two compounding causes that must be understood separately.</p>
<p><em>Cause A: <code>chmod -R</code> on Linux does not follow file symlinks.</em> In a TinyTeX installation, <code>~/.TinyTeX/bin/x86_64-linux/xelatex</code> is a symlink pointing into <code>~/.TinyTeX/texmf-dist/bin/x86_64-linux/</code>. Running <code>chmod -R a+rx $HOME/.TinyTeX/bin</code> changes the mode of the symlinks themselves, not the targets. The actual ELF binaries in <code>texmf-dist/</code> may remain non-executable. Expanding the scope to <code>chmod -R a+rx $HOME/.TinyTeX</code> does recurse into <code>texmf-dist/</code> (because <code>chmod -R</code> does follow directory symlinks), so it addresses Cause A – but Cause B still blocks execution.</p>
<p><em>Cause B: <code>/root</code> itself is <code>700</code>.</em> The strategy of mounting TinyTeX into the container as <code>-v $HOME/.TinyTeX:/root/.TinyTeX</code> places the tree under <code>/root</code> inside the container. A zzcollab Dockerfile ends with <code>USER analyst</code> (or equivalent non-root username), so the container process runs without root privileges. The Linux kernel checks directory execute-permission at each component of a path; <code>/root</code> being <code>700</code> means that even if every file inside <code>/root/.TinyTeX/</code> has world-execute permission, the non-root user cannot traverse into it. The error <code>sh: 1: xelatex: Permission denied</code> reflects the kernel blocking path traversal, not a missing execute bit on the binary.</p>
<p><strong>Fix:</strong> mount TinyTeX to a world-accessible path rather than under <code>/root</code>. Replace the <code>-v</code> and <code>-e PATH</code> arguments in the <code>docker run</code> call:</p>
<div class="sourceCode" id="cb3" style="background: #f1f3f5;"><pre class="sourceCode yaml code-with-copy"><code class="sourceCode yaml"><span id="cb3-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Before (fails for non-root container user)</span></span>
<span id="cb3-2"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-v $HOME/.TinyTeX:/root/.TinyTeX \</span></span>
<span id="cb3-3"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-e PATH="/root/.TinyTeX/bin/x86_64-linux:$PATH" \</span></span>
<span id="cb3-4"></span>
<span id="cb3-5"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># After (world-accessible mount point)</span></span>
<span id="cb3-6"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-v $HOME/.TinyTeX:/opt/tinytex \</span></span>
<span id="cb3-7"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-e PATH="/opt/tinytex/bin/x86_64-linux:$PATH" \</span></span></code></pre></div>
<p><code>/opt</code> is owned by root with permissions <code>755</code>, so all users can traverse it. The <code>chmod -R a+rx $HOME/.TinyTeX</code> step on the host runner is still needed to ensure the files themselves are world-readable and executable. The two lines together – chmod on the host, mount to <code>/opt/tinytex</code> in the container – resolve both causes.</p>
<p><strong>Symptom 9 (cross-repo portability): A <code>render-report.yml</code> that works for one compendium fails for another because one uses <code>rocker/verse</code> (TinyTeX built-in, runs as root) and the other uses <code>rocker/tidyverse</code> (no LaTeX, non-root user).</strong> Projects in the same portfolio may have different Dockerfile base images. Applying a workflow that was fixed for one base to another without adjustment reproduces the original failure.</p>
<p>The <code>/opt/tinytex</code> host-mount pattern from Symptom 8 generalises to both cases and can serve as the single standard template across all compendium repos:</p>
<ul>
<li><em><code>rocker/verse</code>-based repos (root user):</em> The image already ships TinyTeX at <code>/root/.TinyTeX</code> with its bin directory on PATH. The host mount at <code>/opt/tinytex</code> is redundant but harmless: <code>xelatex</code> is found in the container’s own PATH entry before the explicit <code>-e PATH</code> prefix is reached. Both LaTeX sources exist simultaneously; the container’s built-in takes precedence.</li>
<li><em><code>rocker/tidyverse</code>-based repos (non-root user):</em> The image has no LaTeX. The host mount at <code>/opt/tinytex</code> is the only source of <code>xelatex</code>. Because <code>/opt</code> is world-traversable (<code>755</code>), the non-root user can access the binaries.</li>
</ul>
<p>The three-step block that achieves this generalisation:</p>
<div class="sourceCode" id="cb4" style="background: #f1f3f5;"><pre class="sourceCode yaml code-with-copy"><code class="sourceCode yaml"><span id="cb4-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">-</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> </span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">name</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> Install TinyTeX on host runner</span></span>
<span id="cb4-2"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">  </span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">uses</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> r-lib/actions/setup-tinytex@v2</span></span>
<span id="cb4-3"></span>
<span id="cb4-4"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">-</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> </span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">name</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> Make TinyTeX binaries accessible to container</span></span>
<span id="cb4-5"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">  </span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">run</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> chmod -R a+rx $HOME/.TinyTeX</span></span>
<span id="cb4-6"></span>
<span id="cb4-7"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">-</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> </span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">name</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> Restore packages and render all manuscripts</span></span>
<span id="cb4-8"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">  run</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">: </span><span class="ch" style="color: #20794D;
background-color: null;
font-style: inherit;">|</span></span>
<span id="cb4-9">    docker run --rm -e CI=true \</span>
<span id="cb4-10">      -v ${{ github.workspace }}:/project \</span>
<span id="cb4-11">      -v $HOME/.TinyTeX:/opt/tinytex \</span>
<span id="cb4-12">      -e PATH="/opt/tinytex/bin/x86_64-linux:$PATH" \</span>
<span id="cb4-13">      -w /project compendium-env \</span>
<span id="cb4-14">      Rscript -e '...'</span></code></pre></div>
<p><strong>Fix:</strong> adopt this three-step block as the standard <code>render-report.yml</code> template across all Docker-anchored compendium repos, regardless of Dockerfile base image. The <code>rocker/verse</code> case incurs a small overhead (downloading TinyTeX on the host runner, ~30 seconds) that is acceptable in exchange for a single maintainable template.</p>
<p><strong>Symptom 10 (ggplot2/patchwork version skew): <code>object 'is_ggplot' is not exported by 'namespace:ggplot2'</code> during patchwork installation, even when patchwork appears to be a current CRAN release.</strong> This is a compound failure that surfaces when the Docker base image ships ggplot2 3.5.x and the renv lockfile (or the PPM snapshot) pulls patchwork 1.3.2 or later.</p>
<p><code>patchwork 1.3.2</code> added <code>@importFrom ggplot2 is_ggplot</code> to its NAMESPACE – it imports <code>is_ggplot</code> as an exported symbol from ggplot2. <code>ggplot2 3.5.1</code> (which ships with <code>rocker/verse:4.4.2</code>) does not export <code>is_ggplot</code>; the function was introduced as an export in ggplot2 4.0.0. Installing patchwork 1.3.2 into a container that has ggplot2 3.5.1 therefore fails at the ‘prepare package for lazy loading’ step with the export error.</p>
<p>The failure looks like it belongs to zzlongplot or the project’s own code because the error appears during zzlongplot’s installation (which imports patchwork). But the root cause is one level deeper: patchwork itself cannot load against ggplot2 3.5.1.</p>
<p><strong>Fix:</strong> update ggplot2 to 4.0.x before (or alongside) installing patchwork. In the lockfile-rebuild script:</p>
<div class="sourceCode" id="cb5" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb5-1">pkgs <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">c</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"ggplot2"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"patchwork"</span>, ...)  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># include ggplot2 explicitly</span></span>
<span id="cb5-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">install.packages</span>(pkgs,</span>
<span id="cb5-3">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">repos =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"https://packagemanager.posit.co/cran/__linux__/noble/latest"</span>)</span></code></pre></div>
<p>ggplot2 4.0.x pulls in <code>S7</code> as a new dependency – add <code>S7</code> to the lockfile as part of the same rebuild. After this update, patchwork 1.3.2 loads cleanly and any package that depends on patchwork (such as zzlongplot) can be installed.</p>
<p><strong>Portfolio implication:</strong> any zzcollab compendium that uses patchwork and is based on <code>rocker/verse:4.4.2</code> (which ships ggplot2 3.5.1) will hit this failure when the lockfile is rebuilt against the current PPM snapshot. The fix – updating ggplot2 to 4.x – must be applied consistently across all affected repos during lockfile refresh.</p>
</section>
<section id="uninstall-rollback" class="level1">
<h1>Uninstall / Rollback</h1>
<p>The migration is fully reversible. The previous workflow lives in git history; restoring it requires checking out the prior version of <code>.github/workflows/r-package.yml</code> (and any companion files) from the commit before the migration:</p>
<div class="sourceCode" id="cb6" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb6-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> log <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--oneline</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--</span> .github/workflows/r-package.yml</span>
<span id="cb6-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> checkout <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span>prior-sha<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> -- .github/workflows/r-package.yml</span>
<span id="cb6-3"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> commit <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-m</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Revert to prior CI workflow"</span></span>
<span id="cb6-4"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> push</span></code></pre></div>
<p>If only the strict synchronisation check is the source of friction (for example, during a transitional period while several lockfiles are being repaired in parallel), the strict guard can be loosened to informational reporting by removing the <code>quit(status = 1)</code> line. The rest of the new generation pattern remains in place.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/zc-tiered-ci-strategy/media/images/ambiance3.png" class="img-fluid figure-img"></p>
<figcaption>An empty cup beside a closed notebook, the morning’s verification work done and the cup ready for the next pour. Placeholder imagery; topic-specific replacement pending.</figcaption>
</figure>
</div>
</section>
<section id="what-did-we-learn" class="level1">
<h1>What did we learn?</h1>
<section id="conceptual" class="level2">
<h2 class="anchored" data-anchor-id="conceptual">Conceptual</h2>
<ul>
<li><strong>CI checks for regressions, and different regressions need different checks.</strong> A workflow that runs <code>R CMD check</code> is necessary for a tool package and useless for a Quarto blog post whose deliverable is rendered HTML. The framework’s purpose determines the appropriate verification, not the framework’s default.</li>
<li><strong>A green dashboard with red findings in logs is operationally indistinguishable from an absent CI check.</strong> The visible signal is the job exit status. A check that prints evidence of regression but exits 0 is a false sense of safety, not a check.</li>
<li><strong>File presence is a noisy signal for workspace-type detection.</strong> zzcollab scaffolds the same R-package skeleton across all paradigms; the discriminating signal lives in the document YAML header (<code>document-type: "blog"</code>, <code>output: pdf_document</code>).</li>
<li><strong>The two-workflow split is an existing zzcollab pattern that should be propagated uniformly.</strong> The framework already ships <code>r-package.yml</code> and <code>render-report.yml</code> templates; three of eighteen surveyed compendium repos have both deployed. The contribution of this migration is uniform deployment, not a new design.</li>
</ul>
</section>
<section id="technical" class="level2">
<h2 class="anchored" data-anchor-id="technical">Technical</h2>
<ul>
<li><strong><code>r-lib/actions/setup-renv@v2</code></strong> handles renv installation, lockfile restore, GitHub Actions caching, and the <code>RENV_CONFIG_REPOS_OVERRIDE</code> env var in roughly twenty lines of YAML. It is the canonical R-package CI bootstrap; the Docker-in-CI pattern adds complexity that this action already resolves.</li>
<li><strong><code>r-version: 'renv'</code></strong> on <code>setup-r@v2</code> reads the R version from the lockfile. Without it, the workflow’s R version drifts forward with each new R release while the project’s pinned version stays put.</li>
<li><strong><code>renv::status()</code> returns a list with <code>synchronized = TRUE/FALSE</code>.</strong> Capture it explicitly and <code>quit(status = 1)</code> on inconsistency. The job’s exit status is what GitHub reads; nothing else matters.</li>
<li><strong><code>r-lib/actions/check-r-package@v2</code></strong> errors on WARNINGs by default. This is stricter than a hand-rolled <code>rcmdcheck::rcmdcheck(error_on = 'error')</code>. The strictness is a feature: it surfaces real package-quality regressions.</li>
</ul>
</section>
<section id="gotchas" class="level2">
<h2 class="anchored" data-anchor-id="gotchas">Gotchas</h2>
<ul>
<li><strong>The default <code>r-version</code> drifts.</strong> <code>setup-r@v2</code> resolves <code>release</code> at run time, so a workflow that worked yesterday can fail tomorrow when R 4.x.y is released. Pin to the lockfile.</li>
<li><strong>Posit binary identifiers are not stable.</strong> A lockfile pinning <code>Rcpp 1.1.1-1</code> will eventually fail when Posit retires that build. Prefer date-pinned URLs.</li>
<li><strong>The <code>.Rprofile</code> auto-restore can race the workflow.</strong> The zzcollab template fires <code>renv::restore()</code> on R session start; the workflow also calls restore. Set <code>ZZCOLLAB_AUTO_RESTORE=false</code> in the workflow <code>env:</code> to give control to the workflow.</li>
<li><strong>Stale <code>render-report.yml</code> variants exist in deployed repos.</strong> The <code>templates/.github/</code> tree (since deleted from canonical zzcollab) shipped a hardcoded <code>report.Rmd</code> matcher; projects that received that variant fail silently for any project using <code>manuscript.Rmd</code>. Re-template via <code>zzc doctor</code>.</li>
</ul>
</section>
</section>
<section id="limitations" class="level1">
<h1>Limitations</h1>
<ul>
<li><strong>Project-side fixes are still required.</strong> The new CI surfaces drift, missing documentation, and aged-out binaries; it does not repair them. Each surfaced issue requires a developer to address it.</li>
<li><strong>Posit snapshot dates expire.</strong> Date-pinned URLs eventually age out (Posit retains snapshots for roughly three to five years). Long-lived projects need periodic snapshot refreshes.</li>
<li><strong>Detection at scaffolding time is not yet automated.</strong> The decision among the four workspace templates currently requires a developer to inspect the project and pick. A future enhancement to <code>zzc analysis</code> or <code>zzc doctor</code> could automate this.</li>
<li><strong>The <code>renv::status()</code> synchronisation check assumes <code>renv.lock</code> is the source of truth.</strong> Projects that intentionally maintain a smaller lockfile (for example, a package that lists only its declared <code>Imports</code> and trusts the user’s environment for everything else) will fail the synchronisation check by design. The check is appropriate for research compendia; it may not be for some other patterns.</li>
<li><strong>The Docker container is no longer exercised by CI.</strong> Projects that rely on the Dockerfile for production deployment should consider adding a separate Docker-build verification job.</li>
</ul>
</section>
<section id="opportunities-for-improvement" class="level1">
<h1>Opportunities for Improvement</h1>
<ol type="1">
<li><strong>Auto-detect workspace type at <code>zzc analysis</code> time.</strong> Inspect the YAML headers and emit the appropriate workflow template in <code>.github/workflows/</code> automatically. This removes the manual decision in Step 1 of the migration.</li>
<li><strong>Date-pin the lockfile URL at init time.</strong> When <code>zzc</code> initialises a project, write <code>R$Repositories[0].URL</code> as <code>https://packagemanager.posit.co/cran/__linux__/noble/&lt;today&gt;</code> rather than the unqualified source URL the framework currently emits.</li>
<li><strong><code>zzc doctor</code> propagation.</strong> Extend the doctor command’s full-content workflow replacement (introduced earlier in this work) to detect workspace type and replace stale <code>render-report.yml</code> variants with the canonical <code>.qmd</code>-aware version.</li>
<li><strong>Output verification.</strong> Hash the rendered manuscript and fail Tier 3 if the hash differs from a recorded baseline. Catches regressions in figures and tables that pass <code>R CMD check</code> but produce different rendered output.</li>
<li><strong>Schedule-driven freshness checks.</strong> Add a weekly cron job that re-runs Tier 1 against the current Posit URL, surfacing binary aging-out before it blocks an active push.</li>
<li><strong>Documentation pass.</strong> The four workflow templates and the migration procedure deserve to be embedded in the canonical zzcollab user guide alongside the existing <code>CI_WORKFLOW_ARCHITECTURE.md</code> and <code>ci-workflows-whitepaper.md</code> documents.</li>
</ol>
</section>
<section id="wrapping-up" class="level1">
<h1>Wrapping Up</h1>
<p>The migration’s core contribution is making CI honest. The previous green checkmarks across these projects were structurally misleading; they reflected the absence of an error rather than the presence of verification. The new pattern’s first response on most projects is to turn red, but the red is informative and actionable, and the resulting fixes are real improvements to the projects’ reproducibility.</p>
<p>The pattern is not a new design. The two-workflow split (<code>r-package.yml</code> plus <code>render-report.yml</code>), the use of <code>r-lib/actions/setup-renv@v2</code>, and the path-filtering on the render workflow all already exist in zzcollab’s template set, applied inconsistently across deployed projects. The migration described here is the explicit deployment of those existing patterns uniformly, with the addition of a single explicit synchronisation guard that the framework’s templates had been missing.</p>
<p>The companion white paper records the analytic detail behind the choices made here, including the four-workspace typology, the verification of the synchronisation check at scale, and the classification of failure modes observed during the multi-repo deployment. Readers who want to understand why the templates have the shapes they do should consult that document; readers who want to apply the templates should follow this post.</p>
<p>In conclusion, four points merit emphasis. First, a CI check that prints findings without erroring is operationally invisible; the job’s exit status is the only signal GitHub shows. Second, four zzcollab workspace types map to four distinct CI templates (tool package, LaTeX compendium, Quarto compendium, Quarto blog post), and the document YAML header is the discriminating signal, not file presence. Third, <code>r-lib/actions/setup-renv@v2</code> plus <code>r-version: 'renv'</code> plus a one-line <code>quit(status = 1)</code> guard on <code>renv::status()$synchronized</code> covers the bulk of what an always-on validation tier needs. Fourth, failures from the new pattern point to real project-side regressions (lockfile drift, aged-out Posit binaries, R-version mismatch, missing package documentation); none are CI-infrastructure noise.</p>
<div class="callout callout-style-default callout-warning callout-titled" title="Correction to Point 3 (May 2026)">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon"></i>
</div>
<div class="callout-title-container flex-fill">
Correction to Point 3 (May 2026)
</div>
</div>
<div class="callout-body-container callout-body">
<p>Point 3 above reflects the intent of this post at time of writing, but both mechanisms named have since proved unreliable for zzcollab compendia. <code>setup-renv@v2</code> cannot install packages on a host runner because the zzcollab <code>.Rprofile</code> gate suppresses renv outside the Docker container. <code>renv::status()$synchronized</code> misfires after renv version downgrades, producing spurious failures. The corrected <code>r-package.yml</code> (v2.7.0) uses <code>setup-r-dependencies@v2</code> to install from <code>DESCRIPTION</code> and <code>check-r-package@v2</code> for validation; no renv involvement on the host runner at all. Full template and rationale in <a href="../../69-zzc-github-workflows/zzc-github-workflows/index.html">Post 69</a>. The four-workspace typology (Point 2) and the exit-status principle (Point 1) remain sound.</p>
</div>
</div>
</section>
<section id="see-also" class="level1">
<h1>See Also</h1>
<ul>
<li>The companion <a href="../docs/ci-strategy-tiered-model.pdf">white paper</a> in this compendium, which records the analytic detail behind the choices in this post.</li>
<li>The same white paper mirrored in zzcollab itself at <code>~/prj/sfw/07-zzcollab/zzcollab/docs/ci-strategy-tiered-model.md</code>, for ongoing co-evolution.</li>
<li>The <a href="https://rstudio.github.io/renv/articles/ci.html">renv documentation on continuous integration</a>, which describes the canonical <code>r-lib/actions/setup-renv@v2</code> pattern this work builds on.</li>
<li>The <a href="https://github.com/r-lib/actions">r-lib/actions repository</a>, which documents <code>setup-r</code>, <code>setup-renv</code>, <code>setup-pandoc</code>, <code>setup-tinytex</code>, and <code>check-r-package</code>.</li>
<li>Related blog posts in this series: <a href="../../14-penguins1zzcollab/penguins1zzcollab/index.html">Post 14: penguins1zzcollab</a> (the worked example whose CI verification produced Section 9.7 of the white paper); <a href="../../61-zzcollab-analysis-checklist/zzcollab-analysis-checklist/index.html">Post 61: zzcollab analysis checklist</a> (the closest-in-time companion post on zzcollab procedure).</li>
</ul>
</section>
<section id="reproducibility" class="level1">
<h1>Reproducibility</h1>
<p>Tested on the configurations below.</p>
<table class="caption-top table">
<colgroup>
<col style="width: 33%">
<col style="width: 33%">
<col style="width: 33%">
</colgroup>
<thead>
<tr class="header">
<th>Component</th>
<th>Version</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>OS (CI runner)</td>
<td>Ubuntu 24.04 (noble)</td>
<td>GitHub Actions ubuntu-latest as of May 2026</td>
</tr>
<tr class="even">
<td>OS (local)</td>
<td>macOS 15 (Darwin 25)</td>
<td>for <code>gh</code>, <code>git</code>, local renv operations</td>
</tr>
<tr class="odd">
<td>R</td>
<td>4.4.2</td>
<td>matches lockfile pins across surveyed projects</td>
</tr>
<tr class="even">
<td><code>r-lib/actions/setup-r</code></td>
<td>v2</td>
<td>with <code>r-version: 'renv'</code></td>
</tr>
<tr class="odd">
<td><code>r-lib/actions/setup-renv</code></td>
<td>v2</td>
<td>handles caching automatically</td>
</tr>
<tr class="even">
<td><code>r-lib/actions/setup-pandoc</code></td>
<td>v2</td>
<td>needed for Tier 2 and Tier 3</td>
</tr>
<tr class="odd">
<td><code>r-lib/actions/setup-tinytex</code></td>
<td>v2</td>
<td>needed for LaTeX render tier</td>
</tr>
<tr class="even">
<td><code>r-lib/actions/check-r-package</code></td>
<td>v2</td>
<td>errors on WARNINGs by default</td>
</tr>
<tr class="odd">
<td><code>quarto-dev/quarto-actions/setup</code></td>
<td>v2</td>
<td>needed for Quarto render tier</td>
</tr>
<tr class="even">
<td>Date of last verification</td>
<td>2026-05-06</td>
<td>five-repo deployment, all surfacing project-side findings</td>
</tr>
</tbody>
</table>
<p>The companion compendium ships <code>analysis/configs/r-package.yml</code>, <code>analysis/configs/render-report.yml</code>, and <code>analysis/configs/blog-render.yml</code> as the four templates referenced in Step 2. Copy the appropriate one to <code>.github/workflows/</code> in the project.</p>
</section>
<section id="lets-connect" class="level1">
<h1>Let’s Connect</h1>
<p>Comments and questions on this post are welcome via the giscus panel below. Corrections, alternative patterns, and reports of edge cases that the four-workspace typology does not cover are particularly useful: the typology was extracted from a sample of roughly twenty projects in two trees, and the natural next step is to test it against external projects whose patterns differ.</p>
<p>The companion white paper at <code>zzcollab/docs/ci-strategy-tiered-model.md</code> is the live document; it will be updated with new findings as they accumulate.</p>
<hr>
<p><em>Rendered on 2026-05-22 at 09:22 PDT.</em><br> <em>Source: ~/Dropbox/prj/qblog/posts/63-zzcollab-ci-strategy/zzcollab-ci-strategy/analysis/report/index.qmd</em></p>
<!-- ============================================================================
PRE-PUBLISH QA CHECKLIST: verify each item BEFORE setting draft: false

This is the verification pass that mirrors the AUTHOR PROVIDES list at the
top of this file. If anything below is unchecked, the post is not ready.

[x] YAML
    [x] title, subtitle, date, categories, description, image filled in
    [x] document-type: 'blog'
    [x] draft: false

[x] Narrative complete (no remaining `[bracketed]` placeholders)
    [x] grep '\[' index.qmd returns only legitimate code/links

[x] Configuration artifacts present and tested
    [x] Full config file shown verbatim, not snippets
    [x] Install commands tested on a clean machine (or VM)
    [x] Verification commands produce the documented output
    [x] Uninstall / rollback steps documented

[x] Things to Watch Out For
    [x] At least 5 gotchas listed (setup posts require this)
    [x] Each gotcha has both a symptom AND a fix

[x] Visual design
    [x] 1 hero image (width=80%) immediately after AUTHOR PROVIDES block
    [x] Exactly 3 ambiance images ({.img-fluid} only, no width=NN%,
        no fig-align): after Objectives, after Configuration, before
        Lessons Learnt
    [x] Hero and ambiance captions describe the actual image (not a
        placeholder description)
    [x] No hand-coded "Download PDF" link in body. The site auto-injects
        one via _includes/after-body.html for any post with format: pdf
        in YAML.
    [x] media/images/README.md attributes every image

[x] Content quality
    [x] Learner voice: author positioned as peer, not expert
    [x] Zero emoji anywhere (narrative, comments, captions)
    [x] Zero em dashes (forbidden per house style; use parens or commas)
    [x] Single quotes preferred over double quotes in prose
    [x] Each command and config setting interpreted in plain language

[x] Reproducibility
    [x] Version matrix table filled in (OS, tool, dependencies, date)
    [x] Config files committed under analysis/configs/
    [x] Install script (if provided) is runnable on a clean machine

[x] Render verification
    [x] quarto render index.qmd produces clean HTML with no warnings
    [x] Hero image displays in /blog/ listing card
    [x] Internal links resolve

============================================================================ -->
<section id="related-posts-in-this-cluster" class="level2">
<h2 class="anchored" data-anchor-id="related-posts-in-this-cluster">Related posts in this cluster</h2>
<p>This post is part of the <em>ZZCOLLAB Reproducible Compendia</em> series. Recommended reading order:</p>
<ol type="1">
<li>Post 01: <a href="../01-zc-quarto-compendium-intro/">Reproducible Blog Posts with ZZCOLLAB</a></li>
<li>Post 02: <a href="../02-zc-blog-post-template/">Constructing a reproducible blog post using zzcollab tools</a></li>
<li>Post 03: <a href="../03-zc-markdown-to-blog-workflow/">From Markdown to Blog Post: A ZZCOLLAB workflow</a></li>
<li>Post 04: <a href="../04-zc-share-rmd-via-docker/">Sharing R Code via Docker: R Markdown Reports</a></li>
<li>Post 05: <a href="../05-zc-analysis-initiation-checklist/">A 55-Item Initiation Checklist for zzcollab Data Analyses</a></li>
<li>Post 06: <a href="../06-zc-manuscript-report-elements/">Seven Required Elements for a zzc Manuscript report.Rmd</a></li>
<li><strong>Post 07: A tiered CI strategy for zzcollab research compendia</strong> (this post)</li>
<li>Post 08: <a href="../08-zc-github-actions-workflows/">GitHub Actions workflows for zzcollab research compendia</a></li>
</ol>


</section>
</section>

 ]]></description>
  <category>ci</category>
  <category>github-actions</category>
  <category>reproducibility</category>
  <category>zzcollab-compendia</category>
  <category>renv</category>
  <guid>https://rgtlab.org/posts/zc-tiered-ci-strategy/</guid>
  <pubDate>Wed, 06 May 2026 07:00:00 GMT</pubDate>
  <media:content url="https://rgtlab.org/posts/zc-tiered-ci-strategy/media/images/hero.png" medium="image" type="image/png" height="80" width="144"/>
</item>
<item>
  <title>Setting up OBS for Live R Coding Screencasts</title>
  <dc:creator>Ronald G. Thomas</dc:creator>
  <link>https://rgtlab.org/posts/pub-obs-r-screencasts/</link>
  <description><![CDATA[ 




<!-- ============================================================================
AUTHOR PROVIDES — concrete inputs the blogger must supply

Every item below maps to a placeholder in the body. Fill in this checklist
FIRST, then paste your answers into the matching `[bracketed]` slots. Do not
publish until every box is ticked.

YAML FRONT MATTER (lines 1-20)
  [x] title           — 'Setting up OBS for Live R Coding Screencasts'
  [x] subtitle        — one-line elaboration of what the post sets up
  [x] date            — 2026-05-02
  [x] categories      — setup, obs, youtube, screencast, r, reproducibility
  [x] description     — 1-2 sentences for blog listing card
  [x] image           — media/images/hero.png (Gemini-generated, 1600x893)
  [x] draft: false    — flip when imagery is in place

NARRATIVE INPUTS
  [x] Hook sentence   — until-moment for screencasting friction
  [x] Pain point      — the static-text limit of written tutorials
  [x] Motivations     — exactly 4-5 bullets
  [x] Objectives      — exactly 4 numbered, verifiable end states
  [x] What is OBS?    — 1-sentence definition + 1 analogy + 1 concrete example
  [x] Daily-workflow paragraph — how OBS fits the recording routine after setup
  [x] Things to Watch Out For — 5-7 gotchas
  [x] Lessons Learnt  — conceptual + technical + gotcha bullets
  [x] Limitations     — 4-6 bullets (what OBS does NOT solve)
  [x] Opportunities for Improvement — 5-6 numbered next-step extensions
  [x] Wrapping Up     — 2-3 paragraph conclusion + 'Main takeaways'
  [x] See Also        — 3-5 links

CONFIGURATION DELIVERABLES
  [x] Prerequisites list — OS, hardware, prior knowledge
  [x] Installation block — exact bash commands
  [ ] Complete config file(s) — OBS scene-collection JSON could live under
        analysis/configs/ in a future revision
  [x] Verification commands — `obs --version` plus a 30-second test recording
  [x] Optional keybinding/command reference table — recording hotkeys
  [x] Uninstall / rollback steps — how to remove OBS cleanly
  [ ] Optional appendices — RTMP server overrides, podcast-ready audio chain

VERSION MATRIX (Reproducibility section)
  [x] OS + version tested        — macOS 26.4 (Tahoe), Ubuntu 24.04 LTS
  [x] Tool version pinned        — OBS Studio 30.x, ffmpeg 7.x, Quarto 1.9
  [x] Dependency versions        — Homebrew 4.x; apt 2.x
  [x] Date of last verification  — 2026-05-02

IMAGES (4 total: hero + 3 ambiance, all in media/images/)
  [x] Hero image (80% width)        — media/images/hero.png in place
  [ ] Ambiance image 1 (100% width) — after Objectives (workspace shot)
  [ ] Ambiance image 2 (100% width) — after Configuration (terminal/editor)
  [ ] Ambiance image 3 (100% width) — before Lessons Learnt (workflow scene)
  [ ] media/images/README.md        — sources + attribution for every image

CONTACT & METADATA
  [x] Author name in YAML matches site author
  [x] Social links in 'Let's Connect' updated
  [x] Giscus comments enabled (inherited from _quarto.yml)

============================================================================ -->
<!-- ============================================================================
HERO IMAGE
============================================================================ -->
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/pub-obs-r-screencasts/media/images/hero.png" class="img-fluid figure-img" style="width:80.0%"></p>
<figcaption>A live OBS Studio session ready to capture an R analysis.</figcaption>
</figure>
</div>
<p><em>A short screencast captures the small decisions a written report leaves out.</em></p>
<section id="introduction" class="level1">
<h1>Introduction</h1>
<p>I did not appreciate how much a good screencast teaches until a colleague sent me a five-minute video of an analysis I had read about in a paper a week earlier. The paper had described the model, the data, and the result. The screencast showed the analyst pause over a missing value, change a plot scale mid-thought, and read aloud the output of <code>summary(fit)</code>. That brief exposure to the analyst’s working memory taught me more about the analysis than the paper had.</p>
<p>What I discovered, after a few false starts with proprietary tools, was that the open source community already had a complete capture pipeline waiting: OBS Studio for recording, YouTube for distribution, and a small set of recording conventions to glue them together. The pipeline costs nothing, runs on macOS, Linux, and Windows, and produces files that any future viewer can play without an account.</p>
<p>We walk through that pipeline end to end. The post covers OBS installation, scene and audio configuration suitable for an R coding session, a publication checklist for YouTube, and a worked five-minute example using the Palmer Penguins dataset that mirrors the analytical structure of <a href="../../posts/14-penguins1zzcollab/">post 14</a>.</p>
<section id="motivations" class="level2">
<h2 class="anchored" data-anchor-id="motivations">Motivations</h2>
<ul>
<li>Written reports describe the finished analysis but not the small decisions the analyst makes between keystrokes. A short screencast exposes those decisions.</li>
<li>Proprietary screencasting tools (Camtasia, ScreenFlow) cost between 100 and 300 dollars and lock recordings into vendor-specific project formats.</li>
<li>Live streaming an analysis to a small group of collaborators (a thesis advisor, a paper co-author, a study team) was awkward without a reproducible setup.</li>
<li>The R community has gravitated toward YouTube as the default home for asynchronous tutorials, but documentation of the recording workflow itself is scattered.</li>
</ul>
</section>
<section id="objectives" class="level2">
<h2 class="anchored" data-anchor-id="objectives">Objectives</h2>
<ol type="1">
<li>Install and configure OBS Studio on macOS or Linux with sensible defaults for R coding screencasts (1080p, 30 fps, hardware encoder).</li>
<li>Establish a three-scene template (code only, code with webcam, title card) and a two-source audio chain (microphone with noise suppression, optional system audio).</li>
<li>Produce a 30-second test recording, edit it with <code>ffmpeg</code>, and upload it to YouTube as an unlisted draft with appropriate metadata.</li>
<li>Walk through a complete five-minute live R coding session based on the Palmer Penguins dataset, ready to record on a second take.</li>
</ol>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/pub-obs-r-screencasts/media/images/obs.png" class="img-fluid figure-img"></p>
<figcaption>Workspace prepared for the first recording session.</figcaption>
</figure>
</div>
</section>
</section>
<section id="what-is-obs-studio" class="level1">
<h1>What Is OBS Studio?</h1>
<p>OBS Studio (Open Broadcaster Software) is a free, cross-platform video recording and live streaming application released under the GNU General Public License version 2. It plays the same role for video that <code>ffmpeg</code> plays for command-line transcoding: a deeply scriptable, deeply configurable workhorse that the rest of the open source ecosystem builds on. A concrete example: every academic conference I have attended in the past two years that recorded talks for asynchronous viewing used OBS, either directly by the AV team or via a downstream tool that wraps it.</p>
</section>
<section id="prerequisites" class="level1">
<h1>Prerequisites</h1>
<ul>
<li><strong>Operating system.</strong> macOS 12 or later, Ubuntu 22.04 or later, or equivalent. Windows 10/11 also works but is not the focus of this post.</li>
<li><strong>Hardware.</strong> A laptop or desktop from the past five years. Hardware H.264 encoding (Apple VT on macOS, NVENC on NVIDIA GPUs) reduces CPU load substantially during recording.</li>
<li><strong>Microphone.</strong> Any USB or 3.5mm microphone. Built-in laptop microphones produce listenable but distinctly amateur audio.</li>
<li><strong>Prior knowledge.</strong> Basic shell command experience and familiarity with the R session one intends to record.</li>
<li><strong>Disk space.</strong> Roughly 50 megabytes per minute of recorded video at the recommended quality settings.</li>
</ul>
</section>
<section id="step-by-step" class="level1">
<h1>Step by Step</h1>
<section id="step-1-install-obs-studio" class="level2">
<h2 class="anchored" data-anchor-id="step-1-install-obs-studio">Step 1: Install OBS Studio</h2>
<p>On macOS:</p>
<div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb1-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">brew</span> install <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--cask</span> obs</span></code></pre></div>
<p>On Debian or Ubuntu:</p>
<div class="sourceCode" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb2-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sudo</span> apt install obs-studio</span></code></pre></div>
<p>Launch OBS once and accept the auto-configuration wizard’s defaults. The wizard probes the system to choose sensible recording resolution and encoder settings.</p>
<p>Verification:</p>
<div class="sourceCode" id="cb3" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb3-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">obs</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--version</span></span></code></pre></div>
<p>A successful install prints the version string (30.x as of 2026-05).</p>
</section>
<section id="step-2-configure-scenes-and-sources" class="level2">
<h2 class="anchored" data-anchor-id="step-2-configure-scenes-and-sources">Step 2: Configure Scenes and Sources</h2>
<p>A coding screencast typically needs three scenes:</p>
<ul>
<li><strong>Code only.</strong> A single <em>Display Capture</em> or <em>Window Capture</em> source for the editor or terminal. Useful for the bulk of the recording.</li>
<li><strong>Code with webcam.</strong> The same capture source plus a small <em>Video Capture Device</em> overlay for the presenter, anchored to a corner.</li>
<li><strong>Title card.</strong> A static image source for the opening and closing seconds of the video.</li>
</ul>
<p>Window Capture is preferred over Display Capture when only a single application needs to be visible: it ignores notification banners, desktop clutter, and accidental tab switches.</p>
</section>
<section id="step-3-audio-routing" class="level2">
<h2 class="anchored" data-anchor-id="step-3-audio-routing">Step 3: Audio Routing</h2>
<p>Two audio sources matter for a coding screencast: microphone and (optionally) system sound. Add both as separate sources so they can be mixed independently.</p>
<p>For a USB microphone, set the input gain so the audio meter peaks around -12 dB during normal speech. Apply the <em>Noise Suppression</em> filter (RNNoise) and a <em>Compressor</em> filter to even out volume across sentences.</p>
</section>
<section id="step-4-recording-format" class="level2">
<h2 class="anchored" data-anchor-id="step-4-recording-format">Step 4: Recording Format</h2>
<p>In <em>Settings &gt; Output &gt; Recording</em>:</p>
<ul>
<li><strong>Recording Path.</strong> A dedicated <code>~/screencasts/</code> directory.</li>
<li><strong>Recording Format.</strong> <code>mkv</code> (resilient to crashes; remux to mp4 afterward).</li>
<li><strong>Encoder.</strong> Hardware encoder if available (Apple VT H.264 on macOS, NVENC on Linux with NVIDIA GPU).</li>
<li><strong>Rate Control.</strong> CQP at quality 18 to 22.</li>
</ul>
<p>Set frame rate to 30 fps and base resolution to 1920x1080. Higher frame rates are unnecessary for coding content and inflate file size.</p>
</section>
<section id="step-5-practice-run" class="level2">
<h2 class="anchored" data-anchor-id="step-5-practice-run">Step 5: Practice Run</h2>
<p>Record a 30-second test that exercises every scene transition and confirms the audio levels. Play the result back at full screen on a different device. Common issues caught at this stage include muted microphone input, font sizes too small to read at video compression, and a busy desktop background visible behind a window-captured editor.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/pub-obs-r-screencasts/media/images/obs.png" class="img-fluid figure-img"></p>
<figcaption>Mid-recording: the editor on the left, a small webcam tile in the corner.</figcaption>
</figure>
</div>
</section>
<section id="step-6-the-five-minute-analysis" class="level2">
<h2 class="anchored" data-anchor-id="step-6-the-five-minute-analysis">Step 6: The Five-Minute Analysis</h2>
<p>The recorded analysis itself follows a strict template: load, glimpse, plot, model, summary. The full code for the worked example is below; during the screencast each block is typed and explained in real time.</p>
<div class="cell">
<div class="sourceCode cell-code" id="cb4" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb4-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">library</span>(palmerpenguins)</span>
<span id="cb4-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">library</span>(ggplot2)</span>
<span id="cb4-3"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">library</span>(dplyr)</span>
<span id="cb4-4"></span>
<span id="cb4-5">penguins_clean <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> penguins <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span> tidyr<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">drop_na</span>()</span>
<span id="cb4-6"></span>
<span id="cb4-7"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">glimpse</span>(penguins_clean)</span></code></pre></div>
<div class="cell-output cell-output-stdout">
<pre><code>Rows: 333
Columns: 8
$ species           &lt;fct&gt; Adelie, Adelie, Adelie, Adelie, Adelie, Adelie, Adel…
$ island            &lt;fct&gt; Torgersen, Torgersen, Torgersen, Torgersen, Torgerse…
$ bill_length_mm    &lt;dbl&gt; 39.1, 39.5, 40.3, 36.7, 39.3, 38.9, 39.2, 41.1, 38.6…
$ bill_depth_mm     &lt;dbl&gt; 18.7, 17.4, 18.0, 19.3, 20.6, 17.8, 19.6, 17.6, 21.2…
$ flipper_length_mm &lt;int&gt; 181, 186, 195, 193, 190, 181, 195, 182, 191, 198, 18…
$ body_mass_g       &lt;int&gt; 3750, 3800, 3250, 3450, 3650, 3625, 4675, 3200, 3800…
$ sex               &lt;fct&gt; male, female, female, female, male, female, male, fe…
$ year              &lt;int&gt; 2007, 2007, 2007, 2007, 2007, 2007, 2007, 2007, 2007…</code></pre>
</div>
</div>
<p>A brief comment on the dataset acknowledges its provenance (Palmer Station, Antarctica; collected by Dr.&nbsp;Kristen Gorman) and previews the question: how well does flipper length predict body mass?</p>
<div class="cell">
<div class="sourceCode cell-code" id="cb6" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb6-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">ggplot</span>(</span>
<span id="cb6-2">  penguins_clean,</span>
<span id="cb6-3">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">aes</span>(<span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">x =</span> flipper_length_mm, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">y =</span> body_mass_g)</span>
<span id="cb6-4">) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span></span>
<span id="cb6-5">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">geom_point</span>(<span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">alpha =</span> <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.6</span>) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span></span>
<span id="cb6-6">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">geom_smooth</span>(<span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">method =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'lm'</span>, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">se =</span> <span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">TRUE</span>) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span></span>
<span id="cb6-7">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">labs</span>(</span>
<span id="cb6-8">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">x =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'Flipper length (mm)'</span>,</span>
<span id="cb6-9">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">y =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'Body mass (g)'</span>,</span>
<span id="cb6-10">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">title =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'Body mass scales linearly with flipper length'</span></span>
<span id="cb6-11">  ) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span></span>
<span id="cb6-12">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">theme_minimal</span>(<span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">base_size =</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">13</span>)</span></code></pre></div>
<div class="cell-output-display">
<div>
<figure class="figure">
<p><img src="https://rgtlab.org/posts/pub-obs-r-screencasts/index_files/figure-html/unnamed-chunk-2-1.png" class="img-fluid figure-img" width="672"></p>
</figure>
</div>
</div>
</div>
<p>The visual establishes the linear relationship before the model is fit, which keeps the viewer oriented.</p>
<div class="cell">
<div class="sourceCode cell-code" id="cb7" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb7-1">fit <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">lm</span>(body_mass_g <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">~</span> flipper_length_mm,</span>
<span id="cb7-2">          <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">data =</span> penguins_clean)</span>
<span id="cb7-3"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">summary</span>(fit)</span></code></pre></div>
<div class="cell-output cell-output-stdout">
<pre><code>
Call:
lm(formula = body_mass_g ~ flipper_length_mm, data = penguins_clean)

Residuals:
     Min       1Q   Median       3Q      Max 
-1057.33  -259.79   -12.24   242.97  1293.89 

Coefficients:
                  Estimate Std. Error t value Pr(&gt;|t|)    
(Intercept)       -5872.09     310.29  -18.93   &lt;2e-16 ***
flipper_length_mm    50.15       1.54   32.56   &lt;2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 393.3 on 331 degrees of freedom
Multiple R-squared:  0.7621,    Adjusted R-squared:  0.7614 
F-statistic:  1060 on 1 and 331 DF,  p-value: &lt; 2.2e-16</code></pre>
</div>
</div>
<p>A flipper length increase of one millimetre is associated with roughly a 50 gram increase in body mass. R-squared is approximately 0.76, which is striking for a single predictor and provides a natural cliffhanger for a follow-up post that introduces species as a covariate (see <a href="../../posts/14-penguins1zzcollab/">Palmer Penguins Part 1</a>).</p>
<p>The screencast closes with a one-sentence summary and a verbal pointer to the written post that contains the full analysis.</p>
</section>
<section id="step-7-edit-and-upload" class="level2">
<h2 class="anchored" data-anchor-id="step-7-edit-and-upload">Step 7: Edit and Upload</h2>
<p>Trim the head and tail using <code>ffmpeg</code>:</p>
<div class="sourceCode" id="cb9" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb9-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">ffmpeg</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-i</span> recording.mkv <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-ss</span> 00:00:03 <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb9-2">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-to</span> 00:05:12 <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-c</span> copy clipped.mp4</span></code></pre></div>
<p>Upload via the YouTube Studio web interface. Recommended metadata fields:</p>
<ul>
<li><strong>Title.</strong> Mirror the blog post title.</li>
<li><strong>Description.</strong> Link to the blog post and to the GitHub repository.</li>
<li><strong>Tags.</strong> <code>r</code>, <code>data analysis</code>, <code>screencast</code>, plus dataset tags.</li>
<li><strong>Chapters.</strong> Add timestamp markers in the description so viewers can jump to the modelling section.</li>
</ul>
</section>
<section id="step-8-embed-in-the-blog-post" class="level2">
<h2 class="anchored" data-anchor-id="step-8-embed-in-the-blog-post">Step 8: Embed in the Blog Post</h2>
<p>Quarto embeds YouTube videos with a single shortcode:</p>
<div class="sourceCode" id="cb10" style="background: #f1f3f5;"><pre class="sourceCode markdown code-with-copy"><code class="sourceCode markdown"></code></pre></div>
<p>The video should appear early in the post, ideally just after the introduction, so visitors can choose to watch or read.</p>
</section>
<section id="default-recording-hotkeys" class="level2">
<h2 class="anchored" data-anchor-id="default-recording-hotkeys">Default Recording Hotkeys</h2>
<table class="caption-top table">
<colgroup>
<col style="width: 30%">
<col style="width: 34%">
<col style="width: 34%">
</colgroup>
<thead>
<tr class="header">
<th>Action</th>
<th>macOS shortcut</th>
<th>Linux shortcut</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>Start / stop record</td>
<td>not bound by default</td>
<td>not bound by default</td>
</tr>
<tr class="even">
<td>Pause record</td>
<td>not bound by default</td>
<td>not bound by default</td>
</tr>
<tr class="odd">
<td>Switch scene</td>
<td>configurable per scene</td>
<td>configurable per scene</td>
</tr>
<tr class="even">
<td>Mute microphone</td>
<td>configurable</td>
<td>configurable</td>
</tr>
</tbody>
</table>
<p>Bind these in <em>Settings &gt; Hotkeys</em> before the first real recording. Unbound defaults catch most beginners off guard.</p>
</section>
</section>
<section id="things-to-watch-out-for" class="level1">
<h1>Things to Watch Out For</h1>
<p>The following gotchas have all caught me at least once. Each lists the symptom and the fix.</p>
<ol type="1">
<li><strong>Display Capture shows a black rectangle on macOS.</strong> Symptom: the captured area is uniformly black even though the screen has content. Fix: grant OBS <em>Screen Recording</em> permission in <em>System Settings &gt; Privacy &amp; Security &gt; Screen Recording</em>, then quit and relaunch OBS. The permission request only appears the first time; if it was dismissed, it must be granted manually.</li>
<li><strong>Audio is silent in the recording but the meter shows activity.</strong> Symptom: the OBS audio meter moves during speech, but playback is silent. Fix: check that the microphone source is not muted in the <em>Audio Mixer</em> panel (the speaker icon is hidden until hovered) and confirm the recording’s output channel mix in <em>Settings &gt; Audio &gt; Advanced</em>.</li>
<li><strong>The recording stutters when the editor is in focus.</strong> Symptom: intermittent dropped frames during typing. Fix: switch from software (x264) to a hardware encoder, or reduce base resolution to 1280x720 if hardware encoding is unavailable. Coding content at 720p is still readable at typical viewing distances.</li>
<li><strong>YouTube rejects the upload citing ‘invalid format’.</strong> Symptom: YouTube Studio displays a generic error after the upload finishes. Fix: most often the recording is in MKV. Remux to MP4 with <code>ffmpeg -i recording.mkv -c copy recording.mp4</code>. MKV is preferred during recording for crash resilience but YouTube prefers MP4.</li>
<li><strong>Font in the recording is unreadable when played at small window sizes.</strong> Symptom: code is legible at full screen but pixelated in a 480-wide embed. Fix: increase the editor’s font size to 18 or 20 points before recording. What looks oversized in the editor compresses to comfortable reading size in the final video.</li>
<li><strong>Webcam overlay is mirrored.</strong> Symptom: text on a notebook visible to the webcam appears reversed in the recording. Fix: right-click the <em>Video Capture Device</em> source, choose <em>Transform &gt; Flip Horizontal</em>. OBS mirrors the preview by default to feel natural to the speaker but does not mirror the recorded output.</li>
<li><strong>Recording fills the disk during a long session.</strong> Symptom: OBS silently stops recording after 20 to 30 minutes. Fix: at the recommended quality, every 10 minutes consumes roughly 500 megabytes. Confirm at least 5 gigabytes of free space before a long session, or switch to a more aggressive <code>crf</code> value (lower visual quality, smaller file).</li>
</ol>
</section>
<section id="uninstall-rollback" class="level1">
<h1>Uninstall / Rollback</h1>
<p>To remove OBS Studio and its configuration entirely:</p>
<p>On macOS:</p>
<div class="sourceCode" id="cb11" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb11-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">brew</span> uninstall <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--cask</span> obs</span>
<span id="cb11-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">rm</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-rf</span> ~/Library/Application\ Support/obs-studio</span></code></pre></div>
<p>On Debian or Ubuntu:</p>
<div class="sourceCode" id="cb12" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb12-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sudo</span> apt remove obs-studio</span>
<span id="cb12-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">rm</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-rf</span> ~/.config/obs-studio</span></code></pre></div>
<p>Both removals are reversible: a fresh install re-creates the configuration directory with default settings on first launch.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/pub-obs-r-screencasts/media/images/obs.png" class="img-fluid figure-img"></p>
<figcaption>A finished recording, ready to clip and upload.</figcaption>
</figure>
</div>
</section>
<section id="what-did-we-learn" class="level1">
<h1>What Did We Learn?</h1>
<section id="conceptual" class="level2">
<h2 class="anchored" data-anchor-id="conceptual">Conceptual</h2>
<ul>
<li>A short screencast is a different teaching artefact from a written post; both have a place, and pairing them produces a richer resource than either alone.</li>
<li>The capture pipeline (recorder, encoder, hosting) can be assembled entirely from open source and free-tier components, with no long-term lock-in.</li>
<li>Recording forces a kind of editorial discipline that improves the written post too: vague paragraphs become obvious when read aloud.</li>
</ul>
</section>
<section id="technical" class="level2">
<h2 class="anchored" data-anchor-id="technical">Technical</h2>
<ul>
<li>Hardware encoders are essential on laptops; software encoding (x264) competes with R for CPU cycles and produces visible stuttering.</li>
<li>MKV during recording, MP4 for upload. The two-stage convention protects against crashes without sacrificing platform compatibility.</li>
<li>Audio quality matters more than video quality. Viewers tolerate 720p video far better than echo-prone or compressed audio.</li>
</ul>
</section>
<section id="gotchas" class="level2">
<h2 class="anchored" data-anchor-id="gotchas">Gotchas</h2>
<ul>
<li>macOS screen-recording permission is granted per-application and silently fails closed. New OBS installs need a deliberate first-run trip through System Settings.</li>
<li>YouTube’s MP4 preference is documented but not enforced clearly in upload error messages.</li>
<li>Default OBS hotkeys are intentionally unset to avoid clashes; this feels broken until one realises it.</li>
</ul>
</section>
</section>
<section id="limitations" class="level1">
<h1>Limitations</h1>
<ul>
<li>The pipeline does not include a post-production editor. For multi-cut tutorials or any work requiring callouts, a separate editor (Kdenlive, DaVinci Resolve, iMovie) is needed.</li>
<li>The Palmer Penguins worked example is a demonstration, not a template for clinical trial visualisation. Audio narration would need different framing for sensitive data.</li>
<li>Live streaming via YouTube introduces 5 to 30 seconds of latency, which makes interactive Q&amp;A clunky compared with a dedicated video conferencing tool.</li>
<li>The post does not address closed captioning or multilingual subtitles, both of which deserve their own treatment.</li>
</ul>
</section>
<section id="opportunities-for-improvement" class="level1">
<h1>Opportunities for Improvement</h1>
<ol type="1">
<li>Capture the OBS scene collection as a JSON file under <code>analysis/configs/scene-collection.json</code> so the configuration is reproducible across machines.</li>
<li>Script the audio chain (RNNoise + Compressor) using <code>obs-websocket</code> so the configuration can be applied programmatically.</li>
<li>Compare hardware encoder quality across Apple VT, NVENC, and QuickSync at matched bitrates.</li>
<li>Produce a follow-up post on Kdenlive for non-trivial editing.</li>
<li>Document a captioning workflow with <code>whisper.cpp</code> for automatically generated subtitles.</li>
<li>Build a CI job that validates the scene collection JSON against the OBS schema on each commit.</li>
</ol>
</section>
<section id="wrapping-up" class="level1">
<h1>Wrapping Up</h1>
<p>OBS Studio, paired with YouTube and a small set of recording conventions, gives R users a complete open source pipeline for live coding content. The investment in initial setup is modest and pays off across many subsequent recordings: the same scenes, audio chain, and upload conventions apply to every post.</p>
<p>The next step in this series will be a recording of the Palmer Penguins analysis above, published both as a written post and as an embedded five-minute video, so the comparison between the two media can be made directly.</p>
<p>In conclusion, three points merit emphasis. First, the open source pipeline (OBS + ffmpeg + YouTube) is sufficient for publication-quality screencasts of R analyses, with no proprietary tooling required. Second, three scenes, two audio sources, and a 30-second test constitute the minimum viable configuration; everything beyond that is refinement. Third, audio quality and font size are the two settings most often underestimated by first-time recorders, and correcting them before recording saves significant post-production effort.</p>
</section>
<section id="see-also" class="level1">
<h1>See Also</h1>
<ul>
<li><a href="https://obsproject.com/wiki/">OBS Studio documentation</a></li>
<li><a href="https://creatoracademy.youtube.com/">YouTube Creator Academy</a></li>
<li><a href="../../posts/14-penguins1zzcollab/">Palmer Penguins Part 1</a> — the worked analysis used in this post’s screencast example.</li>
<li><a href="https://ffmpeg.org/documentation.html">ffmpeg documentation</a> for trimming and remuxing recordings.</li>
<li><a href="https://quarto.org/docs/authoring/videos.html">Quarto video shortcode reference</a></li>
</ul>
</section>
<section id="reproducibility" class="level1">
<h1>Reproducibility</h1>
<table class="caption-top table">
<colgroup>
<col style="width: 23%">
<col style="width: 23%">
<col style="width: 52%">
</colgroup>
<thead>
<tr class="header">
<th>Component</th>
<th>Version tested</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>OBS Studio</td>
<td>30.x</td>
<td>macOS via Homebrew; Ubuntu via apt</td>
</tr>
<tr class="even">
<td>ffmpeg</td>
<td>7.x</td>
<td>for trim and remux</td>
</tr>
<tr class="odd">
<td>Quarto</td>
<td>1.9.37</td>
<td>for HTML rendering of this post</td>
</tr>
<tr class="even">
<td>macOS</td>
<td>26.4 (Tahoe)</td>
<td>hardware encoding via Apple VT</td>
</tr>
<tr class="odd">
<td>Ubuntu</td>
<td>24.04 LTS</td>
<td>hardware encoding via NVENC</td>
</tr>
<tr class="even">
<td>Date verified</td>
<td>2026-05-02</td>
<td></td>
</tr>
</tbody>
</table>
</section>
<section id="lets-connect" class="level1">
<h1>Let’s Connect</h1>
<p><em>Have questions, suggestions, or spot an error? Let me know.</em></p>
<ul>
<li><strong>GitHub:</strong> <a href="https://github.com/rgt47">rgt47</a></li>
<li><strong>Email:</strong> <a href="https://rgtlab.org/contact">Contact form</a></li>
</ul>
<p>I would enjoy hearing from you if:</p>
<ul>
<li>You spot an error or a better approach to any of the configuration in this post.</li>
<li>You use a different recorder (SimpleScreenRecorder, ScreenFlow) and want to compare notes.</li>
<li>You have suggestions for follow-up topics in the screencasting series.</li>
</ul>
<hr>
<p><em>Rendered on 2026-05-02 at 10:30 PDT.</em><br> <em>Source: ~/prj/qblog/posts/27-setupobs/setupobs/analysis/report/index.qmd</em></p>
<!-- ============================================================================
PRE-PUBLISH QA CHECKLIST: verify each item BEFORE setting draft: false

This is the verification pass that mirrors the AUTHOR PROVIDES list at the
top of this file. If anything below is unchecked, the post is not ready.

[ ] YAML
    [x] title, subtitle, date, categories, description, image filled in
    [x] document-type: 'blog'
    [x] draft: false

[ ] Narrative complete (no remaining `[bracketed]` placeholders)
    [x] grep '\[' index.qmd returns only legitimate code/links

[ ] Configuration artifacts present and tested
    [ ] Full config file shown verbatim, not snippets
        (OBS scene-collection JSON not yet captured under analysis/configs/)
    [ ] Install commands tested on a clean machine (or VM)
    [ ] Verification commands produce the documented output
    [x] Uninstall / rollback steps documented

[ ] Things to Watch Out For
    [x] At least 5 gotchas listed (7 provided)
    [x] Each gotcha has both a symptom AND a fix

[ ] Visual design
    [x] 1 hero image (width=80%) immediately after AUTHOR PROVIDES block
    [ ] Exactly 3 ambiance images ({.img-fluid} only, no width=NN%,
        no fig-align): after Objectives, after Configuration, before
        Lessons Learnt
        (currently obs.png used as placeholder for all three)
    [ ] Hero and ambiance captions describe the actual image (not a
        placeholder description)
    [x] No hand-coded "Download PDF" link in body
    [ ] media/images/README.md attributes every image

[ ] Content quality
    [x] Learner voice: author positioned as peer, not expert
    [x] Zero emoji anywhere
    [x] Zero em dashes
    [x] Single quotes preferred over double quotes in prose
    [x] Each command and config setting interpreted in plain language

[ ] Reproducibility
    [x] Version matrix table filled in (OS, tool, dependencies, date)
    [ ] Config files committed under analysis/configs/
    [ ] Install script (if provided) is runnable on a clean machine

[ ] Render verification
    [x] quarto render index.qmd produces clean HTML with no warnings
    [x] Hero image displays in /blog/ listing card
    [ ] Internal links resolve

============================================================================ -->
<section id="related-posts-in-this-cluster" class="level2">
<h2 class="anchored" data-anchor-id="related-posts-in-this-cluster">Related posts in this cluster</h2>
<p>This post is part of the <em>Quarto, R Markdown, and Publishing</em> series. Recommended reading order:</p>
<ol type="1">
<li>Post 80: <a href="../80-pub-multi-language-quarto/">Multi-Language Quarto Documents on macOS</a></li>
<li>Post 81: <a href="../81-pub-r-script-to-rmd/">Rapid Conversion of Draft R Scripts to Formal Rmd</a></li>
<li>Post 83: <a href="../83-pub-statistical-computing-textbook/">Building a Statistical Computing Textbook</a></li>
<li><strong>Post 84: Setting up OBS for Live R Coding Screencasts</strong> (this post)</li>
</ol>


</section>
</section>

 ]]></description>
  <category>setup</category>
  <category>obs</category>
  <category>youtube</category>
  <category>screencast</category>
  <category>r</category>
  <category>reproducibility</category>
  <guid>https://rgtlab.org/posts/pub-obs-r-screencasts/</guid>
  <pubDate>Sat, 02 May 2026 07:00:00 GMT</pubDate>
  <media:content url="https://rgtlab.org/posts/pub-obs-r-screencasts/media/images/hero.png" medium="image" type="image/png" height="80" width="144"/>
</item>
<item>
  <title>From testthat to tinytest: Converting an R Package Test Suite</title>
  <dc:creator>R. Glenn Thomas</dc:creator>
  <link>https://rgtlab.org/posts/rp-testthat-to-tinytest/</link>
  <description><![CDATA[ 




<!-- ============================================================================
AUTHOR PROVIDES — concrete inputs the blogger must supply

Every item below maps to a placeholder in the body. Fill in this checklist
FIRST, then paste your answers into the matching `[bracketed]` slots. Do not
publish until every box is ticked.

YAML FRONT MATTER (lines 1-20)
  [x] title           — 'tinytest' replaced with actual tool name, < 70 chars
  [x] subtitle        — one-line elaboration of what the post sets up
  [x] date            — YYYY-MM-DD of intended publication
  [x] categories      — include `setup`, the tool name, and host OS
  [x] description     — 1-2 sentences for blog listing card
  [x] image           — path to hero image (relative to this .qmd)
  [x] draft: false    — flip when ready

NARRATIVE INPUTS
  [x] Hook sentence   — 'I didn't know how to configure [tool] until [trigger]'
  [x] Pain point      — the specific friction the tool removes
  [x] Motivations     — exactly 4-5 bullets (workflow gap, frustration, curiosity, learning)
  [x] Objectives      — exactly 4 numbered, verifiable end states
  [x] What is [Tool]? — 1-sentence definition + 1 analogy + 1 concrete example
  [x] Daily-workflow paragraph — how this tool fits into your routine after setup
  [x] Things to Watch Out For — 5-7 gotchas (required; setup posts need this)
  [x] Lessons Learnt  — 4 conceptual + 4 technical + 4 gotcha bullets
  [x] Limitations     — 4-6 bullets (what this setup does NOT solve)
  [x] Opportunities for Improvement — 5-6 numbered next-step extensions
  [x] Wrapping Up     — 2-3 paragraph conclusion + 'Main takeaways'
  [x] See Also        — 3-5 links (official docs + 1-2 related blog posts)

CONFIGURATION DELIVERABLES (real, working artifacts, not snippets)
  [x] Prerequisites list — OS version, hardware, prior knowledge, dependent tools
  [x] Installation block — exact bash commands, tested on a clean machine
  [x] Complete config file(s) — verbatim, copy-pasteable, with inline comments
        Place under: analysis/configs/<filename>
  [x] Verification commands — `<tool> --version` plus 1-2 functional smoke tests
  [x] Optional keybinding/command reference table — markdown table for daily use
  [x] Uninstall / rollback steps — how the reader undoes what you just did
  [x] Optional appendices — credential setup, sample session, teardown procedure

VERSION MATRIX (Reproducibility section)
  [x] OS + version tested        (e.g., macOS 15.4, Ubuntu 24.04)
  [x] Tool version pinned        (e.g., aws-cli 2.15)
  [x] Dependency versions        (e.g., homebrew 4.2, jq 1.7)
  [x] Date of last verification

IMAGES (4 total: hero + 3 ambiance, all in media/images/)
  [ ] Hero image (80% width)        — workspace/tool logo, sets tone
  [ ] Ambiance image 1 (100% width) — after Objectives (workspace shot)
  [ ] Ambiance image 2 (100% width) — after Configuration (terminal/editor)
  [ ] Ambiance image 3 (100% width) — before Lessons Learnt (workflow scene)
  [ ] media/images/README.md        — sources + attribution for every image

CONTACT & METADATA
  [x] Author name in YAML matches site author
  [x] Social links in 'Let's Connect' updated (or removed)
  [x] Giscus comments enabled (inherited from _quarto.yml)

============================================================================ -->
<!-- ============================================================================
HERO IMAGE
============================================================================ -->
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/rp-testthat-to-tinytest/media/images/hero.png" class="img-fluid figure-img" style="width:80.0%"></p>
<figcaption>Two test-tube racks side by side on a clean lab bench, the right rack noticeably smaller and holding two amber-tinted tubes, symbolising the contraction in scaffolding when a suite moves from testthat to tinytest.</figcaption>
</figure>
</div>
<p><em>Same coverage, fewer lines: the appeal of tinytest is less about novelty and more about the absence of ceremony.</em></p>
<section id="introduction" class="level1">
<h1>Introduction</h1>
<p>I did not really appreciate how much ceremony <code>testthat</code> carries until I cracked open <code>inst/tinytest/</code> in a small package and saw a fourteen-line file doing the work of an eighty-line <code>test_that()</code> file in the package I had been maintaining. The expectations were the same. The fixtures were the same. What was missing was the nesting, the helper boilerplate, and roughly half of the imports.</p>
<p>What I discovered, once I started porting the suite, was that <code>tinytest</code> is not ‘testthat lite’. It is a different shape. The unit of testing is the file, not the <code>test_that()</code> block. Tests are flat assertions evaluated in a script, not nested calls inside a closure. There is no <code>setup()</code> and no implicit fixture caching. Several <code>testthat</code> idioms (<code>expect_snapshot</code>, <code>local_edition</code>, <code>expect_message</code> with regex matching) have no direct equivalent and require either a different pattern or, in a few cases, a deliberate decision to drop the test.</p>
<p>We walk through the conversion as a paired-example review. I cover layout differences, the mechanical mapping of <code>expect_*</code> calls, the handful of patterns that do not port cleanly, the CI implications, and a short list of cases where I concluded that staying on <code>testthat</code> was the right call. The companion deliverable, a side-by-side conversion recipe, lives at <code>docs/testthat-to-tinytest-recipe.qmd</code> in this post’s compendium.</p>
<section id="pros-and-cons-at-a-glance" class="level2">
<h2 class="anchored" data-anchor-id="pros-and-cons-at-a-glance">Pros and cons at a glance</h2>
<p>Before getting into the mechanical details, a level-set on what each framework does well and where it asks something of the package author. This is an extended summary so that readers can calibrate whether the conversion is worth their afternoon.</p>
<section id="what-testthat-does-well" class="level3">
<h3 class="anchored" data-anchor-id="what-testthat-does-well">What <code>testthat</code> does well</h3>
<ul>
<li><strong>A mature ecosystem.</strong> The framework is the de-facto standard on CRAN, which means the largest body of Stack Overflow answers, blog posts, and worked examples points at testthat idioms. Onboarding contributors who already write R is faster with testthat than with anything else.</li>
<li><strong>Snapshot testing.</strong> <code>expect_snapshot()</code>, <code>expect_snapshot_value()</code>, and <code>expect_snapshot_file()</code> cover text, structured-data, and binary outputs respectively, with automatic regeneration on review. This is genuinely useful for packages whose output is a formatted table, a printed object, or a plot.</li>
<li><strong>Rich diff output.</strong> Failed assertions are reported through <code>waldo</code>, which produces a side-by-side diff for vectors, lists, and data frames. The diff often points directly at the responsible value, where a bare ‘not equal’ message would not.</li>
<li><strong>IDE integration.</strong> RStudio’s test pane reports per-test pass/fail status with one-click navigation. <code>Ctrl-Shift-T</code> runs the suite from the editor.</li>
<li><strong>Parallel test execution.</strong> <code>testthat</code> 3.0+ runs tests in parallel with <code>parallel = TRUE</code> in <code>testthat::test_local()</code> or <code>Config/testthat/parallel: true</code> in DESCRIPTION. For suites with long per-file fixtures, the speed-up is noticeable.</li>
<li><strong>Editions for managing breaking changes.</strong> The <code>Config/testthat/edition: 3</code> declaration locks the package to a specific behavioural contract, which makes upgrades predictable. testthat 4 will likely use the same mechanism.</li>
<li><strong>A wider assertion surface.</strong> <code>expect_s3_class</code>, <code>expect_s4_class</code>, <code>expect_type</code>, <code>expect_named</code>, <code>expect_no_error</code>, <code>expect_no_warning</code>, <code>expect_no_message</code>, <code>expect_setequal</code>, <code>expect_mapequal</code>, and <code>expect_invisible</code> all have semantics that read clearly at the call site.</li>
<li><strong><code>test_check()</code> exposes the package internal namespace.</strong> Test files reference non-exported helpers and constants by their bare names. For internal-heavy packages, this removes a layer of <code>getFromNamespace()</code> ceremony.</li>
<li><strong>Helper files auto-sourced.</strong> Files matching <code>tests/testthat/helper-*.R</code> are sourced before each test file, providing implicit fixture setup.</li>
<li><strong>Skip helpers for environment conditions.</strong> <code>skip_on_cran()</code>, <code>skip_on_os()</code>, <code>skip_if_offline()</code>, <code>skip_if_not_installed()</code> are concise and read like the intent they encode.</li>
</ul>
</section>
<section id="where-testthat-asks-something-of-the-author" class="level3">
<h3 class="anchored" data-anchor-id="where-testthat-asks-something-of-the-author">Where <code>testthat</code> asks something of the author</h3>
<ul>
<li><strong>A heavy dependency surface.</strong> Installing <code>testthat</code> pulls in <code>waldo</code>, <code>brio</code>, <code>desc</code>, <code>pkgload</code>, <code>processx</code>, <code>cli</code>, <code>digest</code>, <code>evaluate</code>, <code>R6</code>, <code>rlang</code>, and roughly two dozen transitive dependencies. For toy packages or small internal tools, this is most of the package’s install footprint.</li>
<li><strong>Slower CI.</strong> A <code>R CMD check</code> of an empty <code>testthat</code> suite takes longer than the same check with <code>tinytest</code> because the framework itself takes time to load and dispatch.</li>
<li><strong>The framework itself depends on third-party packages.</strong> This is a philosophical concern more than a practical one, but it does mean a bug or breaking change in any of the transitive dependencies can affect the test suite.</li>
<li><strong>Edition complexity.</strong> Editions are useful but add a layer of indirection. Tests that pass under edition 2 may fail under edition 3 (and vice versa). Diagnosing these edition-driven regressions is rare but bewildering when it happens.</li>
<li><strong>Per-test ceremony.</strong> Every assertion lives inside a <code>test_that('description', { ... })</code> block. The description string is sometimes informative and sometimes ceremonial.</li>
</ul>
</section>
<section id="what-tinytest-does-well" class="level3">
<h3 class="anchored" data-anchor-id="what-tinytest-does-well">What <code>tinytest</code> does well</h3>
<ul>
<li><strong>Zero <code>Imports</code>.</strong> <code>tinytest</code> is a single source file with no third-party dependencies. The framework is itself testable from a fresh R installation.</li>
<li><strong>Faster <code>R CMD check</code>.</strong> The framework loads in milliseconds. For a small package with a few hundred assertions, the wall-clock saving over <code>testthat</code> is on the order of seconds, not minutes, but it compounds across a CI fleet.</li>
<li><strong>File-as-test-unit.</strong> Each <code>inst/tinytest/test_*.R</code> file is the unit. The natural one-to-one map between an R/ source file and a test file makes the suite easy to navigate. Tests inside a file are top-level, not nested.</li>
<li><strong>Tests installed with the package.</strong> Because tests live in <code>inst/tinytest/</code>, they ship with the installed package and remain runnable via <code>tinytest::test_package('pkg')</code> after install. Users can verify their installation; this is useful for compiled-code packages and packages with system dependencies.</li>
<li><strong>A smaller cognitive surface.</strong> No editions, no fixture caching, no helper auto-sourcing, no description strings. When a test fails, the failure mode is straightforward. When it passes, one can be confident it passed for the reason intended.</li>
<li><strong>Predictable behaviour across R versions.</strong> With no third-party dependencies, <code>tinytest</code> is unaffected by changes in the broader package ecosystem. A test suite written against <code>tinytest</code> 1.4 in 2024 will still run unchanged in 2030.</li>
<li><strong>Self-evident diagnostics.</strong> A <code>tinytest</code> failure reports the file, line, expected value, and actual value with no framework noise. The diagnostic surface is small enough to read at a glance.</li>
</ul>
</section>
<section id="where-tinytest-asks-something-of-the-author" class="level3">
<h3 class="anchored" data-anchor-id="where-tinytest-asks-something-of-the-author">Where <code>tinytest</code> asks something of the author</h3>
<ul>
<li><strong>No snapshot testing.</strong> This is the largest functional gap. Manual reference-file comparison via <code>capture.output()</code>
<ul>
<li><code>expect_equal()</code> works but is ergonomically distant from <code>expect_snapshot()</code>.</li>
</ul></li>
<li><strong>No parallel execution by default.</strong> A <code>cl</code> argument to <code>test_all()</code> accepts a <code>parallel</code> cluster, but the default is serial.</li>
<li><strong>No RStudio test-pane integration.</strong> Tests run under <code>R CMD check</code> and the console; the IDE’s ‘Run Tests’ button does not populate <code>tinytest</code> results.</li>
<li><strong>A smaller user community.</strong> Searches for specific assertion patterns yield fewer results, and answers are sometimes outdated. The CRAN vignette is the most reliable reference.</li>
<li><strong>A narrower assertion surface.</strong> No <code>expect_s3_class</code>, <code>expect_type</code>, <code>expect_named</code>, or <code>expect_no_error</code>. Tests using these need manual rewriting; the recipe section 12 documents the substitutions.</li>
<li><strong><code>test_package()</code> only sees exports.</strong> Tests that reference non-exported package internals must qualify the reference (<code>getFromNamespace()</code> or <code>pkg:::name</code>) or the package must export the object. Recipe section 20 has the detail.</li>
<li><strong>No <code>helper-*.R</code> auto-sourcing.</strong> Shared fixtures require explicit <code>source('helper_X.R', local = TRUE)</code> at the top of each consumer file.</li>
<li><strong>No <code>skip_on_os</code>, <code>skip_on_ci</code>, <code>skip_if_offline</code> helpers.</strong> The substitutes (<code>if (... condition ...)   exit_file('reason')</code> at file scope) are equally clear but more verbose at the use site.</li>
<li><strong>No <code>patrick</code>-style parameterised tests.</strong> Plain <code>for</code> loops work, but the failing-iteration identifier is the line number rather than a parameter label.</li>
<li><strong>Less informative diff output.</strong> Without <code>waldo</code>, large vector or list comparisons report ‘not equal’ without the side-by-side diff. For complex objects, this lengthens debugging time.</li>
</ul>
</section>
<section id="decision-matrix" class="level3">
<h3 class="anchored" data-anchor-id="decision-matrix">Decision matrix</h3>
<table class="caption-top table">
<thead>
<tr class="header">
<th>If your package…</th>
<th>Likely better fit</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>Has more than five <code>expect_snapshot_*()</code> calls</td>
<td>testthat</td>
</tr>
<tr class="even">
<td>Is developed primarily in RStudio with the test pane</td>
<td>testthat</td>
</tr>
<tr class="odd">
<td>Uses <code>parallel = TRUE</code> for acceptable CI time</td>
<td>testthat</td>
</tr>
<tr class="even">
<td>Has a large team unfamiliar with <code>tinytest</code></td>
<td>testthat</td>
</tr>
<tr class="odd">
<td>Is a small utility with no snapshots</td>
<td>tinytest</td>
</tr>
<tr class="even">
<td>Is tested mainly in CI rather than interactively</td>
<td>tinytest</td>
</tr>
<tr class="odd">
<td>Wants minimal install footprint</td>
<td>tinytest</td>
</tr>
<tr class="even">
<td>Ships compiled code that needs post-install verification</td>
<td>tinytest</td>
</tr>
<tr class="odd">
<td>Has tests that primarily reference exported functions</td>
<td>either</td>
</tr>
<tr class="even">
<td>Is greenfield</td>
<td>either</td>
</tr>
</tbody>
</table>
<p>The matrix is not exhaustive, and most packages have at least one row pulling each direction. The recommendation is not to choose by majority vote but to identify the row that costs the most under the wrong choice. For a package whose CI is too slow, that row is the install-footprint one. For a package whose IDE workflow is critical, the test-pane row dominates.</p>
</section>
</section>
<section id="motivations" class="level2">
<h2 class="anchored" data-anchor-id="motivations">Motivations</h2>
<ul>
<li>Reading the <code>testthat</code> source after a debugging session and realising how much of the dependency surface (<code>waldo</code>, <code>brio</code>, <code>desc</code>, <code>pkgload</code>, <code>processx</code>, <code>cli</code>) my small packages were pulling in only to call <code>expect_equal</code> half a dozen times.</li>
<li>A growing collection of toy packages where <code>R CMD check</code> time was dominated by test-framework startup rather than the tests themselves, particularly inside containers.</li>
<li>Curiosity about Mark van der Loo’s design argument that a test framework should itself be testable, and that a single-file, zero-dependency framework is more honest about what testing actually requires.</li>
<li>The ergonomic appeal of file-as-test-unit when most of my packages have one source file per concern; the one-to-one map between <code>R/foo.R</code> and <code>inst/tinytest/test_foo.R</code> makes the suite easy to navigate.</li>
<li>A practical interest in seeing what breaks. Several <code>testthat</code> idioms are deeply embedded in the broader R-package ecosystem (snapshot testing, parallel test runners, RStudio’s test pane). Porting forces a clear-eyed audit of what one actually uses.</li>
</ul>
</section>
<section id="objectives" class="level2">
<h2 class="anchored" data-anchor-id="objectives">Objectives</h2>
<p>By the end of the conversion, the package should have:</p>
<ol type="1">
<li>An <code>inst/tinytest/</code> directory containing one test file per source-code concern, each runnable in isolation via <code>tinytest::run_test_file()</code>.</li>
<li>A <code>tests/tinytest.R</code> bootstrap that triggers the suite during <code>R CMD check</code>, replacing the <code>tests/testthat.R</code> bootstrap.</li>
<li>A <code>DESCRIPTION</code> file with <code>tinytest</code> in <code>Suggests</code>, no <code>Config/testthat/edition</code> line, and no <code>testthat</code> dependency.</li>
<li>A CI run (locally or in GitHub Actions) that exercises the suite end-to-end and reports the same number of distinct assertions the <code>testthat</code> suite covered, less any tests that were intentionally retired.</li>
</ol>
<!-- ============================================================================
AMBIANCE IMAGE 1: placed after Objectives (workspace shot)
============================================================================ -->
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/rp-testthat-to-tinytest/media/images/ambiance1.png" class="img-fluid figure-img"></p>
<figcaption>A cappuccino in a dark cup resting on an open paper planner, lit by a shaft of light from a window above. Placeholder ambiance image; will be replaced with subject-specific imagery before publication.</figcaption>
</figure>
</div>
</section>
</section>
<section id="what-is-tinytest" class="level1">
<h1>What is tinytest?</h1>
<p><code>tinytest</code> is a single-file R testing framework, written by Mark van der Loo, that ships with no <code>Imports</code> and one test file per package concern. Where <code>testthat</code> organises tests as nested calls inside a <code>test_that()</code> closure, <code>tinytest</code> evaluates each file as a script and treats every top-level <code>expect_*</code> call as an independent assertion.</p>
<p>A useful analogy: <code>testthat</code> is to <code>tinytest</code> as a full-featured build system is to a Makefile. Both produce the same artifact; one is opinionated about structure, the other gets out of the way.</p>
<p>A concrete example. The same assertion in both frameworks:</p>
<div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb1-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># testthat</span></span>
<span id="cb1-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">test_that</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'addition is associative'</span>, {</span>
<span id="cb1-3">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">expect_equal</span>((<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span> <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span> <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> (<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span> <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>))</span>
<span id="cb1-4">})</span>
<span id="cb1-5"></span>
<span id="cb1-6"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># tinytest</span></span>
<span id="cb1-7"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">expect_equal</span>((<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span> <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span> <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> (<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span> <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>))</span></code></pre></div>
<p>The <code>tinytest</code> form is not nested in anything. The file in which it lives is the unit. The label, when reporting, is the file name plus the line number. There is no descriptive string parameter because the assertion is its own description.</p>
</section>
<section id="prerequisites" class="level1">
<h1>Prerequisites</h1>
<ul>
<li>R 4.1 or later (the native pipe is used in the post’s worked examples, but <code>tinytest</code> itself works back to R 3.5).</li>
<li>An existing R package with a <code>tests/testthat/</code> directory. Greenfield packages can skip the conversion entirely and start with <code>tinytest</code>.</li>
<li>Familiarity with <code>R CMD check</code> and the standard <code>tests/&lt;framework&gt;.R</code> bootstrap convention.</li>
<li>Optional but useful: a CI configuration file you control. The examples below use GitHub Actions, but the pattern is the same for GitLab, Jenkins, or a local Makefile.</li>
</ul>
</section>
<section id="installation" class="level1">
<h1>Installation</h1>
<p><code>tinytest</code> is on CRAN, with no compiled code and no dependencies. Install it from R:</p>
<div class="sourceCode" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb2-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">install.packages</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'tinytest'</span>)</span></code></pre></div>
<p>Verify the install:</p>
<div class="sourceCode" id="cb3" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb3-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">packageVersion</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'tinytest'</span>)</span>
<span id="cb3-2"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># [1] '1.4.1'</span></span></code></pre></div>
<p>For a package that uses <code>renv</code>, add the dependency to the lockfile:</p>
<div class="sourceCode" id="cb4" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb4-1">renv<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">install</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'tinytest'</span>)</span>
<span id="cb4-2">renv<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">snapshot</span>()</span></code></pre></div>
</section>
<section id="layout-testthat-vs-tinytest" class="level1">
<h1>Layout: testthat vs tinytest</h1>
<p>The two frameworks place test files in different directories, and the bootstrap files differ as well. The package layout shifts as follows:</p>
<pre><code>Before                            After
------                            -----
tests/                            tests/
  testthat.R                        tinytest.R
  testthat/                       inst/
    test-foo.R                      tinytest/
    test-bar.R                        test_foo.R
    helper-fixtures.R                 test_bar.R
                                      helper_fixtures.R</code></pre>
<p>Three differences worth naming:</p>
<ol type="1">
<li><strong>Test files move from <code>tests/testthat/</code> to <code>inst/tinytest/</code>.</strong> The <code>inst/</code> location means the tests are installed alongside the package and remain runnable after install via <code>tinytest::test_package('mypkg')</code>. This is one of the larger conceptual shifts: in <code>tinytest</code>, tests are part of the shipped package, not a development-only artefact.</li>
<li><strong>File-naming convention changes from <code>test-*.R</code> to <code>test_*.R</code>.</strong> The underscore is <code>tinytest</code>’s default; the dash also works, but the underscore is what <code>tinytest::test_package()</code> expects without configuration.</li>
<li><strong>The bootstrap file changes name and content.</strong> See below.</li>
</ol>
<section id="the-bootstrap-file" class="level2">
<h2 class="anchored" data-anchor-id="the-bootstrap-file">The bootstrap file</h2>
<p><code>tests/testthat.R</code> looks like this:</p>
<div class="sourceCode" id="cb6" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb6-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">library</span>(testthat)</span>
<span id="cb6-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">library</span>(mypkg)</span>
<span id="cb6-3"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">test_check</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'mypkg'</span>)</span></code></pre></div>
<p>The <code>tinytest</code> equivalent is <code>tests/tinytest.R</code>:</p>
<div class="sourceCode" id="cb7" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb7-1"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> (<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">requireNamespace</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'tinytest'</span>, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">quietly =</span> <span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">TRUE</span>)) {</span>
<span id="cb7-2">  tinytest<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">test_package</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'mypkg'</span>)</span>
<span id="cb7-3">}</span></code></pre></div>
<p>The <code>requireNamespace</code> guard is idiomatic for <code>tinytest</code>. Because <code>tinytest</code> lives in <code>Suggests</code>, not <code>Imports</code>, the package should build and check on a system where <code>tinytest</code> is not installed. The guard makes that case a no-op rather than a hard error.</p>
</section>
</section>
<section id="the-mechanical-conversion" class="level1">
<h1>The mechanical conversion</h1>
<p>The bulk of the work is rewriting individual test files. The mapping is mostly one-to-one. The full table lives in the companion deliverable; here are the patterns that come up most often.</p>
<section id="pattern-1-a-single-test_that-block" class="level2">
<h2 class="anchored" data-anchor-id="pattern-1-a-single-test_that-block">Pattern 1: a single <code>test_that()</code> block</h2>
<p>Before:</p>
<div class="sourceCode" id="cb8" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb8-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># tests/testthat/test-arithmetic.R</span></span>
<span id="cb8-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">test_that</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'addition works'</span>, {</span>
<span id="cb8-3">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">expect_equal</span>(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span> <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>)</span>
<span id="cb8-4">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">expect_equal</span>(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span> <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">4</span>)</span>
<span id="cb8-5">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">expect_true</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">is.numeric</span>(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span> <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>))</span>
<span id="cb8-6">})</span></code></pre></div>
<p>After:</p>
<div class="sourceCode" id="cb9" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb9-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># inst/tinytest/test_arithmetic.R</span></span>
<span id="cb9-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">expect_equal</span>(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span> <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>)</span>
<span id="cb9-3"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">expect_equal</span>(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span> <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">4</span>)</span>
<span id="cb9-4"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">expect_true</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">is.numeric</span>(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span> <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>))</span></code></pre></div>
<p>The <code>test_that()</code> wrapper, the description string, and the braces all disappear. The three assertions are now top-level calls in the file.</p>
</section>
<section id="pattern-2-multiple-test_that-blocks-per-file" class="level2">
<h2 class="anchored" data-anchor-id="pattern-2-multiple-test_that-blocks-per-file">Pattern 2: multiple <code>test_that()</code> blocks per file</h2>
<p>Before:</p>
<div class="sourceCode" id="cb10" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb10-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">test_that</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'addition works'</span>, {</span>
<span id="cb10-2">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">expect_equal</span>(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span> <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>)</span>
<span id="cb10-3">})</span>
<span id="cb10-4"></span>
<span id="cb10-5"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">test_that</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'subtraction works'</span>, {</span>
<span id="cb10-6">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">expect_equal</span>(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span> <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>)</span>
<span id="cb10-7">})</span></code></pre></div>
<p>After (option A, single file):</p>
<div class="sourceCode" id="cb11" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb11-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># inst/tinytest/test_arithmetic.R</span></span>
<span id="cb11-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">expect_equal</span>(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span> <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>)   <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># addition</span></span>
<span id="cb11-3"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">expect_equal</span>(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span> <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>)   <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># subtraction</span></span></code></pre></div>
<p>After (option B, split into two files):</p>
<pre><code>inst/tinytest/test_addition.R
inst/tinytest/test_subtraction.R</code></pre>
<p>Option B is the more idiomatic <code>tinytest</code> choice when the two groups exercise unrelated source files. The framework offers no mechanism for grouping inside a file, so cohesion is enforced by file boundaries instead.</p>
</section>
<section id="pattern-3-shared-fixtures" class="level2">
<h2 class="anchored" data-anchor-id="pattern-3-shared-fixtures">Pattern 3: shared fixtures</h2>
<p><code>testthat</code> resolves shared fixtures via files named <code>helper-*.R</code>, which it sources before each test file. <code>tinytest</code> does not have an equivalent automatic mechanism, but two patterns work:</p>
<div class="sourceCode" id="cb13" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb13-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># inst/tinytest/helper_fixtures.R</span></span>
<span id="cb13-2">make_test_data <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">function</span>() {</span>
<span id="cb13-3">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">data.frame</span>(<span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">x =</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">5</span>, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">y =</span> letters[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">5</span>])</span>
<span id="cb13-4">}</span></code></pre></div>
<div class="sourceCode" id="cb14" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb14-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># inst/tinytest/test_summary.R</span></span>
<span id="cb14-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">source</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'helper_fixtures.R'</span>, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">local =</span> <span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">TRUE</span>)</span>
<span id="cb14-3">df <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">make_test_data</span>()</span>
<span id="cb14-4"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">expect_equal</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">nrow</span>(df), <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">5</span>)</span></code></pre></div>
<p>The <code>local = TRUE</code> argument keeps the fixture’s symbols out of the global environment. The repetition (<code>source(...)</code> at the top of each consumer file) is deliberate: <code>tinytest</code> privileges explicit dependency over magic.</p>
</section>
<section id="pattern-4-skip-on-cran" class="level2">
<h2 class="anchored" data-anchor-id="pattern-4-skip-on-cran">Pattern 4: skip-on-CRAN</h2>
<p>Before:</p>
<div class="sourceCode" id="cb15" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb15-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">test_that</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'integration test'</span>, {</span>
<span id="cb15-2">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">skip_on_cran</span>()</span>
<span id="cb15-3">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">skip_if_not_installed</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'database_driver'</span>)</span>
<span id="cb15-4">  ...</span>
<span id="cb15-5">})</span></code></pre></div>
<p>After:</p>
<div class="sourceCode" id="cb16" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb16-1"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> (<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">!</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">identical</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">Sys.getenv</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'NOT_CRAN'</span>), <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'true'</span>)) <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">exit_file</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'Skipping on CRAN'</span>)</span>
<span id="cb16-2"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> (<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">!</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">requireNamespace</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'database_driver'</span>, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">quietly =</span> <span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">TRUE</span>)) {</span>
<span id="cb16-3">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">exit_file</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'database_driver not installed'</span>)</span>
<span id="cb16-4">}</span>
<span id="cb16-5">...</span></code></pre></div>
<p><code>exit_file()</code> halts the current file with the supplied message recorded as a skip. There is no <code>tinytest</code> analogue of <code>skip_on_os()</code> or <code>skip_if_offline()</code>, but those checks are small enough that an <code>if (...) exit_file(...)</code> line at the top of the file is sufficient.</p>
</section>
<section id="pattern-5-snapshots" class="level2">
<h2 class="anchored" data-anchor-id="pattern-5-snapshots">Pattern 5: snapshots</h2>
<p>This is the pattern that does not port. <code>testthat</code> snapshots (<code>expect_snapshot()</code>, <code>expect_snapshot_value()</code>, <code>expect_snapshot_file()</code>) have no direct equivalent in <code>tinytest</code>. The available substitutes are:</p>
<ul>
<li>For text output, capture with <code>capture.output()</code> and compare to a reference string with <code>expect_equal()</code>.</li>
<li>For binary or large outputs, write a reference file once and compare with <code>tools::md5sum()</code> on each run.</li>
<li>For images, use <code>tinytest::expect_equal_to_reference()</code> or drop the test.</li>
</ul>
<p>In practice, snapshot-heavy suites are the strongest argument against converting. The migration cost is high, the failure modes are subtler than <code>expect_equal()</code>, and the resulting tests are noticeably less ergonomic than their <code>testthat</code> counterparts.</p>
</section>
</section>
<section id="description-changes" class="level1">
<h1>DESCRIPTION changes</h1>
<p>The package metadata changes in three places:</p>
<pre><code>Before                            After
------                            -----
Suggests:                         Suggests:
    testthat (&gt;= 3.0.0)               tinytest
Config/testthat/edition: 3        (line removed)</code></pre>
<p>If <code>testthat</code> is the only test framework being dropped, the removal is straightforward. If the package previously declared both, leave whichever frameworks are still in use.</p>
</section>
<section id="ci-github-actions" class="level1">
<h1>CI: GitHub Actions</h1>
<p>The standard <code>r-lib/actions/check-r-package</code> action runs <code>R CMD check</code>, which exercises whichever bootstrap file is in <code>tests/</code>. No CI changes are strictly required: a package with <code>tests/tinytest.R</code> will run its <code>tinytest</code> suite during <code>R CMD check</code> exactly as a <code>testthat</code> package does.</p>
<p>The one wrinkle is that <code>tinytest</code> errors are currently surfaced through <code>R CMD check</code> as ordinary stop conditions, not as a separate test report. If the CI surface relies on a structured test summary (for example, GitHub’s check-run output), an extra step that calls <code>tinytest::test_package()</code> directly and emits a TAP report may be useful:</p>
<div class="sourceCode" id="cb18" style="background: #f1f3f5;"><pre class="sourceCode yaml code-with-copy"><code class="sourceCode yaml"><span id="cb18-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">-</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> </span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">name</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> Run tinytest with TAP output</span></span>
<span id="cb18-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">  run</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">: </span><span class="ch" style="color: #20794D;
background-color: null;
font-style: inherit;">|</span></span>
<span id="cb18-3">    Rscript -e 'tinytest::test_package("mypkg",</span>
<span id="cb18-4">      side_effects = TRUE) |&gt; as.data.frame() |&gt; print()'</span></code></pre></div>
</section>
<section id="verification" class="level1">
<h1>Verification</h1>
<p>After the conversion, three commands should produce output consistent with the previous <code>testthat</code> run:</p>
<div class="sourceCode" id="cb19" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb19-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 1. Bootstrap runs the suite under R CMD check</span></span>
<span id="cb19-2"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">R</span> CMD check <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--as-cran</span> .</span>
<span id="cb19-3"></span>
<span id="cb19-4"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 2. Direct invocation reports per-file pass/fail</span></span>
<span id="cb19-5"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">Rscript</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-e</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'tinytest::test_package(".")'</span></span>
<span id="cb19-6"></span>
<span id="cb19-7"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 3. Single-file run, useful during development</span></span>
<span id="cb19-8"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">Rscript</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-e</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'tinytest::run_test_file(</span></span>
<span id="cb19-9"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  "inst/tinytest/test_arithmetic.R")'</span></span></code></pre></div>
<p>The total number of assertions reported in step 2 should match the <code>testthat</code> baseline, less any tests retired during the migration. If the count differs and the difference is not explained by an explicit retirement, a <code>test_that()</code> block likely contained more <code>expect_*</code> calls than the rewrite captured.</p>
</section>
<section id="daily-workflow" class="level1">
<h1>Daily Workflow</h1>
<table class="caption-top table">
<colgroup>
<col style="width: 44%">
<col style="width: 55%">
</colgroup>
<thead>
<tr class="header">
<th>Action</th>
<th>Command</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>Run the full suite</td>
<td><code>Rscript -e 'tinytest::test_package(".")'</code></td>
</tr>
<tr class="even">
<td>Run a single file</td>
<td><code>tinytest::run_test_file('inst/tinytest/test_X.R')</code></td>
</tr>
<tr class="odd">
<td>Run all files in a directory</td>
<td><code>tinytest::run_test_dir('inst/tinytest')</code></td>
</tr>
<tr class="even">
<td>Run during <code>R CMD check</code></td>
<td><code>R CMD check .</code> (uses <code>tests/tinytest.R</code>)</td>
</tr>
<tr class="odd">
<td>Build a coverage report</td>
<td><code>covr::package_coverage()</code> (works with both)</td>
</tr>
<tr class="even">
<td>Continuous run on file change</td>
<td><code>tinytest::test_package('.', side_effects = TRUE)</code></td>
</tr>
</tbody>
</table>
<!-- ============================================================================
AMBIANCE IMAGE 2: placed mid-body (terminal/editor)
============================================================================ -->
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/rp-testthat-to-tinytest/media/images/ambiance2.png" class="img-fluid figure-img"></p>
<figcaption>An espresso cup viewed from above on a wide hardwood floor, low-key lighting, single subject in deep shadow. Placeholder ambiance image; will be replaced with subject-specific imagery before publication.</figcaption>
</figure>
</div>
</section>
<section id="things-to-watch-out-for" class="level1">
<h1>Things to Watch Out For</h1>
<p>These are the gotchas that cost me time during the conversion. Several are not in the <code>tinytest</code> documentation because they are artefacts of the migration rather than the framework itself.</p>
<ol type="1">
<li><p><strong>Helper files are not auto-sourced.</strong> A <code>testthat</code> package with <code>tests/testthat/helper-data.R</code> gets that file sourced before every test file automatically. <code>tinytest</code> does not. Symptom: <code>object 'make_test_data' not found</code>. Fix: add <code>source('helper_fixtures.R', local = TRUE)</code> at the top of each consumer file.</p></li>
<li><p><strong><code>expect_message()</code> and <code>expect_warning()</code> regex matching.</strong> <code>testthat</code> accepts a regex as the second argument. <code>tinytest</code> does not. Symptom: tests pass that should not, because any message satisfies the assertion. Fix: assert the message content separately:</p>
<div class="sourceCode" id="cb20" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb20-1">m <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">capture.output</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">my_function</span>(), <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">type =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'message'</span>)</span>
<span id="cb20-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">expect_true</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">grepl</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'expected pattern'</span>, m))</span></code></pre></div></li>
<li><p><strong><code>expect_error()</code> returns the error invisibly.</strong> Capturing it for further inspection requires <code>e &lt;- expect_error(my_function())</code> which is the same idiom as <code>testthat</code>, but the error class is reported differently. <code>testthat</code>’s <code>class =</code> argument has no <code>tinytest</code> analogue; inspect <code>class(e)</code> manually.</p></li>
<li><p><strong><code>Config/testthat/edition: 3</code> lingers.</strong> Forgetting to remove this line from <code>DESCRIPTION</code> is harmless but confusing for future maintainers. Symptom: a <code>DESCRIPTION</code> that references a framework the package no longer uses. Fix: remove the line manually; no tooling performs this step automatically.</p></li>
<li><p><strong>Snapshot tests have no clean port.</strong> Listed above as pattern 5; worth re-stating. If a substantial fraction of the suite is <code>expect_snapshot_*()</code>, reconsider the migration.</p></li>
<li><p><strong>The RStudio test pane does not light up.</strong> <code>tinytest</code> integrates with RStudio’s build pane through <code>R CMD check</code> but does not populate the test runner pane the way <code>testthat</code> does. Symptom: ‘Run Tests’ button in the IDE no longer reports per-test results. Fix: run from the console or terminal; the IDE integration is unlikely to change.</p></li>
<li><p><strong>Parallelism requires explicit setup.</strong> <code>testthat</code> 3.0+ parallelises tests with <code>parallel = TRUE</code>. <code>tinytest</code> does not parallelise out of the box, though <code>tinytest::test_all()</code> accepts a <code>cl</code> argument for a <code>parallel</code> cluster. Suites with long-running per-file setup may run noticeably slower in serial.</p></li>
<li><p><strong>Four <code>testthat</code> 3 assertion functions silently resolve via <code>pkgload</code>.</strong> A real conversion of the <code>zzpower</code> package surfaced this: <code>expect_s3_class</code>, <code>expect_type</code>, <code>expect_named</code>, and <code>expect_no_error</code> are exported by <code>testthat</code> but not by <code>tinytest</code>. When a developer runs the converted suite via <code>pkgload::load_all()</code> followed by <code>tinytest::run_test_dir()</code>, <code>pkgload</code> auto-attaches every package in <code>Suggests</code>, including <code>testthat</code>. The four testthat assertions then resolve against the testthat namespace, the run reports ‘all ok’, and the migration appears complete. Symptom: the tinytest assertion count silently undercounts the testthat baseline. Fix: detach <code>testthat</code> before running the suite, or grep the converted files for these four function names and replace them with tinytest equivalents (<code>expect_inherits</code>, <code>expect_true(is.function(...))</code>, <code>expect_equal(sort(names(x)),    sort(...))</code>, and unwrapping the <code>expect_no_error</code> block). Recipe section 12 has the full mapping.</p></li>
<li><p><strong>Non-exported objects are invisible to <code>tinytest::test_package()</code>.</strong> <code>testthat::test_check()</code> runs tests with the package’s internal namespace exposed, so test files can reference non-exported helpers, constants, and S3 methods by their bare names. <code>tinytest::test_package()</code> calls <code>library(pkg)</code>, which only attaches exports. A test file that worked under testthat with <code>expect_true(exists('MY_CONST'))</code> will fail under tinytest with ‘object ’MY_CONST’ not found’, even though the original suite passed. Symptom: under <code>pkgload::load_all() + tinytest::run_test_dir()</code>, tests pass; under <code>R CMD check</code> (which uses <code>tinytest::test_package()</code> against the installed package), the same tests fail. Fix: either export the object via roxygen2 (<code>#' @export</code>) and re-run <code>devtools::document()</code>, or qualify the reference in the test using <code>getFromNamespace('MY_CONST', 'pkg')</code> or <code>pkg:::MY_CONST</code>. The qualified-reference variant is the safer choice for objects that should remain internal.</p></li>
<li><p><strong><code>R CMD check</code> on a source directory does not auto-derive <code>Author</code> and <code>Maintainer</code> from <code>Authors@R</code>.</strong> Under R 4.5.3 on macOS, <code>R CMD check &lt;directory&gt;</code> fails with ‘Required fields missing or empty: Author Maintainer’ when the DESCRIPTION declares only <code>Authors@R</code>. The same DESCRIPTION passes when checked via <code>R CMD build</code> followed by <code>R CMD check &lt;tarball&gt;</code>. The cause is in <code>tools:::.read_description()</code>, which reads the DCF file without injecting derived fields. Symptom: a check that was working last month suddenly errors at the very first step. Fix: prefer the canonical workflow (<code>R CMD build .</code> then <code>R CMD check pkg_X.Y.Z.tar.gz</code>); if direct source-dir checking is required, add explicit <code>Author:</code> and <code>Maintainer:</code> lines to DESCRIPTION alongside <code>Authors@R</code>.</p></li>
<li><p><strong><code>skip_if_not()</code> inside <code>test_that()</code> was scoped to one test; a flat conversion may halt the entire file.</strong> A naive textual rewrite of <code>test_that('memory test', { skip_if_not(...); ... })</code> produces a top-level <code>if (!cond) exit_file(...)</code> followed by the body. <code>exit_file()</code> halts the entire test file, but in the original the skip applied only to that one <code>test_that</code> block; subsequent blocks ran normally. Symptom: the converted suite passes but reports significantly fewer assertions than the testthat baseline, with one entire file showing zero results. Fix: replace the file-level <code>exit_file()</code> with a block-level <code>if (cond) { ... }</code> wrapper around just the assertions that depend on the condition. Recipe section 6 now documents both forms; pick the one that matches the original scope.</p></li>
<li><p><strong>Argument-name differences in <code>expect_error</code> and <code>expect_warning</code>.</strong> testthat accepts both <code>regex =</code> and <code>regexp =</code> as the named-argument form for the matching pattern. tinytest accepts only <code>pattern =</code>. Symptom: <code>unused argument (regex = "...")</code> or <code>unused argument (regexp = "...")</code> errors during the tinytest run. Fix: rename both to <code>pattern =</code>. The recipe now lists this in section 12.</p></li>
</ol>
</section>
<section id="uninstall-rollback" class="level1">
<h1>Uninstall / Rollback</h1>
<p>If the conversion turns out to be a poor fit (snapshot-heavy suite, IDE-dependent workflow, parallelisation requirements), the rollback is straightforward because the original <code>tests/testthat/</code> directory has not been deleted. The git history preserves it.</p>
<div class="sourceCode" id="cb21" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb21-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 1. Restore tests/testthat/ from git</span></span>
<span id="cb21-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> checkout HEAD~N <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--</span> tests/testthat tests/testthat.R</span>
<span id="cb21-3"></span>
<span id="cb21-4"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 2. Remove the tinytest scaffolding</span></span>
<span id="cb21-5"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">rm</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-rf</span> inst/tinytest tests/tinytest.R</span>
<span id="cb21-6"></span>
<span id="cb21-7"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 3. Restore DESCRIPTION</span></span>
<span id="cb21-8"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> checkout HEAD~N <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--</span> DESCRIPTION</span></code></pre></div>
<p>The <code>HEAD~N</code> placeholder is whatever commit preceded the migration. The cleaner approach is to perform the conversion on a branch and revert the branch if needed, rather than committing the migration to <code>main</code>.</p>
<!-- ============================================================================
AMBIANCE IMAGE 3: placed before Lessons Learnt (workflow scene)
============================================================================ -->
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/rp-testthat-to-tinytest/media/images/ambiance3.png" class="img-fluid figure-img"></p>
<figcaption>A close-up of a single espresso drip falling from a machine into a grey ceramic mug. Placeholder ambiance image; will be replaced with subject-specific imagery before publication.</figcaption>
</figure>
</div>
</section>
<section id="what-did-we-learn" class="level1">
<h1>What Did We Learn?</h1>
<section id="lessons-learnt" class="level2">
<h2 class="anchored" data-anchor-id="lessons-learnt">Lessons Learnt</h2>
<p><strong>Conceptual.</strong></p>
<ul>
<li>The unit of testing is a design choice, not a given. <code>testthat</code> treats the assertion as the unit and groups assertions into <code>test_that()</code> closures. <code>tinytest</code> treats the file as the unit and treats assertions as top-level statements. Neither is strictly better; the choice has consequences for fixture scope, error reporting, and how new tests are added.</li>
<li>A test framework’s dependency surface matters more than the number of features it offers. A package whose tests pull in forty transitive dependencies pays an installation cost on every CI run, whether or not the features are used.</li>
<li>‘Snapshot testing’ is a separable concern from unit testing. Treating the two as a single problem (which <code>testthat</code> does via <code>expect_snapshot_*()</code>) makes the framework richer but also more entangled. <code>tinytest</code> declines to merge them, which forces a deliberate choice about whether snapshots belong in the test suite at all.</li>
<li>Assertions read more naturally when they are not nested inside a description string. The test-name parameter that <code>test_that()</code> requires is information that the file name and line number carry implicitly; making it explicit is helpful in some cases and ceremonial in others.</li>
</ul>
<p><strong>Technical.</strong></p>
<ul>
<li><code>tinytest::expect_equal()</code> defaults to <code>tolerance = 1e-6</code>, matching <code>base::all.equal()</code> rather than <code>testthat::expect_equal()</code>’s newer default of strict equality with a configurable tolerance. Tests that previously passed under <code>testthat</code> 3 may need an explicit <code>tolerance = 0</code> on the <code>tinytest</code> side, or vice versa.</li>
<li>The order in which test files run matters more than under <code>testthat</code>. <code>tinytest</code> evaluates files in alphabetical order by default. If a file has side effects on the global state (writes to disk, changes options, alters the working directory), later files will see them.</li>
<li><code>tinytest::test_package()</code> returns a <code>tinytests</code> object whose <code>as.data.frame()</code> method gives a per-assertion table. This is a more programmatic interface than <code>testthat::test_local()</code>’s console-output focus, and pairs well with custom CI summaries.</li>
<li>Coverage tools (<code>covr</code>) work without modification. The package introspection that <code>covr</code> performs is framework- agnostic; it instruments source files rather than test files.</li>
</ul>
<p><strong>Gotchas.</strong></p>
<ul>
<li>Forgetting to rename <code>helper-*.R</code> to <code>helper_*.R</code> (dash vs underscore). <code>tinytest</code> does not source files starting with <code>helper</code>; the convention is purely human-facing, so a typo here is silent.</li>
<li>Treating <code>inst/tinytest/</code> as private. Because tests live in <code>inst/</code>, they are installed with the package and visible to users. This is a feature (users can run the suite to verify their installation) but it means the test files should not contain credentials, paths to private servers, or other artefacts that do not belong in a published package.</li>
<li>Mixing <code>testthat</code> and <code>tinytest</code> during a partial migration. Both bootstraps will run during <code>R CMD check</code>. If the same assertion is duplicated, the count doubles; if it is split, the report fragments. Pick one and finish before merging.</li>
<li>Underestimating the snapshot-test inventory. Run <code>grep -r 'expect_snapshot' tests/testthat/</code> before starting. If the count is non-trivial, the migration will be longer than expected.</li>
</ul>
</section>
<section id="limitations" class="level2">
<h2 class="anchored" data-anchor-id="limitations">Limitations</h2>
<ul>
<li><code>tinytest</code> lacks built-in snapshot testing, parallel test execution, and IDE integration with RStudio’s test pane. For packages that depend on any of these, the migration is a net loss.</li>
<li>The framework’s spartan design means that some <code>testthat</code> affordances (parameterised tests via <code>patrick</code>, fixtures via <code>withr::local_*</code>) require manual reimplementation.</li>
<li>Error messages in <code>tinytest</code> are less informative by default than <code>testthat</code> 3’s <code>waldo</code>-powered diffs. For numeric or list comparison, the diff output is the single feature <code>tinytest</code> users miss most.</li>
<li>The framework has a small user community relative to <code>testthat</code>. Searching for solutions to specific assertion patterns yields fewer results, and the available answers are sometimes outdated.</li>
</ul>
</section>
<section id="opportunities-for-improvement" class="level2">
<h2 class="anchored" data-anchor-id="opportunities-for-improvement">Opportunities for Improvement</h2>
<ol type="1">
<li>Build a <code>tinytest</code> linter that flags accidental <code>testthat</code> idioms (<code>test_that()</code> calls, <code>skip_on_*()</code>, regex arguments to <code>expect_message</code>) during the migration.</li>
<li>Wrap the common conversion patterns in a small package (<code>tinyport</code>?) that automates the file-rename, header rewrite, and DESCRIPTION update steps.</li>
<li>Add a <code>tinytest</code>-aware action to GitHub’s R-lib actions collection so that the structured test-summary step described above is one click away.</li>
<li>Document the snapshot-equivalent patterns more thoroughly. The CRAN vignette covers the basics; a longer reference that addresses image, JSON, and structured-output snapshots would close a real gap.</li>
<li>Build a per-package decision script that reports the number of <code>expect_snapshot_*()</code> calls, the number of <code>helper-*.R</code> files, and an estimated hour-cost for the migration.</li>
<li>Contribute a <code>parallel = TRUE</code> argument to <code>tinytest::test_package()</code> that mirrors <code>testthat</code>’s built-in parallelisation.</li>
</ol>
</section>
</section>
<section id="wrapping-up" class="level1">
<h1>Wrapping Up</h1>
<p>The conversion is a small project, on the order of an afternoon for a package with a few hundred assertions and no snapshots. The mechanical work is straightforward and the patterns repeat. What takes the time is the audit: deciding which <code>testthat</code> features the package actually uses, which can be replaced, and which signal that the migration is the wrong call.</p>
<p>The decision matrix is short. If the package uses snapshot tests heavily, depends on RStudio’s IDE integration, or relies on parallel test execution, stay on <code>testthat</code>. If the package is small, has no snapshots, and is tested in CI rather than interactively, the migration is mostly upside: fewer dependencies, faster <code>R CMD check</code>, a smaller cognitive surface.</p>
<p>This post stops short of recommending the conversion as a default. Both frameworks are well-engineered. The choice between them is an architectural decision about how much ceremony a test framework should impose, and that decision is package-specific.</p>
<section id="wrapping-up-1" class="level2">
<h2 class="anchored" data-anchor-id="wrapping-up-1">Wrapping Up</h2>
<p>In conclusion, six points merit emphasis. First, the mechanical mapping is mostly one-to-one; snapshots are the exception. Second, the <code>inst/tinytest/</code> placement makes tests installable, which is the largest conceptual shift. Third, helper files are not auto-sourced, so explicit <code>source()</code> calls are required. Fourth, <code>Config/testthat/edition</code> lingers in <code>DESCRIPTION</code> if the line is not removed manually. Fifth, the migration is reversible; performing it on a branch is the recommended practice. Sixth, snapshot-heavy suites are a strong reason to remain on <code>testthat</code>.</p>
</section>
</section>
<section id="see-also" class="level1">
<h1>See Also</h1>
<ul>
<li><code>tinytest</code> on CRAN: <a href="https://cran.r-project.org/package=tinytest" class="uri">https://cran.r-project.org/package=tinytest</a></li>
<li>van der Loo, M. (2020). ‘tinytest: A Lightweight and Feature Complete Unit Testing Framework for R Packages’. CRAN vignette.</li>
<li><code>testthat</code> documentation: <a href="https://testthat.r-lib.org/" class="uri">https://testthat.r-lib.org/</a></li>
<li>Post 40 (testing for data analysis workflow), in this blog, for the broader context of testing in compendium-style R projects.</li>
<li>Post 61 (zzcollab analysis checklist), in this blog, for an example of <code>tinytest</code> and <code>testthat</code> used side-by-side in a single project.</li>
</ul>
<hr>
</section>
<section id="reproducibility" class="level1">
<h1>Reproducibility</h1>
<table class="caption-top table">
<colgroup>
<col style="width: 30%">
<col style="width: 16%">
<col style="width: 53%">
</colgroup>
<thead>
<tr class="header">
<th>Component</th>
<th>Version</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>OS</td>
<td>macOS 15.4</td>
<td>also tested on Ubuntu 24.04</td>
</tr>
<tr class="even">
<td>R</td>
<td>4.4.1</td>
<td>matches <code>renv.lock</code></td>
</tr>
<tr class="odd">
<td><code>tinytest</code></td>
<td>1.4.1</td>
<td>from CRAN, 2024-02 release</td>
</tr>
<tr class="even">
<td><code>testthat</code></td>
<td>3.2.1</td>
<td>reference baseline</td>
</tr>
<tr class="odd">
<td><code>covr</code></td>
<td>3.6.4</td>
<td>optional, used for coverage</td>
</tr>
<tr class="even">
<td>Date verified</td>
<td>2026-05-02</td>
<td>claims in this post tested on this date</td>
</tr>
</tbody>
</table>
<div class="sourceCode" id="cb22" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb22-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sessionInfo</span>()</span></code></pre></div>
<hr>
</section>
<section id="appendix-a" class="level1">
<h1>Appendix A: Conversion Atlas</h1>
<p>A condensed mapping of the most common <code>testthat</code> idioms to their <code>tinytest</code> equivalents. The full table, with paired examples for each row, is in <code>docs/testthat-to-tinytest-recipe.qmd</code> in this post’s compendium.</p>
<table class="caption-top table">
<colgroup>
<col style="width: 47%">
<col style="width: 52%">
</colgroup>
<thead>
<tr class="header">
<th>testthat</th>
<th>tinytest</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><code>expect_equal(x, y)</code></td>
<td><code>expect_equal(x, y)</code></td>
</tr>
<tr class="even">
<td><code>expect_identical(x, y)</code></td>
<td><code>expect_identical(x, y)</code></td>
</tr>
<tr class="odd">
<td><code>expect_true(x)</code></td>
<td><code>expect_true(x)</code></td>
</tr>
<tr class="even">
<td><code>expect_false(x)</code></td>
<td><code>expect_false(x)</code></td>
</tr>
<tr class="odd">
<td><code>expect_error(f(), 'msg')</code></td>
<td><code>expect_error(f(), pattern = 'msg')</code></td>
</tr>
<tr class="even">
<td><code>expect_warning(f(), 'msg')</code></td>
<td><code>expect_warning(f(), pattern = 'msg')</code></td>
</tr>
<tr class="odd">
<td><code>expect_message(f(), 'msg')</code></td>
<td><code>expect_message(f(), pattern = 'msg')</code></td>
</tr>
<tr class="even">
<td><code>expect_null(x)</code></td>
<td><code>expect_null(x)</code></td>
</tr>
<tr class="odd">
<td><code>expect_silent(f())</code></td>
<td><code>expect_silent(f())</code></td>
</tr>
<tr class="even">
<td><code>expect_snapshot(x)</code></td>
<td>(no direct equivalent; see pattern 5)</td>
</tr>
<tr class="odd">
<td><code>expect_s3_class(x, 'cls')</code></td>
<td><code>expect_inherits(x, 'cls')</code></td>
</tr>
<tr class="even">
<td><code>expect_is(x, 'cls')</code></td>
<td><code>expect_inherits(x, 'cls')</code></td>
</tr>
<tr class="odd">
<td><code>expect_type(x, 'closure')</code></td>
<td><code>expect_true(is.function(x))</code></td>
</tr>
<tr class="even">
<td><code>expect_type(x, 'double')</code></td>
<td><code>expect_equal(typeof(x), 'double')</code></td>
</tr>
<tr class="odd">
<td><code>expect_named(x, c(...))</code></td>
<td><code>expect_equal(sort(names(x)), sort(c(...)))</code></td>
</tr>
<tr class="even">
<td><code>expect_no_error({block})</code></td>
<td>(unwrap; inner assertions stand on their own)</td>
</tr>
<tr class="odd">
<td><code>expect_lt(x, y)</code></td>
<td><code>expect_true(x &lt; y)</code></td>
</tr>
<tr class="even">
<td><code>expect_gt(x, y)</code></td>
<td><code>expect_true(x &gt; y)</code></td>
</tr>
<tr class="odd">
<td><code>expect_lte(x, y)</code></td>
<td><code>expect_true(x &lt;= y)</code></td>
</tr>
<tr class="even">
<td><code>expect_gte(x, y)</code></td>
<td><code>expect_true(x &gt;= y)</code></td>
</tr>
<tr class="odd">
<td><code>expect_error(f(), regex = 'msg')</code></td>
<td><code>expect_error(f(), pattern = 'msg')</code></td>
</tr>
<tr class="even">
<td><code>expect_warning(f(), regexp = 'msg')</code></td>
<td><code>expect_warning(f(), pattern = 'msg')</code></td>
</tr>
<tr class="odd">
<td><code>skip_on_cran()</code></td>
<td><code>if (!nzchar(Sys.getenv('NOT_CRAN'))) exit_file('CRAN')</code></td>
</tr>
<tr class="even">
<td><code>skip_if_not_installed('pkg')</code></td>
<td><code>if (!requireNamespace('pkg', quietly = TRUE)) exit_file('pkg missing')</code></td>
</tr>
<tr class="odd">
<td><code>skip_on_ci()</code></td>
<td><code>if (nzchar(Sys.getenv('CI'))) exit_file('CI')</code></td>
</tr>
<tr class="even">
<td><code>skip_if_not(cond, msg)</code> (per-test)</td>
<td><code>if (cond) { ... }</code> (block-level wrapper)</td>
</tr>
<tr class="odd">
<td><code>context('label')</code></td>
<td>(remove; tinytest has no equivalent)</td>
</tr>
<tr class="even">
<td><code>library(testthat)</code> in test files</td>
<td>(remove; tinytest is implicit at runtime)</td>
</tr>
<tr class="odd">
<td><code>helper-foo.R</code> (auto-sourced)</td>
<td><code>source('helper_foo.R', local = TRUE)</code></td>
</tr>
</tbody>
</table>
</section>
<section id="appendix-b" class="level1">
<h1>Appendix B: A Real Migration on <code>zzpower</code></h1>
<p>The recipe was validated on <code>zzpower</code> v0.3.0, an interactive Shiny power-analysis package, on branch <code>testthat-to-tinytest</code>. Pre-migration: 10 testthat files, 1,734 LOC, 602 expectations passing under <code>testthat::test_dir()</code>. Post-migration: 10 tinytest files, 1,625 LOC, 600 assertions passing under <code>tinytest::run_test_dir()</code> with <code>testthat</code> detached.</p>
<p>The 6% line-count reduction (109 lines) came almost entirely from removing <code>test_that()</code> wrappers and de-indenting bodies by two spaces. The 2-assertion delta is exactly the two <code>expect_no_error()</code> outer-wrapper counts that no longer count as assertions in their own right; the inner expectations they guarded are still counted.</p>
<p>The migration was not entirely mechanical. Four <code>testthat</code> 3 assertion functions had no direct port and required manual substitution: <code>expect_s3_class</code> (9 calls; replaced with <code>expect_inherits</code>), <code>expect_type</code> (3 calls; replaced with <code>expect_true(is.function(...))</code> for the closure case and <code>expect_equal(typeof(...), ...)</code> otherwise), <code>expect_named</code> (1 call; replaced with <code>expect_equal(sort(names(x)), ...)</code>), and <code>expect_no_error</code> (2 calls; the wrapper was removed). These four patterns were not in the recipe’s first draft, because the conversion script (which wrapped a textual brace-aware transform around <code>test_that()</code> calls) preserved them verbatim. The ‘Things to Watch Out For’ section now includes these patterns as gotcha 8, and the recipe has a new section 12 with paired examples.</p>
<p>A subtler trap surfaced during dry-running. <code>pkgload::load_all()</code> auto-attaches every package in <code>Suggests</code>, which means the testthat namespace becomes available even after the migration removes testthat from the bootstrap. The four testthat-only assertion functions then resolve silently against testthat’s exports, the run reports ‘all ok’, and the migration appears complete. The honest dry-run requires either detaching testthat explicitly or removing it from <code>Suggests</code> before verification. Only after <code>tinytest::run_test_dir()</code> reports a result count consistent with the testthat baseline (less the known <code>expect_no_error</code> deltas) is the migration validated.</p>
<p><code>R CMD check</code> was attempted in two configurations and surfaced two more issues, neither in the recipe’s first draft.</p>
<p>The first configuration, <code>R CMD check &lt;source-directory&gt;</code>, failed at the very first step with ‘Required fields missing or empty: Author Maintainer’. Investigation showed that this is not a parsing issue but a behavioural one: <code>tools:::.read_description()</code>, which <code>R CMD check</code> calls to load the DESCRIPTION, does not auto-derive <code>Author</code> and <code>Maintainer</code> from <code>Authors@R</code>. The same DESCRIPTION passes when checked via the canonical <code>R CMD build .</code> followed by <code>R CMD check pkg_X.Y.Z.tar.gz</code>. The lesson generalises: prefer the build-then-check workflow; if direct source-directory checks are required, declare <code>Author</code> and <code>Maintainer</code> explicitly. Recipe section 17 and gotcha 10 now document this.</p>
<p>The second configuration, <code>R CMD check &lt;tarball&gt;</code>, passed the DESCRIPTION step but failed inside <code>tests/tinytest.R</code> with ‘could not find function “build_sample_size_inputs”’. The cause is that <code>tinytest::test_package('zzpower')</code> calls <code>library('zzpower')</code>, which only attaches exported objects. zzpower’s <code>test_framework.R</code> referenced six non-exported helpers (<code>build_advanced_settings</code>, <code>build_effect_size_inputs</code>, <code>build_sample_size_inputs</code>, <code>get_effect_size_range</code>, <code>logrank_power</code>, <code>trend_power</code>) by their bare names, which worked under <code>testthat::test_check()</code> but not under tinytest. The fix added six <code>getFromNamespace()</code> assignments at the top of <code>test_framework.R</code>, bringing the helpers into the test file’s scope without modifying the package’s NAMESPACE. After the fix, <code>R CMD check</code> reported <code>Status: OK</code> on the resulting tarball. Recipe section 20 and gotcha 9 document the diagnostic and the fix.</p>
<p>Note that <code>ZZPOWER_CONSTANTS</code>, which my first investigation identified as the offender, is in fact exported (<code>export(ZZPOWER_CONSTANTS)</code> in the generated NAMESPACE) and accessible through the search path after <code>library('zzpower')</code>. The earlier failure was a misdiagnosis from a stale install. The lesson here is operational: when an earlier check-run leaves a stale binary in the test library, its NAMESPACE may not match the source. Always rebuild and reinstall when chasing ‘object not found’ errors that contradict a current <code>getNamespaceExports()</code> inspection.</p>
<p>Neither issue invalidates the recipe; both refine it. The final state on the validation branch: <code>R CMD check zzpower_0.4.0.tar.gz</code> returns <code>Status: OK</code>, with all non-skipped checks passing. The total cost of using <code>zzpower</code> as the validation case study was higher than expected (roughly four hours rather than the ‘half a day to a day’ my pre-migration estimate had) because each unanticipated failure required tracing R-internal code to diagnose. The pay-off is that the recipe has been hardened against three real failure modes (the four testthat-only assertion functions, the pkgload-namespace contamination, and the <code>library</code> vs <code>loadNamespace</code> test- visibility difference) before any reader attempts the migration on their own package.</p>
<p>The migration commit and follow-up commits are on branch <code>testthat-to-tinytest</code> of the zzpower repository.</p>
<p>Time spent: roughly two hours, including writing the brace-aware conversion script (about 30 lines of R), running two dry-runs to surface the testthat-namespace contamination issue, and patching the recipe with the four newly-discovered patterns. The migration commit and follow-up commits are on branch <code>testthat-to-tinytest</code> of the zzpower repository.</p>
<section id="second-case-study-zztable1" class="level2">
<h2 class="anchored" data-anchor-id="second-case-study-zztable1">Second case study: <code>zztable1</code></h2>
<p><code>zzpower</code> was small enough that the recipe gaps it surfaced might have been idiosyncratic. A second migration on <code>zztable1</code> (publication-quality summary tables, version 0.5.0; 13 testthat files, 2,393 LOC, 564 passing assertions plus 1 skipped) exercised the recipe at roughly twice the scale and exposed five additional patterns the first case missed.</p>
<p>Pre-migration, <code>zztable1</code> had: 36 <code>expect_s3_class</code> calls, 24 <code>expect_is</code> calls (testthat’s older alias for the same operation), 19 <code>expect_type</code> calls, 18 <code>expect_lt</code> calls and one <code>expect_gte</code>, and several <code>skip_*</code> invocations including an in-block <code>skip_if_not(capabilities('long.double'), ...)</code> inside a <code>test_that</code> block. It also had two condition matchers using the <code>regex =</code> and <code>regexp =</code> argument names, a <code>library(testthat)</code> line in 10 of the 13 files, and a single <code>context(...)</code> call.</p>
<p>The migration applied the pattern table in section 12 and the conversion-atlas additions, with the following new findings folded back into the recipe:</p>
<ol type="1">
<li><strong>Ordering assertions (<code>expect_lt</code>, <code>expect_gt</code>, <code>expect_lte</code>, <code>expect_gte</code>) have no tinytest equivalent.</strong> Replace with <code>expect_true(x &lt; y)</code> and similar forms. This added 38 substitutions in <code>zztable1</code>; the same pattern added zero in <code>zzpower</code> because that package’s tests happened not to use ordering assertions.</li>
<li><strong><code>expect_is</code> is testthat’s deprecated alias for <code>expect_s3_class</code>.</strong> It behaves identically; the substitute is <code>expect_inherits</code>. zztable1’s test suite used both forms inconsistently across files.</li>
<li><strong><code>expect_error</code> and <code>expect_warning</code> accept both <code>regex =</code> and <code>regexp =</code> as the matching-argument name.</strong> tinytest accepts only <code>pattern =</code>. Both names need to be renamed.</li>
<li><strong>A <code>skip_if_not()</code> call placed inside a <code>test_that()</code> block was scoped to that block alone in testthat.</strong> A naive flat conversion places <code>exit_file()</code> at the file level, which halts the entire file. This dropped 12 assertions from <code>test_performance.R</code> until the conversion was rewritten as a block-level <code>if (cond) { ... }</code> wrapper.</li>
<li><strong><code>library(testthat)</code> calls inside test files and bare <code>context('...')</code> declarations</strong> are testthat-specific and must be removed from the converted files. Neither surfaces as an error if the testthat package is still installed (because <code>library(testthat)</code> succeeds and <code>context</code> is exported), but both are noise that will error once testthat is finally removed from <code>Suggests</code>.</li>
</ol>
<p>After applying these substitutions, <strong>564 of 564 expected assertions passed</strong> under <code>tinytest::test_package('zztable1')</code>. <code>R CMD check zztable1_0.5.0.tar.gz</code> reports <code>checking tests ... OK</code>. Three other warnings remain (<code>build</code> directory, <code>dependencies in R code</code>, <code>package vignettes</code>), all stemming from a missing <code>zzobj2fig</code> package in <code>Suggests/Remotes</code> and unrelated to the migration.</p>
<p>Time spent on <code>zztable1</code>: approximately three hours. The incremental cost over <code>zzpower</code> came almost entirely from the five new patterns; the brace-aware conversion script itself ran without modification. The recipe has been updated to cover all nine testthat-only assertion functions and the two scoping issues, so a third migration should not turn up additional surprises at this scale.</p>
<hr>
</section>
</section>
<section id="appendix-c-the-zzedc-migration-origins-of-the-translator" class="level1">
<h1>Appendix C: The <code>zzedc</code> Migration (Origins of the Translator)</h1>
<p>The recipe in this post was hardened against the <code>zzedc</code> electronic data capture package before <code>zzpower</code> or <code>zztable1</code> were attempted. <code>zzedc</code> is the largest package in the portfolio: 51 test files, 989 test blocks, 3,064 assertions. This appendix records why the migration was attempted on that package first, what the Python translator was designed to handle, and the seven-round arc that produced the post-round-7 artifact now in the companion repository.</p>
<section id="c.1-when-migration-is-worth-the-cost" class="level2">
<h2 class="anchored" data-anchor-id="c.1-when-migration-is-worth-the-cost">C.1 When migration is worth the cost</h2>
<p>Most R packages should leave <code>testthat</code> alone. The framework is well documented, well integrated with <code>usethis</code> and <code>devtools</code>, and has a much larger user base than any alternative. If a package is already working well on <code>testthat</code>, that is the correct choice.</p>
<p>For a smaller class of packages the dependency calculus is different. <code>testthat</code> 3 brings roughly thirty transitive packages into a package’s <code>Suggests</code> field. <code>tinytest</code>, by Mark van der Loo, brings zero. The migration becomes worth considering in five concrete contexts:</p>
<ul>
<li>CRAN-bound packages where reviewers see and weigh every dependency.</li>
<li>CI matrices where each row reinstalls <code>testthat</code> and its full transitive tail.</li>
<li>Long-term reproducibility stacks (Docker images, <code>renv</code> lockfiles) where a thirty-package surface ages less gracefully than a one-package surface.</li>
<li>Pharmaverse and FDA-submission contexts, where every dependency appears in a software bill of materials and must be justified.</li>
<li>Embedded R deployments (AWS Lambda, scratch containers, minimal Posit Connect images) where install size matters.</li>
</ul>
<p>For <code>zzedc</code>, the relevant constraint was the third and fourth: the package targets clinical research environments where the dependency graph is itself a documentation artifact, and the existing research-compendium workflow already exercised <code>tinytest</code> elsewhere in the portfolio. If none of these apply, the migration is not worth the disruption.</p>
</section>
<section id="c.2-the-naive-approach-and-why-it-fails" class="level2">
<h2 class="anchored" data-anchor-id="c.2-the-naive-approach-and-why-it-fails">C.2 The naive approach and why it fails</h2>
<p>The temptation is to reach for <code>sed</code>. Most <code>expect_*</code> names are shared between the two frameworks, and the obvious script handles a useful fraction of cases:</p>
<div class="sourceCode" id="cb23" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb23-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sed</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-E</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb23-2">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-e</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'s/library\(testthat\)/library(tinytest)/'</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb23-3">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-e</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'/^context\(/d'</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb23-4">  tests/testthat/test-<span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">*</span>.R</span></code></pre></div>
<p>The result parses. It also fails in at least four substantive ways.</p>
<p>First, <code>test_that()</code> has no <code>tinytest</code> analogue. The wrapper must be removed. A naive deletion of the wrapping line and its closing brace breaks block scoping: <code>testthat</code> runs each block in a fresh function frame, so <code>on.exit()</code> fires at the end of the block and local variables do not leak. Top-level tinytest code has neither property.</p>
<p>Second, <code>skip_if(requireNamespace('haven', quietly = TRUE), 'msg')</code> contains a comma inside the inner call. A regex that splits at the first comma will mangle the condition. The translator needs a paren-balanced parser, not a regex.</p>
<p>Third, multi-line string literals containing YAML, SQL, or JSON fixtures often use indentation that is part of the string content. An early version of the translator de-indented test bodies to compensate for the dropped wrapper, which silently corrupted those literals.</p>
<p>Fourth, <code>expect_warning(x, NA)</code> is <code>testthat</code> idiom for ‘expect no warning’. A direct translation passes <code>NA</code> to <code>tinytest::expect_warning</code> and reports a confusing failure. The correct rewrite is <code>expect_silent(x)</code>.</p>
<p>These are not edge cases. All four appeared in the first hundred lines of the <code>zzedc</code> test suite.</p>
</section>
<section id="c.3-what-the-python-translator-handles" class="level2">
<h2 class="anchored" data-anchor-id="c.3-what-the-python-translator-handles">C.3 What the Python translator handles</h2>
<p>The Python translator (<code>testthat_to_tinytest.py</code>) handles the patterns below. Each is a fix for a class of failure surfaced during the <code>zzedc</code> migration.</p>
<p><strong>Block dropping with scope preservation.</strong> <code>test_that('desc', { ... })</code> becomes a <code># Test: desc</code> comment plus <code>local({ ... })</code> around the body:</p>
<div class="sourceCode" id="cb24" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb24-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">test_that</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'creates database'</span>, {</span>
<span id="cb24-2">  con <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">dbConnect</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">SQLite</span>(), <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">':memory:'</span>)</span>
<span id="cb24-3">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">on.exit</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">dbDisconnect</span>(con))</span>
<span id="cb24-4">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">expect_true</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">dbIsValid</span>(con))</span>
<span id="cb24-5">})</span></code></pre></div>
<p>becomes</p>
<div class="sourceCode" id="cb25" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb25-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Test: creates database</span></span>
<span id="cb25-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">local</span>({</span>
<span id="cb25-3">  con <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">dbConnect</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">SQLite</span>(), <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">':memory:'</span>)</span>
<span id="cb25-4">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">on.exit</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">dbDisconnect</span>(con))</span>
<span id="cb25-5">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">expect_true</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">dbIsValid</span>(con))</span>
<span id="cb25-6">})</span></code></pre></div>
<p>The <code>local()</code> wrapper is the cheapest way to recover testthat’s function-frame semantics without rewriting every test.</p>
<p><strong>Nested BDD blocks.</strong> <code>describe(...) { it(...) { ... } }</code> is handled by iterating the block translator to a fixed point. Each pass strips one layer; ten passes covers everything seen in practice.</p>
<p><strong>Comparison rewrites.</strong> <code>tinytest</code> does not provide <code>expect_lt</code>/<code>expect_gt</code>/<code>expect_lte</code>/<code>expect_gte</code> as first-class expectations. The translator rewrites them to <code>expect_true(x &lt; y)</code> and so on. Similarly <code>expect_length(x, n)</code> becomes <code>expect_equal(length(x), n)</code>, <code>expect_null(x)</code> becomes <code>expect_true(is.null(x))</code>, <code>expect_s3_class(x, 'c')</code> becomes <code>expect_true(inherits(x, 'c'))</code>, and <code>expect_no_error(expr)</code> becomes <code>expect_silent(expr)</code>.</p>
<p><strong>No-warning idiom.</strong> <code>expect_warning(x, NA)</code> and <code>expect_warning(x, regexp = NA)</code> translate to <code>expect_silent(x)</code>. The same applies to <code>expect_error(x, NA)</code>.</p>
<p><strong>Argument renames.</strong> <code>regexp = '...'</code> becomes <code>pattern = '...'</code> for <code>tinytest</code>’s <code>expect_match</code> and related functions.</p>
<p><strong>Paren-balanced skip translation.</strong> <code>skip_if(requireNamespace('haven', quietly = TRUE), 'msg')</code> is parsed as a single call with two top-level arguments. The result is <code>if ((requireNamespace('haven', quietly = TRUE))) exit_file('msg')</code>.</p>
<p><strong>Block-scoped skip semantics.</strong> <code>tinytest</code>’s <code>exit_file()</code> aborts the entire file, whereas testthat’s <code>skip_*</code> aborts only the current block. When a <code>skip_*</code> call is the first directive of a <code>test_that</code> body, the translator wraps the rest of the block in <code>if (!cond) local({ ... })</code> rather than emitting a top-level <code>exit_file()</code>. The skip stays scoped to its original block.</p>
</section>
<section id="c.4-the-helper-file-consolidation-problem" class="level2">
<h2 class="anchored" data-anchor-id="c.4-the-helper-file-consolidation-problem">C.4 The helper-file consolidation problem</h2>
<p><code>testthat</code> auto-loads any file in <code>tests/testthat/</code> whose name begins with <code>helper-</code>. <code>tinytest</code> has no such convention. It does auto-load files in <code>inst/tinytest/</code> that begin with an underscore, but only when invoked through <code>tinytest::test_package()</code>; running individual files via <code>tinytest::run_test_file()</code> does not trigger the auto-load.</p>
<p>The convention used for <code>zzedc</code> is to consolidate the four <code>helper-*.R</code> files (<code>helper-test-setup.R</code>, <code>helper-skip.R</code>, <code>helper-db-backends.R</code>, <code>helper-test-utilities.R</code>) into a single <code>inst/tinytest/_setup.R</code>, and to source it explicitly from the top of each translated test file:</p>
<div class="sourceCode" id="cb26" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb26-1"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> (<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">file.exists</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'_setup.R'</span>)) <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">source</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'_setup.R'</span>)</span></code></pre></div>
<p>This is honest: one large setup file is less ergonomic than four small ones, but <code>tinytest</code>’s model is simpler for users to reason about, and the explicit <code>source()</code> keeps individual test files runnable without invoking the package-level harness.</p>
<p>The translator emits the <code>source()</code> line; the consolidated <code>_setup.R</code> is hand-written. The <code>zzedc</code> <code>_setup.R</code> runs to about 680 lines, combining database fixtures, multi-backend test harnesses, custom expectations, and <code>Sys.setenv</code>/<code>Sys.unsetenv</code> lifecycle helpers. The <code>testthat::skip()</code> call is reimplemented as a thin wrapper around <code>exit_file()</code> so existing helper code continues to compile.</p>
</section>
<section id="c.5-the-seven-round-migration-arc" class="level2">
<h2 class="anchored" data-anchor-id="c.5-the-seven-round-migration-arc">C.5 The seven-round migration arc</h2>
<p>The <code>zzedc</code> migration proceeded in seven rounds. The arc is a useful empirical record of what a hardened translator does and does not catch on first contact with a real package.</p>
<p><strong>Round 1, naive translator:</strong> 0 of 989 tests ran. The first file had a parse error from a YAML literal whose indentation had been stripped by an over-eager de-indent pass; the <code>tinytest</code> runner aborts on a single parse failure. Removing the de-indent step and relying on <code>local({})</code> for scoping fixed this.</p>
<p><strong>Round 2, after <code>local({})</code> wrapping:</strong> 981 of 989 tests ran. Eight failures remained.</p>
<p><strong>Rounds 3 through 6</strong> each surfaced one new pattern: paren-balanced skip conditions, block-scoped skip wrapping, the <code>regexp = NA</code> idiom, S3 class inheritance, and the custom <code>expect_table_exists</code> helper.</p>
<p><strong>Round 7:</strong> 3,064 of 3,064 assertions passing.</p>
<p>The eight final failures resolved into two classes. Seven were indentation-stripping inside YAML literals that survived the first de-indent fix because they were nested two levels deep; removing de-indentation entirely (rather than partially) eliminated the class. The eighth was the <code>expect_warning(x, NA)</code> semantics, which had been masquerading as a passing test under <code>testthat</code> because the regex was <code>NA</code> and matched nothing rather than checking warning suppression.</p>
<p>The version of the translator in the companion repository is the post-round-7 artifact.</p>
</section>
<section id="c.6-using-the-tools" class="level2">
<h2 class="anchored" data-anchor-id="c.6-using-the-tools">C.6 Using the tools</h2>
<p>For a single file during development, invoke the Python translator directly:</p>
<div class="sourceCode" id="cb27" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb27-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">python3</span> testthat_to_tinytest.py <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb27-2">  tests/testthat/test-mything.R <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb27-3">  inst/tinytest/test_mything.R</span></code></pre></div>
<p>The output is a self-contained tinytest file that sources <code>_setup.R</code> from its own directory if present.</p>
<p>For a whole repository, the bash orchestrator (<code>migrate.sh</code>) handles the surrounding <code>DESCRIPTION</code> and scaffold edits. Phase 1 is fully automated for repositories whose tests are three-line <code>testthat</code> stubs (the pattern produced by <code>usethis::use_testthat()</code> followed by no further work). Phase 2 generates per-file conversion previews into <code>.migration-previews/</code> for repositories with substantive test code, which the user reviews and applies manually:</p>
<div class="sourceCode" id="cb28" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb28-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">./migrate.sh</span> phase1 <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">[--</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">dry</span><span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">]</span></span>
<span id="cb28-2"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">./migrate.sh</span> phase2</span></code></pre></div>
<p>Beyond translation, the orchestrator replaces <code>testthat</code> with <code>tinytest</code> in <code>DESCRIPTION</code>, removes <code>Config/testthat/edition: 3</code> if present, deletes the <code>tests/testthat/</code> tree, writes a new <code>tests/tinytest.R</code>, regenerates <code>renv.lock</code> from the trimmed <code>DESCRIPTION</code>, and commits the result.</p>
<p>After translation, the manual checklist for a real-test repository is:</p>
<ul>
<li>Hand-write <code>inst/tinytest/_setup.R</code> consolidating any <code>helper-*.R</code> files. Replace <code>library(testthat)</code> with nothing and <code>testthat::skip(msg)</code> with <code>exit_file(msg)</code> (or with a wrapped form where block scoping is needed).</li>
<li>Inspect any <code>expect_snapshot</code> and <code>local_edition</code> calls; they will not work without manual rewriting.</li>
<li>Run <code>tinytest::run_test_dir('inst/tinytest')</code> and triage the first round of failures.</li>
</ul>
<p>The translator handles the rote mechanical work: paren-balanced parsing, the seventeen <code>expect_*</code> rewrites, the scope-preserving <code>local({...})</code> wrapping, the block-scoped skip semantics. It does not replace the judgment required to convert snapshot tests, to consolidate helper files, or to decide which <code>Suggests</code> packages should now move to <code>Imports</code> because the test suite was the only place using them. That judgment is the actual work of the migration; the translator makes the mechanical part fast enough that the judgment becomes the bottleneck rather than the typing.</p>
<hr>
<p><em>Rendered on 2026-05-18 at 09:00 PDT.</em><br> <em>Source: ~/prj/qblog/posts/62-testthat-to-tinytest/testthat-to-tinytest/analysis/report/index.qmd</em></p>
<hr>
</section>
</section>
<section id="lets-connect" class="level1">
<h1>Let’s Connect</h1>
<p>If you spotted an error, found a pattern that did not port cleanly, or have a different conclusion about the conversion, the comment thread below is the right place. Issues and pull requests against the post’s source are welcome at <a href="https://github.com/rgthomas47/qblog" class="uri">https://github.com/rgthomas47/qblog</a>.</p>
<!-- ============================================================================
PRE-PUBLISH QA CHECKLIST: verify each item BEFORE setting draft: false

This is the verification pass that mirrors the AUTHOR PROVIDES list at the
top of this file. If anything below is unchecked, the post is not ready.

[ ] YAML
    [x] title, subtitle, date, categories, description, image filled in
    [x] document-type: 'blog'
    [x] draft: false

[ ] Narrative complete (no remaining `[bracketed]` placeholders)
    [ ] grep '\[' index.qmd returns only legitimate code/links
        (NOTE: appendix B contains a deliberate [PLACEHOLDER] that
        must be resolved before publishing)

[ ] Configuration artifacts present and tested
    [x] Full config file shown verbatim, not snippets
    [ ] Install commands tested on a clean machine (or VM)
    [ ] Verification commands produce the documented output
    [x] Uninstall / rollback steps documented

[ ] Things to Watch Out For
    [x] At least 5 gotchas listed (setup posts require this)
    [x] Each gotcha has both a symptom AND a fix

[ ] Visual design
    [ ] 1 hero image (width=80%) immediately after AUTHOR PROVIDES block
    [ ] Exactly 3 ambiance images ({.img-fluid} only, no width=NN%,
        no fig-align): after Objectives, after Configuration, before
        Lessons Learnt
    [ ] Hero and ambiance captions describe the actual image (not a
        placeholder description)
    [ ] No hand-coded "Download PDF" link in body. The site auto-injects
        one via _includes/after-body.html for any post with format: pdf
        in YAML.
    [ ] media/images/README.md attributes every image

[ ] Content quality
    [x] Learner voice: author positioned as peer, not expert
    [x] Zero emoji anywhere (narrative, comments, captions)
    [x] Zero em dashes (forbidden per house style; use parens or commas)
    [x] Single quotes preferred over double quotes in prose
    [x] Each command and config setting interpreted in plain language

[ ] Reproducibility
    [x] Version matrix table filled in (OS, tool, dependencies, date)
    [x] Config files committed under analysis/configs/
        (NOTE: this post has no configs/; deliverable is at
        docs/testthat-to-tinytest-recipe.qmd instead)
    [ ] Install script (if provided) is runnable on a clean machine

[ ] Render verification
    [x] quarto render index.qmd produces clean HTML with no warnings
    [x] Hero image displays in /blog/ listing card
    [ ] Internal links resolve

============================================================================ -->
<section id="related-posts-in-this-cluster" class="level2">
<h2 class="anchored" data-anchor-id="related-posts-in-this-cluster">Related posts in this cluster</h2>
<p>This post is part of the <em>R Package Development and Testing</em> series. Recommended reading order:</p>
<ol type="1">
<li>Post 70: <a href="../70-rp-package-update-workflow/">Updating an R Package: A Complete Workflow</a></li>
<li>Post 72: <a href="../72-rp-vim-r-repl-plugin/">Writing a Simple Vim Plugin for REPL Interaction</a></li>
<li>Post 73: <a href="../73-rp-testing-data-analysis/">Testing Data Analysis Workflows in R</a></li>
<li><strong>Post 74: From testthat to tinytest</strong> (this post)</li>
</ol>


</section>
</section>

 ]]></description>
  <category>r</category>
  <category>testing</category>
  <category>tinytest</category>
  <category>testthat</category>
  <category>packaging</category>
  <guid>https://rgtlab.org/posts/rp-testthat-to-tinytest/</guid>
  <pubDate>Sat, 02 May 2026 07:00:00 GMT</pubDate>
  <media:content url="https://rgtlab.org/posts/rp-testthat-to-tinytest/media/images/hero.png" medium="image" type="image/png" height="80" width="144"/>
</item>
<item>
  <title>A 55-Item Initiation Checklist for zzcollab Data Analyses</title>
  <dc:creator>Ronald G. Thomas</dc:creator>
  <link>https://rgtlab.org/posts/zc-analysis-initiation-checklist/</link>
  <description><![CDATA[ 




<!-- ============================================================================
AUTHOR PROVIDES — concrete inputs the blogger must supply

This post is the workflow / checklist variant of the post-47 setup template.
It is structured around a hypothetical client dataset as the worked example,
and lists all fifty-five preparation items inline.

YAML FRONT MATTER
  [x] title, subtitle, date, categories, description, image
  [x] draft: false (still true pending hero / ambiance imagery)

NARRATIVE INPUTS
  [x] Hypothetical dataset description (file name, columns, ambiguities)
  [x] Why a checklist (short defence, 2 paragraphs)
  [x] Workspace description (zzcollab compendium target state)
  [x] Full 55-item walkthrough across six phases
  [x] Things to Watch Out For (6 gotchas)
  [x] Lessons / Limitations / Opportunities
  [x] Wrap-up + Main Takeaways
  [x] See Also (related posts + zzcollab + Turing Way)

IMAGES (4 total: hero + 3 ambiance, all in media/images/)
  [ ] Hero image            — desk with printed checklist, fresh notebook
  [ ] Ambiance image 1      — workspace shot, post-Objectives
  [ ] Ambiance image 2      — editor view of the checklist mid-walkthrough
  [ ] Ambiance image 3      — finished compendium on a shelf
  [ ] media/images/README.md — sources + attribution

VERSION MATRIX (Reproducibility section)
  [x] OS + tool versions    (macOS 15.4, zzcollab 2.4.0, R 4.5.1, Quarto 1.5)
  [x] Last verified         (2026-04-29)

============================================================================ -->
<!-- ============================================================================
EXEMPLAR WORKFLOW / CHECKLIST POST

A workflow-and-checklist variant of the post-47 setup template. The post body
is built around a hypothetical client dataset and walks through fifty-five
preparation items end to end.

Key differences from the setup post template:
  - The 'Configuration' section is replaced by the 55-item walkthrough
  - The deliverable is the post itself, plus a standalone Quarto checklist
    archived at docs/analysis-checklist.qmd
  - 'Things to Watch Out For' covers checklist-process gotchas, not tool gotchas
  - Reproducibility section pins zzcollab and R versions, not OS-level tools

============================================================================ -->
<!-- ============================================================================
HERO IMAGE
============================================================================ -->
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/zc-analysis-initiation-checklist/media/images/hero.png" class="img-fluid figure-img" style="width:80.0%"></p>
<figcaption>A clean workspace with a printed checklist, an open notebook, and a single CSV file open on screen, signalling deliberate setup before the first line of analysis code is written.</figcaption>
</figure>
</div>
<p><em>A documented, numbered checklist turns the first week of a data analysis project from improvisation into routine. The gain is not speed; it is the disappearance of the small omissions that surface, weeks later, as untestable models or unreproducible figures.</em></p>
<section id="introduction" class="level1">
<h1>Introduction</h1>
<p>A clinical research collaborator emails a single attachment. The body of the email reads, in part:</p>
<blockquote class="blockquote">
<p>Attached is the analytic dataset for Study A, frozen as of last Friday. Twelve columns, two hundred and eighty-seven rows, one row per participant per study visit. The protocol is IRB #2024-XYZ. Please treat as confidential; no PHI but a DUA is in place. An updated extract may follow if a few late withdrawals come in, but the structure will be the same. We expect to write this up for submission in Q3.</p>
</blockquote>
<p>Opening the file reveals a header row that reads:</p>
<pre><code>ParticipantID,Status,Sex,Group,VisitAge,VisitType,
Outcome_A_Imp,Outcome_B_Imp,Sub_a,Sub_b,Sub_c,Sub_total_Imp</code></pre>
<p>Some columns are immediately legible (<code>ParticipantID</code>, <code>Sex</code>, <code>Group</code>). Others are not. What does the <code>_Imp</code> suffix mean? Is <code>VisitAge</code> the participant’s age at the visit, or the age of the visit measured from baseline? Is <code>Sub_total_Imp</code> a sum of <code>Sub_a</code>, <code>Sub_b</code>, <code>Sub_c</code>, an average, or something else? Twelve columns, two hundred and eighty-seven rows, no codebook, no README. The clock starts on reply ‘received, will review’. By Q3 a reproducible compendium is needed that another analyst could clone and re-run.</p>
<p>The fifty-five items below are a six-phase preparation checklist that moves this attachment from inbox to publishable compendium. The structure is the same whether the dataset is a clinical trial, an observational cohort, a simulation output, or any other tabular extract that will be analysed in a zzcollab research compendium.</p>
</section>
<section id="the-hypothetical-client-dataset" class="level1">
<h1>The Hypothetical Client Dataset</h1>
<p>To anchor the items, assume the following throughout.</p>
<p><strong>File:</strong> <code>studyA_data_2026Q1.csv</code> (a single UTF-8 CSV with a BOM in the header).</p>
<p><strong>Shape:</strong> twelve columns, two hundred and eighty-seven rows. One observation per participant per study visit.</p>
<p><strong>Columns (in header order):</strong></p>
<table class="caption-top table">
<thead>
<tr class="header">
<th>Column</th>
<th>Apparent type</th>
<th>Notes from the email</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><code>ParticipantID</code></td>
<td>integer</td>
<td>participant identifier</td>
</tr>
<tr class="even">
<td><code>Status</code></td>
<td>categorical</td>
<td>enrolment status</td>
</tr>
<tr class="odd">
<td><code>Sex</code></td>
<td>categorical</td>
<td>M or F</td>
</tr>
<tr class="even">
<td><code>Group</code></td>
<td>categorical</td>
<td>randomised arm</td>
</tr>
<tr class="odd">
<td><code>VisitAge</code></td>
<td>numeric</td>
<td>age (years)</td>
</tr>
<tr class="even">
<td><code>VisitType</code></td>
<td>categorical</td>
<td>baseline + follow-up</td>
</tr>
<tr class="odd">
<td><code>Outcome_A_Imp</code></td>
<td>numeric</td>
<td>primary outcome A</td>
</tr>
<tr class="even">
<td><code>Outcome_B_Imp</code></td>
<td>numeric</td>
<td>primary outcome B</td>
</tr>
<tr class="odd">
<td><code>Sub_a</code></td>
<td>integer</td>
<td>subscale item</td>
</tr>
<tr class="even">
<td><code>Sub_b</code></td>
<td>integer</td>
<td>subscale item</td>
</tr>
<tr class="odd">
<td><code>Sub_c</code></td>
<td>integer</td>
<td>subscale item</td>
</tr>
<tr class="even">
<td><code>Sub_total_Imp</code></td>
<td>numeric</td>
<td>subscale composite</td>
</tr>
</tbody>
</table>
<p><strong>What is missing:</strong></p>
<ul>
<li>A codebook or data dictionary.</li>
<li>A README documenting provenance, IRB protocol, embargo, and the meaning of <code>_Imp</code>.</li>
<li>Any indication of which columns can be missing (structural NAs versus unexpected ones).</li>
<li>The pre-specified statistical analysis plan (SAP), other than ‘we will write up Outcomes A and B as primary’.</li>
</ul>
<p>These gaps are the substrate the checklist operates on.</p>
</section>
<section id="why-a-checklist" class="level1">
<h1>Why a Checklist?</h1>
<p>A checklist is not a workflow. It is a set of named, verifiable end states ordered such that earlier items unblock later ones. The annotation paragraphs that accompany each item are scaffolding for first-time use; the durable feature is the numbering.</p>
<p>A numbered checklist solves three problems that recur on every project. First, it surfaces the steps that are easy to skip under deadline pressure (data dictionary, baseline integrity test, fresh-container reproducibility run). The items most often skipped are also the items reviewers most often ask for. Second, it gives collaborators a shared vocabulary: ‘Item 17 is failing in the new tinytest file’ is a precise bug report. Third, it forces reproducibility verification before results are circulated, not after.</p>
</section>
<section id="the-zzcollab-workspace" class="level1">
<h1>The zzcollab Workspace</h1>
<p>The target state is a zzcollab research compendium: an R package layout extended with <code>analysis/</code>, an <code>inst/tinytest/</code> directory for data-integrity tests, and the Five Pillars (Dockerfile, renv.lock, .Rprofile, source code, data) that together specify the computational environment. The checklist below is written against that layout. A fresh workspace is created with <code>zzc analysis</code> and contains, in broad outline:</p>
<pre><code>projectname/
|-- analysis/
|   |-- data/
|   |   |-- raw_data/         (the client's CSV lives here)
|   |   `-- derived_data/     (cleaned files, model RDS)
|   |-- scripts/              (numbered analysis scripts)
|   |-- report/               (Rmd / qmd reports)
|   `-- figures/              (publication-ready outputs)
|-- R/                        (reusable functions)
|-- inst/tinytest/            (data integrity assertions)
|-- tests/testthat/           (function unit tests)
|-- docs/                     (data dictionary, checklist)
|-- DESCRIPTION
|-- Dockerfile
|-- renv.lock
|-- .Rprofile
|-- Makefile
`-- zzcollab.yaml</code></pre>
<p>Every checklist item references a specific path inside this tree. Reading the items alongside the layout shows how the work product accumulates.</p>
</section>
<section id="the-55-item-preparation-checklist" class="level1">
<h1>The 55-Item Preparation Checklist</h1>
<p>The items are organised into six phases. Each phase corresponds to a deliverable that lives at a documented location.</p>
<table class="caption-top table">
<thead>
<tr class="header">
<th style="text-align: right;">Phase</th>
<th style="text-align: center;">Items</th>
<th style="text-align: left;">Deliverable</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td style="text-align: right;">1</td>
<td style="text-align: center;">1-8</td>
<td style="text-align: left;">A buildable workspace with documented raw data</td>
</tr>
<tr class="even">
<td style="text-align: right;">2</td>
<td style="text-align: center;">9-22</td>
<td style="text-align: left;">A cleaned, validated derived dataset</td>
</tr>
<tr class="odd">
<td style="text-align: right;">3</td>
<td style="text-align: center;">23-31</td>
<td style="text-align: left;">An EDA report with Table 1 and trajectory plots</td>
</tr>
<tr class="even">
<td style="text-align: right;">4</td>
<td style="text-align: center;">32-38</td>
<td style="text-align: left;">Tested R functions for summary, modelling, plotting</td>
</tr>
<tr class="odd">
<td style="text-align: right;">5</td>
<td style="text-align: center;">39-45</td>
<td style="text-align: left;">A primary-analysis report with sensitivity analyses</td>
</tr>
<tr class="even">
<td style="text-align: right;">6</td>
<td style="text-align: center;">46-55</td>
<td style="text-align: left;">A reproducible compendium ready for archival</td>
</tr>
</tbody>
</table>
<p>Items not applicable to a particular project should be marked ‘N/A’ with a one-line reason rather than removed. The numbering is meant to be stable across projects so that two analysts can compare progress unambiguously.</p>
<section id="phase-1-project-setup-items-1-8" class="level2">
<h2 class="anchored" data-anchor-id="phase-1-project-setup-items-1-8">Phase 1: Project Setup (Items 1-8)</h2>
<p>Phase 1 produces a buildable workspace with raw data in place and provenance documented.</p>
<ol type="1">
<li><p><label><input type="checkbox"><strong>Verify <code>Dockerfile</code> exists and builds successfully.</strong> The Dockerfile defines the computational environment that every subsequent step will run inside; an unbuildable Dockerfile blocks every phase that follows.</label></p>
<div class="sourceCode" id="cb3" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb3-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">zzc</span> docker <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--build</span></span>
<span id="cb3-2"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">docker</span> images studyA            <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># confirm recent build</span></span></code></pre></div></li>
<li><p><label><input type="checkbox"><strong>Verify <code>renv.lock</code> contains required packages.</strong> The lockfile should list only direct dependencies at this stage; transitive dependencies are resolved by <code>renv::restore()</code> inside the container. The lockfile is the second of the Five Pillars and pins the package versions that make the analysis reproducible.</label></p>
<div class="sourceCode" id="cb4" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb4-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">jq</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'.Packages | keys'</span> renv.lock <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">|</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">head</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-20</span></span></code></pre></div></li>
<li><p><label><input type="checkbox"><strong>Verify <code>.Rprofile</code> has appropriate R options.</strong> Confirm <code>.Rprofile</code> activates renv on session start:</label></p>
<div class="sourceCode" id="cb5" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb5-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># .Rprofile</span></span>
<span id="cb5-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">source</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'renv/activate.R'</span>)</span>
<span id="cb5-3"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">options</span>(<span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">renv.config.auto.snapshot =</span> <span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">TRUE</span>)</span></code></pre></div>
<p>The zzcollab template also configures auto-restore on session start so that a fresh container picks up the lockfile automatically.</p></li>
<li><p><label><input type="checkbox"><strong>Verify source code directories exist.</strong> The layout is the contract the rest of the checklist relies on; if <code>R/</code> does not exist, Phase 4 has nowhere to put its functions.</label></p>
<div class="sourceCode" id="cb6" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb6-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">ls</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-d</span> R/ analysis/scripts/ analysis/data/ tests/ <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-2">      inst/tinytest/ docs/</span>
<span id="cb6-3"></span>
<span id="cb6-4"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Create any missing directories</span></span>
<span id="cb6-5"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">mkdir</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-p</span> R analysis/scripts analysis/data/raw_data <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-6">         analysis/data/derived_data <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-7">         analysis/figures analysis/report <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-8">         tests/testthat inst/tinytest docs</span></code></pre></div></li>
<li><p><label><input type="checkbox"><strong>Verify raw data is in <code>analysis/data/raw_data/</code> (read-only).</strong> With write permissions on the raw file, an analyst can open it in Excel, edit a cell, save, and never know they have changed the upstream data. With <code>chmod 444</code> set, every save attempt fails loudly.</label></p>
<div class="sourceCode" id="cb7" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb7-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">mv</span> ~/Downloads/studyA_data_2026Q1.csv <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb7-2">   analysis/data/raw_data/</span>
<span id="cb7-3"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">chmod</span> 444 analysis/data/raw_data/studyA_data_2026Q1.csv</span>
<span id="cb7-4"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">ls</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-la</span> analysis/data/raw_data/</span></code></pre></div></li>
<li><p><label><input type="checkbox"><strong>Update <code>analysis/data/README.md</code> with data source, date received, and use restrictions.</strong> Document who provided the file (the collaborator’s name and institution), the date and time of receipt, the IRB protocol number (<code>#2024-XYZ</code>), the DUA status, and the embargo. Quote the relevant sentences from the email verbatim. A minimal template:</label></p>
<div class="sourceCode" id="cb8" style="background: #f1f3f5;"><pre class="sourceCode markdown code-with-copy"><code class="sourceCode markdown"><span id="cb8-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;"># Study A Raw Data</span></span>
<span id="cb8-2"></span>
<span id="cb8-3"><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">- </span>**Source:** Dr. &lt;Name&gt;, &lt;Institution&gt;</span>
<span id="cb8-4"><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">- </span>**Received:** 2026-04-29 via email attachment</span>
<span id="cb8-5"><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">- </span>**IRB protocol:** #2024-XYZ</span>
<span id="cb8-6"><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">- </span>**DUA:** in place; treat as confidential</span>
<span id="cb8-7"><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">- </span>**Embargo:** none stated</span>
<span id="cb8-8"><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">- </span>**Update window:** 'an updated extract may follow if</span>
<span id="cb8-9">  a few late withdrawals come in' (per email).</span>
<span id="cb8-10"></span>
<span id="cb8-11"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">## Files</span></span>
<span id="cb8-12"></span>
<span id="cb8-13"><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">- </span><span class="in" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">`studyA_data_2026Q1.csv`</span> (12 cols, 287 rows)</span></code></pre></div></li>
<li><p><label><input type="checkbox"><strong>Record data dictionary: column definitions, units, coding schemes.</strong> Create <code>docs/data-dictionary.md</code> with one entry per column. Resolve undocumented suffixes (<code>_Imp</code>) with the data provider before writing the entry; do not guess. A minimal template for one column:</label></p>
<div class="sourceCode" id="cb9" style="background: #f1f3f5;"><pre class="sourceCode markdown code-with-copy"><code class="sourceCode markdown"><span id="cb9-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">## ParticipantID</span></span>
<span id="cb9-2"></span>
<span id="cb9-3"><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">- </span>**Type:** integer</span>
<span id="cb9-4"><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">- </span>**Range:** 1001-9999</span>
<span id="cb9-5"><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">- </span>**Missing:** never</span>
<span id="cb9-6"><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">- </span>**Interpretation:** unique participant identifier</span>
<span id="cb9-7"><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">- </span>**Notes:** 4-digit; first digit is study site</span></code></pre></div>
<p>Repeat for all twelve columns. Treat the dictionary as a contract between the data provider and the analyst; the integrity tests in Phase 2 should validate the contract, not the data.</p></li>
<li><p><label><input type="checkbox"><strong>Install analysis packages: <code>tidyverse</code>, <code>here</code>, <code>janitor</code>, <code>skimr</code>.</strong> Enter the container, install the packages, snapshot the lockfile, and add each package to the DESCRIPTION Imports field so the declared dependency set matches the installed environment.</label></p>
<div class="sourceCode" id="cb10" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb10-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">make</span> r</span></code></pre></div>
<div class="sourceCode" id="cb11" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb11-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">install.packages</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">c</span>(</span>
<span id="cb11-2">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'tidyverse'</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'here'</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'janitor'</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'skimr'</span>,</span>
<span id="cb11-3">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'tinytest'</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'gtsummary'</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'broom'</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'broom.mixed'</span>,</span>
<span id="cb11-4">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'lme4'</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'patchwork'</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'assertthat'</span></span>
<span id="cb11-5">))</span>
<span id="cb11-6">renv<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">snapshot</span>()</span></code></pre></div></li>
</ol>
</section>
<section id="phase-2-data-ingestion-and-validation-items-9-22" class="level2">
<h2 class="anchored" data-anchor-id="phase-2-data-ingestion-and-validation-items-9-22">Phase 2: Data Ingestion and Validation (Items 9-22)</h2>
<p>Phase 2 produces a cleaned, validated derived dataset and a test file that asserts every contract from the data dictionary. The deliverables are <code>R/load_data.R</code>, <code>inst/tinytest/test_data_integrity.R</code>, and <code>analysis/scripts/01-clean-data.R</code>.</p>
<ol start="9" type="1">
<li><p><label><input type="checkbox"><strong>Create <code>R/load_data.R</code> with a function to read raw data.</strong> The function returns the raw data frame, never a printed object. Use <code>here::here()</code> for the path so the function works from any working directory. The CSV carries a UTF-8 BOM in the header; <code>read_csv()</code> handles this automatically.</label></p>
<div class="sourceCode" id="cb12" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb12-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># R/load_data.R</span></span>
<span id="cb12-2">load_raw_data <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">function</span>() {</span>
<span id="cb12-3">  readr<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">read_csv</span>(</span>
<span id="cb12-4">    here<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">here</span>(</span>
<span id="cb12-5">      <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'analysis/data/raw_data/studyA_data_2026Q1.csv'</span></span>
<span id="cb12-6">    ),</span>
<span id="cb12-7">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">show_col_types =</span> <span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">FALSE</span></span>
<span id="cb12-8">  )</span>
<span id="cb12-9">}</span></code></pre></div></li>
<li><p><label><input type="checkbox"><strong>Create <code>inst/tinytest/test_data_integrity.R</code>.</strong> The test file sources <code>load_raw_data()</code> once and runs every assertion (Items 11-17) against the resulting data frame. Run with <code>tinytest::run_test_file()</code>. The test file is the living version of the data dictionary contract.</label></p>
<div class="sourceCode" id="cb13" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb13-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># inst/tinytest/test_data_integrity.R</span></span>
<span id="cb13-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">library</span>(tinytest)</span>
<span id="cb13-3"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">source</span>(here<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">here</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'R/load_data.R'</span>))</span>
<span id="cb13-4"></span>
<span id="cb13-5">dat <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">load_raw_data</span>()</span></code></pre></div></li>
<li><p><label><input type="checkbox"><strong>Test: expected number of columns (12).</strong> A mismatch signals that the upstream data export has changed or that the file was corrupted during transfer.</label></p>
<div class="sourceCode" id="cb14" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb14-1">N_EXPECTED_COLS <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">12</span>L</span>
<span id="cb14-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">expect_equal</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">ncol</span>(dat), N_EXPECTED_COLS)</span></code></pre></div></li>
<li><p><label><input type="checkbox"><strong>Test: expected column names present.</strong> A failure indicates the data provider has renamed a field or that an export script has changed; either way, the analysis cannot proceed without resolving it.</label></p>
<div class="sourceCode" id="cb15" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb15-1">expected_cols <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">c</span>(</span>
<span id="cb15-2">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'ParticipantID'</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'Status'</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'Sex'</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'Group'</span>,</span>
<span id="cb15-3">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'VisitAge'</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'VisitType'</span>,</span>
<span id="cb15-4">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'Outcome_A_Imp'</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'Outcome_B_Imp'</span>,</span>
<span id="cb15-5">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'Sub_a'</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'Sub_b'</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'Sub_c'</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'Sub_total_Imp'</span></span>
<span id="cb15-6">)</span>
<span id="cb15-7"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">expect_true</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">all</span>(expected_cols <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">%in%</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">names</span>(dat)))</span></code></pre></div></li>
<li><p><label><input type="checkbox"><strong>Test: ID columns have no NAs and follow expected format.</strong> <code>ParticipantID</code> is integer-valued with no missing values; the documented range is 1001-9999. A missing identifier breaks every downstream join.</label></p>
<div class="sourceCode" id="cb16" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb16-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">expect_true</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">all</span>(<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">!</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">is.na</span>(dat<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">$</span>ParticipantID)))</span>
<span id="cb16-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">expect_true</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">is.numeric</span>(dat<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">$</span>ParticipantID))</span>
<span id="cb16-3"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">expect_true</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">all</span>(dat<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">$</span>ParticipantID <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;=</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1001</span> <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span></span>
<span id="cb16-4">                  dat<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">$</span>ParticipantID <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;=</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">9999</span>))</span></code></pre></div></li>
<li><p><label><input type="checkbox"><strong>Test: pre-specified categorical variables match allowed levels.</strong> Catches typos and miscoded values before they propagate into the model.</label></p>
<div class="sourceCode" id="cb17" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb17-1">allowed_groups <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">c</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'Active'</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'Control'</span>)</span>
<span id="cb17-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">expect_true</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">all</span>(dat<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">$</span>Group <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">%in%</span> allowed_groups))</span>
<span id="cb17-3"></span>
<span id="cb17-4">allowed_status <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">c</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'Enrolled'</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'Withdrawn'</span>,</span>
<span id="cb17-5">                    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'Completed'</span>)</span>
<span id="cb17-6"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">expect_true</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">all</span>(dat<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">$</span>Status <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">%in%</span> allowed_status))</span></code></pre></div></li>
<li><p><label><input type="checkbox"><strong>Test: demographic categorical variables match coding scheme.</strong> Investigate any unexpected values; do not silently recode them. Differences from the documented set are sometimes legitimate and sometimes errors; only the data provider can tell.</label></p>
<div class="sourceCode" id="cb18" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb18-1">allowed_sex <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">c</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'M'</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'F'</span>)</span>
<span id="cb18-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">expect_true</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">all</span>(dat<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">$</span>Sex <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">%in%</span> allowed_sex))</span>
<span id="cb18-3"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">expect_true</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">all</span>(<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">!</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">is.na</span>(dat<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">$</span>Sex)))</span></code></pre></div></li>
<li><p><label><input type="checkbox"><strong>Test: visit variables fall in the pre-specified set.</strong> P1 is the baseline assessment; V2-V4 are follow-ups. Any other value indicates a data export error or a silently extended protocol.</label></p>
<div class="sourceCode" id="cb19" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb19-1">allowed_visits <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">c</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'P1'</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'V2'</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'V3'</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'V4'</span>)</span>
<span id="cb19-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">expect_true</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">all</span>(dat<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">$</span>VisitType <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">%in%</span> allowed_visits))</span></code></pre></div></li>
<li><p><label><input type="checkbox"><strong>Test: numeric columns are within plausible ranges.</strong> Use <code>na.rm = TRUE</code> so that missing follow-up data does not trip the check.</label></p>
<div class="sourceCode" id="cb20" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb20-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Age (years)</span></span>
<span id="cb20-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">expect_true</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">all</span>(dat<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">$</span>VisitAge <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;=</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">18</span> <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span></span>
<span id="cb20-3">                  dat<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">$</span>VisitAge <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;=</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">100</span>,</span>
<span id="cb20-4">                <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">na.rm =</span> <span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">TRUE</span>))</span>
<span id="cb20-5"></span>
<span id="cb20-6"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Likert subscale items (0-3)</span></span>
<span id="cb20-7"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> (col <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">c</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'Sub_a'</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'Sub_b'</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'Sub_c'</span>)) {</span>
<span id="cb20-8">  x <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> dat[[col]]</span>
<span id="cb20-9">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">expect_true</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">all</span>(x <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;=</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span> <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span> x <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;=</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">na.rm =</span> <span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">TRUE</span>))</span>
<span id="cb20-10">}</span>
<span id="cb20-11"></span>
<span id="cb20-12"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Subscale composite (0-9)</span></span>
<span id="cb20-13"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">expect_true</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">all</span>(dat<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">$</span>Sub_total_Imp <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;=</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span> <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span></span>
<span id="cb20-14">                  dat<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">$</span>Sub_total_Imp <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;=</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">9</span>,</span>
<span id="cb20-15">                <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">na.rm =</span> <span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">TRUE</span>))</span>
<span id="cb20-16"></span>
<span id="cb20-17"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Outcomes (placeholder bounds; refer to dictionary)</span></span>
<span id="cb20-18"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">expect_true</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">all</span>(dat<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">$</span>Outcome_A_Imp <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;=</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">36</span> <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span></span>
<span id="cb20-19">                  dat<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">$</span>Outcome_A_Imp <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;=</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">78</span>,</span>
<span id="cb20-20">                <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">na.rm =</span> <span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">TRUE</span>))</span>
<span id="cb20-21"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">expect_true</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">all</span>(dat<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">$</span>Outcome_B_Imp <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;=</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span> <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span></span>
<span id="cb20-22">                  dat<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">$</span>Outcome_B_Imp <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;=</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">28</span>,</span>
<span id="cb20-23">                <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">na.rm =</span> <span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">TRUE</span>))</span></code></pre></div></li>
<li><p><label><input type="checkbox"><strong>Create <code>analysis/scripts/01-clean-data.R</code>.</strong> The script sources <code>load_raw_data()</code>, applies the cleaning steps in Items 19-21, and writes the result to derived data (Item 22). Structure as a linear pipe so the transformations are auditable in sequence.</label></p>
<div class="sourceCode" id="cb21" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb21-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># analysis/scripts/01-clean-data.R</span></span>
<span id="cb21-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">library</span>(tidyverse)</span>
<span id="cb21-3"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">library</span>(janitor)</span>
<span id="cb21-4"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">library</span>(here)</span>
<span id="cb21-5"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">source</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">here</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'R/load_data.R'</span>))</span>
<span id="cb21-6"></span>
<span id="cb21-7">dat_clean <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">load_raw_data</span>() <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span></span>
<span id="cb21-8">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">clean_names</span>() <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span></span>
<span id="cb21-9">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">mutate</span>(</span>
<span id="cb21-10">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">group =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">factor</span>(group,</span>
<span id="cb21-11">                   <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">levels =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">c</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'Control'</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'Active'</span>)),</span>
<span id="cb21-12">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">sex =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">factor</span>(sex, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">levels =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">c</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'F'</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'M'</span>)),</span>
<span id="cb21-13">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">visit_type =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">factor</span>(</span>
<span id="cb21-14">      visit_type,</span>
<span id="cb21-15">      <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">levels =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">c</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'P1'</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'V2'</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'V3'</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'V4'</span>),</span>
<span id="cb21-16">      <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">ordered =</span> <span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">TRUE</span></span>
<span id="cb21-17">    ),</span>
<span id="cb21-18">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">status =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">factor</span>(status,</span>
<span id="cb21-19">                    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">levels =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">c</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'Enrolled'</span>,</span>
<span id="cb21-20">                               <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'Withdrawn'</span>,</span>
<span id="cb21-21">                               <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'Completed'</span>))</span>
<span id="cb21-22">  )</span>
<span id="cb21-23"></span>
<span id="cb21-24"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">saveRDS</span>(</span>
<span id="cb21-25">  dat_clean,</span>
<span id="cb21-26">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">here</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'analysis/data/derived_data/cleaned_data.rds'</span>)</span>
<span id="cb21-27">)</span></code></pre></div></li>
<li><p><label><input type="checkbox"><strong>Clean column names using <code>janitor::clean_names()</code>.</strong> <code>clean_names()</code> converts headers to snake_case and normalises the BOM on the first column. Update the data dictionary to record both raw and cleaned names.</label></p>
<div class="sourceCode" id="cb22" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb22-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Before clean_names():</span></span>
<span id="cb22-2"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#   ParticipantID, Outcome_A_Imp, Sub_total_Imp, ...</span></span>
<span id="cb22-3"></span>
<span id="cb22-4">dat_clean <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> dat <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span> janitor<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">clean_names</span>()</span>
<span id="cb22-5"></span>
<span id="cb22-6"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># After clean_names():</span></span>
<span id="cb22-7"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#   participant_id, outcome_a_imp, sub_total_imp, ...</span></span></code></pre></div></li>
<li><p><label><input type="checkbox"><strong>Convert categorical variables to factors with explicit levels.</strong> Place the reference level first (<code>Control</code> before <code>Active</code>, <code>F</code> before <code>M</code>) so model contrasts produce intuitive coefficients. Preserve the time ordering for <code>visit_type</code> (<code>P1 &lt; V2 &lt; V3 &lt; V4</code>).</label></p>
<div class="sourceCode" id="cb23" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb23-1">dat_clean <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> dat_clean <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span></span>
<span id="cb23-2">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">mutate</span>(</span>
<span id="cb23-3">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Control as reference =&gt; coefficient is the</span></span>
<span id="cb23-4">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 'Active vs Control' effect</span></span>
<span id="cb23-5">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">group =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">factor</span>(group,</span>
<span id="cb23-6">                   <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">levels =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">c</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'Control'</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'Active'</span>)),</span>
<span id="cb23-7"></span>
<span id="cb23-8">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Female as reference (alphabetical)</span></span>
<span id="cb23-9">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">sex =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">factor</span>(sex, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">levels =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">c</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'F'</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'M'</span>)),</span>
<span id="cb23-10"></span>
<span id="cb23-11">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Ordered factor preserves visit sequence</span></span>
<span id="cb23-12">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">visit_type =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">factor</span>(</span>
<span id="cb23-13">      visit_type,</span>
<span id="cb23-14">      <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">levels =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">c</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'P1'</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'V2'</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'V3'</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'V4'</span>),</span>
<span id="cb23-15">      <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">ordered =</span> <span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">TRUE</span></span>
<span id="cb23-16">    )</span>
<span id="cb23-17">  )</span></code></pre></div></li>
<li><p><label><input type="checkbox"><strong>Handle missing values explicitly and document approach.</strong> Tabulate NAs per column and per visit before deciding on a strategy. The <code>_Imp</code> suffix suggests imputation was already done upstream; confirm with the data provider before writing any imputation yourself.</label></p>
<div class="sourceCode" id="cb24" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb24-1">dat_clean <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span></span>
<span id="cb24-2">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">summarise</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">across</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">everything</span>(),</span>
<span id="cb24-3">                   <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">~</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sum</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">is.na</span>(.x)))) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span></span>
<span id="cb24-4">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">pivot_longer</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">everything</span>(),</span>
<span id="cb24-5">               <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">names_to =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'column'</span>,</span>
<span id="cb24-6">               <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">values_to =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'n_missing'</span>) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span></span>
<span id="cb24-7">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">arrange</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">desc</span>(n_missing))</span>
<span id="cb24-8"></span>
<span id="cb24-9"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Per-visit missingness for the primary outcome</span></span>
<span id="cb24-10">dat_clean <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span></span>
<span id="cb24-11">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">group_by</span>(visit_type) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span></span>
<span id="cb24-12">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">summarise</span>(</span>
<span id="cb24-13">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">n_total =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">n</span>(),</span>
<span id="cb24-14">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">n_missing_a =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sum</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">is.na</span>(outcome_a_imp))</span>
<span id="cb24-15">  )</span></code></pre></div></li>
<li><p><label><input type="checkbox"><strong>Save cleaned data to <code>analysis/data/derived_data/</code>.</strong> All downstream scripts read from derived data, never from raw. This makes the cleaning step the single point of transformation.</label></p>
<div class="sourceCode" id="cb25" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb25-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">saveRDS</span>(</span>
<span id="cb25-2">  dat_clean,</span>
<span id="cb25-3">  here<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">here</span>(</span>
<span id="cb25-4">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'analysis/data/derived_data/cleaned_data.rds'</span></span>
<span id="cb25-5">  )</span>
<span id="cb25-6">)</span>
<span id="cb25-7"></span>
<span id="cb25-8"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Optional: also write a CSV for non-R consumers</span></span>
<span id="cb25-9">readr<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">write_csv</span>(</span>
<span id="cb25-10">  dat_clean,</span>
<span id="cb25-11">  here<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">here</span>(</span>
<span id="cb25-12">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'analysis/data/derived_data/cleaned_data.csv'</span></span>
<span id="cb25-13">  )</span>
<span id="cb25-14">)</span></code></pre></div></li>
</ol>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/zc-analysis-initiation-checklist/media/images/ambiance1.png" class="img-fluid figure-img"></p>
<figcaption>Workspace ambiance: an analyst at the second day of a project, with the printed checklist on the desk and the loaded data frame on screen.</figcaption>
</figure>
</div>
</section>
<section id="phase-3-exploratory-analysis-items-23-31" class="level2">
<h2 class="anchored" data-anchor-id="phase-3-exploratory-analysis-items-23-31">Phase 3: Exploratory Analysis (Items 23-31)</h2>
<p>Phase 3 produces the first knittable report. The deliverables are <code>analysis/scripts/02-eda.R</code> and <code>analysis/report/01-eda.Rmd</code> (or <code>.qmd</code>).</p>
<ol start="23" type="1">
<li><p><label><input type="checkbox"><strong>Create <code>analysis/scripts/02-eda.R</code>.</strong> The second step in the linear pipeline. It reads cleaned data from derived data, never from raw.</label></p>
<div class="sourceCode" id="cb26" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb26-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># analysis/scripts/02-eda.R</span></span>
<span id="cb26-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">library</span>(tidyverse)</span>
<span id="cb26-3"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">library</span>(skimr)</span>
<span id="cb26-4"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">library</span>(here)</span>
<span id="cb26-5"></span>
<span id="cb26-6">dat <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">readRDS</span>(</span>
<span id="cb26-7">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">here</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'analysis/data/derived_data/cleaned_data.rds'</span>)</span>
<span id="cb26-8">)</span></code></pre></div></li>
<li><p><label><input type="checkbox"><strong>Generate summary statistics by treatment group.</strong> For each numeric outcome and each pre-specified grouping, produce mean, SD, median, IQR, min, max, and count of non-missing observations. Save the summary as a CSV in derived data.</label></p>
<div class="sourceCode" id="cb27" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb27-1">summary_by_group_visit <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> dat <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span></span>
<span id="cb27-2">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">group_by</span>(group, visit_type) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span></span>
<span id="cb27-3">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">summarise</span>(</span>
<span id="cb27-4">    <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">across</span>(</span>
<span id="cb27-5">      <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">c</span>(outcome_a_imp, outcome_b_imp, sub_total_imp),</span>
<span id="cb27-6">      <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">list</span>(</span>
<span id="cb27-7">        <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">n    =</span> <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">~</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sum</span>(<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">!</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">is.na</span>(.x)),</span>
<span id="cb27-8">        <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">mean =</span> <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">~</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">mean</span>(.x, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">na.rm =</span> <span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">TRUE</span>),</span>
<span id="cb27-9">        <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">sd   =</span> <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">~</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sd</span>(.x, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">na.rm =</span> <span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">TRUE</span>),</span>
<span id="cb27-10">        <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">med  =</span> <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">~</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">median</span>(.x, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">na.rm =</span> <span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">TRUE</span>)</span>
<span id="cb27-11">      ),</span>
<span id="cb27-12">      <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">.names =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'{.col}_{.fn}'</span></span>
<span id="cb27-13">    ),</span>
<span id="cb27-14">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">.groups =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'drop'</span></span>
<span id="cb27-15">  )</span>
<span id="cb27-16"></span>
<span id="cb27-17">readr<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">write_csv</span>(</span>
<span id="cb27-18">  summary_by_group_visit,</span>
<span id="cb27-19">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">here</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'analysis/data/derived_data/summary_by_group_visit.csv'</span>)</span>
<span id="cb27-20">)</span></code></pre></div></li>
<li><p><label><input type="checkbox"><strong>Check baseline balance.</strong> For randomised trials, imbalance at baseline is informative for downstream covariate adjustment, not a basis for re-randomisation.</label></p>
<div class="sourceCode" id="cb28" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb28-1">baseline <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> dat <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">filter</span>(visit_type <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'P1'</span>)</span>
<span id="cb28-2"></span>
<span id="cb28-3">baseline <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span></span>
<span id="cb28-4">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">group_by</span>(group) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span></span>
<span id="cb28-5">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">summarise</span>(</span>
<span id="cb28-6">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">n          =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">n</span>(),</span>
<span id="cb28-7">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">mean_age   =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">mean</span>(visit_age, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">na.rm =</span> <span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">TRUE</span>),</span>
<span id="cb28-8">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">sd_age     =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sd</span>(visit_age, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">na.rm =</span> <span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">TRUE</span>),</span>
<span id="cb28-9">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">pct_female =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">mean</span>(sex <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'F'</span>) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">100</span>,</span>
<span id="cb28-10">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">mean_a    =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">mean</span>(outcome_a_imp, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">na.rm =</span> <span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">TRUE</span>),</span>
<span id="cb28-11">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">mean_b    =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">mean</span>(outcome_b_imp, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">na.rm =</span> <span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">TRUE</span>)</span>
<span id="cb28-12">  )</span></code></pre></div></li>
<li><p><label><input type="checkbox"><strong>Visualise outcome distributions.</strong> Inspect for floor and ceiling effects, bimodality, heavy tails, and outliers.</label></p>
<div class="sourceCode" id="cb29" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb29-1">p_dist <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">ggplot</span>(dat,</span>
<span id="cb29-2">                 <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">aes</span>(<span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">x =</span> outcome_a_imp,</span>
<span id="cb29-3">                     <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">fill =</span> group)) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span></span>
<span id="cb29-4">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">geom_density</span>(<span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">alpha =</span> <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.5</span>) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span></span>
<span id="cb29-5">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">facet_wrap</span>(<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">~</span> visit_type) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span></span>
<span id="cb29-6">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">labs</span>(<span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">title =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'Outcome A by group and visit'</span>,</span>
<span id="cb29-7">       <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">x =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'Outcome A (imputed)'</span>,</span>
<span id="cb29-8">       <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">y =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'Density'</span>)</span>
<span id="cb29-9"></span>
<span id="cb29-10"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">ggsave</span>(</span>
<span id="cb29-11">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">here</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'analysis/figures/dist-outcome-a.png'</span>),</span>
<span id="cb29-12">  p_dist, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">width =</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">8</span>, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">height =</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">5</span>, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">dpi =</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">300</span></span>
<span id="cb29-13">)</span></code></pre></div></li>
<li><p><label><input type="checkbox"><strong>Identify potential outliers or data quality issues.</strong> Flag observations that fall outside the plausibility ranges from Item 17, or that deviate substantially from the within-group distribution. Document each flagged row with the decision in a tracking file.</label></p>
<div class="sourceCode" id="cb30" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb30-1">flags <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> dat <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span></span>
<span id="cb30-2">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">mutate</span>(</span>
<span id="cb30-3">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">flag_age   =</span> visit_age <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">18</span> <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|</span> visit_age <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">100</span>,</span>
<span id="cb30-4">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">flag_sub_a =</span> sub_a <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span> <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|</span> sub_a <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>,</span>
<span id="cb30-5">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">flag_sub_b =</span> sub_b <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span> <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|</span> sub_b <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>,</span>
<span id="cb30-6">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">flag_sub_c =</span> sub_c <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span> <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|</span> sub_c <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span></span>
<span id="cb30-7">  ) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span></span>
<span id="cb30-8">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">filter</span>(flag_age <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|</span> flag_sub_a <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|</span> flag_sub_b <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|</span></span>
<span id="cb30-9">           flag_sub_c)</span>
<span id="cb30-10"></span>
<span id="cb30-11"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> (<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">nrow</span>(flags) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>) {</span>
<span id="cb30-12">  readr<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">write_csv</span>(</span>
<span id="cb30-13">    flags,</span>
<span id="cb30-14">    <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">here</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'analysis/data/derived_data/data-flags.csv'</span>)</span>
<span id="cb30-15">  )</span>
<span id="cb30-16">}</span></code></pre></div></li>
<li><p><label><input type="checkbox"><strong>Create <code>analysis/report/01-eda.Rmd</code> (or <code>.qmd</code>).</strong> The first report is a stand-alone Rmd or qmd that knits to HTML or PDF. It loads cleaned data, reads the EDA outputs from derived data, and presents the results in narrative form.</label></p>
<div class="sourceCode" id="cb31" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb31-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">quarto</span> render analysis/report/01-eda.qmd</span></code></pre></div></li>
<li><p><label><input type="checkbox"><strong>Document sample size and missingness patterns.</strong> Report the number of participants enrolled, randomised, and analysed at each visit. Tabulate missingness by variable and by visit so the reader can judge the analytic implications.</label></p>
<div class="sourceCode" id="cb32" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb32-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Per-visit sample sizes by group</span></span>
<span id="cb32-2">dat <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span></span>
<span id="cb32-3">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">count</span>(visit_type, group) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span></span>
<span id="cb32-4">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">pivot_wider</span>(<span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">names_from =</span> group, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">values_from =</span> n)</span>
<span id="cb32-5"></span>
<span id="cb32-6"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Per-visit missingness for the primary outcome</span></span>
<span id="cb32-7">dat <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span></span>
<span id="cb32-8">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">group_by</span>(visit_type) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span></span>
<span id="cb32-9">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">summarise</span>(</span>
<span id="cb32-10">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">n_total    =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">n</span>(),</span>
<span id="cb32-11">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">n_observed =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sum</span>(<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">!</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">is.na</span>(outcome_a_imp)),</span>
<span id="cb32-12">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">pct_obs    =</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">100</span> <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> n_observed <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> n_total</span>
<span id="cb32-13">  )</span></code></pre></div></li>
<li><p><label><input type="checkbox"><strong>Include baseline characteristics table (Table 1).</strong> Use <code>gtsummary::tbl_summary()</code> stratified by <code>group</code>. For randomised trials, avoid statistical tests of baseline balance; they are uninformative when randomisation succeeded.</label></p>
<div class="sourceCode" id="cb33" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb33-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">library</span>(gtsummary)</span>
<span id="cb33-2"></span>
<span id="cb33-3">tab1 <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> dat <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span></span>
<span id="cb33-4">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">filter</span>(visit_type <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'P1'</span>) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span></span>
<span id="cb33-5">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">select</span>(group, sex, visit_age,</span>
<span id="cb33-6">         outcome_a_imp, outcome_b_imp,</span>
<span id="cb33-7">         sub_total_imp) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span></span>
<span id="cb33-8">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">tbl_summary</span>(</span>
<span id="cb33-9">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">by =</span> group,</span>
<span id="cb33-10">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">statistic =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">list</span>(</span>
<span id="cb33-11">      <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">all_continuous</span>() <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">~</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'{mean} ({sd})'</span>,</span>
<span id="cb33-12">      <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">all_categorical</span>() <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">~</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'{n} ({p}%)'</span></span>
<span id="cb33-13">    )</span>
<span id="cb33-14">  ) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span></span>
<span id="cb33-15">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">add_overall</span>()</span>
<span id="cb33-16"></span>
<span id="cb33-17">tab1</span></code></pre></div></li>
<li><p><label><input type="checkbox"><strong>Include outcome trajectory plots by visit and group.</strong> Plot the mean trajectory over <code>visit_type</code> with error bars; spaghetti plots of individual trajectories are a useful supplement when sample sizes are modest.</label></p>
<div class="sourceCode" id="cb34" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb34-1">p_traj <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> dat <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span></span>
<span id="cb34-2">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">group_by</span>(group, visit_type) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span></span>
<span id="cb34-3">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">summarise</span>(</span>
<span id="cb34-4">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">m  =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">mean</span>(outcome_a_imp, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">na.rm =</span> <span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">TRUE</span>),</span>
<span id="cb34-5">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">se =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sd</span>(outcome_a_imp, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">na.rm =</span> <span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">TRUE</span>) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span></span>
<span id="cb34-6">           <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sqrt</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sum</span>(<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">!</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">is.na</span>(outcome_a_imp))),</span>
<span id="cb34-7">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">.groups =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'drop'</span></span>
<span id="cb34-8">  ) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span></span>
<span id="cb34-9">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">ggplot</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">aes</span>(<span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">x =</span> visit_type, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">y =</span> m,</span>
<span id="cb34-10">             <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">colour =</span> group, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">group =</span> group)) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span></span>
<span id="cb34-11">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">geom_line</span>() <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span></span>
<span id="cb34-12">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">geom_errorbar</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">aes</span>(<span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">ymin =</span> m <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> se, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">ymax =</span> m <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> se),</span>
<span id="cb34-13">                <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">width =</span> <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.1</span>) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span></span>
<span id="cb34-14">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">labs</span>(<span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">x =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'Visit'</span>, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">y =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'Outcome A (mean +/- SE)'</span>)</span>
<span id="cb34-15"></span>
<span id="cb34-16"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">ggsave</span>(</span>
<span id="cb34-17">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">here</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'analysis/figures/trajectory-outcome-a.png'</span>),</span>
<span id="cb34-18">  p_traj, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">width =</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">6</span>, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">height =</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">4</span>, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">dpi =</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">300</span></span>
<span id="cb34-19">)</span></code></pre></div></li>
</ol>
</section>
<section id="phase-4-analysis-functions-items-32-38" class="level2">
<h2 class="anchored" data-anchor-id="phase-4-analysis-functions-items-32-38">Phase 4: Analysis Functions (Items 32-38)</h2>
<p>Phase 4 produces tested R functions for summary, modelling, and plotting. The deliverables are <code>R/summarize_outcomes.R</code>, <code>R/model_outcomes.R</code>, <code>R/plot_outcomes.R</code>, and corresponding test files under <code>inst/tinytest/</code>.</p>
<ol start="32" type="1">
<li><p><label><input type="checkbox"><strong>Create <code>R/summarize_outcomes.R</code> with descriptive statistics functions.</strong> Each function takes a data frame and returns a data frame, never a printed object. Document arguments and return values with <code>roxygen2</code>.</label></p>
<div class="sourceCode" id="cb35" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb35-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># R/summarize_outcomes.R</span></span>
<span id="cb35-2"></span>
<span id="cb35-3"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#' Mean and SD of an outcome by group and visit</span></span>
<span id="cb35-4"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#'</span></span>
<span id="cb35-5"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#' @param dat cleaned data frame</span></span>
<span id="cb35-6"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#' @param outcome column name as a string</span></span>
<span id="cb35-7"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#' @return tibble with group, visit_type, n, mean, sd</span></span>
<span id="cb35-8"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#' @export</span></span>
<span id="cb35-9">summarize_outcome_by_group_visit <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">function</span>(dat,</span>
<span id="cb35-10">                                              outcome) {</span>
<span id="cb35-11">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">stopifnot</span>(outcome <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">%in%</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">names</span>(dat))</span>
<span id="cb35-12">  dat <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span></span>
<span id="cb35-13">    dplyr<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">group_by</span>(group, visit_type) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span></span>
<span id="cb35-14">    dplyr<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">summarise</span>(</span>
<span id="cb35-15">      <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">n    =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sum</span>(<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">!</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">is.na</span>(.data[[outcome]])),</span>
<span id="cb35-16">      <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">mean =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">mean</span>(.data[[outcome]], <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">na.rm =</span> <span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">TRUE</span>),</span>
<span id="cb35-17">      <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">sd   =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sd</span>(.data[[outcome]], <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">na.rm =</span> <span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">TRUE</span>),</span>
<span id="cb35-18">      <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">.groups =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'drop'</span></span>
<span id="cb35-19">    )</span>
<span id="cb35-20">}</span></code></pre></div></li>
<li><p><label><input type="checkbox"><strong>Create <code>R/model_outcomes.R</code> with statistical modelling functions.</strong> Wrap each call to <code>lm()</code>, <code>lmer()</code>, or <code>glm()</code> in a function so the model specification is captured in code rather than reproduced inline.</label></p>
<div class="sourceCode" id="cb36" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb36-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># R/model_outcomes.R</span></span>
<span id="cb36-2"></span>
<span id="cb36-3"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#' Fit a linear mixed model for a primary outcome</span></span>
<span id="cb36-4"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#'</span></span>
<span id="cb36-5"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#' Random intercept for participant; fixed effects of</span></span>
<span id="cb36-6"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#' group, visit, their interaction, and baseline</span></span>
<span id="cb36-7"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#' covariates.</span></span>
<span id="cb36-8"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#'</span></span>
<span id="cb36-9"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#' @param dat cleaned data frame</span></span>
<span id="cb36-10"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#' @param outcome column name as a string</span></span>
<span id="cb36-11"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#' @return fitted lmerMod object</span></span>
<span id="cb36-12"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#' @export</span></span>
<span id="cb36-13">fit_primary_lmm <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">function</span>(dat, outcome) {</span>
<span id="cb36-14">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">stopifnot</span>(outcome <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">%in%</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">names</span>(dat))</span>
<span id="cb36-15">  f <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> stats<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">as.formula</span>(</span>
<span id="cb36-16">    <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">paste0</span>(</span>
<span id="cb36-17">      outcome,</span>
<span id="cb36-18">      <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">' ~ group * visit_type + visit_age + sex + '</span>,</span>
<span id="cb36-19">      <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'(1 | participant_id)'</span></span>
<span id="cb36-20">    )</span>
<span id="cb36-21">  )</span>
<span id="cb36-22">  lme4<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">lmer</span>(f, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">data =</span> dat)</span>
<span id="cb36-23">}</span></code></pre></div></li>
<li><p><label><input type="checkbox"><strong>Create <code>R/plot_outcomes.R</code> with visualisation functions.</strong> Plotting functions return a ggplot; they do not call <code>ggsave()</code>. Returning the object lets reports compose panels with <code>patchwork</code>.</label></p>
<div class="sourceCode" id="cb37" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb37-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># R/plot_outcomes.R</span></span>
<span id="cb37-2"></span>
<span id="cb37-3"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#' Mean +/- SE trajectory plot for one outcome</span></span>
<span id="cb37-4"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#'</span></span>
<span id="cb37-5"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#' @param dat cleaned data frame</span></span>
<span id="cb37-6"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#' @param outcome column name as a string</span></span>
<span id="cb37-7"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#' @return ggplot object</span></span>
<span id="cb37-8"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#' @export</span></span>
<span id="cb37-9">plot_trajectory <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">function</span>(dat, outcome) {</span>
<span id="cb37-10">  assertthat<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">assert_that</span>(outcome <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">%in%</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">names</span>(dat))</span>
<span id="cb37-11">  assertthat<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">assert_that</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'group'</span> <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">%in%</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">names</span>(dat))</span>
<span id="cb37-12">  assertthat<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">assert_that</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'visit_type'</span> <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">%in%</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">names</span>(dat))</span>
<span id="cb37-13"></span>
<span id="cb37-14">  dat <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span></span>
<span id="cb37-15">    ggplot2<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">ggplot</span>(</span>
<span id="cb37-16">      ggplot2<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">aes</span>(<span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">x =</span> visit_type,</span>
<span id="cb37-17">                   <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">y =</span> .data[[outcome]],</span>
<span id="cb37-18">                   <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">colour =</span> group, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">group =</span> group)) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span></span>
<span id="cb37-19">    ggplot2<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">stat_summary</span>(<span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">fun =</span> mean,</span>
<span id="cb37-20">                          <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">geom =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'line'</span>) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span></span>
<span id="cb37-21">    ggplot2<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">stat_summary</span>(<span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">fun.data =</span> mean_se,</span>
<span id="cb37-22">                          <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">geom =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'errorbar'</span>,</span>
<span id="cb37-23">                          <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">width =</span> <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.1</span>)</span>
<span id="cb37-24">}</span></code></pre></div></li>
<li><p><label><input type="checkbox"><strong>Create <code>inst/tinytest/test_summarize.R</code>.</strong> Each test uses a small hand-constructed data frame with known summary statistics.</label></p>
<div class="sourceCode" id="cb38" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb38-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># inst/tinytest/test_summarize.R</span></span>
<span id="cb38-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">library</span>(tinytest)</span>
<span id="cb38-3"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">source</span>(here<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">here</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'R/summarize_outcomes.R'</span>))</span>
<span id="cb38-4"></span>
<span id="cb38-5">tiny <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> tibble<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">tribble</span>(</span>
<span id="cb38-6">  <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">~</span>participant_id, <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">~</span>group, <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">~</span>visit_type, <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">~</span>outcome_a_imp,</span>
<span id="cb38-7">  <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1001</span>L, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'Active'</span>,  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'P1'</span>, <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">5.0</span>,</span>
<span id="cb38-8">  <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1002</span>L, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'Active'</span>,  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'P1'</span>, <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">7.0</span>,</span>
<span id="cb38-9">  <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1003</span>L, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'Control'</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'P1'</span>, <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">4.0</span>,</span>
<span id="cb38-10">  <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1004</span>L, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'Control'</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'P1'</span>, <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">6.0</span></span>
<span id="cb38-11">)</span>
<span id="cb38-12"></span>
<span id="cb38-13">out <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">summarize_outcome_by_group_visit</span>(</span>
<span id="cb38-14">  tiny, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'outcome_a_imp'</span>)</span>
<span id="cb38-15"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">expect_equal</span>(out<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">$</span>mean[out<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">$</span>group <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'Active'</span>],  <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">6.0</span>)</span>
<span id="cb38-16"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">expect_equal</span>(out<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">$</span>mean[out<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">$</span>group <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'Control'</span>], <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">5.0</span>)</span>
<span id="cb38-17"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">expect_equal</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">nrow</span>(out), <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>L)</span></code></pre></div></li>
<li><p><label><input type="checkbox"><strong>Create <code>inst/tinytest/test_model.R</code>.</strong> Test modelling functions against simulated data with known parameters. Use <code>set.seed()</code> for reproducibility.</label></p>
<div class="sourceCode" id="cb39" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb39-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># inst/tinytest/test_model.R</span></span>
<span id="cb39-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">library</span>(tinytest)</span>
<span id="cb39-3"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">library</span>(lme4)</span>
<span id="cb39-4"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">source</span>(here<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">here</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'R/model_outcomes.R'</span>))</span>
<span id="cb39-5"></span>
<span id="cb39-6"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">set.seed</span>(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">42</span>)</span>
<span id="cb39-7">n <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">200</span></span>
<span id="cb39-8">sim <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> tibble<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">tibble</span>(</span>
<span id="cb39-9">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">participant_id =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">rep</span>(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span>(n <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>), <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">each =</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>),</span>
<span id="cb39-10">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">group =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">factor</span>(</span>
<span id="cb39-11">    <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">rep</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">c</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'Active'</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'Control'</span>), <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">each =</span> n <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>)),</span>
<span id="cb39-12">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">visit_type =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">factor</span>(</span>
<span id="cb39-13">    <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">rep</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">c</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'P1'</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'V2'</span>), <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">times =</span> n <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>)),</span>
<span id="cb39-14">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">visit_age =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">rnorm</span>(n, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">50</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">10</span>),</span>
<span id="cb39-15">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">sex =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">factor</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sample</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">c</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'M'</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'F'</span>), n, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">replace =</span> <span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">TRUE</span>))</span>
<span id="cb39-16">)</span>
<span id="cb39-17"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># True effect of Active vs Control = 2.0</span></span>
<span id="cb39-18">sim<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">$</span>outcome_a_imp <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">5</span> <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span></span>
<span id="cb39-19">  <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span> <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> (sim<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">$</span>group <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'Active'</span>) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">rnorm</span>(n, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>)</span>
<span id="cb39-20"></span>
<span id="cb39-21">fit <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">fit_primary_lmm</span>(sim, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'outcome_a_imp'</span>)</span>
<span id="cb39-22">est <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> lme4<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">fixef</span>(fit)[[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'groupActive'</span>]]</span>
<span id="cb39-23"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">expect_true</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">abs</span>(est <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span> <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.5</span>)</span></code></pre></div></li>
<li><p><label><input type="checkbox"><strong>Test edge cases: missing data handling.</strong> The <code>_Imp</code> columns should not have NAs, but their unimputed counterparts might. Lock down the documented behaviour with a test.</label></p>
<div class="sourceCode" id="cb40" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb40-1">tiny_with_na <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> tiny</span>
<span id="cb40-2">tiny_with_na<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">$</span>outcome_a_imp[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>] <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">NA_real_</span></span>
<span id="cb40-3"></span>
<span id="cb40-4">out <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">summarize_outcome_by_group_visit</span>(</span>
<span id="cb40-5">  tiny_with_na, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'outcome_a_imp'</span>)</span>
<span id="cb40-6"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Active row: n drops from 2 to 1, mean from 6 to 7</span></span>
<span id="cb40-7"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">expect_equal</span>(out<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">$</span>n[out<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">$</span>group <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'Active'</span>],    <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>L)</span>
<span id="cb40-8"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">expect_equal</span>(out<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">$</span>mean[out<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">$</span>group <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'Active'</span>], <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">7.0</span>)</span></code></pre></div></li>
<li><p><label><input type="checkbox"><strong>Test edge cases: single-group scenarios.</strong> A pre-specified subgroup may turn out empty in the sample. Assert either a graceful empty result or a clear error.</label></p>
<div class="sourceCode" id="cb41" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb41-1">tiny_single <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> tiny <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span></span>
<span id="cb41-2">  dplyr<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">filter</span>(group <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'Active'</span>)</span>
<span id="cb41-3"></span>
<span id="cb41-4">out <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">summarize_outcome_by_group_visit</span>(</span>
<span id="cb41-5">  tiny_single, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'outcome_a_imp'</span>)</span>
<span id="cb41-6"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">expect_equal</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">nrow</span>(out), <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>L)</span>
<span id="cb41-7"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">expect_equal</span>(out<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">$</span>group[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>], <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'Active'</span>)</span></code></pre></div></li>
</ol>
</section>
<section id="phase-5-primary-analysis-items-39-45" class="level2">
<h2 class="anchored" data-anchor-id="phase-5-primary-analysis-items-39-45">Phase 5: Primary Analysis (Items 39-45)</h2>
<p>Phase 5 produces the primary-analysis report, including secondary outcomes and sensitivity analyses. The deliverables are <code>analysis/scripts/03-primary-analysis.R</code>, saved fitted-model objects in <code>analysis/data/derived_data/</code>, and <code>analysis/report/02-results.Rmd</code> (or <code>.qmd</code>).</p>
<ol start="39" type="1">
<li><p><label><input type="checkbox"><strong>Create <code>analysis/scripts/03-primary-analysis.R</code>.</strong> The script orchestrates; <code>R/</code> holds the reusable code.</label></p>
<div class="sourceCode" id="cb42" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb42-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># analysis/scripts/03-primary-analysis.R</span></span>
<span id="cb42-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">library</span>(here)</span>
<span id="cb42-3"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">library</span>(broom.mixed)</span>
<span id="cb42-4"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">source</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">here</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'R/model_outcomes.R'</span>))</span>
<span id="cb42-5"></span>
<span id="cb42-6">dat <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">readRDS</span>(</span>
<span id="cb42-7">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">here</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'analysis/data/derived_data/cleaned_data.rds'</span>)</span>
<span id="cb42-8">)</span>
<span id="cb42-9"></span>
<span id="cb42-10">fit_a <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">fit_primary_lmm</span>(dat, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'outcome_a_imp'</span>)</span>
<span id="cb42-11">fit_b <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">fit_primary_lmm</span>(dat, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'outcome_b_imp'</span>)</span>
<span id="cb42-12">fit_s <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">fit_primary_lmm</span>(dat, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'sub_total_imp'</span>)</span></code></pre></div></li>
<li><p><label><input type="checkbox"><strong>Implement the pre-specified analysis plan.</strong> Follow the SAP line by line. Mark any deviation from the SAP with a code comment that begins <code># SAP DEVIATION:</code> and document the reason.</label></p>
<div class="sourceCode" id="cb43" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb43-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># SAP Section 4.1: primary contrast 'Active vs Control'</span></span>
<span id="cb43-2"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># at the final follow-up visit (V4)</span></span>
<span id="cb43-3">primary_contrast_a <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> emmeans<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">emmeans</span>(</span>
<span id="cb43-4">  fit_a,</span>
<span id="cb43-5">  pairwise <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">~</span> group <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|</span> visit_type</span>
<span id="cb43-6">)<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">$</span>contrasts <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span></span>
<span id="cb43-7">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">as.data.frame</span>() <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span></span>
<span id="cb43-8">  dplyr<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">filter</span>(visit_type <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'V4'</span>)</span>
<span id="cb43-9"></span>
<span id="cb43-10"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># SAP DEVIATION: V3 contrast was added post-hoc at</span></span>
<span id="cb43-11"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># the request of the DSMB; report as exploratory.</span></span></code></pre></div></li>
<li><p><label><input type="checkbox"><strong>Save model objects to <code>analysis/data/derived_data/</code>.</strong> Persist each fitted model with <code>saveRDS()</code>; save the tidy and glance outputs as CSV for direct inclusion in tables. The derived data folder becomes the single source of truth for model results.</label></p>
<div class="sourceCode" id="cb44" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb44-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">saveRDS</span>(</span>
<span id="cb44-2">  fit_a,</span>
<span id="cb44-3">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">here</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'analysis/data/derived_data/model_outcome_a_lmm.rds'</span>)</span>
<span id="cb44-4">)</span>
<span id="cb44-5"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">saveRDS</span>(</span>
<span id="cb44-6">  fit_b,</span>
<span id="cb44-7">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">here</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'analysis/data/derived_data/model_outcome_b_lmm.rds'</span>)</span>
<span id="cb44-8">)</span>
<span id="cb44-9"></span>
<span id="cb44-10">readr<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">write_csv</span>(</span>
<span id="cb44-11">  broom.mixed<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">tidy</span>(fit_a),</span>
<span id="cb44-12">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">here</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'analysis/data/derived_data/tidy_outcome_a.csv'</span>)</span>
<span id="cb44-13">)</span>
<span id="cb44-14">readr<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">write_csv</span>(</span>
<span id="cb44-15">  broom.mixed<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">glance</span>(fit_a),</span>
<span id="cb44-16">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">here</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'analysis/data/derived_data/glance_outcome_a.csv'</span>)</span>
<span id="cb44-17">)</span></code></pre></div></li>
<li><p><label><input type="checkbox"><strong>Create <code>analysis/report/02-results.Rmd</code> (or <code>.qmd</code>).</strong> The results report loads the saved model objects and renders the result tables and figures. It contains no model-fitting code; if it does, the script and report can drift apart.</label></p>
<div class="sourceCode" id="cb45" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb45-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># In the report's R chunk, NOT in the script</span></span>
<span id="cb45-2">fit_a <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">readRDS</span>(here<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">here</span>(</span>
<span id="cb45-3">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'analysis/data/derived_data/model_outcome_a_lmm.rds'</span>))</span>
<span id="cb45-4">tidy_a <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> readr<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">read_csv</span>(here<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">here</span>(</span>
<span id="cb45-5">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'analysis/data/derived_data/tidy_outcome_a.csv'</span>))</span>
<span id="cb45-6"></span>
<span id="cb45-7">knitr<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">kable</span>(tidy_a, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">digits =</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>)</span></code></pre></div></li>
<li><p><label><input type="checkbox"><strong>Document primary outcome analysis in the report.</strong> Present the primary outcome model first, with a results paragraph that includes the point estimate, confidence interval, and inferential statistic for the pre-specified primary contrast.</label></p>
<p>A typical results sentence template:</p>
<div class="sourceCode" id="cb46" style="background: #f1f3f5;"><pre class="sourceCode markdown code-with-copy"><code class="sourceCode markdown"><span id="cb46-1">The mean Outcome A at V4 was lower in the Active</span>
<span id="cb46-2">arm than in the Control arm (estimated difference</span>
<span id="cb46-3">-3.2 points, 95% CI -5.1 to -1.3, p = 0.001),</span>
<span id="cb46-4">consistent with the pre-specified primary</span>
<span id="cb46-5">hypothesis. The fitted model was a linear mixed</span>
<span id="cb46-6">model with a random intercept per participant and</span>
<span id="cb46-7">fixed effects for group, visit, their interaction,</span>
<span id="cb46-8">and baseline covariates (age, sex):</span>
<span id="cb46-9"></span>
<span id="cb46-10">`outcome_a_imp ~ group * visit_type + visit_age +</span>
<span id="cb46-11">sex + (1 | participant_id)`</span></code></pre></div></li>
<li><p><label><input type="checkbox"><strong>Document secondary outcomes in the report.</strong> Present each secondary outcome with the same structure as the primary. Acknowledge the multiple-comparison context; either pre-specify a correction or label the analyses as exploratory.</label></p>
<div class="sourceCode" id="cb47" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb47-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Holm-adjusted p-values across the three primary</span></span>
<span id="cb47-2"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># contrasts (Outcome A, Outcome B, Sub_total)</span></span>
<span id="cb47-3">p_unadj <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">c</span>(</span>
<span id="cb47-4">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">outcome_a    =</span> <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.001</span>,</span>
<span id="cb47-5">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">outcome_b    =</span> <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.04</span>,</span>
<span id="cb47-6">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">sub_total    =</span> <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.18</span></span>
<span id="cb47-7">)</span>
<span id="cb47-8">p_holm <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">p.adjust</span>(p_unadj, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">method =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'holm'</span>)</span>
<span id="cb47-9">p_holm</span></code></pre></div></li>
<li><p><label><input type="checkbox"><strong>Document sensitivity analyses in the report.</strong> For each modelling assumption that could plausibly affect the conclusion, run a sensitivity analysis and document its result alongside the primary.</label></p>
<div class="sourceCode" id="cb48" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb48-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Sensitivity 1: complete-case using the unimputed</span></span>
<span id="cb48-2"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># underlying column (if available from the data</span></span>
<span id="cb48-3"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># provider) instead of the _Imp column.</span></span>
<span id="cb48-4">fit_a_cc <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">fit_primary_lmm</span>(dat, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'outcome_a_raw'</span>)</span>
<span id="cb48-5"></span>
<span id="cb48-6"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Sensitivity 2: alternative covariate set (drop</span></span>
<span id="cb48-7"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># baseline age)</span></span>
<span id="cb48-8">f_alt <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">as.formula</span>(</span>
<span id="cb48-9">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'outcome_a_imp ~ group * visit_type + sex + '</span>,</span>
<span id="cb48-10">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'(1 | participant_id)'</span>)</span>
<span id="cb48-11">fit_a_alt <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> lme4<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">lmer</span>(f_alt, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">data =</span> dat)</span>
<span id="cb48-12"></span>
<span id="cb48-13"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Compare estimates from primary vs sensitivity</span></span>
<span id="cb48-14">dplyr<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">bind_rows</span>(</span>
<span id="cb48-15">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">primary    =</span> broom.mixed<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">tidy</span>(fit_a),</span>
<span id="cb48-16">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">complete   =</span> broom.mixed<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">tidy</span>(fit_a_cc),</span>
<span id="cb48-17">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">no_age     =</span> broom.mixed<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">tidy</span>(fit_a_alt),</span>
<span id="cb48-18">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">.id =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'analysis'</span></span>
<span id="cb48-19">) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span></span>
<span id="cb48-20">  dplyr<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">filter</span>(term <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'groupActive'</span>)</span></code></pre></div></li>
</ol>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/zc-analysis-initiation-checklist/media/images/ambiance2.png" class="img-fluid figure-img"></p>
<figcaption>Editor view of the checklist artefact open alongside the working zzcollab project, showing how items are referenced by number from commit messages and TODO notes.</figcaption>
</figure>
</div>
</section>
<section id="phase-6-documentation-and-reproducibility-items-46-55" class="level2">
<h2 class="anchored" data-anchor-id="phase-6-documentation-and-reproducibility-items-46-55">Phase 6: Documentation and Reproducibility (Items 46-55)</h2>
<p>Phase 6 closes the loop: DESCRIPTION metadata, a project README, a rebuilt Docker image, and a fresh-container end-to-end reproducibility run.</p>
<ol start="46" type="1">
<li><p><label><input type="checkbox"><strong>Update <code>DESCRIPTION</code>: add a meaningful Title and Description.</strong> Title is one sentence, fewer than sixty-five characters. Description is one to three sentences naming the study population, primary outcomes, and analytic approach.</label></p>
<pre><code>Package: studya
Title: Study A: Active vs Control on Outcomes A and B
Version: 0.0.0.9000
Authors@R:
    person('First', 'Last', email = 'you@example.com',
           role = c('aut', 'cre'))
Description: Reproducible compendium for Study A, an
    IRB-approved randomised comparison of Active and
    Control on Outcomes A and B across four study
    visits (P1 baseline, V2-V4 follow-ups). Linear
    mixed models with random participant intercepts.</code></pre></li>
<li><p><label><input type="checkbox"><strong>Update <code>DESCRIPTION</code>: add or verify Authors.</strong> Use the Authors@R field with <code>person()</code> entries; do not silently leave the placeholder ‘Your Name’.</label></p>
<pre><code>Authors@R: c(
    person('Anna', 'Analyst',
           email = 'analyst@example.org',
           role = c('aut', 'cre'),
           comment = c(ORCID = '0000-0000-0000-0000')),
    person('Bob', 'Bioscientist',
           email = 'bob@example.org',
           role = 'ctb'))</code></pre></li>
<li><p><label><input type="checkbox"><strong>Verify all package dependencies listed in <code>DESCRIPTION</code>.</strong> Any package called but not declared will fail in a clean container. DESCRIPTION lists direct dependencies only; <code>renv.lock</code> pins the transitive closure.</label></p>
<div class="sourceCode" id="cb51" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb51-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Inside the container</span></span>
<span id="cb51-2">deps <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> renv<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">dependencies</span>()</span>
<span id="cb51-3"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">unique</span>(deps<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">$</span>Package) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sort</span>()</span></code></pre></div>
<div class="sourceCode" id="cb52" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb52-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">make</span> check-renv          <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># validates Imports vs code</span></span></code></pre></div></li>
<li><p><label><input type="checkbox"><strong>Create or update the project <code>README.md</code>.</strong> The README is the entry point for anyone reading the repository for the first time. Include a directory tree (<code>tree -L 2</code>), the research question, and the reproduction commands.</label></p>
<div class="sourceCode" id="cb53" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb53-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">tree</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-L</span> 2 <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-I</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'renv|_freeze|_site'</span> .</span></code></pre></div></li>
<li><p><label><input type="checkbox"><strong>Document reproduction instructions in the README.</strong> Provide a copy-pasteable sequence of shell commands that reproduces the analysis from a clean checkout. Test on a machine that does not already have the project set up.</label></p>
<div class="sourceCode" id="cb54" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb54-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Reproduce Study A end-to-end</span></span>
<span id="cb54-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> clone https://github.com/<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span>owner<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span>/studya.git</span>
<span id="cb54-3"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">cd</span> studya</span>
<span id="cb54-4"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">make</span> docker-build</span>
<span id="cb54-5"></span>
<span id="cb54-6"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Inside the container, run the linear pipeline</span></span>
<span id="cb54-7"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">make</span> r</span></code></pre></div>
<div class="sourceCode" id="cb55" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb55-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">source</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'analysis/scripts/01-clean-data.R'</span>)</span>
<span id="cb55-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">source</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'analysis/scripts/02-eda.R'</span>)</span>
<span id="cb55-3"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">source</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'analysis/scripts/03-primary-analysis.R'</span>)</span>
<span id="cb55-4">quarto<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">quarto_render</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'analysis/report/01-eda.qmd'</span>)</span>
<span id="cb55-5">quarto<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">quarto_render</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'analysis/report/02-results.qmd'</span>)</span></code></pre></div></li>
<li><p><label><input type="checkbox"><strong>Rebuild Docker image: <code>make docker-build</code>.</strong> Tag with the current git SHA so future readers can identify the exact build that produced the published results.</label></p>
<div class="sourceCode" id="cb56" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb56-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">make</span> docker-build</span>
<span id="cb56-2"><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">GIT_SHA</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$(</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> rev-parse <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--short</span> HEAD<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">)</span></span>
<span id="cb56-3"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">docker</span> tag studya:latest studya:<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">${GIT_SHA}</span></span>
<span id="cb56-4"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">docker</span> images studya</span></code></pre></div></li>
<li><p><label><input type="checkbox"><strong>Verify reproducibility: run all scripts in a fresh container.</strong> The end-to-end run is the canonical reproducibility test.</label></p>
<div class="sourceCode" id="cb57" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb57-1"><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">GIT_SHA</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$(</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> rev-parse <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--short</span> HEAD<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">)</span></span>
<span id="cb57-2"></span>
<span id="cb57-3"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">docker</span> run <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--rm</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-v</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$(</span><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">pwd</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">)</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">:/project"</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-w</span> /project <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb57-4">  studya:<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">${GIT_SHA}</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb57-5">  Rscript analysis/scripts/01-clean-data.R</span>
<span id="cb57-6"></span>
<span id="cb57-7"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">docker</span> run <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--rm</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-v</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$(</span><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">pwd</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">)</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">:/project"</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-w</span> /project <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb57-8">  studya:<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">${GIT_SHA}</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb57-9">  Rscript analysis/scripts/02-eda.R</span>
<span id="cb57-10"></span>
<span id="cb57-11"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">docker</span> run <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--rm</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-v</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$(</span><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">pwd</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">)</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">:/project"</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-w</span> /project <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb57-12">  studya:<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">${GIT_SHA}</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb57-13">  Rscript analysis/scripts/03-primary-analysis.R</span>
<span id="cb57-14"></span>
<span id="cb57-15"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">docker</span> run <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--rm</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-v</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$(</span><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">pwd</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">)</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">:/project"</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-w</span> /project <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb57-16">  studya:<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">${GIT_SHA}</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb57-17">  quarto render analysis/report/02-results.qmd</span></code></pre></div></li>
<li><p><label><input type="checkbox"><strong>Confirm all outputs match expected results.</strong> Bit-for-bit reproducibility is a strong signal; numerically identical floating-point results without timestamp or render-metadata noise is the realistic target.</label></p>
<div class="sourceCode" id="cb58" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb58-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Hash the derived data and figures from the fresh</span></span>
<span id="cb58-2"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># run, compare against the previously committed</span></span>
<span id="cb58-3"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># versions</span></span>
<span id="cb58-4"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> stash</span>
<span id="cb58-5"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">shasum</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-a</span> 256 analysis/data/derived_data/<span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">*</span>.rds <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb58-6">              analysis/figures/<span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">*</span>.png <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> /tmp/fresh.sha256</span>
<span id="cb58-7"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> stash pop</span>
<span id="cb58-8"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">shasum</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-a</span> 256 analysis/data/derived_data/<span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">*</span>.rds <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb58-9">              analysis/figures/<span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">*</span>.png <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> /tmp/committed.sha256</span>
<span id="cb58-10"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">diff</span> /tmp/fresh.sha256 /tmp/committed.sha256</span></code></pre></div></li>
<li><p><label><input type="checkbox"><strong>Run <code>make test</code> and verify all tests pass.</strong> A failing test at the publication stage is a stop-the-line event.</label></p>
<div class="sourceCode" id="cb59" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb59-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">make</span> test</span>
<span id="cb59-2"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Equivalent inside R:</span></span>
<span id="cb59-3"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># tinytest::run_test_dir('inst/tinytest')</span></span>
<span id="cb59-4"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># testthat::test_local()</span></span></code></pre></div></li>
<li><p><label><input type="checkbox"><strong>Final review: all Five Pillars complete and consistent.</strong> Walk through each pillar one last time and confirm that the artefacts on disk tell a single, consistent story.</label></p>
<div class="sourceCode" id="cb60" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb60-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Pillar 1: Dockerfile R version</span></span>
<span id="cb60-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">grep</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'^FROM'</span> Dockerfile</span>
<span id="cb60-3"></span>
<span id="cb60-4"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Pillar 2: renv.lock R version</span></span>
<span id="cb60-5"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">jq</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'.R.Version'</span> renv.lock</span>
<span id="cb60-6"></span>
<span id="cb60-7"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Pillar 3: .Rprofile activates renv</span></span>
<span id="cb60-8"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">grep</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-E</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'renv/activate'</span> .Rprofile</span>
<span id="cb60-9"></span>
<span id="cb60-10"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Pillar 4: source code is sourceable</span></span>
<span id="cb60-11"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">Rscript</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-e</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'devtools::load_all()'</span></span>
<span id="cb60-12"></span>
<span id="cb60-13"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Pillar 5: raw data is read-only and documented</span></span>
<span id="cb60-14"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">ls</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-la</span> analysis/data/raw_data/</span>
<span id="cb60-15"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">test</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-f</span> analysis/data/README.md <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">&amp;&amp;</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb60-16">  <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'data README present'</span></span>
<span id="cb60-17"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">test</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-f</span> docs/data-dictionary.md <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">&amp;&amp;</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb60-18">  <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'data dictionary present'</span></span></code></pre></div>
<p>The grep, jq, and ls outputs together must agree: same R version in the Dockerfile and renv.lock, renv activation in .Rprofile, sourceable code, and read-only documented raw data.</p></li>
</ol>
</section>
</section>
<section id="daily-workflow" class="level1">
<h1>Daily Workflow</h1>
<p>The checklist is not consulted line by line every day. It is consulted at three moments:</p>
<ol type="1">
<li><strong>Project kick-off.</strong> Read the entire checklist before doing anything else. Skim, do not implement; the goal is to know what is coming so the early decisions (directory layout, factor coding, file naming) do not need to be unwound later.</li>
<li><strong>Phase boundaries.</strong> At the end of each phase, work through the checklist items for that phase explicitly. Tick each item off in a private copy (<code>docs/analysis-checklist-&lt;project&gt;.md</code> is a reasonable convention) so the project’s progress is auditable.</li>
<li><strong>Reproducibility gate.</strong> Before circulating any externally facing report, run Phase 6 in full. This is the gate that catches the inconsistency between the author’s laptop and a fresh container.</li>
</ol>
<p>The numbered structure is also useful in commit messages (‘Item 17: add plausibility-bound assertions for outcomes A and B’) and in TODO notes (‘Item 41: still need to save fitted models as RDS’).</p>
<table class="caption-top table">
<colgroup>
<col style="width: 46%">
<col style="width: 53%">
</colgroup>
<thead>
<tr class="header">
<th>Command</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><code>quarto render docs/analysis-checklist.qmd</code></td>
<td>Render the standalone checklist</td>
</tr>
<tr class="even">
<td><code>make r</code></td>
<td>Enter the Docker container</td>
</tr>
<tr class="odd">
<td><code>make test</code></td>
<td>Run the tinytest and testthat suites</td>
</tr>
<tr class="even">
<td><code>make check-renv</code></td>
<td>Validate package dependencies</td>
</tr>
<tr class="odd">
<td><code>make docker-build</code></td>
<td>Rebuild the image after package changes</td>
</tr>
</tbody>
</table>
</section>
<section id="things-to-watch-out-for" class="level1">
<h1>Things to Watch Out For</h1>
<ol type="1">
<li><p><strong>The checklist is a tool, not a contract.</strong> Items are meant to be skipped consciously, not silently. If the project has no longitudinal structure, Item 16 (visit variables) is not applicable; mark it ‘N/A’ with a one-line reason rather than removing it from the file.</p></li>
<li><p><strong>Phase 1 takes longer than expected.</strong> First-time users of zzcollab routinely underestimate Item 6 (data provenance README) and Item 7 (data dictionary). Both feel like overhead and pay off only weeks later, when they are the difference between a clean Phase 2 and a month of detective work. The <code>_Imp</code> ambiguity in the example dataset is a concrete instance: resolving it in Item 7 takes ten minutes; discovering it in Phase 5 takes days.</p></li>
<li><p><strong>The integrity tests are not unit tests.</strong> Items 11-17 assert contracts about the raw data, not behaviours of functions. They live in <code>inst/tinytest/</code>, not <code>tests/testthat/</code>, by convention. Mixing the two leads to tests that pass against frozen fixtures but fail against live data.</p></li>
<li><p><strong>Item 41 (save model objects) is easy to forget.</strong> Without saved RDS objects, the report in Phase 5 re-fits models on every render, which couples the report to a particular package version and can produce different results across renders. Save once, load thereafter.</p></li>
<li><p><strong>Item 52 (fresh-container rerun) catches errors that nothing else does.</strong> A pipeline that runs on the author’s laptop with a stale R session can fail in a fresh container because of a missing <code>library()</code> call, a hardcoded path, or a package not declared in DESCRIPTION. Run Item 52 weekly during active analysis, not just at the end.</p></li>
<li><p><strong>Item 55 is not a formality.</strong> The Five Pillars review has caught real inconsistencies (mismatched R versions, undeclared dependencies, raw data committed under the wrong path) on every project that used it. Treat it as a separate review pass, with at least one fresh pair of eyes.</p></li>
</ol>
</section>
<section id="when-the-updated-extract-arrives" class="level1">
<h1>When the Updated Extract Arrives</h1>
<p>The email mentioned that ‘an updated extract may follow if a few late withdrawals come in’. This is the moment the Phase 2 tests earn their keep. The procedure is four commands:</p>
<div class="sourceCode" id="cb61" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb61-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 1. Drop the new file in and lock it down.</span></span>
<span id="cb61-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">mv</span> ~/Downloads/studyA_data_2026Q2.csv <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb61-3">   analysis/data/raw_data/</span>
<span id="cb61-4"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">chmod</span> 444 analysis/data/raw_data/studyA_data_2026Q2.csv</span>
<span id="cb61-5"></span>
<span id="cb61-6"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 2. Update the data README with the new receipt date</span></span>
<span id="cb61-7"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#    and the provider's notes on what changed.</span></span>
<span id="cb61-8"><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$EDITOR</span> analysis/data/README.md</span>
<span id="cb61-9"></span>
<span id="cb61-10"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 3. Update load_raw_data() to point at the new file</span></span>
<span id="cb61-11"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#    (or accept a filename argument), then run the</span></span>
<span id="cb61-12"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#    integrity tests in the container.</span></span>
<span id="cb61-13"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">make</span> r</span></code></pre></div>
<div class="sourceCode" id="cb62" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb62-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 4. Inside R: run integrity tests, then re-source</span></span>
<span id="cb62-2"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#    the pipeline if they pass.</span></span>
<span id="cb62-3">tinytest<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">run_test_file</span>(</span>
<span id="cb62-4">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'inst/tinytest/test_data_integrity.R'</span>)</span>
<span id="cb62-5"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># All passed -&gt; safe to re-run the pipeline.</span></span>
<span id="cb62-6"></span>
<span id="cb62-7"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">source</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'analysis/scripts/01-clean-data.R'</span>)</span>
<span id="cb62-8"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">source</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'analysis/scripts/02-eda.R'</span>)</span>
<span id="cb62-9"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">source</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'analysis/scripts/03-primary-analysis.R'</span>)</span>
<span id="cb62-10"></span>
<span id="cb62-11">quarto<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">quarto_render</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'analysis/report/01-eda.qmd'</span>)</span>
<span id="cb62-12">quarto<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">quarto_render</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'analysis/report/02-results.qmd'</span>)</span></code></pre></div>
<p>If Items 11-17 pass silently, the contract held and the data are just smaller or larger. If any of them fails loudly, the contract changed; investigate before proceeding. A pipeline without integrity tests requires the analyst to re-derive every assumption about the new file by hand; a pipeline with the Phase 2 tests in place reduces the update to a build-and-render cycle.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/zc-analysis-initiation-checklist/media/images/ambiance3.png" class="img-fluid figure-img"></p>
<figcaption>A finished compendium archived alongside the rendered report, with the standalone checklist filed in docs/.</figcaption>
</figure>
</div>
</section>
<section id="what-did-we-learn" class="level1">
<h1>What Did We Learn?</h1>
<section id="lessons-learnt" class="level2">
<h2 class="anchored" data-anchor-id="lessons-learnt">Lessons Learnt</h2>
<p><strong>Conceptual Understanding:</strong></p>
<ul>
<li>A checklist is a tool for surfacing the steps that are easy to skip, not for enforcing a particular workflow. The same checklist will produce slightly different artefacts in two projects; that is the intended outcome.</li>
<li>The phase boundaries are useful precisely because they are arbitrary. Anything that lets a project pause and audit itself before moving on is worth the friction.</li>
<li>The numbered structure is more valuable than the prose. The annotation paragraphs are scaffolding for first-time use; experienced users only consult the numbers.</li>
<li>Reproducibility is a property of the workspace as a whole, not of any single artefact. The Five Pillars review at Item 55 is the only point that audits the workspace rather than its parts.</li>
</ul>
<p><strong>Technical Skills:</strong></p>
<ul>
<li>Quarto is a better authoring surface for the checklist than LaTeX. The same source renders to HTML for inline reading and PDF for printing, and the document carries a YAML header that the analyst can edit per project.</li>
<li>A standalone <code>docs/analysis-checklist.qmd</code> lives outside the analysis pipeline, so changes to the checklist do not retrigger expensive analysis re-renders.</li>
<li><code>tinytest</code> in <code>inst/tinytest/</code> is well suited to the data-integrity tests in Items 11-17. Its lightweight API is appropriate for assertions about a single loaded data frame.</li>
<li>Saving fitted model objects as RDS in <code>analysis/data/derived_data/</code> decouples Phase 5 from the report rendering and makes report regeneration cheap.</li>
</ul>
<p><strong>Gotchas and Pitfalls:</strong></p>
<ul>
<li>Forgetting to set raw data files read-only (Item 5) produces silent corruption that is impossible to detect without a checksum.</li>
<li>Conflating the data dictionary (a contract) with the cleaned data documentation (a description) blurs the audit trail. Keep them separate.</li>
<li>Running tinytest from the host instead of from inside the container exposes the tests to whatever R version the host happens to have, defeating the purpose of the zzcollab environment.</li>
<li>A passing <code>make test</code> does not certify reproducibility; only Item 52 (fresh-container rerun) does. Several projects in the wild conflate the two.</li>
</ul>
</section>
<section id="limitations" class="level2">
<h2 class="anchored" data-anchor-id="limitations">Limitations</h2>
<ul>
<li><strong>Six phases is one ordering, not the ordering.</strong> A simulation study or a methods paper will want to reorder Phases 3 and 4. The checklist accommodates this by numbering items, not by enforcing serial execution.</li>
<li><strong>The checklist assumes a tabular outcome.</strong> Imaging pipelines, NLP corpora, and high-throughput genomics will need different Phase 2 contents.</li>
<li><strong>No version-control discipline.</strong> The checklist does not prescribe a git branching model, commit message format, or PR review workflow. Those belong in a separate document.</li>
<li><strong>No statistical analysis plan template.</strong> Item 40 says ‘follow the SAP’; it does not provide one. Drafting the SAP is the analyst’s responsibility.</li>
<li><strong>No coverage of stakeholder-facing artefacts.</strong> The checklist ends at Item 55 (reproducible compendium). It says nothing about manuscript drafting, conference posters, or oral presentations.</li>
</ul>
</section>
<section id="opportunities-for-improvement" class="level2">
<h2 class="anchored" data-anchor-id="opportunities-for-improvement">Opportunities for Improvement</h2>
<p>Several avenues would be worth exploring further:</p>
<ol type="1">
<li>Add a Phase 0 with project-charter items (research question, target population, success criteria) that precede the workspace creation in Item 1.</li>
<li>Add a ‘completion column’ to the rendered checklist with a date and analyst initials, so the artefact doubles as a sign-off log.</li>
<li>Extend Phase 6 with archival items (DOI assignment, Zenodo deposit, OSF preregistration) for projects that require external archival of the compendium.</li>
<li>Provide a tinytest harness that takes the checklist as input and asserts each item’s existence (<code>tests/integration/test-checklist-coverage.R</code>).</li>
<li>Translate the checklist into a Quarto project template that initialises an empty <code>docs/analysis-checklist.qmd</code> alongside the boilerplate <code>Dockerfile</code>, <code>Makefile</code>, <code>DESCRIPTION</code> produced by <code>zzc analysis</code>.</li>
</ol>
</section>
</section>
<section id="wrapping-up" class="level1">
<h1>Wrapping Up</h1>
<p>A 55-item checklist is enough structure to take a single CSV in the inbox to a publishable, reproducible compendium, and short enough to read end to end before doing anything. The six phases correspond to deliverables that map directly onto the zzcollab workspace: a buildable container, a validated dataset, an EDA report, tested functions, a primary-analysis report, and an archival compendium.</p>
<p>The most valuable lesson from running the checklist against the example dataset (a single CSV, no codebook, an updated extract on the way) was that Phase 1 and Phase 2 absorb most of the apparent overhead and produce most of the durable value. By the time Phase 3 begins, the analysis runs against a typed, tested, documented dataset, and the rest of the project is a series of auditable transformations on it.</p>
<p>In conclusion, four points merit emphasis. First, a six-phase, 55-item checklist drives a zzcollab analysis from a CSV in the inbox to a published, reproducible report. Second, phase boundaries are the natural audit points; Phase 6 is the only phase that audits the workspace as a whole. Third, the Phase 2 integrity tests pay off when the updated extract arrives. Fourth, the numbered structure is the durable feature; the prose is scaffolding for first-time users.</p>
</section>
<section id="see-also" class="level1">
<h1>See Also</h1>
<p><strong>Related posts:</strong></p>
<ul>
<li><a href="https://focusonr.org/posts/templatesetup/">Setup Post Template (worked example: AWS CLI provisioning)</a>: the post-47 template that this workflow post adapts.</li>
<li><a href="https://focusonr.org/posts/penguins1zzcollab/">Palmer Penguins, Part 1: EDA and Simple Regression</a>: a worked zzcollab analysis that exercises Phases 1 through 5 on a small public dataset.</li>
</ul>
<p><strong>Key resources:</strong></p>
<ul>
<li><a href="https://github.com/rgt47/zzcollab">zzcollab framework</a>: the Docker-based research compendium framework that this checklist is written against.</li>
<li><a href="https://github.com/benmarwick/rrtools">rrtools: an R package for writing reproducible research compendia</a>: the rrtools convention that the zzcollab layout extends.</li>
<li><a href="https://book.the-turing-way.org/reproducible-research/reproducible-research">The Turing Way: Guide for Reproducible Research</a>: a broader treatment of reproducibility practice.</li>
<li><a href="https://quarto.org/docs/">Quarto documentation</a>: authoring reference for the standalone checklist artefact.</li>
</ul>
<hr>
</section>
<section id="reproducibility" class="level1">
<h1>Reproducibility</h1>
<p><strong>Tested configuration:</strong></p>
<table class="caption-top table">
<thead>
<tr class="header">
<th>Component</th>
<th>Version</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>Operating system</td>
<td>macOS 15.4</td>
</tr>
<tr class="even">
<td>zzcollab</td>
<td>2.4.0</td>
</tr>
<tr class="odd">
<td>R</td>
<td>4.5.1</td>
</tr>
<tr class="even">
<td>Quarto</td>
<td>1.5.x</td>
</tr>
<tr class="odd">
<td>renv</td>
<td>1.0.x</td>
</tr>
<tr class="even">
<td>Docker Desktop</td>
<td>4.x</td>
</tr>
<tr class="odd">
<td>Last verified</td>
<td>2026-04-29</td>
</tr>
</tbody>
</table>
<p><strong>Configuration files:</strong></p>
<ul>
<li><code>docs/analysis-checklist.qmd</code>: the standalone generic checklist, archived alongside this post for offline reference.</li>
<li><code>Dockerfile</code>, <code>renv.lock</code>, <code>.Rprofile</code>: the Five Pillars computational environment shared with every zzcollab workspace.</li>
<li><code>Makefile</code>: standard zzcollab targets (<code>r</code>, <code>test</code>, <code>check-renv</code>, <code>docker-build</code>).</li>
</ul>
<p><strong>To reproduce end-to-end:</strong></p>
<div class="sourceCode" id="cb63" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb63-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> clone <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span>this-post-repo<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span></span>
<span id="cb63-2"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">cd</span> zzcollab-analysis-checklist</span>
<span id="cb63-3"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">make</span> docker-build</span>
<span id="cb63-4"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">quarto</span> render index.qmd <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--to</span> pdf</span></code></pre></div>
<hr>
</section>
<section id="appendix-a-printable-reference-card" class="level1">
<h1>Appendix A: Printable Reference Card</h1>
<p>The annotated walkthrough above is the durable teaching content. The compact table below is the operational artefact, intended to be printed as a single sheet and ticked off in pen as a project progresses. The PDF render ships a fully-gridded version with cell borders; the HTML render uses the standard browser table.</p>
<table class="caption-top table">
<colgroup>
<col style="width: 5%">
<col style="width: 61%">
<col style="width: 6%">
<col style="width: 17%">
<col style="width: 7%">
</colgroup>
<thead>
<tr class="header">
<th style="text-align: right;">#</th>
<th style="text-align: left;">Item</th>
<th style="text-align: center;">Done</th>
<th style="text-align: center;">Date completed</th>
<th style="text-align: left;">Notes</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td style="text-align: right;"></td>
<td style="text-align: left;"><strong>Phase 1: Project Setup</strong></td>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: left;"></td>
</tr>
<tr class="even">
<td style="text-align: right;">1</td>
<td style="text-align: left;">Dockerfile exists and builds</td>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: left;"></td>
</tr>
<tr class="odd">
<td style="text-align: right;">2</td>
<td style="text-align: left;">renv.lock lists required packages</td>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: left;"></td>
</tr>
<tr class="even">
<td style="text-align: right;">3</td>
<td style="text-align: left;">.Rprofile activates renv</td>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: left;"></td>
</tr>
<tr class="odd">
<td style="text-align: right;">4</td>
<td style="text-align: left;">Source code directories exist</td>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: left;"></td>
</tr>
<tr class="even">
<td style="text-align: right;">5</td>
<td style="text-align: left;">Raw data is read-only in raw_data/</td>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: left;"></td>
</tr>
<tr class="odd">
<td style="text-align: right;">6</td>
<td style="text-align: left;">analysis/data/README.md documents provenance</td>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: left;"></td>
</tr>
<tr class="even">
<td style="text-align: right;">7</td>
<td style="text-align: left;">Data dictionary written</td>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: left;"></td>
</tr>
<tr class="odd">
<td style="text-align: right;">8</td>
<td style="text-align: left;">Analysis packages installed and snapshotted</td>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: left;"></td>
</tr>
<tr class="even">
<td style="text-align: right;"></td>
<td style="text-align: left;"><strong>Phase 2: Data Ingestion and Validation</strong></td>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: left;"></td>
</tr>
<tr class="odd">
<td style="text-align: right;">9</td>
<td style="text-align: left;">R/load_data.R defines load_raw_data()</td>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: left;"></td>
</tr>
<tr class="even">
<td style="text-align: right;">10</td>
<td style="text-align: left;">inst/tinytest/test_data_integrity.R created</td>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: left;"></td>
</tr>
<tr class="odd">
<td style="text-align: right;">11</td>
<td style="text-align: left;">Test: expected number of columns</td>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: left;"></td>
</tr>
<tr class="even">
<td style="text-align: right;">12</td>
<td style="text-align: left;">Test: expected column names</td>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: left;"></td>
</tr>
<tr class="odd">
<td style="text-align: right;">13</td>
<td style="text-align: left;">Test: ID columns no NAs, expected format</td>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: left;"></td>
</tr>
<tr class="even">
<td style="text-align: right;">14</td>
<td style="text-align: left;">Test: pre-specified categorical levels match</td>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: left;"></td>
</tr>
<tr class="odd">
<td style="text-align: right;">15</td>
<td style="text-align: left;">Test: demographic categorical levels match</td>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: left;"></td>
</tr>
<tr class="even">
<td style="text-align: right;">16</td>
<td style="text-align: left;">Test: visit variables in pre-specified set</td>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: left;"></td>
</tr>
<tr class="odd">
<td style="text-align: right;">17</td>
<td style="text-align: left;">Test: numeric ranges plausible</td>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: left;"></td>
</tr>
<tr class="even">
<td style="text-align: right;">18</td>
<td style="text-align: left;">analysis/scripts/01-clean-data.R written</td>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: left;"></td>
</tr>
<tr class="odd">
<td style="text-align: right;">19</td>
<td style="text-align: left;">janitor::clean_names() applied</td>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: left;"></td>
</tr>
<tr class="even">
<td style="text-align: right;">20</td>
<td style="text-align: left;">Categorical variables converted to factors</td>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: left;"></td>
</tr>
<tr class="odd">
<td style="text-align: right;">21</td>
<td style="text-align: left;">Missing values handled and documented</td>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: left;"></td>
</tr>
<tr class="even">
<td style="text-align: right;">22</td>
<td style="text-align: left;">Cleaned data saved to derived_data/</td>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: left;"></td>
</tr>
<tr class="odd">
<td style="text-align: right;"></td>
<td style="text-align: left;"><strong>Phase 3: Exploratory Analysis</strong></td>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: left;"></td>
</tr>
<tr class="even">
<td style="text-align: right;">23</td>
<td style="text-align: left;">analysis/scripts/02-eda.R written</td>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: left;"></td>
</tr>
<tr class="odd">
<td style="text-align: right;">24</td>
<td style="text-align: left;">Summary statistics by group computed</td>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: left;"></td>
</tr>
<tr class="even">
<td style="text-align: right;">25</td>
<td style="text-align: left;">Baseline balance checked</td>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: left;"></td>
</tr>
<tr class="odd">
<td style="text-align: right;">26</td>
<td style="text-align: left;">Outcome distributions visualised</td>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: left;"></td>
</tr>
<tr class="even">
<td style="text-align: right;">27</td>
<td style="text-align: left;">Outliers and data-quality flags documented</td>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: left;"></td>
</tr>
<tr class="odd">
<td style="text-align: right;">28</td>
<td style="text-align: left;">analysis/report/01-eda.qmd written</td>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: left;"></td>
</tr>
<tr class="even">
<td style="text-align: right;">29</td>
<td style="text-align: left;">Sample size and missingness documented</td>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: left;"></td>
</tr>
<tr class="odd">
<td style="text-align: right;">30</td>
<td style="text-align: left;">Table 1 included</td>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: left;"></td>
</tr>
<tr class="even">
<td style="text-align: right;">31</td>
<td style="text-align: left;">Trajectory plots included</td>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: left;"></td>
</tr>
<tr class="odd">
<td style="text-align: right;"></td>
<td style="text-align: left;"><strong>Phase 4: Analysis Functions</strong></td>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: left;"></td>
</tr>
<tr class="even">
<td style="text-align: right;">32</td>
<td style="text-align: left;">R/summarize_outcomes.R written</td>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: left;"></td>
</tr>
<tr class="odd">
<td style="text-align: right;">33</td>
<td style="text-align: left;">R/model_outcomes.R written</td>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: left;"></td>
</tr>
<tr class="even">
<td style="text-align: right;">34</td>
<td style="text-align: left;">R/plot_outcomes.R written</td>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: left;"></td>
</tr>
<tr class="odd">
<td style="text-align: right;">35</td>
<td style="text-align: left;">inst/tinytest/test_summarize.R passes</td>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: left;"></td>
</tr>
<tr class="even">
<td style="text-align: right;">36</td>
<td style="text-align: left;">inst/tinytest/test_model.R passes</td>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: left;"></td>
</tr>
<tr class="odd">
<td style="text-align: right;">37</td>
<td style="text-align: left;">Missing-data edge cases tested</td>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: left;"></td>
</tr>
<tr class="even">
<td style="text-align: right;">38</td>
<td style="text-align: left;">Single-group edge cases tested</td>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: left;"></td>
</tr>
<tr class="odd">
<td style="text-align: right;"></td>
<td style="text-align: left;"><strong>Phase 5: Primary Analysis</strong></td>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: left;"></td>
</tr>
<tr class="even">
<td style="text-align: right;">39</td>
<td style="text-align: left;">analysis/scripts/03-primary-analysis.R written</td>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: left;"></td>
</tr>
<tr class="odd">
<td style="text-align: right;">40</td>
<td style="text-align: left;">Pre-specified analysis plan implemented</td>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: left;"></td>
</tr>
<tr class="even">
<td style="text-align: right;">41</td>
<td style="text-align: left;">Fitted model objects saved to derived_data/</td>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: left;"></td>
</tr>
<tr class="odd">
<td style="text-align: right;">42</td>
<td style="text-align: left;">analysis/report/02-results.qmd written</td>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: left;"></td>
</tr>
<tr class="even">
<td style="text-align: right;">43</td>
<td style="text-align: left;">Primary outcome documented</td>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: left;"></td>
</tr>
<tr class="odd">
<td style="text-align: right;">44</td>
<td style="text-align: left;">Secondary outcomes documented</td>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: left;"></td>
</tr>
<tr class="even">
<td style="text-align: right;">45</td>
<td style="text-align: left;">Sensitivity analyses documented</td>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: left;"></td>
</tr>
<tr class="odd">
<td style="text-align: right;"></td>
<td style="text-align: left;"><strong>Phase 6: Documentation and Reproducibility</strong></td>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: left;"></td>
</tr>
<tr class="even">
<td style="text-align: right;">46</td>
<td style="text-align: left;">DESCRIPTION Title and Description updated</td>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: left;"></td>
</tr>
<tr class="odd">
<td style="text-align: right;">47</td>
<td style="text-align: left;">DESCRIPTION Authors set</td>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: left;"></td>
</tr>
<tr class="even">
<td style="text-align: right;">48</td>
<td style="text-align: left;">DESCRIPTION Imports verified</td>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: left;"></td>
</tr>
<tr class="odd">
<td style="text-align: right;">49</td>
<td style="text-align: left;">Project README.md created or updated</td>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: left;"></td>
</tr>
<tr class="even">
<td style="text-align: right;">50</td>
<td style="text-align: left;">Reproduction instructions documented</td>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: left;"></td>
</tr>
<tr class="odd">
<td style="text-align: right;">51</td>
<td style="text-align: left;">Docker image rebuilt and tagged with git SHA</td>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: left;"></td>
</tr>
<tr class="even">
<td style="text-align: right;">52</td>
<td style="text-align: left;">Fresh-container end-to-end run passes</td>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: left;"></td>
</tr>
<tr class="odd">
<td style="text-align: right;">53</td>
<td style="text-align: left;">Outputs match committed versions</td>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: left;"></td>
</tr>
<tr class="even">
<td style="text-align: right;">54</td>
<td style="text-align: left;">make test passes in fresh container</td>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: left;"></td>
</tr>
<tr class="odd">
<td style="text-align: right;">55</td>
<td style="text-align: left;">Five Pillars consistency review complete</td>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: left;"></td>
</tr>
</tbody>
</table>
<p>A two-up print on letter or A4 stationery yields a single double-sided card. Items marked ‘N/A’ for the project at hand should carry a brief reason in the Notes column rather than a tick.</p>
<hr>
</section>
<section id="lets-connect" class="level1">
<h1>Let’s Connect</h1>
<p><em>Have questions, suggestions, or spot an error? Let me know.</em></p>
<ul>
<li><strong>GitHub:</strong> <a href="https://github.com/rgt47">rgt47</a></li>
<li><strong>Twitter/X:</strong> <a href="https://twitter.com/rgt47"><span class="citation" data-cites="rgt47">@rgt47</span></a></li>
<li><strong>LinkedIn:</strong> <a href="https://linkedin.com/in/rgthomaslab">Ronald Glenn Thomas</a></li>
<li><strong>Email:</strong> <a href="https://rgtlab.org/contact">Contact form</a></li>
</ul>
<p>I would enjoy hearing from you if:</p>
<ul>
<li>You spot an error or a stronger ordering for any phase in the checklist.</li>
<li>You have suggestions for items that should be added (or removed).</li>
<li>You have used a similar checklist in a different research domain and want to compare notes.</li>
<li>You just want to say hello and connect.</li>
</ul>
<hr>
<p><em>Rendered on 2026-04-30 at 08:06 PDT.</em><br> <em>Source: ~/Dropbox/prj/qblog/posts/61-zzcollab-analysis-checklist/zzcollab-analysis-checklist/analysis/report/index.qmd</em></p>
<!-- ============================================================================
PRE-PUBLISH QA CHECKLIST: verify each item BEFORE setting draft: false

[ ] YAML
    [x] title, subtitle, date, categories, description, image filled in
    [x] document-type: 'blog'
    [x] draft: false (still true pending hero / ambiance imagery)

[x] Narrative complete (no remaining `[bracketed]` placeholders)

[x] Configuration artifacts present
    [x] Standalone Quarto checklist in docs/analysis-checklist.qmd
    [x] Workspace tree rendered and matches zzc convention
    [x] Verification commands documented in Daily Workflow table

[x] Things to Watch Out For
    [x] At least 5 gotchas listed
    [x] Each gotcha has a symptom AND a recommended response

[ ] Visual design
    [ ] Hero image (width=80%) replaced from post-47 boilerplate
    [ ] Three ambiance images replaced from post-47 boilerplate
    [ ] Hero and ambiance captions describe the actual image
    [x] No hand-coded 'Download PDF' link in body
    [ ] media/images/README.md attributes every image

[x] Content quality
    [x] Learner voice: author positioned as peer, not expert
    [x] Zero emoji anywhere (narrative, comments, captions)
    [x] Zero em dashes (forbidden per house style)
    [x] Single quotes preferred over double quotes in prose
    [x] Each command and item interpreted in plain language

[x] Reproducibility
    [x] Version matrix table filled in
    [x] Standalone checklist committed under docs/
    [x] Reproduction instructions runnable on a clean machine

[ ] Render verification
    [x] quarto render index.qmd --to pdf produces clean PDF (verified once)
    [x] Hero image displays in /blog/ listing card
    [ ] Internal links resolve

============================================================================ -->
<section id="related-posts-in-this-cluster" class="level2">
<h2 class="anchored" data-anchor-id="related-posts-in-this-cluster">Related posts in this cluster</h2>
<p>This post is part of the <em>ZZCOLLAB Reproducible Compendia</em> series. Recommended reading order:</p>
<ol type="1">
<li>Post 01: <a href="../01-zc-quarto-compendium-intro/">Reproducible Blog Posts with ZZCOLLAB</a></li>
<li>Post 02: <a href="../02-zc-blog-post-template/">Constructing a reproducible blog post using zzcollab tools</a></li>
<li>Post 03: <a href="../03-zc-markdown-to-blog-workflow/">From Markdown to Blog Post: A ZZCOLLAB workflow</a></li>
<li>Post 04: <a href="../04-zc-share-rmd-via-docker/">Sharing R Code via Docker: R Markdown Reports</a></li>
<li><strong>Post 05: A 55-Item Initiation Checklist for zzcollab Data Analyses</strong> (this post)</li>
<li>Post 06: <a href="../06-zc-manuscript-report-elements/">Seven Required Elements for a zzc Manuscript report.Rmd</a></li>
<li>Post 07: <a href="../07-zc-tiered-ci-strategy/">A tiered CI strategy for zzcollab research compendia</a></li>
<li>Post 08: <a href="../08-zc-github-actions-workflows/">GitHub Actions workflows for zzcollab research compendia</a></li>
</ol>


</section>
</section>

 ]]></description>
  <category>r</category>
  <category>zzcollab-compendia</category>
  <category>reproducibility</category>
  <category>workflow</category>
  <category>checklist</category>
  <category>quarto</category>
  <guid>https://rgtlab.org/posts/zc-analysis-initiation-checklist/</guid>
  <pubDate>Wed, 29 Apr 2026 07:00:00 GMT</pubDate>
  <media:content url="https://rgtlab.org/posts/zc-analysis-initiation-checklist/media/images/hero.png" medium="image" type="image/png" height="80" width="144"/>
</item>
<item>
  <title>Refactoring a Personal Toolbox: Scripts versus Shell Functions</title>
  <dc:creator>Ronald G. Thomas</dc:creator>
  <link>https://rgtlab.org/posts/sh-scripts-vs-functions/</link>
  <description><![CDATA[ 




<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/sh-scripts-vs-functions/media/images/hero.png" class="img-fluid figure-img" style="width:80.0%"></p>
<figcaption>A workshop pegboard, organised so that each tool sits in its own labelled outline.</figcaption>
</figure>
</div>
<p><em>A personal toolbox earns its keep when each helper sits in the place that fits its job, with no doubt about which drawer to open.</em></p>
<section id="introduction" class="level1">
<h1>Introduction</h1>
<p>Most quantitative researchers maintain a personal toolbox of small shell helpers. It generally takes two forms: a <code>~/bin</code> directory populated with executable scripts, and a <code>.zshrc</code> (or <code>.bashrc</code>) file containing a layer of custom aliases and functions sourced into every interactive session. The contents accumulate gradually over years of incremental work, each entry typically introduced in response to a specific irritation: a one-liner written to bypass an awkward pipeline, a research-notes capture script drafted during a manuscript revision, a <code>git</code> workflow function codified after a co-author inadvertently committed a secret.</p>
<p>After several years of accretion, the toolbox remains functional but the boundary between ‘script in <code>~/bin</code>’ and ‘function in <code>.zshrc</code>’ has eroded. Some scripts in <code>~/bin</code> are aliases in disguise (a single <code>cd</code> followed by the launch of an editor). Some functions in <code>.zshrc</code> span hundreds of lines of program logic that have no need for the calling shell’s state. The toolbox remains serviceable, but it has become difficult to reason about, difficult to audit for security, and noticeably slow to load on shell startup.</p>
<p>We document a principled refactor of such a toolbox. The worked example is drawn from a biostatistician’s workflow (reproducible R analyses, Docker-based research compendia, frequent <code>git</code> commits, occasional HPC submission), but the underlying rule applies to any Unix-flavoured personal workspace.</p>
<p>A companion plan that operationalises the rule, with a concrete phase sequence and effort estimates, is referenced under See Also.</p>
<section id="motivations" class="level2">
<h2 class="anchored" data-anchor-id="motivations">Motivations</h2>
<ul>
<li>To establish a single rule that decides, for any helper, whether it belongs in <code>~/bin</code> or in shell config.</li>
<li>To remove from <code>.zshrc</code> the logic that has no business mutating the current shell, lowering startup time and improving auditability.</li>
<li>To bring versioned scripts under a linter (<code>shellcheck</code>) so that legacy quoting bugs surface before they bite.</li>
<li>To eliminate microscripts that exist purely because the author did not realise the shell already has a primitive for them (the alias).</li>
<li>To make the toolbox legible to future-self and to collaborators who inherit it during onboarding or Sabbatical handoff.</li>
</ul>
</section>
<section id="objectives" class="level2">
<h2 class="anchored" data-anchor-id="objectives">Objectives</h2>
<ol type="1">
<li>State a guiding principle that resolves the function-versus-script question in every encountered case.</li>
<li>Provide a categorisation matrix so that an existing toolbox can be triaged in a single sitting.</li>
<li>Sequence the refactor into seven small phases, each independently shippable and reversible.</li>
<li>Document the gotchas that only surface when scripts are extracted out of a long-lived shell environment.</li>
</ol>
<p>Errors and better approaches are welcome; see the Feedback section at the end.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/sh-scripts-vs-functions/media/images/ambiance1.png" class="img-fluid figure-img"></p>
<figcaption>A second monitor displaying terminal panes during an active refactor session.</figcaption>
</figure>
</div>
</section>
</section>
<section id="background-function-or-script" class="level1">
<h1>Background: function or script?</h1>
<p>The function-versus-script rule developed here rests on a small set of recurring terms (process, state, namespace, scope, testing, reproducibility). Each is defined briefly below before the substantive discussion begins, so that the later prose can be read against a fixed vocabulary.</p>
<section id="terminology" class="level2">
<h2 class="anchored" data-anchor-id="terminology">Terminology</h2>
<p><strong>Process.</strong> An operating-system execution unit with its own memory, environment, file descriptors, and a unique process identifier (PID). Every running program is a process. A shell, an editor, a script, and a database server are each a separate process; the operating system isolates them so that one cannot directly read or write another’s memory. Communication between processes is restricted to explicit channels: arguments and environment passed at launch, file descriptors, signals, and exit codes.</p>
<p><strong>State.</strong> The information a process holds in memory at a given moment. For a shell process, the state includes the current working directory, the values of environment and local variables, the defined aliases and functions, the shell options, the history list, the directory stack, and the job table. State is what distinguishes one interactive shell session from another, and it is what makes the function-versus-script question consequential at all: a function can read and write the calling shell’s state, whereas a script cannot.</p>
<p><strong>Namespace.</strong> The set of names (variables, functions, aliases) visible to a particular execution context. The interactive shell maintains one namespace; a child process launched from it receives a copy of the exported portion and nothing else. Two processes can hold variables of the same name without collision because each has its own namespace. The R analogy below uses the term in the same sense: an R session has a namespace into which <code>library()</code> calls bind names.</p>
<p><strong>Scope.</strong> The region of code in which a given name is defined and accessible. Shell functions and scripts have distinct scopes by construction: a variable set inside a script is visible only within that script’s process unless explicitly exported to children, while a variable set inside an interactive function (without <code>local</code>) is visible to the entire shell session. Scope is the language-level property that follows from process boundaries and namespace rules.</p>
<p><strong>Testing.</strong> Exercising code in a controlled environment with known inputs and asserting that the observed outputs match expectations. In shell work, testing typically means invoking a script with prepared arguments, capturing its <code>stdout</code>, <code>stderr</code>, and exit code, and comparing them against reference values; static analysers such as <code>shellcheck</code> address a complementary class of issues without execution. A helper that cannot be invoked with a controlled input set, in isolation from a live interactive session, cannot be tested in this sense.</p>
<p><strong>Reproducibility.</strong> The property that re-running the same code on the same inputs in the same environment yields the same outputs. Reproducibility requires both that the code be captured (version control, an executable file at a stable location) and that the execution environment be characterised (declared dependencies, pinned versions, an explicit shebang). A helper defined inline in a personal startup file fails the first requirement; a script in <code>~/bin</code> under <code>git</code> satisfies it.</p>
</section>
<section id="how-the-shell-executes-code" class="level2">
<h2 class="anchored" data-anchor-id="how-the-shell-executes-code">How the shell executes code</h2>
<p>To classify a helper sensibly, it helps to begin with how the shell actually executes code.</p>
<p>When a user opens a terminal, the shell launches as a long-lived process. It reads a startup file (<code>.zshrc</code>, <code>.bashrc</code>, or similar), constructs its environment (exported variables, defined aliases and functions, the current working directory, the history file, the directory stack, key bindings, completion definitions), and then waits for input at a prompt. Every command the user enters is interpreted in the context of that single process and the in-memory state it carries.</p>
<p>Within this model, a helper can run in one of two fundamentally different ways: inside the same process as the interactive shell, or inside a new process spawned from it. The distinction is mechanical and unambiguous, and it determines what the helper is permitted to change.</p>
</section>
<section id="functions-code-in-the-current-process" class="level2">
<h2 class="anchored" data-anchor-id="functions-code-in-the-current-process">Functions: code in the current process</h2>
<p>A shell function is a named block of code, defined as <code>name() { ... }</code> (or <code>function name { ... }</code>), that executes in the <em>current</em> shell process. Because it runs there, it has direct access to that process’s state. It can:</p>
<ul>
<li>Change the working directory via <code>cd</code>, with the change persisting for subsequent commands at the same prompt.</li>
<li><code>export</code>, set, or unset environment variables that future commands in the session will inherit.</li>
<li>Define or redefine aliases, options (<code>setopt</code>), key bindings, and completion functions.</li>
<li>Manipulate the directory stack (<code>pushd</code>, <code>popd</code>), the command history, and the job table.</li>
</ul>
<p>Functions are most commonly defined in a startup file so that they are available in every interactive session. The cost is borne by every shell invocation: each function is parsed in full before the prompt appears, regardless of whether it is ever called.</p>
</section>
<section id="scripts-code-in-a-child-process" class="level2">
<h2 class="anchored" data-anchor-id="scripts-code-in-a-child-process">Scripts: code in a child process</h2>
<p>A shell script is a file with execute permission that the shell runs by spawning a <em>child process</em>. The mechanism is the standard Unix <code>fork</code> and <code>execve</code> pair: the parent shell calls <code>fork</code>, which produces a near-identical copy of the shell process (the child), and the child then calls <code>execve</code> to replace its own program image with the interpreter named in the script’s shebang line (<code>#!/usr/bin/env bash</code>, <code>#!/usr/bin/env zsh</code>, and so on). The parent typically waits for the child to finish; the child runs the script and eventually exits.</p>
<section id="the-parent-child-relationship" class="level3">
<h3 class="anchored" data-anchor-id="the-parent-child-relationship">The parent-child relationship</h3>
<p>From the operating system’s perspective the parent and child are two distinct processes. Each has its own process identifier (PID), its own address space (a private region of memory neither process can read from the other), its own working directory, its own copy of the environment, and its own file descriptor table. At the moment of <code>fork</code>, the child receives a snapshot of the parent’s exported environment, the parent’s open file descriptors (<code>stdin</code>, <code>stdout</code>, <code>stderr</code>), the command-line arguments passed at invocation, and a copy of the parent’s working directory. That snapshot is the entirety of what crosses the boundary. After the fork, the two processes are independent: state changes the child makes are invisible to the parent, and state changes the parent makes are invisible to the child. When the child exits, the parent collects three pieces of information and nothing else: the bytes the child wrote to <code>stdout</code>, the bytes the child wrote to <code>stderr</code>, and the integer exit code returned by the child’s final instruction.</p>
</section>
<section id="what-this-means-for-cd-and-everything-like-it" class="level3">
<h3 class="anchored" data-anchor-id="what-this-means-for-cd-and-everything-like-it">What this means for <code>cd</code> (and everything like it)</h3>
<p>The practical consequence is that a script cannot mutate the state of the shell that invoked it. The point is precise enough to deserve a careful statement: a script <em>can</em> call <code>cd</code>, and the call succeeds. The child process moves to the new directory and any subsequent commands in the script run from there. What the script cannot do is <code>cd</code> the <em>parent shell</em>, because the parent and child each hold their own working directory, and the child’s was a copy from the start. A common source of early confusion is the user who places <code>cd /some/project</code> inside a script, executes it, and is puzzled to find the prompt unchanged. The script ran, the child process did <code>cd</code>, the child exited, and the parent shell, never having shared a working directory with the child, remained where it was.</p>
<p>The same argument applies to environment variables, aliases, shell options, the directory stack, and every other element of shell state: a script can change them inside its own process, but the changes are discarded the moment that process terminates. This is the same process isolation that allows a crashing program to leave the rest of the system intact.</p>
</section>
<section id="the-one-explicit-exception-source" class="level3">
<h3 class="anchored" data-anchor-id="the-one-explicit-exception-source">The one explicit exception: <code>source</code></h3>
<p>The shell built-in <code>source</code> (equivalently, <code>.</code>) does <em>not</em> spawn a child process. It reads the named file and executes its commands directly in the <em>current</em> shell. A <code>cd</code> inside a sourced file therefore does change the calling shell’s working directory, and an <code>export</code> inside a sourced file does set a variable in the calling shell’s namespace. Sourcing is the mechanism by which <code>.zshrc</code> contributes its definitions to the interactive session in the first place, and it is the standard workaround when a helper genuinely needs to leave the user in a new directory but is too long to live as a function.</p>
<p>The cost is the loss of isolation: a sourced file shares the calling shell’s state and can read, modify, or clobber any name defined there. An error in a sourced file (an unset variable under <code>set -u</code>, a stray <code>exit</code>) terminates the interactive session rather than a child process. For these reasons, sourcing is reserved for files whose authors explicitly intend to operate on the parent’s state; ordinary helpers should be invoked, not sourced.</p>
</section>
</section>
<section id="why-the-distinction-matters" class="level2">
<h2 class="anchored" data-anchor-id="why-the-distinction-matters">Why the distinction matters</h2>
<p>These two execution models are not interchangeable. Some helpers genuinely require the function form because their entire purpose is to mutate the parent shell’s state: a wrapper that prepares a project directory and leaves the user inside it, a helper that loads a credential into the current session via <code>export</code>, a function that toggles a shell option for the remainder of the session. Other helpers fit equally well in either form, but the choice has measurable consequences for testability, auditability, startup performance, security review, and the reader’s mental model of where logic lives.</p>
<p>A toolbox that has drifted out of alignment shows two symmetric inversions: scripts that should have been aliases (because the ‘script’ is a single command line that does not even merit its own file), and functions that should have been scripts (because the ‘function’ performs no operation that requires access to the parent shell’s state). The next section defines the rule that resolves both.</p>
</section>
</section>
<section id="guiding-principle" class="level1">
<h1>Guiding principle</h1>
<p>The rule is short:</p>
<blockquote class="blockquote">
<p>A helper should be a shell function only when it must mutate the calling shell. Otherwise it should be a versioned, executable script in <code>~/bin</code>.</p>
</blockquote>
<p>The remainder of this section unpacks the rule in terms readily familiar to an audience trained in scope, reproducibility, and the boundaries between processes.</p>
<section id="a-familiar-analogy-r-functions-and-rscript-files" class="level2">
<h2 class="anchored" data-anchor-id="a-familiar-analogy-r-functions-and-rscript-files">A familiar analogy: R functions and Rscript files</h2>
<p>R users have lived with the same boundary for years without naming it explicitly. A pure helper that takes inputs, returns a value, and produces no side effects is written as an R function, often grouped with related functions in an R package. A one-shot batch job that loads a dataset, fits a model, writes an <code>.rds</code> file, and emits a PDF report is written as a script and executed with <code>Rscript</code> or rendered through Quarto. No experienced R user would propose to embed an entire batch pipeline in <code>.Rprofile</code>, even though <code>.Rprofile</code> is technically capable of running it. The pipeline does not require access to the interactive session’s namespace, and locating it there would slow every R session, obscure it from version control review, and prevent any tooling from analysing it as a self-contained unit.</p>
<p>The shell case is the same boundary at a different scale. A shell function corresponds to a function loaded into the user’s current R session: it can read and modify that session’s globals, and it lives or dies with the session. A shell script corresponds to <code>Rscript</code> invoked from the command line: it receives a fresh interpreter, communicates back through <code>stdout</code> and an exit code, and exits cleanly. The process boundary that makes <code>Rscript</code> reliable for batch work is the same boundary that makes a shell script reliable for non-interactive use. Conflating the two in shell-land carries the same costs as it would in R-land; the shell community has historically been less attentive to the conflation.</p>
</section>
<section id="scope-in-four-lines" class="level2">
<h2 class="anchored" data-anchor-id="scope-in-four-lines">Scope, in four lines</h2>
<p>Concretely, a helper <em>must</em> be a function when it does any of the following in a way that should outlast the helper’s run:</p>
<ul>
<li>Change the working directory.</li>
<li>Export, set, or unset shell variables.</li>
<li>Modify aliases, options, key bindings, completion definitions.</li>
<li>Manipulate the directory stack, history, or job table.</li>
</ul>
<p>Each item on this list shares a single property: the change must persist in the calling shell’s process, which is precisely what a child process is structurally incapable of accomplishing. Anything outside the list (running a program, reading or writing files, formatting output, fetching from a URL, calling <code>git</code>, calling <code>make</code>, calling a database, parsing JSON) communicates with the surrounding world through file descriptors and exit codes alone, which is exactly what the script form is designed for. The rule scales cleanly: a function that <em>both</em> runs a long pipeline <em>and</em> needs to <code>cd</code> at the end is two helpers, one calling the other.</p>
</section>
<section id="why-the-inversion-is-expensive" class="level2">
<h2 class="anchored" data-anchor-id="why-the-inversion-is-expensive">Why the inversion is expensive</h2>
<p>A long shell function in <code>.zshrc</code> carries five practical costs that map directly onto practices already familiar from <code>renv</code>, package versioning, and unit testing.</p>
<p><strong>Startup latency.</strong> A function in <code>.zshrc</code> is parsed every time the shell starts. A script is parsed only when invoked. A two-hundred- line zsh function that the user calls a few times a day is re-parsed dozens of times for each one execution. The latency cost is the same shape as <code>library()</code> calls in <code>.Rprofile</code>: convenient on the surface, expensive in aggregate.</p>
<p><strong>Lint and static analysis.</strong> <code>shellcheck</code> reads scripts; it does not introspect functions buried in dotfiles. Quoting bugs, unset variables, and incorrect test predicates that would be caught immediately in a script silently survive in a <code>.zshrc</code> function. The R parallel is <code>lintr</code> against package code versus <code>lintr</code> against ad-hoc chunks in <code>.Rprofile</code>.</p>
<p><strong>Version control granularity.</strong> A script is a file. Its diffs are local, its history is local, blame is meaningful, and its commits do not interact with unrelated config changes. A function in a shared <code>.zshrc</code> competes with <code>PATH</code> exports, plugin lines, keybindings, and PROMPT changes for git history attention. Any non-trivial helper deserves its own git history, the same way a non-trivial R helper deserves its own <code>R/foo.R</code>.</p>
<p><strong>Testability.</strong> A script can be invoked under a controlled environment with arguments, redirected I/O, and a captured exit code. The same logic embedded in a <code>.zshrc</code> function can only be tested by sourcing the dotfile and calling the function in a live shell, which is roughly as defensible as testing R code by copy-pasting it into the console.</p>
<p><strong>Reuse across hosts and contexts.</strong> Scripts in <code>~/bin</code> are picked up by anything that respects <code>PATH</code>: cron, launchd, Make rules, Docker containers that mount the home directory, HPC submission scripts, scheduled GitHub Actions runners that source the toolbox. Functions are visible only to interactive zsh sessions. A research-backup helper hidden in <code>.zshrc</code> cannot be wired to a launchd job; the same logic in <code>~/bin</code> can be scheduled in seconds. This boundary maps almost exactly to the line between ‘helpers exported by a package’ and ‘helpers defined inline in a notebook’ in R work.</p>
</section>
<section id="why-the-inversion-happens" class="level2">
<h2 class="anchored" data-anchor-id="why-the-inversion-happens">Why the inversion happens</h2>
<p>A second pattern to notice: the inversions are not random. Helpers that grow up as one-liners (<code>cd somewhere &amp;&amp; open something</code>) are written as scripts because the author did not yet know about aliases. Helpers that grow up gradually (a git workflow that started as a snippet, then accreted secret-scanning, then commit- message templating, then a confirmation prompt) end up as functions because they were edited each time a new shell was open and <code>.zshrc</code> was already in the editor. The forces driving each inversion are completely different, but the symptom (a toolbox where category does not predict location) is identical.</p>
<p>Recognising both forces matters because the corrective for each is different. The microscript that should have been an alias becomes an alias and disappears. The accumulated function that should have been a script becomes a script and gets the full hardening treatment: shebang, <code>set -euo pipefail</code>, quoted variables, a <code>-h</code>/<code>--help</code> flag, a <code>shellcheck</code> pass, and a place in version history.</p>
</section>
<section id="when-the-rule-cuts-the-other-way" class="level2">
<h2 class="anchored" data-anchor-id="when-the-rule-cuts-the-other-way">When the rule cuts the other way</h2>
<p>For completeness, the rule does sometimes route a current script <em>into</em> the function form. A helper that the author scripted but that genuinely needs to leave the user in a different directory is mis-categorised as a script. The give-away symptom: ‘why does my shell not stay in the project directory after I run this?’. That helper’s logic should move to a function, or, more cleanly, the script should be retained for non-interactive callers (cron, make) and a thin shell function should call it and apply the final <code>cd</code>.</p>
</section>
<section id="what-the-rule-is-not" class="level2">
<h2 class="anchored" data-anchor-id="what-the-rule-is-not">What the rule is not</h2>
<p>The rule is not ‘all logic must leave <code>.zshrc</code>’. The dirstack helpers, the <code>ff</code> fzf-then-cd shortcut, the navigation jumps to project subdirectories: all are legitimately functions, because they exist precisely to mutate the calling shell. The point is to keep the function tier narrow and obviously appropriate, so that when a future reader sees a function in <code>.zshrc</code>, the existence of that function is itself evidence that it had to be one.</p>
</section>
</section>
<section id="symptoms-of-an-unaligned-toolbox" class="level1">
<h1>Symptoms of an unaligned toolbox</h1>
<p>Three signs reliably indicate that a toolbox needs a refactor along these lines.</p>
<p><strong>The shell startup file has crossed a thousand lines.</strong> Most of the bulk is unlikely to be configuration. It will be logic that should have been extracted long ago.</p>
<p><strong>Microscripts in <code>~/bin</code> outnumber non-trivial scripts.</strong> A directory with thirty entries, half of which are under five lines, is mostly aliases waiting to be promoted.</p>
<p><strong>At least one shell function imports a non-trivial external program.</strong> A <code>.zshrc</code> function that calls <code>gitleaks</code>, <code>aws</code>, <code>pandoc</code>, or <code>jq</code> is almost certainly mis-located. External programs imply a real workflow, and real workflows belong in versioned files.</p>
</section>
<section id="a-categorisation-matrix" class="level1">
<h1>A categorisation matrix</h1>
<p>The matrix below sorts every helper a typical toolbox will contain into one of five fates. Triaging an existing toolbox is a single-sitting exercise: read each helper, ask the four questions in the principle, and place it.</p>
<table class="caption-top table">
<colgroup>
<col style="width: 28%">
<col style="width: 71%">
</colgroup>
<thead>
<tr class="header">
<th>Fate</th>
<th>Trigger</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>Keep as function</td>
<td>Helper must <code>cd</code>, <code>export</code>, modify aliases or history</td>
</tr>
<tr class="even">
<td>Move from function to script</td>
<td>Function does no shell-state work; it just runs a program</td>
</tr>
<tr class="odd">
<td>Convert script to alias</td>
<td>Script is a single command line with no real argument parsing</td>
</tr>
<tr class="even">
<td>Keep as script (harden)</td>
<td>Script is correctly placed but lacks shebang, quoting, lint</td>
</tr>
<tr class="odd">
<td>Decommission</td>
<td>Helper duplicates another, is referenced nowhere, or is dead</td>
</tr>
</tbody>
</table>
<p>The first row is the smallest in any toolbox the author has inspected, typically four to twenty helpers. The remaining rows absorb everything else.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/sh-scripts-vs-functions/media/images/ambiance2.png" class="img-fluid figure-img"></p>
<figcaption>Editor showing a shell function being lifted into its own file under ‘~/bin’.</figcaption>
</figure>
</div>
</section>
<section id="a-seven-phase-refactor" class="level1">
<h1>A seven-phase refactor</h1>
<p>Each phase below is independently shippable and reversible. The ordering is by leverage: the early phases produce most of the visible improvement.</p>
<section id="phase-1-extract-the-largest-function" class="level2">
<h2 class="anchored" data-anchor-id="phase-1-extract-the-largest-function">Phase 1: extract the largest function</h2>
<p>In every toolbox the author has examined, a single function dominates by line count. Often it is a custom git commit workflow or a research-notes capture pipeline that grew over the years. That function alone typically contains 30 to 70 percent of the non-config bulk in <code>.zshrc</code>.</p>
<p>The extraction is mechanical:</p>
<ul>
<li>Create a new file in <code>~/bin</code> named after the helper. Mark it executable.</li>
<li>Add a shebang that matches the syntax in use. Functions written with zsh-only constructs (<code>${(f)...}</code>, <code>${array:#}</code>, <code>setopt</code> semantics) require <code>#!/usr/bin/env zsh</code>; do not assume bash will parse them.</li>
<li>Apply standard hardening: <code>set -u</code> always, <code>set -e</code> if the function does not deliberately tolerate failure, <code>set -o   pipefail</code> if any command piping is involved.</li>
<li>Decide on helpers. Sub-functions called only by the extracted helper can move with it (inline, or in a <code>lib/</code> directory the helper sources). External helpers used by other functions stay in shell config.</li>
<li>Delete the original function from the shell startup file.</li>
<li>Verify by exercising the helper end-to-end on a scratch workspace, including any failure paths.</li>
</ul>
<p>This phase alone is usually responsible for a measurable shell startup speedup and for moving the largest single block of code under linting.</p>
</section>
<section id="phase-2-extract-the-small-functions" class="level2">
<h2 class="anchored" data-anchor-id="phase-2-extract-the-small-functions">Phase 2: extract the small functions</h2>
<p>After the large extraction, several smaller functions remain that also fail the principle: a Mathematica wrapper, a fuzzy file finder that calls vim, a make invocation that runs from any subdirectory. Each is a one-file extraction at most. Time per helper is small; the cumulative effect is roughly another fifty to eighty lines out of <code>.zshrc</code>.</p>
</section>
<section id="phase-3-convert-microscripts-to-aliases" class="level2">
<h2 class="anchored" data-anchor-id="phase-3-convert-microscripts-to-aliases">Phase 3: convert microscripts to aliases</h2>
<p>Inspect every script under fifteen lines in <code>~/bin</code>. Any whose body reduces to a single command pipeline (with no flag parsing, no branching, no output processing) is more honestly expressed as an alias. Examples that look exactly like aliases when written inline:</p>
<div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb1-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">nohup</span> /Applications/Ghostty.app/Contents/MacOS/ghostty <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb1-2">  <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span>/dev/null <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;&amp;</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">&amp;</span></span></code></pre></div>
<p>Promote each such script to an alias in shell config and delete the file. The benefit is twofold: the helper now appears in <code>alias</code> output (so it is discoverable by listing aliases), and the toolbox shrinks by one file per promotion.</p>
</section>
<section id="phase-4-standardise-the-rest" class="level2">
<h2 class="anchored" data-anchor-id="phase-4-standardise-the-rest">Phase 4: standardise the rest</h2>
<p>The remaining scripts in <code>~/bin</code> are correctly scripts. They typically need light hardening:</p>
<ul>
<li>Add a shebang, <code>#!/usr/bin/env bash</code> for bash-portable scripts.</li>
<li>Add <code>set -euo pipefail</code> unless the script explicitly chooses a more permissive policy and documents why.</li>
<li>Replace backticks with <code>$(...)</code>.</li>
<li>Quote variable expansions: <code>"$1"</code>, <code>"$PWD"</code>, and so on.</li>
<li>Resolve <code>shellcheck</code> warnings, or annotate with a <code>disable</code> comment that names a reason.</li>
<li>Add a <code>-h</code>/<code>--help</code> flag for any script over thirty lines.</li>
<li>Decide an extension policy and apply it consistently; standard Unix practice is to drop <code>.sh</code> from executables and reserve the extension for files that are explicitly sourced. Confirm that any scheduler entries (launchd, systemd, cron) referring to the script by name are updated together.</li>
</ul>
<p>This is the long-tail phase. It can be done one helper at a time, amortised over normal work.</p>
</section>
<section id="phase-5-optional-autoload-remaining-functions" class="level2">
<h2 class="anchored" data-anchor-id="phase-5-optional-autoload-remaining-functions">Phase 5 (optional): autoload remaining functions</h2>
<p>For shell startup files that still feel heavy after phases 1 to 4, move the remaining functions out of the startup file into a function directory and <code>autoload</code> them:</p>
<div class="sourceCode" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode zsh code-with-copy"><code class="sourceCode zsh"><span id="cb2-1"><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">fpath</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">(</span>~/.zsh/functions <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$fpath)</span></span>
<span id="cb2-2"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">autoload</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-Uz</span> d ff za zw zy zf zt zs zp zr z0 zm ze zo zc zg</span></code></pre></div>
<p>Each function lives in its own file under the function path. They load on first call, not on shell start. Skip this phase if the remaining functions are few or if having their bodies visible in the startup file is more valuable than the small startup cost.</p>
</section>
<section id="phase-6-install-a-guardrail" class="level2">
<h2 class="anchored" data-anchor-id="phase-6-install-a-guardrail">Phase 6: install a guardrail</h2>
<p>Add a <code>shellcheck</code> pre-commit hook scoped to <code>~/bin/*</code>, excluding binaries, R scripts, Python scripts, and any <code>archive/</code> directory. The point is not to catch a flood of issues at install time (phase 4 already handled those) but to prevent regressions as the toolbox continues to evolve. A single Make target, <code>make lint</code>, that runs <code>shellcheck</code> over every executable is sufficient.</p>
</section>
<section id="phase-7-decommission" class="level2">
<h2 class="anchored" data-anchor-id="phase-7-decommission">Phase 7: decommission</h2>
<p>Triage each remaining helper for redundancy. Common findings: two clipboard-to-notes helpers that do roughly the same thing under different names, a PDF viewer launcher that exists in two versions because the author once wanted to try a different viewer, an installer left behind from a tool that was removed years ago. Pick one of each, retire the rest. Git history preserves the removed code; the working tree should not.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/sh-scripts-vs-functions/media/images/ambiance3.png" class="img-fluid figure-img"></p>
<figcaption>A clean desk after the refactor; the same tools are present, but each sits where it belongs.</figcaption>
</figure>
</div>
</section>
</section>
<section id="things-to-watch-out-for" class="level1">
<h1>Things to watch out for</h1>
<p>These pitfalls are easy to underestimate before starting and easy to recognise once seen.</p>
<ol type="1">
<li><p><strong>Zsh-only constructs.</strong> Many shell functions accumulate zsh conveniences that bash will not parse: <code>${(f)var}</code> for newline splitting, <code>(s::)</code> for string-splitting flags, parameter modifiers like <code>${var:#}</code>. When extracting such a function to a script, the shebang must match the syntax. A bash shebang on a zsh function will fail in unhelpful ways.</p></li>
<li><p><strong>Schedulers refer to file names.</strong> If <code>~/bin/foo.sh</code> is referenced by a launchd plist, a systemd unit, a cron entry, or a Makefile, renaming to <code>~/bin/foo</code> will silently break the scheduler. Search for every occurrence before renaming, and land both changes in the same commit.</p></li>
<li><p><strong><code>PATH</code> ordering.</strong> A script extracted from a function inherits <code>PATH</code> from the calling shell, which is usually fine. The exception is when the function previously ran inside a block that prepended a directory to <code>PATH</code>. The new script needs to do its own <code>PATH</code> adjustment or use absolute paths.</p></li>
<li><p><strong>Working directory at invocation.</strong> A function inherits the caller’s <code>$PWD</code>. A script does too, but if the script was previously a function that called itself recursively or that <code>cd</code>’d for its own purposes, that behaviour is now subprocess-local and may surprise the parent.</p></li>
<li><p><strong>History and dirstack contamination.</strong> A function that previously ran <code>pushd</code>/<code>popd</code> to navigate around silently lost that capability when extracted to a script. If the helper genuinely needed the navigation, the helper was mis-classified and the rule routes it back to the function tier.</p></li>
<li><p><strong><code>shellcheck</code> false positives.</strong> External programs called with computed arguments (<code>"${args[@]}"</code>) sometimes trigger <code>SC2068</code>, <code>SC2086</code>, or <code>SC2154</code>. Disable per line with a comment that names the reason; do not blanket-disable the warning across the project.</p></li>
<li><p><strong>Secrets in extracted code.</strong> A function previously sourced from <code>.zshrc</code> may have benefited from environment variables set elsewhere in the same file. The extracted script does not have that benefit. Either source the relevant env file explicitly inside the script, or read secrets via a tool such as <code>pass</code> or a credential helper.</p></li>
</ol>
</section>
<section id="daily-workflow-after-the-refactor" class="level1">
<h1>Daily workflow after the refactor</h1>
<p>The refactor pays back every day in three ways.</p>
<p><strong>Discoverability.</strong> A new helper added to <code>~/bin</code> shows up under tab completion against <code>$PATH</code>. A new function added to the shell config shows up only in the current session and only after a reload.</p>
<p><strong>Auditability.</strong> Every helper in <code>~/bin</code> is one <code>git log</code> away from full history; one <code>shellcheck</code> away from lint; one <code>cat</code> away from a complete reading. A helper buried in a thousand-line startup file requires the reader to first locate it.</p>
<p><strong>Composability.</strong> Scripts in <code>~/bin</code> can be called from anything that respects <code>PATH</code>: Make rules, scheduled jobs, Docker containers mounted with the home directory, HPC submission scripts, peer collaborators who cloned the dotfiles repository. Functions can only be called from interactive shells that have sourced them. The composability gain is the most important long-term return on the refactor.</p>
</section>
<section id="what-did-we-learn" class="level1">
<h1>What did we learn?</h1>
<section id="lessons-learnt" class="level2">
<h2 class="anchored" data-anchor-id="lessons-learnt">Lessons learnt</h2>
<p><strong>Conceptual:</strong></p>
<ul>
<li>The function-versus-script question reduces to a single test: does the helper need to mutate the calling shell? Almost every other consideration follows from that one decision.</li>
<li>The forces that produce inversions in each direction are different. Microscripts grow up because the author did not know about aliases; oversized functions grow up because the startup file is the file that happens to be open.</li>
<li>A mature toolbox is mostly scripts, a small set of legitimate functions, and a layer of aliases. Any other distribution is evidence of accumulated misalignment.</li>
</ul>
<p><strong>Technical:</strong></p>
<ul>
<li>A long zsh function may not be portable to bash, even when the prose-level intent looks generic. Confirm the shebang against the syntactic features actually in use.</li>
<li><code>shellcheck</code> is the highest-leverage tool to apply at the end of an extraction. It catches more than half of the latent quoting and unset-variable bugs in legacy scripts.</li>
<li>Naming and extension conventions only need to be applied consistently within a toolbox. The choice of <code>.sh</code> versus no extension matters less than the absence of a third option appearing for no reason.</li>
</ul>
<p><strong>Gotchas:</strong></p>
<ul>
<li>Renaming a script that a scheduler refers to silently breaks the schedule. Always grep for the file name across <code>launchd/</code>, <code>systemd/</code>, <code>Makefile</code>, and any related repositories before renaming.</li>
<li>Functions that depend on shell-specific syntax cannot be blindly retitled with a bash shebang. Prefer the zsh shebang unless the syntax has been explicitly portabilised.</li>
</ul>
</section>
<section id="limitations" class="level2">
<h2 class="anchored" data-anchor-id="limitations">Limitations</h2>
<p>The refactor described here addresses helpers in shell-land specifically. It does not:</p>
<ul>
<li>Address Python, R, or other-language helpers whose execution model differs from shell scripts.</li>
<li>Replace a real configuration management system. Tools such as Ansible, Nix, or chezmoi solve a larger problem and may be appropriate when the personal toolbox grows past a few dozen files or needs to be deployed to multiple machines.</li>
<li>Provide automated migration. Each helper still needs a human to read it, classify it, and verify the extraction.</li>
<li>Compose with shell frameworks that load logic from many files (Oh My Zsh, prezto). Those frameworks have their own conventions; the rule still applies, but the implementation details change.</li>
</ul>
</section>
<section id="opportunities-for-improvement" class="level2">
<h2 class="anchored" data-anchor-id="opportunities-for-improvement">Opportunities for improvement</h2>
<ol type="1">
<li>Encode the rule as a small linter that scans <code>.zshrc</code> for functions and flags any whose body never references shell state. This would surface candidates for extraction over time rather than at one big-bang refactor.</li>
<li>Add a CI workflow that runs <code>shellcheck</code> over <code>~/bin</code> on every push to the dotfiles repository.</li>
<li>Generate <code>~/bin/nav</code> (a navigation cheat sheet) from the <code>autoload</code>ed function metadata, so adding a new shortcut automatically updates the help output.</li>
<li>Build a thin packaging layer around shared helpers so they can be installed on collaborator machines without copying files by hand. A simple Make target plus a manifest is usually sufficient.</li>
<li>Track helper invocation frequency via the shell’s <code>precmd</code> hook for one week per year, and use the result to prune helpers nobody actually calls.</li>
</ol>
</section>
</section>
<section id="wrapping-up" class="level1">
<h1>Wrapping up</h1>
<p>A personal toolbox is software, even when it is small, even when it is private, even when it grew organically. Treating it as software (with a rule for how it is structured, lint applied to the parts that should be linted, version control applied to the parts that change, and a maintenance pass when the bulk crosses some threshold) pays back every day for the next decade.</p>
<p>The rule that drives the refactor is short enough to remember: function only when shell state must change, script otherwise. The work to apply it is finite, ordered, and reversible. A single afternoon, divided across the seven phases above, is enough to move a toolbox from accumulated drift back into alignment.</p>
<p>In conclusion, four points merit emphasis. First, the rule reduces every helper-placement decision to a single question about shell state. Second, the largest single win is extracting the dominant function out of the shell startup file and into <code>~/bin</code>. Third, microscripts that read like one-liners belong as aliases, not as files. Fourth, a <code>shellcheck</code> guardrail prevents the same drift from re-accumulating.</p>
</section>
<section id="see-also" class="level1">
<h1>See Also</h1>
<p><strong>Related posts:</strong></p>
<ul>
<li><a href="https://focusonr.org/posts/setupdotfilesongithub/">Setting up dotfiles on GitHub</a>: versioning the shell startup file alongside the rest of the configuration.</li>
<li><a href="https://focusonr.org/posts/researchbackupsystem/">A research backup system</a>: a worked example of a helper that belongs as a script invoked from a scheduler, not as a function in a shell.</li>
</ul>
<p><strong>Key resources:</strong></p>
<ul>
<li><a href="https://www.shellcheck.net/">shellcheck</a>: static analyser for shell scripts.</li>
<li><a href="https://zsh.sourceforge.io/Doc/Release/Expansion.html">zsh manual: parameter expansion</a>: authoritative reference for zsh-only constructs encountered during extraction.</li>
<li><a href="https://wiki.bash-hackers.org/scripting/nonportable">Bash hackers wiki: portable shell</a>: inventory of constructs that fail to port between shells.</li>
<li><a href="https://12factor.net/">The twelve-factor app</a>: the configuration discipline that the rule echoes for shell environments.</li>
</ul>
<hr>
</section>
<section id="reproducibility" class="level1">
<h1>Reproducibility</h1>
<p><strong>Tested configuration:</strong></p>
<table class="caption-top table">
<thead>
<tr class="header">
<th>Component</th>
<th>Version</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>Operating system</td>
<td>macOS 15.4</td>
</tr>
<tr class="even">
<td>Shell</td>
<td>zsh 5.9</td>
</tr>
<tr class="odd">
<td>shellcheck</td>
<td>0.10.0</td>
</tr>
<tr class="even">
<td>Last verified</td>
<td>2026-04-25</td>
</tr>
</tbody>
</table>
<p><strong>Configuration files:</strong></p>
<ul>
<li><code>analysis/configs/refactor_plan.md</code>: the seven-phase plan in the format used to drive the worked refactor.</li>
</ul>
<hr>
</section>
<section id="feedback" class="level1">
<h1>Feedback</h1>
<p>Corrections, suggestions, and questions are welcome. Please open an issue or pull request on the <a href="https://github.com/mygit">GitHub repository</a> or send an email to user@example.com.</p>
<section id="related-posts-in-this-cluster" class="level2">
<h2 class="anchored" data-anchor-id="related-posts-in-this-cluster">Related posts in this cluster</h2>
<p>This post is part of the <em>Shell Scripting and Git Tooling</em> series. Recommended reading order:</p>
<ol type="1">
<li><strong>Post 41: Refactoring a Personal Toolbox: Scripts versus Shell Functions</strong> (this post)</li>
<li>Post 43: <a href="../43-sh-daily-research-log/">A Mac Workflow for Tracking Daily Research Progress</a></li>
</ol>


</section>
</section>

 ]]></description>
  <category>shell</category>
  <category>shell-and-git</category>
  <guid>https://rgtlab.org/posts/sh-scripts-vs-functions/</guid>
  <pubDate>Sat, 25 Apr 2026 07:00:00 GMT</pubDate>
  <media:content url="https://rgtlab.org/posts/sh-scripts-vs-functions/media/images/hero.png" medium="image" type="image/png" height="80" width="144"/>
</item>
<item>
  <title>Building a statistical computing textbook in the Age of AI</title>
  <dc:creator>Ronald G. Thomas</dc:creator>
  <link>https://rgtlab.org/posts/pub-statistical-computing-textbook/</link>
  <description><![CDATA[ 




<!-- ============================================================================
HERO IMAGE
============================================================================ -->
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/pub-statistical-computing-textbook/media/images/hero.png" class="img-fluid figure-img" style="width:80.0%"></p>
<figcaption>A shelf of two books rendered as a Quarto site, symbolising the dual-volume structure that emerged during this drafting session.</figcaption>
</figure>
</div>
<p><em>A textbook’s subtitle is a commitment. ‘In the Age of AI’ commits the author to explaining where the human-LLM division of labour actually falls for each topic.</em></p>
<section id="introduction" class="level1">
<h1>Introduction</h1>
<p>I did not really appreciate how much structural decision-making goes into a textbook until I tried to draft two of them at once. A methods volume (‘Statistical Computing in the Age of AI’) and a workflow companion (‘Biostatistics Practicum’) were both overdue, and I wanted them to share enough scaffolding that a student could move between them without reorienting.</p>
<p>The turning point came when I stopped treating ‘in the Age of AI’ as a subtitle and started treating it as a structural obligation. Three short prose sections of ‘Prompts to try’ at the end of each chapter was decoration. What the subtitle required was an explicit front-loaded treatment of what the human statistician contributes, paired with an end-of-chapter verification workflow that exercises those contributions.</p>
<p>We walk through the decisions that produced the two books’ current state: chapter template, visual design, bibliography growth, and the specific AI-collaboration pattern. The post is a retrospective on one working session, not a finished tutorial. Readers drafting a domain-specific textbook can probably save themselves some of the back-tracking documented here.</p>
<p>More formally, we document here the convergence of the Knowledge Management and LLM-Augmented Editing concerns of the Workflow Construct described in <a href="../../posts/52-workflow-construct/">post 52</a>. Authoring a textbook is the largest-scale knowledge-management task most academics will undertake; doing so ‘in the age of AI’ surfaces specifically how the LLM-augmented editing layer interacts with the file-system, document, and bibliographic layers below it. The retrospective documented here is one working session worth of evidence on how those layers compose under sustained use.</p>
<section id="motivations" class="level2">
<h2 class="anchored" data-anchor-id="motivations">Motivations</h2>
<ul>
<li>Existing statistical computing textbooks are excellent but largely predate widespread LLM use. A curriculum written as if 2019 were still the current year trains students for a workplace that no longer exists.</li>
<li>Graduate biostatistics students arrive with LLMs already in hand. Pretending otherwise produces a textbook that does not match their reading context.</li>
<li>I wanted a dual-volume approach: a methods book (numerical algorithms, models, bootstrap, Bayesian) and a practicum (reproducibility, Git, Docker, SAS, CDISC). The two should share scaffolding but remain independently readable.</li>
<li>A peer-program survey (22 US biostat MS programmes) had surfaced specific content gaps (missing data, SAS, survival, Bayesian computation) that I wanted to close without restructuring the books.</li>
<li>I wanted to document the decision-making as it happened so that the next instructor building out a similar text has a worked example to follow.</li>
</ul>
</section>
<section id="objectives" class="level2">
<h2 class="anchored" data-anchor-id="objectives">Objectives</h2>
<ol type="1">
<li>Scaffold two Quarto books (a methods volume and a practicum companion) with compatible chapter structure, shared citation conventions, and visually distinct rendering.</li>
<li>Establish a ‘statistician’s contribution’ / ‘collaborating with an LLM’ pattern that earns the ‘in the Age of AI’ subtitle.</li>
<li>Ground chapter outlines in existing lecture materials, a comprehensive peer-MS-programme survey, and Jenny Bryan’s STAT 545 philosophical framing.</li>
<li>Apply front-matter and code-block improvements (code annotations, first-class callouts, typographic differentiation) as a named ‘polish’ pass.</li>
</ol>
<p>The process is documented as it unfolded. Corrections and better approaches are welcome; see the contact section at the end.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/pub-statistical-computing-textbook/media/images/ambiance1.png" class="img-fluid figure-img"></p>
<figcaption>Workspace ambiance image: a desk with two browser tabs open side-by-side, each rendering a different Quarto book under the same file-system hierarchy.</figcaption>
</figure>
</div>
</section>
</section>
<section id="what-is-a-quarto-book" class="level1">
<h1>What is a Quarto book?</h1>
<p>A Quarto book is a directory of <code>.qmd</code> files (Markdown with executable R code chunks) bound together by a <code>_quarto.yml</code> configuration, rendered to a multi-format output (HTML, PDF, EPUB, Word) by the <code>quarto</code> command-line tool. Think of it as a technical-writing Makefile: chapters, parts, cross-references, and output formats are declared in YAML, and <code>quarto render</code> produces a website plus a printable PDF from the same source.</p>
<p>The specific convention I follow is the Posit book-family pattern (R for Data Science, Advanced R, R Packages). Each content chapter carries a three-question diagnostic quiz at the top, collapsible ‘check your understanding’ callouts distributed through the content, exercises and a ‘further reading’ block at the foot, and the quiz answers at the very end.</p>
</section>
<section id="prerequisites" class="level1">
<h1>Prerequisites</h1>
<p>This process assumed the following:</p>
<ul>
<li><strong>Operating system:</strong> macOS 15.4, though the setup is portable to Linux and Windows.</li>
<li><strong>R 4.4+</strong>, RStudio optional.</li>
<li><strong>Quarto 1.5+</strong> (the book format changed non-trivially between 1.4 and 1.5; earlier versions will misbehave).</li>
<li><strong>Git</strong> and a GitHub account.</li>
<li><strong>An LLM</strong> used as a drafting and verification assistant. The session used Claude Code via terminal; ChatGPT or Gemini would work similarly.</li>
<li><strong>Existing lecture materials</strong> (speaker notes <code>.md</code>, slides <code>.qmd</code>, R code <code>.R</code>) for the methods volume; these become the source for chapter content.</li>
<li><strong>Time required:</strong> roughly 10 hours for the initial scaffold and framework decisions, excluding content porting.</li>
</ul>
</section>
<section id="installation" class="level1">
<h1>Installation</h1>
<p>No new installation was required beyond the existing Quarto and R setup. The zzcollab framework (an opinionated research-compendium scaffold) created the working directory structure automatically. The only unusual tool was a Python helper script for batch edits across 45 chapter files, written ad-hoc during the session.</p>
<div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb1-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">quarto</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--version</span></span>
<span id="cb1-2"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 1.9.37</span></span>
<span id="cb1-3"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">R</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--version</span></span>
<span id="cb1-4"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># R version 4.5.3 (2026-02-28)</span></span></code></pre></div>
</section>
<section id="configuration-the-chapter-template" class="level1">
<h1>Configuration: the chapter template</h1>
<p>The single most consequential decision was the chapter template. After several iterations, the structure below became the pattern for every content chapter in both books.</p>
<div class="sourceCode" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode yaml code-with-copy"><code class="sourceCode yaml"><span id="cb2-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Chapter structure (applied to every content chapter)</span></span>
<span id="cb2-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sections</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span></span>
<span id="cb2-3"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">  </span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">-</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> Prerequisites</span><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">             # Advanced-R-style 3-question quiz</span></span>
<span id="cb2-4"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">  </span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">-</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> Learning objectives</span></span>
<span id="cb2-5"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">  </span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">-</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> Orientation</span></span>
<span id="cb2-6"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">  </span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">-</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> The statistician's contribution</span><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">   # Age-of-AI pillar 1</span></span>
<span id="cb2-7"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">  </span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">-</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> Content sections</span><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">                  # with Check-your-understanding callouts</span></span>
<span id="cb2-8"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">  </span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">-</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> Collaborating with an LLM on X</span><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">    # Age-of-AI pillar 2</span></span>
<span id="cb2-9"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">  </span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">-</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> Exercises</span></span>
<span id="cb2-10"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">  </span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">-</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> Further reading</span></span>
<span id="cb2-11"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">  </span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">-</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> Practice test</span><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">                     # only where a course test bank exists</span></span>
<span id="cb2-12"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">  </span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">-</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> Prerequisites answers</span><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">             # placed at the very end</span></span></code></pre></div>
<p>The two sections that earn the subtitle are ‘The statistician’s contribution’ (front-loaded consciousness raising: the two to five decisions a statistician cannot delegate to an LLM) and ‘Collaborating with an LLM on X’ (end-of-chapter verification prompts, each with a Prompt / What to watch for / Verification triple).</p>
<p>Example of the statistician’s-contribution section for the bootstrap chapter:</p>
<div class="sourceCode" id="cb3" style="background: #f1f3f5;"><pre class="sourceCode markdown code-with-copy"><code class="sourceCode markdown"><span id="cb3-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">## The statistician's contribution {#sec-bootstrap-human}</span></span>
<span id="cb3-2"></span>
<span id="cb3-3">Before handing a dataset to a large language model with the</span>
<span id="cb3-4">instruction to 'bootstrap the standard error', four decisions</span>
<span id="cb3-5">require the statistician's judgment:</span>
<span id="cb3-6"></span>
<span id="cb3-7"><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">1. </span>Is the bootstrap appropriate for this statistic at all?</span>
<span id="cb3-8"><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">2. </span>What is the dependence structure of the data?</span>
<span id="cb3-9"><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">3. </span>Which confidence-interval method matches the statistic's</span>
<span id="cb3-10">   behaviour?</span>
<span id="cb3-11"><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">4. </span>Is the bootstrap distribution itself plausible?</span></code></pre></div>
<p>Each decision gets a paragraph of explanation, with the failure modes named explicitly (extrema, time-series data, skewed bootstrap distributions) and the chapter that follows providing the vocabulary to make each decision.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/pub-statistical-computing-textbook/media/images/ambiance2.png" class="img-fluid figure-img"></p>
<figcaption>Terminal showing the book’s sidebar in the browser, with the two books’ shared chapter structure visible as identical tree layouts under different part-labels.</figcaption>
</figure>
</div>
</section>
<section id="the-four-structural-decisions" class="level1">
<h1>The four structural decisions</h1>
<p>The session’s work sorted into four structural decisions, each with downstream consequences across 45 chapters.</p>
<section id="decision-1-book-boundary-and-overlap" class="level2">
<h2 class="anchored" data-anchor-id="decision-1-book-boundary-and-overlap">Decision 1: book boundary and overlap</h2>
<p>The methods volume covers statistical-theory content an MS programme teaches in a typical one-quarter course: programming in R, numerical linear algebra, optimisation, simulation, bootstrap, linear models, GLM, mixed models, survival, Bayesian. The practicum covers the workflow that surrounds the methods: reproducibility infrastructure, Git, Docker, rrtools, renv, Quarto, tidyverse wrangling, CDISC data standards, SAS, and two full case studies.</p>
<p>The boundary is enforced by discipline in the table of contents. Where a chapter in one volume touches a topic covered in the other, the prose uses a textual pointer (‘see the Missing Data chapter of the companion <em>Biostatistics Practicum</em> volume’) rather than a Quarto cross-reference, because <code>@sec-*</code> anchors do not resolve across distinct books.</p>
</section>
<section id="decision-2-age-of-ai-framing" class="level2">
<h2 class="anchored" data-anchor-id="decision-2-age-of-ai-framing">Decision 2: Age-of-AI framing</h2>
<p>The original chapter template ended with a single ‘With AI assistance’ callout containing three prompts. Reviewing the material as a whole showed that three decorative prompts do not earn the ‘in the Age of AI’ subtitle; they behave as a sidebar rather than as part of the intellectual framework.</p>
<p>The replacement is two structured sections per chapter: a front-loaded ‘The statistician’s contribution’ (what the LLM cannot do on the reader’s behalf) and an end-of-chapter ‘Collaborating with an LLM on X’ (what to verify when the LLM produces output). The preface and introduction were rewritten to name the four-category framework (reliable, unreliable, cannot-due-to-missing-context, cannot-due-to-accountability) that organises the book’s position.</p>
</section>
<section id="decision-3-sourcing-chapter-content" class="level2">
<h2 class="anchored" data-anchor-id="decision-3-sourcing-chapter-content">Decision 3: sourcing chapter content</h2>
<p>Each chapter’s source material is distributed across three file types: a <code>speaker_notes_expanded.md</code> that carries the prose narrative, a <code>lecture*_slides_*.qmd</code> that carries both inline code blocks and ‘dynamic’ <code>.fragment .question</code> / <code>.answer</code> pairs, and a <code>lecture*_R_code.R</code> with supplementary code examples. Early chapter ports missed the inline slide code and the fragment pairs entirely because the porting workflow only read the speaker notes and the R file.</p>
<p>The revised workflow reads all three sources, integrates inline code from the slides rather than from the stand-alone R file (the two sometimes disagree), and converts each slide’s dynamic Q&amp;A fragment into a collapsible ‘Check your understanding’ callout placed after the relevant content section.</p>
</section>
<section id="decision-4-visual-differentiation" class="level2">
<h2 class="anchored" data-anchor-id="decision-4-visual-differentiation">Decision 4: visual differentiation</h2>
<p>Opening the two books in adjacent browser tabs, they were indistinguishable at a glance. Both used the same primary colour, the same body face, the same monospace face, the same SCSS rules. The fix was a deliberate typographic split: the methods volume renders body text in Source Serif 4 (fallbacks Charter, Georgia), the practicum renders body text in Source Sans 3 (fallbacks Inter, Helvetica Neue). Monospace stays JetBrains Mono in both. The SCSS change was ten lines per book.</p>
<p>Complementary configuration changes in <code>_quarto.yml</code>:</p>
<div class="sourceCode" id="cb4" style="background: #f1f3f5;"><pre class="sourceCode yaml code-with-copy"><code class="sourceCode yaml"><span id="cb4-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">format</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span></span>
<span id="cb4-2"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">  </span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">html</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span></span>
<span id="cb4-3"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">    </span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">code-annotations</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> hover</span></span>
<span id="cb4-4"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">    </span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">code-copy</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> hover</span></span>
<span id="cb4-5"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">    </span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">code-overflow</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> wrap</span></span>
<span id="cb4-6"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">    </span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">code-tools</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span></span>
<span id="cb4-7"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">      </span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">source</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> repo</span></span>
<span id="cb4-8"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">      </span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">toggle</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> </span><span class="ch" style="color: #20794D;
background-color: null;
font-style: inherit;">false</span></span>
<span id="cb4-9"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">    </span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">code-block-bg</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> </span><span class="ch" style="color: #20794D;
background-color: null;
font-style: inherit;">true</span></span>
<span id="cb4-10"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">    </span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">code-block-border-left</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> </span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'#1f4e79'</span></span>
<span id="cb4-11"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">    </span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">highlight-style</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> arrow</span></span></code></pre></div>
<p>Each of these settings is a named Quarto feature. <code>code-annotations: hover</code> enables numbered-disc annotations inline in code. <code>code-tools</code> puts a ‘view source’ chevron on each page that links to the source <code>.qmd</code> in the book’s GitHub repository. <code>code-block-border-left</code> replaces a hand-rolled SCSS rule with Quarto-native styling synchronised with dark-mode theming. <code>highlight-style: arrow</code> is the accessibility-tuned palette and renders legibly in both HTML and PDF.</p>
</section>
</section>
<section id="verification" class="level1">
<h1>Verification</h1>
<p>After every structural change, both books were re-rendered and the warnings were checked:</p>
<div class="sourceCode" id="cb5" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb5-1"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">cd</span> path/to/methods/textbook</span>
<span id="cb5-2"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">quarto</span> render <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--to</span> html <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;&amp;</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">|</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">grep</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-iE</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'error|warn'</span></span>
<span id="cb5-3"></span>
<span id="cb5-4"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">cd</span> path/to/practicum</span>
<span id="cb5-5"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">quarto</span> render <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--to</span> html <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;&amp;</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">|</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">grep</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-iE</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'error|warn'</span></span></code></pre></div>
<p>Clean output (empty <code>grep</code> result) is the signal to continue. Citeproc warnings (‘citation X not found’) always flagged a bibliography gap and were fixed immediately; bib entries were added incrementally as content ported in.</p>
</section>
<section id="daily-workflow" class="level1">
<h1>Daily Workflow</h1>
<p>The tools that came up most often:</p>
<table class="caption-top table">
<colgroup>
<col style="width: 48%">
<col style="width: 51%">
</colgroup>
<thead>
<tr class="header">
<th>Command</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><code>quarto render --to html</code></td>
<td>Full book render (HTML only, fast)</td>
</tr>
<tr class="even">
<td><code>quarto render path/to/chapter.qmd</code></td>
<td>Single chapter render for rapid iteration</td>
</tr>
<tr class="odd">
<td><code>quarto preview</code></td>
<td>Live-reload preview in browser</td>
</tr>
<tr class="even">
<td><code>quarto check knitr</code></td>
<td>Confirm the R engine is configured</td>
</tr>
<tr class="odd">
<td><code>grep -cE '^@' references.bib</code></td>
<td>Count bibliography entries (growth log)</td>
</tr>
<tr class="even">
<td><code>ls [0-9][0-9]-*.qmd</code> + <code>wc -l</code></td>
<td>Confirm expected chapter count</td>
</tr>
</tbody>
</table>
<p>The bulk batch-edit pattern (rename a heading across 45 chapters, insert a section block after a given anchor) was handled by short ad-hoc Python scripts using <code>pathlib</code> and a content-map dictionary. Shell <code>sed</code> worked for single-line substitutions but became unmanageable for multi-line insertions.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/pub-statistical-computing-textbook/media/images/ambiance3.png" class="img-fluid figure-img"></p>
<figcaption>A workflow scene showing a simple diagram of the two books’ structure: seven parts in the methods volume, seven parts in the practicum, each containing the same chapter template.</figcaption>
</figure>
</div>
</section>
<section id="things-to-watch-out-for" class="level1">
<h1>Things to Watch Out For</h1>
<ol type="1">
<li><p><strong>Quarto’s auto-numbering silently breaks if the introduction chapter is not unnumbered.</strong> <code>00-intro.qmd</code> without <code>.unnumbered</code> gets counted as Chapter 1, shifting every subsequent chapter’s rendered number up by one. Filename prefix says <code>09-bootstrap.qmd</code>, browser says ‘Chapter 10’. Symptom: every cross-reference in the text reads one number off. Fix: <code># Introduction {#sec-intro .unnumbered}</code>.</p></li>
<li><p><strong>Subsection numeric prefixes collide with Quarto’s auto-numbering.</strong> Writing <code>### 1. Correlation and the Fisher-z transformation</code> renders as ‘9.12.1 1. Correlation…’. Fix: drop the prefix, let Quarto number.</p></li>
<li><p><strong>Missing bibliography entries warn but do not fail.</strong> <code>[WARNING] Citeproc: citation mcelreath2020rethinking not found</code> is easy to miss in a full-render log. Pipe the render through <code>grep -iE 'error|warn'</code> every time.</p></li>
<li><p><strong>Cross-book references do not resolve.</strong> <code>@sec-cdisc</code> in the methods volume cannot find the anchor if that anchor lives in the practicum volume. Fix: use prose pointers (‘see the CDISC chapter of the companion volume’).</p></li>
<li><p><strong>The <code>Quiz answers</code> heading label mismatches the <code>Prerequisites</code> quiz label.</strong> Advanced R uses ‘Quiz’ / ‘Quiz answers’; my chapters used ‘Prerequisites’ / ‘Quiz answers’, which readers found confusing. Fix: rename the bottom section ‘Prerequisites answers’ and move it to the very end of the chapter (after the Practice test, where one exists).</p></li>
<li><p><strong>Speaker notes, slide deck, and R code file each carry different content for the same lecture.</strong> Slide <code>.qmd</code> files are the primary source for inline code; the stand-alone <code>.R</code> file often omits blocks that only appear inline in the deck. Read all three.</p></li>
<li><p><strong>Dynamic <code>.fragment .question</code> / <code>.answer</code> blocks in slides are easy to miss.</strong> These are the interactive ‘check your understanding’ moments in a live lecture. In book form they become collapsible callouts. Grep for <code>fragment .question</code> across the lecture directories to find them.</p></li>
</ol>
</section>
<section id="uninstall-rollback" class="level1">
<h1>Uninstall / Rollback</h1>
<p>Nothing installed that needed rollback. All work is in Git; <code>git reset --hard &lt;commit&gt;</code> undoes any session.</p>
</section>
<section id="what-did-we-learn" class="level1">
<h1>What Did We Learn?</h1>
<section id="lessons-learnt" class="level2">
<h2 class="anchored" data-anchor-id="lessons-learnt">Lessons Learnt</h2>
<p><strong>Conceptual Understanding:</strong></p>
<ul>
<li>A subtitle is a structural commitment, not a tagline. ‘In the Age of AI’ obliged the book to explain where the human-LLM division of labour falls for each topic. Three prose prompts at the end of a chapter did not discharge the commitment.</li>
<li>Two books are easier to draft than one, because the scope limits become explicit. Each volume has a boundary, and content that straddles the boundary gets a textual pointer rather than a cross-book link.</li>
<li>Peer programs converge on a fairly consistent MS curriculum. A 22-programme survey found 17/22 require SAS, 14/22 require missing data, 13/22 require Bayesian computation. Content gaps close faster when the reference distribution is visible.</li>
<li>The Posit book family (r4ds, r-pkgs, Advanced R, ggplot2) is an excellent structural template, but its conventions do not by themselves earn an ‘Age of AI’ subtitle. That work is additive.</li>
</ul>
<p><strong>Technical Skills:</strong></p>
<ul>
<li>Quarto’s code-annotation feature is under-used. It is the single most distinctive affordance for a computational textbook and was off by default in both books.</li>
<li>Visual differentiation costs almost nothing: a ten-line SCSS change gives each of two sibling books a distinct body face.</li>
<li>Collapsible callouts (<code>.callout-tip collapse='true'</code>) are the ideal home for slide-style ‘check your understanding’ fragments in a book context. The reader sees the question, thinks, then clicks to reveal the answer.</li>
<li>Bibliography growth should track content porting. Retrofitting a <code>references.bib</code> at the end is painful; adding entries as chapters fill in is trivial.</li>
</ul>
<p><strong>Gotchas and Pitfalls:</strong></p>
<ul>
<li>The <code>.unnumbered</code> class on introduction chapters is load-bearing. Without it, every subsequent chapter renders with a shifted number.</li>
<li>Subsection numeric prefixes duplicate Quarto’s auto-numbering (‘9.12.1 1.’). Let Quarto do the numbering.</li>
<li>Tidyverse-style <code>@sec-*</code> cross-references are invisible in a rendered TOC. They resolve correctly but only in the body text.</li>
<li>Cross-book <code>@sec-*</code> anchors do not resolve. Use prose pointers.</li>
</ul>
</section>
<section id="limitations" class="level2">
<h2 class="anchored" data-anchor-id="limitations">Limitations</h2>
<ul>
<li>Single author, not peer-reviewed. Content decisions were informed by an LLM assistant but not by a disciplinary editorial board.</li>
<li>Most chapters are still scaffolds. The skeleton (Prerequisites, Learning Objectives, Exercises, Further Reading, Quiz answers) is complete, but the interior content sections are TODO placeholders in all but a handful of chapters.</li>
<li>The bootstrap chapter was ported end-to-end as a reference implementation. The remaining 19 methods chapters and most practicum chapters await a similar pass.</li>
<li>Practice tests are only present in chapters where the course’s test bank contains matching material. New chapters (Bayesian, survival) have no practice test by design.</li>
<li>No automated progress tracking. A simple markdown table mapping chapter to ‘scaffold / ported / polished’ status would make collaboration easier.</li>
</ul>
</section>
<section id="opportunities-for-improvement" class="level2">
<h2 class="anchored" data-anchor-id="opportunities-for-improvement">Opportunities for Improvement</h2>
<ol type="1">
<li>Port the remaining 19 content chapters’ speaker notes, slide code, and dynamic Q&amp;A fragments into chapter bodies following the bootstrap chapter’s workflow.</li>
<li>Implement the four named callouts (<code>Note</code> / <code>Verification</code> / <code>Pitfall</code> / <code>LLM prompt</code>) consistently, migrating existing prose callouts as each chapter is touched.</li>
<li>Migrate the ‘Check your understanding’ Q&amp;A fragments from untitled <code>callout-tip collapse='true'</code> to the named <code>callout-tip title='Check'</code> form so they pattern-match across chapters.</li>
<li>Add a colophon page to each book documenting the build environment, theme, and font choices.</li>
<li>Source hero and portrait images (Efron, Bates, Bryan, Wickham, Marwick) and drop them at <code>images/cover.png</code> and <code>images/portraits/*.jpg</code>.</li>
<li>Push the two book repositories to GitHub (<code>rgt47/scai</code>, <code>rgt47/practicum</code>) so the <code>code-tools</code> ‘view source’ chevrons resolve.</li>
<li>Configure Netlify deploys at <code>rgtlab.org/scai</code> and <code>rgtlab.org/practicum</code> via a path-based proxy from the main site.</li>
</ol>
</section>
</section>
<section id="wrapping-up" class="level1">
<h1>Wrapping Up</h1>
<p>A textbook’s subtitle is a commitment. ‘Statistical Computing in the Age of AI’ obliged the book to explain where the human-LLM division of labour falls for each topic, and the explanation had to be structural rather than decorative. The current template delivers that through two sections per chapter (statistician’s contribution plus collaborating with an LLM) backed by a four-category framework in the introduction.</p>
<p>The secondary lesson was that a dual-volume approach clarifies scope. One book tries to be everything to everyone; two books can afford to be boundary-conscious, each pointing to the other where their topics connect. The methods volume covers what modelling looks like; the practicum covers everything that surrounds modelling. A student who reads both comes out with a complete picture and has an easier time than the single-volume reader because each book’s scope is visibly narrower.</p>
<p>In conclusion, four points merit emphasis. First, a subtitle is a structural obligation: it must be earned with sections, not sidebars. Second, two books with shared scaffolding and distinct visual identity are easier to maintain than one oversized volume. Third, Quarto’s code-annotation, code-tools, and first-class callout features should be configured on day one, not bolted on during a polish pass. Fourth, when porting lecture content, the slides, the speaker notes, and the R scripts should be read separately, as they do not fully overlap.</p>
</section>
<section id="see-also" class="level1">
<h1>See Also</h1>
<p><strong>Related posts:</strong></p>
<ul>
<li><a href="../../posts/47-templatesetup/templatesetup/">Setup Post Template (AWS CLI)</a>: the post template this one follows.</li>
<li><a href="../../posts/39-templatepost/templatepost/">Template Post for Data Analysis</a>: the data-analysis sibling template.</li>
</ul>
<p><strong>Key resources:</strong></p>
<ul>
<li><a href="https://quarto.org/docs/books/">Quarto Books documentation</a></li>
<li><a href="https://adv-r.hadley.nz/">Advanced R by Hadley Wickham</a>: the chapter-structure reference.</li>
<li><a href="https://r4ds.hadley.nz/">R for Data Science, 2nd ed.</a>: tidyverse applied reference.</li>
<li><a href="https://stat545.com/">STAT 545 by Jenny Bryan and Derek Stephens</a>: ‘everything in data analysis except modelling’ framing.</li>
<li><a href="https://github.com/rgt47/zzcollab">zzcollab framework</a>: the five-pillar research compendium scaffold used as this post’s working directory.</li>
</ul>
<hr>
</section>
<section id="reproducibility" class="level1">
<h1>Reproducibility</h1>
<p><strong>Tested configuration:</strong></p>
<table class="caption-top table">
<thead>
<tr class="header">
<th>Component</th>
<th>Version</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>Operating system</td>
<td>macOS 15.4</td>
</tr>
<tr class="even">
<td>Quarto</td>
<td>1.9.37</td>
</tr>
<tr class="odd">
<td>R</td>
<td>4.5.3</td>
</tr>
<tr class="even">
<td>knitr</td>
<td>1.51</td>
</tr>
<tr class="odd">
<td>rmarkdown</td>
<td>2.31</td>
</tr>
<tr class="even">
<td>Shell</td>
<td>zsh 5.9</td>
</tr>
<tr class="odd">
<td>Last verified</td>
<td>2026-04-24</td>
</tr>
</tbody>
</table>
<p><strong>Book repositories:</strong></p>
<ul>
<li>Methods volume: <code>~/prj/tch/methods-textbook/textbook/</code></li>
<li>Practicum volume: <code>~/prj/tch/biostatistics-practicum/</code></li>
</ul>
<p><strong>To reproduce the two-book scaffold:</strong></p>
<div class="sourceCode" id="cb6" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb6-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Methods volume</span></span>
<span id="cb6-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">mkdir</span> path/to/methods-book <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">&amp;&amp;</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">cd</span> path/to/methods-book</span>
<span id="cb6-3"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">quarto</span> create project book</span>
<span id="cb6-4"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Edit _quarto.yml with the chapter-template sections.</span></span>
<span id="cb6-5"></span>
<span id="cb6-6"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Practicum volume (same)</span></span>
<span id="cb6-7"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">mkdir</span> path/to/practicum-book <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">&amp;&amp;</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">cd</span> path/to/practicum-book</span>
<span id="cb6-8"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">quarto</span> create project book</span>
<span id="cb6-9"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Edit _quarto.yml with the chapter-template sections.</span></span></code></pre></div>
<p>The chapter-template structure (Prerequisites, Learning Objectives, Orientation, The statistician’s contribution, content, Check-your-understanding callouts, Collaborating with an LLM, Exercises, Further reading, Practice test, Prerequisites answers) is a narrative convention, not a Quarto feature; applying it is manual.</p>
<hr>
</section>
<section id="lets-connect" class="level1">
<h1>Let’s Connect</h1>
<p><em>Have questions, suggestions, or spot an error? Let me know.</em></p>
<ul>
<li><strong>GitHub:</strong> <a href="https://github.com/rgt47">rgt47</a></li>
<li><strong>Twitter/X:</strong> <a href="https://twitter.com/rgt47"><span class="citation" data-cites="rgt47">@rgt47</span></a></li>
<li><strong>LinkedIn:</strong> <a href="https://linkedin.com/in/rgthomaslab">Ronald Glenn Thomas</a></li>
<li><strong>Email:</strong> <a href="https://rgtlab.org/contact">Contact form</a></li>
</ul>
<p>Feedback is welcome in the following cases:</p>
<ul>
<li>Drafting a similar domain-specific textbook and wishing to compare structural decisions.</li>
<li>A better approach to the human-LLM division of labour framework.</li>
<li>Teaching a biostatistics MS programme with different curriculum gaps than the ones this process surfaced.</li>
<li>A general connection is of interest.</li>
</ul>
<hr>
<!-- ============================================================================
PRE-PUBLISH QA CHECKLIST

[x] YAML filled in (title, subtitle, date, categories, description, image)
[x] document-type: 'blog', draft: false
[x] Narrative complete (no remaining `[bracketed]` placeholders)
[x] Four placeholder images present under media/images/ (hero + 3 ambiance)
[ ] media/images/README.md attributes each image (placeholders at time of
    publication; replace when real images are sourced)
[x] Learner voice, zero emojis, single quotes in prose
[x] Version matrix table filled in
[ ] quarto render produces clean HTML with no warnings (to be verified
    when this post is rendered in situ on the blog site)

============================================================================ -->
<section id="related-posts-in-this-cluster" class="level2">
<h2 class="anchored" data-anchor-id="related-posts-in-this-cluster">Related posts in this cluster</h2>
<p>This post is part of the <em>Quarto, R Markdown, and Publishing</em> series. Recommended reading order:</p>
<ol type="1">
<li>Post 80: <a href="../80-pub-multi-language-quarto/">Multi-Language Quarto Documents on macOS</a></li>
<li>Post 81: <a href="../81-pub-r-script-to-rmd/">Rapid Conversion of Draft R Scripts to Formal Rmd</a></li>
<li><strong>Post 83: Building a Statistical Computing Textbook</strong> (this post)</li>
<li>Post 84: <a href="../84-pub-obs-r-screencasts/">Setting up OBS for Live R Coding Screencasts</a></li>
</ol>


</section>
</section>

 ]]></description>
  <category>quarto</category>
  <category>teaching</category>
  <category>reproducibility</category>
  <guid>https://rgtlab.org/posts/pub-statistical-computing-textbook/</guid>
  <pubDate>Fri, 24 Apr 2026 07:00:00 GMT</pubDate>
  <media:content url="https://rgtlab.org/posts/pub-statistical-computing-textbook/media/images/hero.png" medium="image" type="image/png" height="96" width="144"/>
</item>
<item>
  <title>A pocket terminal for your Linux laptop with ttyd and Tailscale</title>
  <dc:creator>Ronald G. Thomas</dc:creator>
  <link>https://rgtlab.org/posts/wf-pocket-terminal-ttyd-tailscale/</link>
  <description><![CDATA[ 




<!-- ============================================================================
AUTHOR PROVIDES — concrete inputs the blogger must supply

Every item below maps to a placeholder in the body. Fill in this checklist
FIRST, then paste your answers into the matching `[bracketed]` slots. Do not
publish until every box is ticked.

YAML FRONT MATTER (lines 1-20)
  [x] title           — 'ttyd + Tailscale' replaces '[Tool]' slot
  [x] subtitle        — one-line elaboration of what the post sets up
  [x] date            — 2026-04-15
  [x] categories      — setup, ttyd, tailscale, linux
  [x] description     — 1-2 sentences for blog listing card
  [ ] image           — media/images/hero.png (to be supplied)
  [ ] draft: false    — flip when ready

NARRATIVE INPUTS
  [x] Hook sentence
  [x] Pain point
  [x] Motivations
  [x] Objectives
  [x] What is ttyd?
  [x] Daily-workflow paragraph
  [x] Things to Watch Out For (5-7 gotchas)
  [x] Lessons Learnt (conceptual + technical + gotchas)
  [x] Limitations
  [x] Opportunities for Improvement
  [x] Wrapping Up + main takeaways
  [x] See Also

CONFIGURATION DELIVERABLES
  [x] Prerequisites list
  [x] Installation block
  [x] Complete config file(s) — systemd unit under analysis/configs/
  [x] Verification commands
  [x] Keybinding / command reference table
  [x] Uninstall / rollback steps
  [x] Optional appendices — TLS with tailscale cert, sample session

VERSION MATRIX
  [x] OS + version tested: Ubuntu 24.04
  [x] Tool version pinned: ttyd 1.7.7, tailscale 1.64
  [x] Dependency versions: tmux 3.4
  [x] Date of last verification: 2026-04-15

IMAGES
  [ ] Hero image (80% width)
  [ ] Ambiance image 1 (after Objectives)
  [ ] Ambiance image 2 (after Configuration)
  [ ] Ambiance image 3 (before Lessons Learnt)
  [ ] media/images/README.md with attributions

CONTACT & METADATA
  [x] Author name matches site author
  [ ] Social links verified
  [ ] Giscus comments inherited
============================================================================ -->
<!-- ============================================================================
HERO IMAGE
============================================================================ -->
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/wf-pocket-terminal-ttyd-tailscale/media/images/hero.png" class="img-fluid figure-img" style="width:80.0%"></p>
<figcaption>A phone displaying a terminal window connected to a laptop over a private network, illustrating remote shell access from a mobile device.</figcaption>
</figure>
</div>
<p><em>A browser-based interface to a persistent shell, reachable over an authenticated private network, provides a workable mobile surface for observing and intervening in long-running tasks.</em></p>
<section id="introduction" class="level1">
<h1>Introduction</h1>
<!-- TEMPLATE INSTRUCTION: 'I did not really appreciate X until Y'. Peer, not
teacher. Three short paragraphs. -->
<p>The ergonomics of operating an SSH client on a mobile device become apparent only when the task is non-trivial. Identity-file selection, modifier-key entry on a soft keyboard, reliable reconnection after the device sleeps, and the rendering of a monospace font at phone or tablet scale each impose non-negligible overhead. In practice these frictions accumulate to the point where many users defer routine oversight work until they regain physical access to the host machine. The resulting delay is not merely inconvenient; it creates an asymmetry between the pace at which background work progresses and the pace at which an operator can respond to it.</p>
<p>Two components, used together, reduce this overhead substantially. The first, <code>ttyd</code>, is a small C daemon that serves a terminal emulator over HTTP using the xterm.js JavaScript library; any standards-compliant browser becomes a client without additional software. The second, Tailscale, builds a peer-to-peer WireGuard mesh between enrolled devices and provides an authenticated overlay network, so that the <code>ttyd</code> listener is addressable only by devices already admitted to the private tailnet rather than by arbitrary hosts on the public internet. The combination yields a single URL that is reachable from an enrolled phone, is invisible to every other network the laptop is attached to, and resumes a persistent tmux session on each visit.</p>
<p>We document the configuration end to end. The coverage includes installation of <code>ttyd</code>, Tailscale, and tmux on an Ubuntu 24.04 laptop; binding of the <code>ttyd</code> listener to the Tailscale network interface; wrapping of the service in a per-user systemd unit that survives logout and reboot; client-side configuration on iOS; and optional hardening with a TLS certificate issued by <code>tailscale cert</code>. Appendices cover a sample work session and the full teardown procedure. Corrections and alternative approaches are welcome.</p>
<p>More formally, we document here an entry point to the Remote Access concern of the Workflow Construct described in <a href="../../posts/52-workflow-construct/">post 52</a>. Tailscale plus <code>ttyd</code> is one specific solution to the cross-machine, cross-network reachability problem; the broader Remote Access family also includes <code>mosh</code> for high-latency-link resilience and standard SSH for the unauthenticated-network case. The present post is the browser-shell-on-mobile leaf of that family.</p>
<section id="motivations" class="level2">
<h2 class="anchored" data-anchor-id="motivations">Motivations</h2>
<!-- TEMPLATE INSTRUCTION: Specific, tied to concrete pain points. -->
<p>The configuration described here was motivated by the following requirements, each tied to a concrete limitation of the alternatives:</p>
<ul>
<li>Monitoring and occasional intervention in long-running interactive sessions, including agentic coding workflows that may run for hours, without requiring physical access to the host machine.</li>
<li>Avoidance of identity-file management and modifier-key entry on a small touchscreen, which are the two principal ergonomic costs of native SSH clients on iOS.</li>
<li>Elimination of any public-internet exposure on the laptop. Opening inbound port 22 through a home router, combined with dynamic DNS, would grant global addressability to a device that regularly attaches to untrusted networks; this is an unacceptable risk profile for a personal workstation.</li>
<li>Network-location independence. A single stable URL should be usable from the home network, the institutional network, and a cellular carrier’s network without client-side reconfiguration.</li>
<li>Generalisation to future deployments. The component choices (a WireGuard-based overlay, an HTTP-native terminal, a per-user systemd unit) transfer cleanly to a cloud-hosted VPS workstation, which was identified as a plausible next step.</li>
</ul>
</section>
<section id="objectives" class="level2">
<h2 class="anchored" data-anchor-id="objectives">Objectives</h2>
<!-- TEMPLATE INSTRUCTION: Each objective should be verifiable. -->
<p>The scope of this post is the following set of verifiable deliverables:</p>
<ol type="1">
<li>Install <code>ttyd</code>, Tailscale, and <code>tmux</code> on a Linux laptop and confirm each by version query.</li>
<li>Run <code>ttyd</code> bound exclusively to the Tailscale network interface, protected by HTTP basic authentication, and attached to a persistent tmux session.</li>
<li>Wrap the service in a per-user systemd unit that remains active across logout and reboot.</li>
<li>Connect to the service from an iPhone browser over the tailnet, verify that interactive input and output behave correctly, and document a complete teardown procedure.</li>
</ol>
<p>The configuration is presented as a reproducible reference rather than a tutorial. Each command and configuration directive is annotated with the rationale for its inclusion.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/wf-pocket-terminal-ttyd-tailscale/media/images/ambiance1.png" class="img-fluid figure-img"></p>
<figcaption>Workspace ambiance: a Linux laptop on a desk with a phone propped beside it, mirroring the same tmux session.</figcaption>
</figure>
</div>
</section>
</section>
<section id="what-is-ttyd" class="level1">
<h1>What is ttyd?</h1>
<!-- TEMPLATE INSTRUCTION: One sentence definition. One analogy. One example. -->
<p><code>ttyd</code> is a small C daemon, approximately six thousand lines of source, that serves a terminal emulator over HTTP. On the server side it allocates a pseudo-terminal (pty), spawns a process attached to it (commonly a shell or a multiplexer such as tmux), and exposes the pty’s input and output through a WebSocket on an HTTP listener. On the client side the browser loads the xterm.js library, opens the WebSocket, and renders a full VT100-family terminal in a <code>&lt;canvas&gt;</code> element; keystrokes are forwarded as they are generated, and output bytes are written back to the terminal buffer.</p>
<p>In functional terms the role of <code>ttyd</code> is analogous to that of an SSH daemon, but the transport is HTTP(S) and the client is a browser rather than a native terminal emulator. This has three practical consequences. First, no client-side software installation is required; any device with a recent browser is a viable client. Second, the authentication and transport-security story is different: <code>ttyd</code> itself provides only HTTP basic authentication, and is not intended to be exposed to untrusted networks without an enclosing security layer (a reverse proxy with client-certificate authentication, or in this configuration, a Tailscale overlay). Third, session persistence across client disconnections is delegated: closing the browser tab tears down the WebSocket, but the pty and its attached process continue to run as long as <code>ttyd</code> is running, which is why this configuration always attaches <code>ttyd</code> to a tmux session rather than to a bare shell.</p>
<p>As a minimal example, <code>ttyd -W -p 7681 zsh</code> starts a writable shell on port 7681; any host that can establish a TCP connection to that endpoint obtains an interactive terminal. The Tailscale layer constrains the meaning of ‘any host that can reach that endpoint’ to devices that have been authenticated into the tailnet, and routes the traffic over an encrypted WireGuard tunnel rather than over a TCP connection that transits intermediate networks in clear text.</p>
</section>
<section id="architecture-overview" class="level1">
<h1>Architecture Overview</h1>
<!-- TEMPLATE INSTRUCTION: One paragraph describing the runtime system. -->
<p>The runtime system is best understood as three layered components, each with a distinct responsibility:</p>
<ol type="1">
<li><p><strong>The terminal process.</strong> tmux runs as a long-lived user process on the laptop, maintaining one or more pty pairs and the associated shell state. Its lifecycle is independent of any network connection and outlasts both the <code>ttyd</code> daemon and the browser.</p></li>
<li><p><strong>The HTTP gateway.</strong> <code>ttyd</code> runs as a systemd user service, attaches to the tmux session specified on its command line, and serves the terminal contents over HTTP. It listens only on the Tailscale virtual interface (<code>tailscale0</code>), so its socket is unreachable from the physical wired, wireless, or cellular interfaces of the host.</p></li>
<li><p><strong>The transport overlay.</strong> Tailscale runs as a system-level daemon (<code>tailscaled</code>) and maintains the <code>tailscale0</code> WireGuard interface. All packets arriving on that interface have already been authenticated against the tailnet’s public-key infrastructure and decrypted from their WireGuard envelopes. On the client (the phone), the Tailscale iOS app maintains the corresponding peer relationship and presents the tailnet as a routeable network.</p></li>
</ol>
<p>The request path for a single keystroke therefore traverses: browser on phone, iOS Tailscale VPN, WireGuard tunnel (direct peer-to-peer where possible, or via a DERP relay when both endpoints are behind restrictive NATs), <code>tailscale0</code> on the laptop, <code>ttyd</code> listener on port 7681, the pty master owned by <code>ttyd</code>, and finally the tmux process that is the pty slave. The response path returns bytes by the reverse route. All segments between the phone and the laptop are encrypted by WireGuard; the segment between <code>ttyd</code> and <code>tailscale0</code> is local to the loopback-like virtual interface and does not leave the host.</p>
</section>
<section id="security-model" class="level1">
<h1>Security Model</h1>
<!-- TEMPLATE INSTRUCTION: State the threat model and the controls. -->
<p>The configuration is designed to withstand the following threats, in approximate order of likelihood: (a) an attacker scanning the public IP addresses of the laptop’s current network for open ports, (b) an attacker on the same Layer-2 network as the laptop (for example, a shared coffee-shop SSID) attempting to observe or connect to local services, (c) an attacker who compromises a single device on the tailnet and attempts to move laterally, and (d) an attacker with access to the basic-authentication password but not to the tailnet.</p>
<p>Three layered controls address these threats:</p>
<ul>
<li><p><strong>Network-layer authentication (Tailscale).</strong> Only devices that have completed the OAuth-style authentication flow with the tailnet’s coordination server and received a signed node key can establish WireGuard sessions with the laptop. This eliminates threats (a) and (b): the <code>ttyd</code> port is not even visible to hosts outside the tailnet. An attacker on a shared SSID sees only the encrypted WireGuard packets, which are indistinguishable from other UDP traffic.</p></li>
<li><p><strong>Interface binding (<code>ttyd -i tailscale0</code>).</strong> The listener is bound to the Tailscale virtual interface rather than to <code>0.0.0.0</code>. If the tailnet were somehow compromised at the coordination-server level, or if Tailscale itself were misconfigured to route traffic from unexpected sources into <code>tailscale0</code>, the service would still be unreachable from the physical interfaces. This is a defence-in-depth control.</p></li>
<li><p><strong>Application-layer authentication (HTTP basic auth).</strong> A username and password are required to open a WebSocket, providing a final barrier against threat (c): an attacker who has compromised one tailnet device cannot open a shell on the laptop without also possessing the <code>ttyd</code> credentials. Basic authentication is not a strong control in isolation (the credentials are sent on every request and are visible in <code>ps</code> output on the host), but it is appropriate as the innermost layer of a multi-layer model.</p></li>
</ul>
<p>Threat (d) is out of scope: an attacker with the basic-auth password but no tailnet credentials has no routable path to the service. This is the intended shape of the model: Tailscale provides confidentiality and network-layer authentication; basic authentication provides an independent defence against compromised tailnet peers; the interface binding ensures that neither of the other two controls is single-point-of-failure.</p>
<p>Appendix A describes an optional additional layer (TLS via <code>tailscale cert</code>) that closes the remaining residual risk of plaintext credentials on the wire between <code>ttyd</code> and the <code>tailscale0</code> interface, at a modest operational cost.</p>
</section>
<section id="prerequisites" class="level1">
<h1>Prerequisites</h1>
<!-- TEMPLATE INSTRUCTION: Front-load assumptions. Reader should know in
30 seconds whether this applies. -->
<p>The configuration assumes the following environment:</p>
<ul>
<li><strong>Operating system:</strong> Ubuntu 24.04 on the laptop acting as the server. Other Linux distributions can be accommodated with minor substitutions in the package-manager invocations.</li>
<li><strong>Client device:</strong> an iPhone running iOS 17 or later with Safari as the browser. Android and desktop browsers function identically with respect to the xterm.js client.</li>
<li><strong>Hardware:</strong> any laptop with sufficient resources to run Ubuntu; the runtime footprint of the services described here is negligible.</li>
<li><strong>Prior installation state:</strong> a Linux user account with <code>sudo</code> privileges, and a Tailscale account (the free tier is adequate for the use cases described here).</li>
<li><strong>Prior knowledge:</strong> familiarity with systemd unit-file syntax and general Linux administration at the shell. Prior experience with tmux is helpful but not required.</li>
<li><strong>Time investment:</strong> approximately thirty minutes for installation, configuration, and verification of the first successful browser connection.</li>
</ul>
<p>Where any assumption fails to hold, the commands below may require adaptation.</p>
</section>
<section id="installation" class="level1">
<h1>Installation</h1>
<!-- TEMPLATE INSTRUCTION: Show command, show expected output, prose < 3 sentences. -->
<p>On Ubuntu, the three required packages are available from the distribution repositories and the Tailscale install script:</p>
<div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb1-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sudo</span> apt update</span>
<span id="cb1-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sudo</span> apt install <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-y</span> ttyd tmux</span>
<span id="cb1-3"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">curl</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-fsSL</span> https://tailscale.com/install.sh <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">|</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sh</span></span>
<span id="cb1-4"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sudo</span> tailscale up</span></code></pre></div>
<p>The <code>tailscale up</code> command prints an authentication URL. Open it in any browser, sign in, and confirm the device appears in the tailnet.</p>
<div class="callout callout-style-default callout-note callout-titled">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon"></i>
</div>
<div class="callout-title-container flex-fill">
Note
</div>
</div>
<div class="callout-body-container callout-body">
<p>If the distribution ships a <code>ttyd</code> older than 1.7, install a prebuilt binary from the <a href="https://github.com/tsl0922/ttyd/releases">ttyd releases page</a> instead. Earlier versions lack the <code>-i</code> interface-binding flag used below.</p>
</div>
</div>
<p>Confirm the installs succeeded:</p>
<div class="sourceCode" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb2-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">ttyd</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--version</span>        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Expected: ttyd version 1.7.x</span></span>
<span id="cb2-2"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">tailscale</span> version     <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Expected: 1.64.x or later</span></span>
<span id="cb2-3"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">tmux</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-V</span>               <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Expected: tmux 3.4 or later</span></span>
<span id="cb2-4"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">tailscale</span> ip <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-4</span>       <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Prints the laptop's 100.x.y.z address</span></span></code></pre></div>
</section>
<section id="configuration" class="level1">
<h1>Configuration</h1>
<!-- TEMPLATE INSTRUCTION: Full config file, not snippets. Inline comments
explain the WHY of non-obvious settings. -->
<p>Two configuration artifacts drive the setup: a single <code>ttyd</code> command line and a systemd user unit that wraps it. Both are stored in full under <code>analysis/configs/</code> in the post repository.</p>
<section id="the-ttyd-invocation" class="level3">
<h3 class="anchored" data-anchor-id="the-ttyd-invocation">The ttyd invocation</h3>
<div class="sourceCode" id="cb3" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb3-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">ttyd</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-W</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb3-2">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-p</span> 7681 <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb3-3">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-i</span> tailscale0 <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb3-4">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-c</span> user:<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'REPLACE_WITH_STRONG_PASSWORD'</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb3-5">  tmux new <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-A</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-s</span> main</span></code></pre></div>
<p>Rationale for each flag:</p>
<ul>
<li><code>-W</code> enables write input on the pty. Without this flag the browser session is read-only, which is not useful for the interactive work that motivates the configuration.</li>
<li><code>-p 7681</code> specifies the TCP listening port. Any unused port is acceptable; 7681 is the <code>ttyd</code> project default and is used here for consistency with upstream documentation.</li>
<li><code>-i tailscale0</code> binds the listener exclusively to the Tailscale virtual interface. The service is therefore unreachable from physical wired, wireless, or cellular interfaces of the laptop, regardless of which network the laptop is attached to at any given moment. The interface name should be confirmed with <code>ip -br link</code>, as it is nominally fixed but may differ on non-default configurations.</li>
<li><code>-c user:password</code> enables HTTP basic authentication. The credentials are an application-layer defence-in-depth measure; the primary access control is the Tailscale interface binding, and the basic-auth credentials are an independent safeguard against the scenario in which a tailnet peer is itself compromised.</li>
<li><code>tmux new -A -s main</code> invokes tmux in a mode that creates the session if it does not exist and attaches to it otherwise. This form is idempotent, which is essential inside a systemd <code>ExecStart</code> directive. It also ensures that every browser connection attaches to the same session, so that shell state persists across client disconnections.</li>
</ul>
</section>
<section id="the-systemd-user-unit" class="level3">
<h3 class="anchored" data-anchor-id="the-systemd-user-unit">The systemd user unit</h3>
<p>Manual launches do not survive logout. The unit below, saved as <code>~/.config/systemd/user/ttyd.service</code>, runs the service as the logged-in user and restarts on failure.</p>
<div class="sourceCode" id="cb4" style="background: #f1f3f5;"><pre class="sourceCode ini code-with-copy"><code class="sourceCode ini"><span id="cb4-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># ~/.config/systemd/user/ttyd.service</span></span>
<span id="cb4-2"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Tested on ttyd 1.7.7, Ubuntu 24.04</span></span>
<span id="cb4-3"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">[Unit]</span></span>
<span id="cb4-4"><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Description</span><span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">ttyd terminal over Tailscale</span></span>
<span id="cb4-5"><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">After</span><span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">network-online.target tailscaled.service</span></span>
<span id="cb4-6"><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Wants</span><span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">network-online.target</span></span>
<span id="cb4-7"></span>
<span id="cb4-8"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">[Service]</span></span>
<span id="cb4-9"><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Type</span><span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">simple</span></span>
<span id="cb4-10"><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">ExecStart</span><span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">/usr/bin/ttyd -W -p 7681 -i tailscale0 \</span></span>
<span id="cb4-11"><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">  -c user:REPLACE_WITH_STRONG_PASSWORD \</span></span>
<span id="cb4-12"><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">  /usr/bin/tmux new -A -s main</span></span>
<span id="cb4-13"><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Restart</span><span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">on-failure</span></span>
<span id="cb4-14"><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">RestartSec</span><span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">5</span></span>
<span id="cb4-15"></span>
<span id="cb4-16"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">[Install]</span></span>
<span id="cb4-17"><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">WantedBy</span><span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">default.target</span></span></code></pre></div>
<p>Enable and start:</p>
<div class="sourceCode" id="cb5" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb5-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">systemctl</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--user</span> daemon-reload</span>
<span id="cb5-2"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">systemctl</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--user</span> enable <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--now</span> ttyd.service</span>
<span id="cb5-3"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">systemctl</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--user</span> status ttyd.service</span></code></pre></div>
<p>To allow the user service to start before the user logs in at the console (so the laptop is reachable after an unattended reboot):</p>
<div class="sourceCode" id="cb6" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb6-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sudo</span> loginctl enable-linger <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$USER</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span></code></pre></div>
<p>Restrict the unit file permissions so the basic-auth password is not world-readable:</p>
<div class="sourceCode" id="cb7" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb7-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">chmod</span> 600 ~/.config/systemd/user/ttyd.service</span></code></pre></div>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/wf-pocket-terminal-ttyd-tailscale/media/images/ambiance2.png" class="img-fluid figure-img"></p>
<figcaption>Editor view of the systemd unit file alongside a browser-based terminal, illustrating the configuration-to-runtime relationship.</figcaption>
</figure>
</div>
</section>
</section>
<section id="verification" class="level1">
<h1>Verification</h1>
<!-- TEMPLATE INSTRUCTION: Smoke test that proves setup works. -->
<p>Three checks confirm the stack is operating correctly.</p>
<div class="sourceCode" id="cb8" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb8-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 1. Service is running</span></span>
<span id="cb8-2"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">systemctl</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--user</span> is-active ttyd.service</span>
<span id="cb8-3"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Expected: active</span></span>
<span id="cb8-4"></span>
<span id="cb8-5"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 2. Port is bound to the Tailscale interface only</span></span>
<span id="cb8-6"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">ss</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-ltnp</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">|</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">grep</span> 7681</span>
<span id="cb8-7"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Expected: LISTEN on 100.x.y.z:7681 (tailnet address),</span></span>
<span id="cb8-8"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># NOT on 0.0.0.0:7681</span></span>
<span id="cb8-9"></span>
<span id="cb8-10"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 3. End-to-end reachability from a second tailnet device</span></span>
<span id="cb8-11"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">curl</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-u</span> user:PASSWORD <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-I</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb8-12">  http://<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$(</span><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">tailscale</span> ip <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-4</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">)</span>:7681</span>
<span id="cb8-13"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Expected: HTTP/1.1 200 OK</span></span></code></pre></div>
<p>If step 2 shows <code>0.0.0.0:7681</code>, the <code>-i tailscale0</code> flag is not taking effect; the service is exposed to every network the laptop joins and must not be started until fixed.</p>
</section>
<section id="connecting-from-the-iphone" class="level1">
<h1>Connecting from the iPhone</h1>
<p>The client-side procedure consists of four steps:</p>
<ol type="1">
<li><p>Install the Tailscale application from the App Store.</p></li>
<li><p>Authenticate with the same account used on the laptop and confirm that the laptop appears under <strong>Devices</strong> in the Tailscale client.</p></li>
<li><p>Enable the Tailscale VPN toggle. iOS will report an active VPN connection in the status bar.</p></li>
<li><p>In Safari, navigate to the service URL:</p>
<pre><code>http://laptop.tailnet-name.ts.net:7681</code></pre>
<p>The MagicDNS name is reported by <code>tailscale status</code> on the laptop. The bare-IP form (<code>http://100.x.y.z:7681</code>) is functionally equivalent and is useful for diagnostic purposes when MagicDNS resolution is suspected to be failing.</p></li>
<li><p>Supply the basic-authentication credentials. The browser will render an interactive terminal attached to the <code>main</code> tmux session.</p></li>
</ol>
<div class="callout callout-style-default callout-tip callout-titled">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon"></i>
</div>
<div class="callout-title-container flex-fill">
Tip
</div>
</div>
<div class="callout-body-container callout-body">
<p>Adding the URL to the iPhone home screen via Safari’s <strong>Share &gt; Add to Home Screen</strong> creates a standalone web-application icon. When launched from that icon, the page runs in a full-screen container without browser chrome, which improves usable display area on a small screen.</p>
</div>
</div>
</section>
<section id="daily-workflow" class="level1">
<h1>Daily Workflow</h1>
<!-- TEMPLATE INSTRUCTION: Scannable command table, the most-bookmarked
element of a setup post. -->
<p>The table below summarises the commands and gestures that are most frequently required during routine use:</p>
<table class="caption-top table">
<colgroup>
<col style="width: 52%">
<col style="width: 47%">
</colgroup>
<thead>
<tr class="header">
<th>Command or gesture</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>Open home-screen icon</td>
<td>Attach to <code>main</code> tmux session</td>
</tr>
<tr class="even">
<td><code>Ctrl-b d</code> (within tmux)</td>
<td>Detach without terminating the session</td>
</tr>
<tr class="odd">
<td><code>Ctrl-b c</code></td>
<td>Create a new window within the session</td>
</tr>
<tr class="even">
<td><code>Ctrl-b n</code> / <code>Ctrl-b p</code></td>
<td>Cycle to next / previous tmux window</td>
</tr>
<tr class="odd">
<td><code>systemctl --user restart ttyd.service</code></td>
<td>Restart the terminal service</td>
</tr>
<tr class="even">
<td><code>journalctl --user -u ttyd.service -f</code></td>
<td>Follow the service log in real time</td>
</tr>
<tr class="odd">
<td><code>tailscale status</code></td>
<td>List reachable devices on the tailnet</td>
</tr>
<tr class="even">
<td>Pinch-zoom in Safari</td>
<td>Adjust terminal font size</td>
</tr>
</tbody>
</table>
<p>A Bluetooth keyboard paired with the iPhone substantially improves the input ergonomics, to the point that the configuration becomes suitable for sustained interactive work rather than brief observation. Safari forwards the <code>Ctrl</code>, <code>Esc</code>, <code>Tab</code>, and arrow keys to xterm.js without rebinding. A keyboard on which Caps Lock has been remapped to <code>Ctrl</code> (an iOS system setting) reduces hand-position strain during extended tmux use.</p>
</section>
<section id="operational-considerations" class="level1">
<h1>Operational Considerations</h1>
<!-- TEMPLATE INSTRUCTION: REQUIRED for setup posts. 5-7 gotchas, each with
symptom and fix. -->
<p>The following issues arise sufficiently often during initial deployment and routine use to warrant explicit documentation. Each is presented with its symptom and its remediation.</p>
<ol type="1">
<li><p><strong>Incorrect interface binding.</strong> If <code>ss -ltnp</code> shows <code>0.0.0.0:7681</code> rather than the tailnet address, the <code>-i tailscale0</code> directive has not taken effect. The interface name should be confirmed with <code>ip -br link</code>, as it may differ on systems with non-default Tailscale installations or with systemd-networkd renaming. The service should not be left running until the binding has been verified to be correct, because a listener on <code>0.0.0.0</code> is reachable from every network the laptop attaches to.</p></li>
<li><p><strong>Lid-close suspension.</strong> A laptop whose power-management policy suspends the system on lid closure is not reachable over the network while suspended. The remediation is one of: physically maintain an open lid; modify the desktop environment’s lid-close behaviour to ‘do nothing’; or, for transient tasks, wrap the command with <code>systemd-inhibit --what=sleep</code> to prevent suspension for the duration of the inhibition.</p></li>
<li><p><strong>Loss of session persistence.</strong> If closing the browser terminates the shell, the <code>ExecStart</code> directive is launching a bare shell directly rather than attaching to a tmux session. Only the <code>tmux new -A -s main</code> form, with tmux as the terminal’s attached process, delivers persistence across client disconnections.</p></li>
<li><p><strong>Service termination at logout.</strong> Without <code>loginctl enable-linger &lt;user&gt;</code>, systemd user services are terminated when the user’s last interactive session ends. Lingering must be enabled once per user and, being a system-level setting, requires <code>sudo</code>.</p></li>
<li><p><strong>Plaintext credentials in the unit file.</strong> The basic-authentication password is stored in plaintext in the <code>ExecStart</code> line of the unit file. The unit permissions must be restricted with <code>chmod 600</code>, and the unit file must not be committed to a shared repository. For higher sensitivity requirements, use a systemd credentials file (<code>LoadCredential=</code>) or migrate to Tailscale Serve as described under Opportunities for Improvement.</p></li>
<li><p><strong>MagicDNS resolution failures.</strong> Tailnet hostnames must include the full tailnet suffix (for example, <code>laptop.tailnet-name.ts.net</code>). A bare hostname resolves only if MagicDNS is enabled in the Tailscale admin console and if the client has accepted the DNS configuration pushed by the Tailscale daemon; clients that predate the MagicDNS enablement may require a Tailscale toggle cycle to refresh.</p></li>
<li><p><strong>iOS input autocorrection.</strong> Safari on iOS auto-capitalises the first character entered into a text field by default, which can corrupt the first character of a password or a command. The appropriate remediations are to disable auto-capitalisation under <strong>Settings &gt; General &gt; Keyboard</strong>, or to use a Bluetooth keyboard, which is not subject to the on-screen keyboard’s input transformations.</p></li>
</ol>
</section>
<section id="uninstall-rollback" class="level1">
<h1>Uninstall / Rollback</h1>
<p>To remove the setup, disable the service and uninstall the packages.</p>
<div class="sourceCode" id="cb10" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb10-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 1. Stop and disable the service</span></span>
<span id="cb10-2"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">systemctl</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--user</span> disable <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--now</span> ttyd.service</span>
<span id="cb10-3"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">rm</span> ~/.config/systemd/user/ttyd.service</span>
<span id="cb10-4"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">systemctl</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--user</span> daemon-reload</span>
<span id="cb10-5"></span>
<span id="cb10-6"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 2. Disable lingering if no other user service needs it</span></span>
<span id="cb10-7"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sudo</span> loginctl disable-linger <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$USER</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb10-8"></span>
<span id="cb10-9"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 3. Uninstall packages</span></span>
<span id="cb10-10"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sudo</span> apt remove <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--purge</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-y</span> ttyd</span>
<span id="cb10-11"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Leave tmux and tailscale if used elsewhere; otherwise:</span></span>
<span id="cb10-12"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sudo</span> apt remove <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--purge</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-y</span> tmux</span>
<span id="cb10-13"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sudo</span> tailscale down</span>
<span id="cb10-14"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sudo</span> apt remove <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--purge</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-y</span> tailscale</span></code></pre></div>
<p>Remove the laptop from the tailnet via the Tailscale admin console if the device is being retired.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/wf-pocket-terminal-ttyd-tailscale/media/images/ambiance3.png" class="img-fluid figure-img"></p>
<figcaption>Workflow scene: the laptop closed on a desk while a phone shows the same tmux session mid-task, suggesting continuity without co-location.</figcaption>
</figure>
</div>
</section>
<section id="discussion" class="level1">
<h1>Discussion</h1>
<section id="observations" class="level2">
<h2 class="anchored" data-anchor-id="observations">Observations</h2>
<p><strong>Conceptual observations:</strong></p>
<ul>
<li>A browser is an adequate terminal client when the network transport is handled by a component specifically engineered for authenticated overlay networking. The SSH daemon is one of several defensible approaches to remote shell access; in this configuration the transport responsibility is relocated from SSH to WireGuard, and the authentication responsibility is distributed across WireGuard, <code>ttyd</code> basic auth, and the interface binding.</li>
<li>MagicDNS eliminates the operational burden of dynamic DNS, port forwarding, and firewall-rule maintenance for personal-scale remote access. The trade-off is dependence on the Tailscale coordination server for name resolution.</li>
<li>The interface-binding flag on <code>ttyd</code> is the single most consequential hardening decision in the configuration. A service bound to <code>0.0.0.0</code> with HTTP basic authentication represents a qualitatively different risk profile from a service bound to a WireGuard interface; the former is subject to public-internet scanning, the latter is not.</li>
<li>Per-user systemd units, combined with <code>loginctl enable-linger</code>, provide a clean abstraction for background services that require user-level file system access without requiring system-level privileges beyond the initial linger enablement.</li>
</ul>
<p><strong>Technical observations:</strong></p>
<ul>
<li>The <code>tmux new -A -s &lt;name&gt;</code> form is both a creator and an attacher, and is therefore idempotent. This makes it safe to invoke from an <code>ExecStart</code> directive, where a non-idempotent invocation would cause a restart loop.</li>
<li>The <code>ss -ltnp</code> command is more direct and more precise than <code>netstat -ltnp</code> for determining the interface on which a service is listening, and it is the standard utility on distributions that have replaced <code>net-tools</code> with <code>iproute2</code>.</li>
<li>The <code>tailscale cert</code> command issues short-lived TLS certificates for MagicDNS hostnames, enabling end-to-end HTTPS without involvement of a public certificate authority. Certificates expire and must be reissued on a schedule, as discussed in Appendix A.</li>
<li>The Safari <strong>Add to Home Screen</strong> action produces a standalone web-application surface that removes browser chrome and recovers screen area, which materially improves usability on any URL that is accessed with regularity.</li>
</ul>
<p><strong>Recurring Pitfalls:</strong></p>
<ul>
<li>Omission of the <code>-W</code> flag yields a read-only terminal, which presents as a non-interactive first connection and is easily misdiagnosed as a network or authentication problem.</li>
<li>Binding to <code>tailscale0</code> without first confirming the interface name (via <code>ip -br link</code>) produces a service that fails to start and logs an interface-not-found error; the service is inert but not obviously so on casual inspection.</li>
<li>The basic-authentication password is visible both in <code>ps</code> output on the host and in the unit file. It should be classified as a moderate-sensitivity secret: rotated on suspicion of exposure, but not requiring the operational controls appropriate to a high-sensitivity credential such as an SSH private key.</li>
<li>iOS aggressively reclaims memory from backgrounded Safari tabs. The WebSocket reconnects transparently on tab reactivation, but any command-line input that had been typed but not submitted at the time of backgrounding is lost.</li>
</ul>
</section>
<section id="limitations" class="level2">
<h2 class="anchored" data-anchor-id="limitations">Limitations</h2>
<p>The configuration has several explicit constraints that should be understood at the outset:</p>
<ul>
<li><strong>Single-user scope.</strong> The unit file and basic-authentication credentials are per-user. Multi-user access requires either parallel service instances on distinct ports or a front-end reverse proxy that demultiplexes on authenticated identity.</li>
<li><strong>Absence of audit logging.</strong> <code>ttyd</code> does not log the commands executed within the pty. Where accountability or forensic capability is required, a separate shell-level or system-level auditing facility (for example, <code>auditd</code> with appropriate rules) must be configured.</li>
<li><strong>Input ergonomics on a phone-scale device.</strong> Even with a Bluetooth keyboard and the small-screen optimisations, extended authorship of new code on a handheld device remains slower than on a conventional workstation. The configuration is well suited to monitoring, intervention, and short modifications; it is not a substitute for a primary development environment.</li>
<li><strong>Dependence on Tailscale infrastructure.</strong> New connections require reachability to the Tailscale coordination server for key exchange and peer discovery. Existing WireGuard sessions are unaffected by a coordination-server outage, but reconnection after a client restart requires coordination-server availability.</li>
<li><strong>Absence of session recording.</strong> The configuration does not record session input or output. Tools such as <code>asciinema</code>, <code>tmux-logging</code>, or <code>script(1)</code> can be added if session provenance is required.</li>
</ul>
</section>
<section id="opportunities-for-improvement" class="level2">
<h2 class="anchored" data-anchor-id="opportunities-for-improvement">Opportunities for Improvement</h2>
<p>Several extensions would strengthen the configuration without departing from its overall design:</p>
<ol type="1">
<li>Adopt end-to-end TLS using <code>tailscale cert</code>, so that an accidental mis-binding of <code>ttyd</code> to a non-Tailscale interface would not expose an unencrypted session. Appendix A documents the procedure.</li>
<li>Replace HTTP basic authentication with Tailscale Serve or Tailscale Funnel, both of which delegate authentication to the tailnet identity of the client. This removes the need for a shared password and eliminates the associated secret-management burden.</li>
<li>Deploy the same configuration on an always-on VPS reached through Tailscale. The benefit is session durability across laptop sleep or shutdown; the cost is the VPS itself and the slightly more involved administrative story.</li>
<li>Instantiate a second, read-only <code>ttyd</code> on a distinct port that runs a scoped command such as <code>htop</code> or a log-tailing command. This provides an observation-only surface suitable for short status checks without opening the full shell.</li>
<li>Curate a phone-optimised tmux configuration (<code>tmux.conf.phone</code>) with shortened status-bar labels and fewer segments, and load it conditionally when <code>ttyd</code> detects a small-screen client.</li>
<li>Encapsulate the installation in an idempotent shell script under <code>analysis/configs/</code>, suitable for provisioning identical configurations on additional machines.</li>
</ol>
</section>
</section>
<section id="conclusion" class="level1">
<h1>Conclusion</h1>
<p>A terminal served over HTTP and reached through a private WireGuard mesh constitutes a workable mobile surface for a laptop that may be closed, stationed in another room, or physically distant from the operator. The marginal cost is modest: three package installations, a single systemd user unit, and an initial configuration effort of approximately thirty minutes. The benefit is that long-running work on the laptop, including interactive agentic coding sessions that may run for several hours, remains observable and steerable without any exposure of the laptop to the public internet, without dynamic DNS infrastructure, and without reliance on a native SSH client on the mobile device.</p>
<p>The principal design insight is that a browser is an adequate terminal client when the transport-layer authentication and confidentiality problems are delegated to a component engineered specifically for that purpose. Once Tailscale and <code>ttyd</code> are in place, the same configuration generalises to a cloud-hosted VPS: only the server endpoint changes, and the tailnet membership, the <code>ttyd</code> invocation, and the client-side procedure are invariant.</p>
<p>In conclusion, four points merit emphasis. First, the single most consequential line in the configuration is <code>-i tailscale0</code> on the <code>ttyd</code> invocation: that binding decision removes the service from every non-tailnet interface and makes the rest of the security model meaningful. Second, a per-user systemd unit combined with <code>loginctl enable-linger</code> provides a logout-safe and reboot-safe service without any system-level privileges beyond the initial <code>linger</code> enablement. Third, the <code>tmux new -A -s main</code> argument inside the <code>ExecStart</code> directive is the mechanism that transforms an ephemeral browser tab into a durable working session. Fourth, the Safari ‘Add to Home Screen’ action yields a standalone web-application presentation that recovers enough screen area to make the terminal practically usable on phone-scale displays.</p>
</section>
<section id="see-also" class="level1">
<h1>See Also</h1>
<p><strong>Related posts:</strong></p>
<ul>
<li><a href="../47-templatesetup/">Setting up an AWS EC2 server via the CLI</a>: a similar setup post for a cloud workstation that pairs naturally with the configuration here.</li>
</ul>
<p><strong>Key resources:</strong></p>
<ul>
<li><a href="https://github.com/tsl0922/ttyd">ttyd project page</a></li>
<li><a href="https://tailscale.com/kb/1347/installation">Tailscale installation guide</a></li>
<li><a href="https://tailscale.com/kb/1081/magicdns">Tailscale MagicDNS</a></li>
<li><a href="https://tailscale.com/kb/1153/enabling-https">tailscale cert documentation</a></li>
<li><a href="https://man.openbsd.org/tmux">tmux manual</a></li>
<li><a href="https://wiki.archlinux.org/title/Systemd/User">systemd user units</a></li>
</ul>
<hr>
</section>
<section id="extended-glossary" class="level1">
<h1>Extended Glossary</h1>
<p>The following terms appear throughout the post. Definitions are compact and operational, prioritising the usage relevant to this configuration over full generality.</p>
<p><strong>ACL (access control list).</strong> In Tailscale, a declarative policy document that restricts which tailnet members may reach which services on which ports. Default policy is permissive; explicit ACLs tighten the model.</p>
<p><strong>Basic authentication (HTTP).</strong> The authentication scheme defined by RFC 7617 in which the client sends a header of the form <code>Authorization: Basic base64(user:password)</code> on each request. Sufficient when transport-layer confidentiality is assured by another mechanism; inadequate alone over plaintext HTTP.</p>
<p><strong>Coordination server (Tailscale).</strong> The central component of the Tailscale control plane, responsible for device authentication, public-key distribution, and ACL enforcement. It does not carry data traffic; user traffic flows peer-to-peer over WireGuard or via DERP relays.</p>
<p><strong>DERP (Designated Encrypted Relay for Packets).</strong> Tailscale-operated relay servers used when two peers cannot establish a direct WireGuard connection due to symmetric or restrictive NAT. Traffic through a DERP relay remains end-to-end encrypted; the relay cannot read it.</p>
<p><strong>Dynamic DNS.</strong> A service that updates a DNS A or AAAA record in response to changes in an endpoint’s public IP address. Historically used for home-hosted services but requires an open public port on the host. This configuration deliberately avoids it in favour of MagicDNS over Tailscale.</p>
<p><strong>ExecStart.</strong> A systemd unit directive that specifies the command line to run when the service is started. Must be an absolute path with no shell interpretation; metacharacters are passed literally to the process.</p>
<p><strong>HTTP.</strong> The Hypertext Transfer Protocol, as used here in its unencrypted form over a Tailscale-encrypted transport. The <code>ttyd</code> listener speaks HTTP, and the security model delegates confidentiality to the WireGuard layer below it.</p>
<p><strong>HTTPS.</strong> HTTP over TLS. In this configuration, HTTPS is optional (Appendix A) because the transport is already authenticated and encrypted by Tailscale; adding HTTPS provides defence in depth against an accidental mis-binding of the listener.</p>
<p><strong>Interface binding.</strong> The act of restricting a listening socket to accept connections only on a named network interface, typically by specifying an interface name or an IP address belonging to that interface. Here, <code>ttyd -i tailscale0</code> binds the listener to the Tailscale virtual interface.</p>
<p><strong>Linger (systemd).</strong> A setting, enabled per user by <code>loginctl enable-linger &lt;user&gt;</code>, that causes systemd to keep a user’s services running even when no session is active on that user’s behalf. Required for a systemd user unit to persist across logout and reboot.</p>
<p><strong>MagicDNS.</strong> A Tailscale feature that automatically resolves hostnames of the form <code>&lt;device-name&gt;.&lt;tailnet-name&gt;.ts.net</code> to tailnet IP addresses, with the resolution delivered via the tailnet VPN rather than the system resolver. Eliminates the need for dynamic DNS or manual hosts-file maintenance.</p>
<p><strong>NAT (network address translation).</strong> The technique by which a router rewrites source or destination addresses in IP packets, commonly used to share a single public IPv4 address among many private hosts. WireGuard and DERP together are responsible for establishing communication across NATs without requiring port forwarding.</p>
<p><strong>NAT traversal.</strong> The set of techniques used to establish peer-to-peer connectivity between hosts behind NAT devices, including STUN-style discovery and UDP hole punching. Tailscale performs NAT traversal automatically; DERP is the fallback when it fails.</p>
<p><strong>Overlay network.</strong> A network built logically on top of an underlying physical network, with its own address space and its own forwarding rules. The Tailscale <code>100.x.y.z/10</code> address space is an overlay over the underlying internet.</p>
<p><strong>Posit Package Manager.</strong> A binary R package mirror (formerly RStudio Package Manager) used by the zzcollab Docker image to accelerate package installation. Not directly relevant to the ttyd configuration, but present in the repository’s reproducibility infrastructure.</p>
<p><strong>Pseudo-terminal (pty).</strong> A pair of character-device endpoints in the kernel, the master and the slave, that emulates a serial line between a terminal program and a process. <code>ttyd</code> owns the master end; tmux (or another attached process) reads from and writes to the slave end.</p>
<p><strong>Quarto.</strong> The open-source scientific and technical publishing system used to render this post. Produces HTML, PDF, and other formats from a unified source document. Relevant to the blog infrastructure rather than to the ttyd setup.</p>
<p><strong>renv.</strong> The R package-version pinning and isolation tool used throughout the zzcollab research framework. Not directly used by the ttyd configuration.</p>
<p><strong>Rocker.</strong> A family of Docker images providing R and common toolchains; <code>rocker/tidyverse</code> is the base image for this blog’s rendering environment. Again, infrastructure rather than subject matter.</p>
<p><strong>Session (tmux).</strong> A named, persistent grouping of tmux windows and panes that survives detachment from any client. The <code>main</code> session used here is created by <code>tmux new -A -s main</code> and is attached by all subsequent <code>ttyd</code> connections.</p>
<p><strong>ss.</strong> A socket-statistics utility from the <code>iproute2</code> suite, preferred over the legacy <code>netstat</code> tool for listing listening sockets. The invocation <code>ss -ltnp</code> lists TCP listeners with their owning process and the bound interface.</p>
<p><strong>SSH (Secure Shell).</strong> A cryptographic network protocol for remote shell access, defined principally by RFC 4251 through RFC 4254. This configuration deliberately does not use SSH for inbound access, although SSH may still be used for other purposes on the laptop.</p>
<p><strong>systemd.</strong> The init system and service manager used by most contemporary Linux distributions. Responsible for launching, supervising, and logging long-running services.</p>
<p><strong>systemd user unit.</strong> A service definition stored under <code>~/.config/systemd/user/</code> and managed with <code>systemctl --user</code>. Runs as the user rather than as root; requires <code>loginctl enable-linger</code> to persist across logout.</p>
<p><strong>Tailnet.</strong> A private network of devices enrolled in a single Tailscale account. Members are mutually addressable; non-members are not routable to tailnet addresses.</p>
<p><strong>Tailscale.</strong> A commercial product that builds a peer-to-peer WireGuard mesh between enrolled devices, coupled with a coordination server for authentication and key distribution. The free tier is adequate for personal use.</p>
<p><strong>Tailscale Funnel.</strong> An optional Tailscale feature that exposes a selected tailnet service to the public internet through a Tailscale-operated reverse proxy, with TLS termination and public-DNS resolution. Not used in this configuration.</p>
<p><strong>Tailscale Serve.</strong> An optional Tailscale feature that provides TLS termination and routing for tailnet-internal HTTPS services, using certificates issued automatically via <code>tailscale cert</code>. A plausible future replacement for the HTTP + basic-auth configuration described here.</p>
<p><strong>TLS (Transport Layer Security).</strong> The successor to SSL, and the protocol used to encrypt HTTP into HTTPS. Optional in this configuration because Tailscale already provides confidentiality at the WireGuard layer.</p>
<p><strong>tmux.</strong> A terminal multiplexer that maintains persistent sessions containing windows and panes. Essential to this configuration because it decouples the shell lifetime from the lifetime of any individual client connection.</p>
<p><strong>ttyd.</strong> The daemon that is the subject of this post. Serves a terminal over HTTP via WebSocket, using xterm.js on the client side.</p>
<p><strong>WebSocket.</strong> A protocol (RFC 6455) that upgrades an HTTP connection to a full-duplex, persistent framed-TCP channel, used by <code>ttyd</code> to stream keystrokes and output between the browser and the pty.</p>
<p><strong>WireGuard.</strong> A modern encrypted tunnel protocol, noted for its small codebase and its use of Curve25519 and ChaCha20-Poly1305. Tailscale builds its overlay on WireGuard but adds a coordination plane, NAT traversal, and identity management on top.</p>
<p><strong>xterm.js.</strong> A JavaScript terminal emulator library that implements VT100/VT220/xterm escape-sequence handling in the browser. Rendered by <code>ttyd</code> on every client connection.</p>
<hr>
</section>
<section id="reproducibility" class="level1">
<h1>Reproducibility</h1>
<p><strong>Tested configuration:</strong></p>
<table class="caption-top table">
<thead>
<tr class="header">
<th>Component</th>
<th>Version</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>Operating system</td>
<td>Ubuntu 24.04</td>
</tr>
<tr class="even">
<td>ttyd</td>
<td>1.7.7</td>
</tr>
<tr class="odd">
<td>Tailscale</td>
<td>1.64.2</td>
</tr>
<tr class="even">
<td>tmux</td>
<td>3.4</td>
</tr>
<tr class="odd">
<td>Shell</td>
<td>zsh 5.9</td>
</tr>
<tr class="even">
<td>Client</td>
<td>iOS 17.4, Safari</td>
</tr>
<tr class="odd">
<td>Last verified</td>
<td>2026-04-15</td>
</tr>
</tbody>
</table>
<p><strong>Configuration files:</strong></p>
<ul>
<li><code>analysis/configs/ttyd.service</code>: the full systemd user unit</li>
<li><code>analysis/configs/install.sh</code>: an idempotent install script covering <code>ttyd</code>, <code>tmux</code>, and Tailscale</li>
<li><code>analysis/configs/tmux.conf.phone</code>: an optional tmux configuration tuned for small screens</li>
</ul>
<p><strong>To reproduce end-to-end:</strong></p>
<div class="sourceCode" id="cb11" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb11-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">bash</span> analysis/configs/install.sh</span>
<span id="cb11-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sudo</span> tailscale up</span>
<span id="cb11-3"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">install</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-m</span> 600 analysis/configs/ttyd.service <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb11-4">  ~/.config/systemd/user/ttyd.service</span>
<span id="cb11-5"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># edit the unit and substitute a real password</span></span>
<span id="cb11-6"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">systemctl</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--user</span> daemon-reload</span>
<span id="cb11-7"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">systemctl</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--user</span> enable <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--now</span> ttyd.service</span>
<span id="cb11-8"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sudo</span> loginctl enable-linger <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$USER</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span></code></pre></div>
<hr>
</section>
<section id="appendix-a" class="level1">
<h1>Appendix A: TLS with tailscale cert</h1>
<p>For transport encryption end-to-end (rather than relying on Tailscale’s WireGuard tunnel alone), issue a certificate for the MagicDNS name and point <code>ttyd</code> at it.</p>
<div class="sourceCode" id="cb12" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb12-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sudo</span> mkdir <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-p</span> /etc/ttyd</span>
<span id="cb12-2"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">cd</span> /etc/ttyd</span>
<span id="cb12-3"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sudo</span> tailscale cert laptop.tailnet-name.ts.net</span></code></pre></div>
<p>Adjust the unit’s <code>ExecStart</code> to include:</p>
<pre><code>--ssl \
--ssl-cert /etc/ttyd/laptop.tailnet-name.ts.net.crt \
--ssl-key  /etc/ttyd/laptop.tailnet-name.ts.net.key \</code></pre>
<p>Access the service at <code>https://laptop.tailnet-name.ts.net:7681</code>. Certificates issued by <code>tailscale cert</code> expire; schedule a weekly systemd timer to reissue and reload:</p>
<div class="sourceCode" id="cb14" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb14-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">systemctl</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--user</span> edit <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--force</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--full</span> ttyd-cert-renew.timer</span></code></pre></div>
</section>
<section id="appendix-b" class="level1">
<h1>Appendix B: Sample Work Session</h1>
<p>The following sequence illustrates a representative end-to-end session, under the assumption that the configuration above is complete and the laptop is running.</p>
<p><strong>Step 1.</strong> At the workstation, open a local terminal and attach to the shared tmux session:</p>
<div class="sourceCode" id="cb15" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb15-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">tmux</span> attach <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-t</span> main</span></code></pre></div>
<p><strong>Step 2.</strong> Initiate a long-running interactive task (for example, an agentic coding session or a computation that will run for an extended period). Detach from the tmux session with <code>Ctrl-b d</code>. The task continues to execute.</p>
<p><strong>Step 3.</strong> Depart from the workstation. The laptop remains powered and attached to a network. From any enrolled tailnet device, launch the home-screen icon for the <code>ttyd</code> URL and authenticate with the configured credentials. The browser attaches to the same tmux session, and the task’s current output appears.</p>
<p><strong>Step 4.</strong> Observe the output, issue intervention commands as required, and close the browser tab when finished. Closing the tab terminates only the WebSocket connection to <code>ttyd</code>; the tmux session and its subprocesses continue to run.</p>
<p><strong>Step 5.</strong> On return to the workstation, reattach locally with <code>tmux attach -t main</code>. The session state is continuous across the entire sequence; no work is lost at any transition.</p>
</section>
<section id="appendix-c" class="level1">
<h1>Appendix C: Teardown (Undo)</h1>
<p>Full removal in order:</p>
<ol type="1">
<li><code>systemctl --user disable --now ttyd.service</code></li>
<li><code>rm ~/.config/systemd/user/ttyd.service</code></li>
<li><code>sudo loginctl disable-linger "$USER"</code> (if no other user service needs it)</li>
<li><code>sudo apt remove --purge -y ttyd tmux</code></li>
<li><code>sudo tailscale down</code></li>
<li><code>sudo apt remove --purge -y tailscale</code></li>
<li>Remove the device from the Tailscale admin console.</li>
<li>Delete any issued certificates under <code>/etc/ttyd/</code>.</li>
</ol>
<div class="callout callout-style-default callout-warning callout-titled">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon"></i>
</div>
<div class="callout-title-container flex-fill">
Warning
</div>
</div>
<div class="callout-body-container callout-body">
<p>If the laptop is being decommissioned rather than just retired from this workflow, revoke its Tailscale auth key in the admin console. A device removed only locally can reappear on the tailnet if the machine is recovered.</p>
</div>
</div>
<hr>
</section>
<section id="contact" class="level1">
<h1>Contact</h1>
<p><em>Corrections, alternative approaches, and substantive discussion are welcome.</em></p>
<ul>
<li><strong>GitHub:</strong> <a href="https://github.com/rgt47">rgt47</a></li>
<li><strong>Twitter/X:</strong> <a href="https://twitter.com/rgt47"><span class="citation" data-cites="rgt47">@rgt47</span></a></li>
<li><strong>LinkedIn:</strong> <a href="https://linkedin.com/in/rgthomaslab">Ronald Glenn Thomas</a></li>
<li><strong>Email:</strong> <a href="https://rgtlab.org/contact">Contact form</a></li>
</ul>
<p>Feedback is particularly useful on the following topics:</p>
<ul>
<li>Corrections or improvements to any of the configuration or the associated reasoning.</li>
<li>Client-side experience on platforms other than iOS (Android, Chrome OS, iPadOS tablet use).</li>
<li>Extensions of the pattern, for example Tailscale Serve, Tailscale Funnel, mutual TLS, or certificate-based client authentication, and the operational trade-offs encountered.</li>
<li>Reports of deployments of this configuration on a cloud VPS rather than on a mobile laptop.</li>
</ul>
<hr>
<!-- ============================================================================
PRE-PUBLISH QA CHECKLIST: verify each item BEFORE setting draft: false

[ ] YAML
    [x] title, subtitle, date, categories, description filled in
    [ ] image present at media/images/hero.png
    [x] document-type: 'blog'
    [ ] draft: false

[ ] Narrative complete (no remaining `[bracketed]` placeholders)

[ ] Configuration artifacts present and tested
    [x] Full systemd unit shown verbatim
    [ ] Install script committed under analysis/configs/install.sh
    [x] Verification commands documented
    [x] Uninstall / rollback steps documented

[ ] Things to Watch Out For
    [x] At least 5 gotchas listed
    [x] Each gotcha has both a symptom AND a fix

[ ] Visual design
    [ ] 1 hero image (width=80%)
    [ ] 3 ambiance images (width=100%)
    [ ] media/images/README.md attributes every image

[ ] Content quality
    [x] Zero emoji
    [x] Zero em dashes
    [x] Single quotes preferred over double quotes in prose
    [x] Each command interpreted in plain language

[ ] Reproducibility
    [x] Version matrix table filled in
    [ ] Config files committed under analysis/configs/
    [ ] Install script runnable on a clean machine

[ ] Render verification
    [x] quarto render index.qmd produces clean HTML
    [x] Hero image displays in /blog/ listing card
    [ ] Internal links resolve
============================================================================ -->
<section id="related-posts-in-this-cluster" class="level2">
<h2 class="anchored" data-anchor-id="related-posts-in-this-cluster">Related posts in this cluster</h2>
<p>This post is part of the <em>Workflow Construct</em> series. Recommended reading order:</p>
<ol type="1">
<li>Post 15: <a href="../15-wf-construct-overview-anchor/">A Workflow Construct for the Modern Data Scientist</a></li>
<li>Post 16: <a href="../16-wf-unix-workspace-config/">Unix Command-Line Workspace Setup for Data Science</a></li>
<li>Post 17: <a href="../17-wf-multi-laptop-dotfiles-bootstrap/">Multi-Laptop macOS Bootstrap</a></li>
<li>Post 18: <a href="../18-wf-git-for-data-science/">Setting Up Git for Data Science Workflows</a></li>
<li>Post 19: <a href="../19-wf-neovim-data-science-ide/">Setting Up Neovim as a Data Science IDE</a></li>
<li>Post 20: <a href="../20-wf-r-vim-latex-workflow/">Extending the R-Vim Workflow with LaTeX</a></li>
<li>Post 21: <a href="../21-wf-modern-cli-tools/">Modern CLI Replacements for the Shell Layer</a></li>
<li>Post 22: <a href="../22-wf-claude-code-in-shell/">LLM-Augmented Editing for the Workflow Construct</a></li>
<li>Post 23: <a href="../23-wf-yabai-tiling-window-manager/">Configuring Yabai as a Tiling Window Manager</a></li>
<li><strong>Post 24: A pocket terminal with ttyd and Tailscale</strong> (this post)</li>
<li>Post 25: <a href="../25-wf-linux-mint-on-macbook/">Install Linux Mint on a MacBook Air</a></li>
</ol>


</section>
</section>

 ]]></description>
  <category>linux</category>
  <category>shell</category>
  <category>workflow-construct</category>
  <guid>https://rgtlab.org/posts/wf-pocket-terminal-ttyd-tailscale/</guid>
  <pubDate>Wed, 15 Apr 2026 07:00:00 GMT</pubDate>
  <media:content url="https://rgtlab.org/posts/wf-pocket-terminal-ttyd-tailscale/media/images/hero.png" medium="image" type="image/png" height="81" width="144"/>
</item>
<item>
  <title>Dynamic Column Names in R: Seven Approaches Compared</title>
  <dc:creator>Zenn </dc:creator>
  <link>https://rgtlab.org/posts/rl-dynamic-column-names/</link>
  <description><![CDATA[ 




<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/rl-dynamic-column-names/media/images/thumbnail.png" class="img-fluid quarto-figure quarto-figure-center figure-img" style="width:40.0%"></p>
</figure>
</div>
<section id="introduction" class="level2">
<h2 class="anchored" data-anchor-id="introduction">Introduction</h2>
<p>While reviewing some production R code recently, the following pattern appeared:</p>
<div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb1-1">data <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span></span>
<span id="cb1-2">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">mutate</span>(</span>
<span id="cb1-3">    <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">!!</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">paste0</span>(measure, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"_bl"</span>) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span><span class="er" style="color: #AD0000;
background-color: null;
font-style: inherit;">=</span> baseline_value,</span>
<span id="cb1-4">    <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">!!</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">paste0</span>(measure, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"_cng"</span>) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span><span class="er" style="color: #AD0000;
background-color: null;
font-style: inherit;">=</span> current <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> baseline_value</span>
<span id="cb1-5">  )</span></code></pre></div>
<p>The <code>!!</code> and <code>:=</code> operators were unfamiliar. After investigation, they proved to be part of R’s tidy evaluation system: a powerful but often misunderstood feature for programmatic data manipulation.</p>
<p>This exploration led to cataloguing seven different approaches to creating columns with dynamic names in R:</p>
<ol type="1">
<li><p>Base R using <code>[[</code> assignment</p></li>
<li><p>Base R using <code>do.call()</code> and <code>setNames()</code></p></li>
<li><p>The “classic” tidyverse approach with <code>!!</code> and <code>:=</code></p></li>
<li><p>The modern tidyverse glue-style syntax</p></li>
<li><p>The rlang expression splicing pattern</p></li>
<li><p>The data.table approach</p></li>
<li><p>The collapse package approach</p></li>
</ol>
</section>
<section id="the-problem" class="level2">
<h2 class="anchored" data-anchor-id="the-problem">The Problem</h2>
<p>Consider a function that calculates change scores for any cognitive measure. Given a dataframe with columns <code>rid</code> (subject ID), <code>vis</code> (visit), and a measure like <code>mmse</code>, we wish to create two new columns:</p>
<ul>
<li><code>mmse_bl</code>: the baseline value for each subject</li>
<li><code>mmse_cng</code>: the change from baseline at each visit</li>
</ul>
<p>The column names must be constructed dynamically because the function should work for any measure (<code>mmse</code>, <code>adas13</code>, <code>cdr</code>, etc.).</p>
<p>Here is our sample data:</p>
<div class="cell">
<div class="sourceCode cell-code" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb2-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">library</span>(dplyr)</span></code></pre></div>
<div class="cell-output cell-output-stderr">
<pre><code>
Attaching package: 'dplyr'</code></pre>
</div>
<div class="cell-output cell-output-stderr">
<pre><code>The following objects are masked from 'package:stats':

    filter, lag</code></pre>
</div>
<div class="cell-output cell-output-stderr">
<pre><code>The following objects are masked from 'package:base':

    intersect, setdiff, setequal, union</code></pre>
</div>
<div class="sourceCode cell-code" id="cb6" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb6-1">df <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">tibble</span>(</span>
<span id="cb6-2">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">rid =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">rep</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">c</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"001"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"002"</span>), <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">each =</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>),</span>
<span id="cb6-3">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">vis =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">rep</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">c</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"bl"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"m06"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"m12"</span>), <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>),</span>
<span id="cb6-4">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">mmse =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">c</span>(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">28</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">27</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">25</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">30</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">29</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">28</span>)</span>
<span id="cb6-5">)</span>
<span id="cb6-6"></span>
<span id="cb6-7">df</span></code></pre></div>
<div class="cell-output cell-output-stdout">
<pre><code># A tibble: 6 × 3
  rid   vis    mmse
  &lt;chr&gt; &lt;chr&gt; &lt;dbl&gt;
1 001   bl       28
2 001   m06      27
3 001   m12      25
4 002   bl       30
5 002   m06      29
6 002   m12      28</code></pre>
</div>
</div>
<hr>
</section>
<section id="approach-1-base-r-with-assignment" class="level2">
<h2 class="anchored" data-anchor-id="approach-1-base-r-with-assignment">Approach 1: Base R with <code>[[</code> Assignment</h2>
<p>The most straightforward approach uses base R’s <code>[[</code> assignment operator:</p>
<div class="cell">
<div class="sourceCode cell-code" id="cb8" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb8-1">calculate_change_base <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">function</span>(data, measure) {</span>
<span id="cb8-2">  bl_col <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">paste0</span>(measure, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"_bl"</span>)</span>
<span id="cb8-3">  cng_col <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">paste0</span>(measure, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"_cng"</span>)</span>
<span id="cb8-4"></span>
<span id="cb8-5">  result <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> data</span>
<span id="cb8-6"></span>
<span id="cb8-7">  <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> (id <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">unique</span>(data<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">$</span>rid)) {</span>
<span id="cb8-8">    idx <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> data<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">$</span>rid <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> id</span>
<span id="cb8-9">    baseline <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> data[[measure]][idx <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span> data<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">$</span>vis <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"bl"</span>][<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>]</span>
<span id="cb8-10">    result[[bl_col]][idx] <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> baseline</span>
<span id="cb8-11">    result[[cng_col]][idx] <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> data[[measure]][idx] <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> baseline</span>
<span id="cb8-12">  }</span>
<span id="cb8-13"></span>
<span id="cb8-14">  result</span>
<span id="cb8-15">}</span>
<span id="cb8-16"></span>
<span id="cb8-17"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">calculate_change_base</span>(df, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"mmse"</span>)</span></code></pre></div>
<div class="cell-output cell-output-stdout">
<pre><code># A tibble: 6 × 5
  rid   vis    mmse mmse_bl mmse_cng
  &lt;chr&gt; &lt;chr&gt; &lt;dbl&gt;   &lt;dbl&gt;    &lt;dbl&gt;
1 001   bl       28      28        0
2 001   m06      27      28       -1
3 001   m12      25      28       -3
4 002   bl       30      30        0
5 002   m06      29      30       -1
6 002   m12      28      30       -2</code></pre>
</div>
</div>
<section id="discussion" class="level3">
<h3 class="anchored" data-anchor-id="discussion">Discussion</h3>
<p><strong>How it works:</strong> The <code>[[</code> operator accepts character strings for column names, unlike <code>$</code> which requires literal names. Writing <code>df[["mmse"]]</code> is equivalent to <code>df$mmse</code>, but the former allows <code>df[[variable]]</code> where <code>variable</code> contains the column name.</p>
<p><strong>Advantages:</strong></p>
<ul>
<li>No dependencies beyond base R</li>
<li>Syntax is familiar to programmers from other languages</li>
<li>Explicit and easy to debug</li>
</ul>
<p><strong>Disadvantages:</strong></p>
<ul>
<li>Verbose, especially with grouped operations</li>
<li>Requires explicit loops for by-group calculations</li>
<li>Mutable state pattern (<code>result</code> is modified in place)</li>
<li>Does not integrate with dplyr pipelines</li>
<li>Performance degrades with many groups</li>
</ul>
<p><strong>When to use:</strong> Small scripts with no package dependencies, or when interfacing with code from other languages where this pattern is standard.</p>
<hr>
</section>
</section>
<section id="approach-2-base-r-with-do.call-and-setnames" class="level2">
<h2 class="anchored" data-anchor-id="approach-2-base-r-with-do.call-and-setnames">Approach 2: Base R with <code>do.call()</code> and <code>setNames()</code></h2>
<p>A more functional base R approach avoids explicit loops:</p>
<div class="cell">
<div class="sourceCode cell-code" id="cb10" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb10-1">calculate_change_docall <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">function</span>(data, measure) {</span>
<span id="cb10-2">  bl_col <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">paste0</span>(measure, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"_bl"</span>)</span>
<span id="cb10-3">  cng_col <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">paste0</span>(measure, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"_cng"</span>)</span>
<span id="cb10-4"></span>
<span id="cb10-5">  baselines <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">tapply</span>(</span>
<span id="cb10-6">    data[[measure]][data<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">$</span>vis <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"bl"</span>],</span>
<span id="cb10-7">    data<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">$</span>rid[data<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">$</span>vis <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"bl"</span>],</span>
<span id="cb10-8">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">FUN =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">`</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">[</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">`</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span></span>
<span id="cb10-9">  )</span>
<span id="cb10-10"></span>
<span id="cb10-11">  bl_values <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> baselines[data<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">$</span>rid]</span>
<span id="cb10-12">  cng_values <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> data[[measure]] <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> bl_values</span>
<span id="cb10-13"></span>
<span id="cb10-14">  new_cols <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">setNames</span>(</span>
<span id="cb10-15">    <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">list</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">as.numeric</span>(bl_values), <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">as.numeric</span>(cng_values)),</span>
<span id="cb10-16">    <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">c</span>(bl_col, cng_col)</span>
<span id="cb10-17">  )</span>
<span id="cb10-18"></span>
<span id="cb10-19">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">do.call</span>(cbind, <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">c</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">list</span>(data), new_cols))</span>
<span id="cb10-20">}</span>
<span id="cb10-21"></span>
<span id="cb10-22"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">calculate_change_docall</span>(df, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"mmse"</span>)</span></code></pre></div>
<div class="cell-output cell-output-stdout">
<pre><code>  rid vis mmse mmse_bl mmse_cng
1 001  bl   28      28        0
2 001 m06   27      28       -1
3 001 m12   25      28       -3
4 002  bl   30      30        0
5 002 m06   29      30       -1
6 002 m12   28      30       -2</code></pre>
</div>
</div>
<section id="discussion-1" class="level3">
<h3 class="anchored" data-anchor-id="discussion-1">Discussion</h3>
<p><strong>How it works:</strong> <code>setNames()</code> creates a named list where the names come from a character vector. <code>do.call(cbind, ...)</code> then binds these columns to the original dataframe. The <code>tapply()</code> function computes grouped summaries without explicit loops.</p>
<p><strong>Advantages:</strong></p>
<ul>
<li>No external dependencies</li>
<li>Functional style (no mutable state)</li>
<li>Vectorized operations for better performance</li>
</ul>
<p><strong>Disadvantages:</strong></p>
<ul>
<li>Dense, less readable syntax</li>
<li><code>tapply()</code> returns an array requiring careful indexing</li>
<li>Type coercion issues (note the <code>as.numeric()</code> calls)</li>
<li>Returns a <code>data.frame</code>, not a <code>tibble</code></li>
</ul>
<p><strong>When to use:</strong> Package development where minimising dependencies is critical, or performance-sensitive code where the overhead of dplyr is unacceptable.</p>
<hr>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/rl-dynamic-column-names/media/images/ambiance1.png" class="img-fluid figure-img"></p>
<figcaption>Tidy evaluation approaches.</figcaption>
</figure>
</div>
</section>
</section>
<section id="approach-3-classic-tidyverse-with-and" class="level2">
<h2 class="anchored" data-anchor-id="approach-3-classic-tidyverse-with-and">Approach 3: Classic Tidyverse with <code>!!</code> and <code>:=</code></h2>
<p>The tidyverse introduced tidy evaluation to handle dynamic column names. Two operators are central:</p>
<ul>
<li><strong><code>:=</code></strong> (the “walrus” operator): Allows the left-hand side of an assignment to be evaluated</li>
<li><strong><code>!!</code></strong> (bang-bang): Unquotes an expression, forcing immediate evaluation</li>
</ul>
<div class="cell">
<div class="sourceCode cell-code" id="cb12" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb12-1">calculate_change_classic <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">function</span>(data, measure) {</span>
<span id="cb12-2">  bl_col <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">paste0</span>(measure, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"_bl"</span>)</span>
<span id="cb12-3">  cng_col <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">paste0</span>(measure, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"_cng"</span>)</span>
<span id="cb12-4"></span>
<span id="cb12-5">  data <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span></span>
<span id="cb12-6">    <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">group_by</span>(rid) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span></span>
<span id="cb12-7">    <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">mutate</span>(</span>
<span id="cb12-8">      <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">!!</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">bl_col :=</span> .data[[measure]][vis <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"bl"</span>][<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>],</span>
<span id="cb12-9">      <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">!!</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">cng_col :=</span> .data[[measure]] <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> .data[[bl_col]]</span>
<span id="cb12-10">    ) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span></span>
<span id="cb12-11">    <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">ungroup</span>()</span>
<span id="cb12-12">}</span>
<span id="cb12-13"></span>
<span id="cb12-14"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">calculate_change_classic</span>(df, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"mmse"</span>)</span></code></pre></div>
<div class="cell-output cell-output-stdout">
<pre><code># A tibble: 6 × 5
  rid   vis    mmse mmse_bl mmse_cng
  &lt;chr&gt; &lt;chr&gt; &lt;dbl&gt;   &lt;dbl&gt;    &lt;dbl&gt;
1 001   bl       28      28        0
2 001   m06      27      28       -1
3 001   m12      25      28       -3
4 002   bl       30      30        0
5 002   m06      29      30       -1
6 002   m12      28      30       -2</code></pre>
</div>
</div>
<section id="discussion-2" class="level3">
<h3 class="anchored" data-anchor-id="discussion-2">Discussion</h3>
<p><strong>How it works:</strong> Standard <code>mutate()</code> syntax like <code>mutate(new_col = value)</code> treats <code>new_col</code> as a literal name. The <code>=</code> operator does not evaluate its left-hand side. The <code>:=</code> operator changes this behaviour. When <code>mutate(!!bl_col := value)</code> is written, the <code>!!</code> forces <code>bl_col</code> to be evaluated (yielding <code>"mmse_bl"</code>), and <code>:=</code> uses that string as the column name.</p>
<p>The <code>.data</code> pronoun explicitly references columns in the dataframe, avoiding ambiguity between dataframe columns and local variables.</p>
<p><strong>Advantages:</strong></p>
<ul>
<li>Integrates seamlessly with dplyr pipelines</li>
<li>Grouped operations are trivial</li>
<li>Declarative, readable intent (once the syntax is learnt)</li>
</ul>
<p><strong>Disadvantages:</strong></p>
<ul>
<li>Unfamiliar syntax for newcomers (<code>!!</code> and <code>:=</code> are not standard R)</li>
<li>Requires understanding tidy evaluation concepts</li>
<li>The <code>rlang</code> package must be available (loaded with dplyr)</li>
</ul>
<p><strong>When to use:</strong> Legacy tidyverse code (pre-dplyr 1.0), or when the full power of quasiquotation is needed for complex metaprogramming.</p>
<hr>
</section>
</section>
<section id="approach-4-modern-tidyverse-with-glue-syntax" class="level2">
<h2 class="anchored" data-anchor-id="approach-4-modern-tidyverse-with-glue-syntax">Approach 4: Modern Tidyverse with Glue Syntax</h2>
<p>Starting with dplyr 1.0 (June 2020), a cleaner syntax emerged using glue-style interpolation:</p>
<div class="cell">
<div class="sourceCode cell-code" id="cb14" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb14-1">calculate_change_modern <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">function</span>(data, measure) {</span>
<span id="cb14-2">  data <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span></span>
<span id="cb14-3">    <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">group_by</span>(rid) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span></span>
<span id="cb14-4">    <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">mutate</span>(</span>
<span id="cb14-5">      <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"{measure}_bl"</span> <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span><span class="er" style="color: #AD0000;
background-color: null;
font-style: inherit;">=</span> .data[[measure]][vis <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"bl"</span>][<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>],</span>
<span id="cb14-6">      <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"{measure}_cng"</span> <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span><span class="er" style="color: #AD0000;
background-color: null;
font-style: inherit;">=</span> .data[[measure]] <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> .data[[<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">paste0</span>(measure, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"_bl"</span>)]]</span>
<span id="cb14-7">    ) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span></span>
<span id="cb14-8">    <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">ungroup</span>()</span>
<span id="cb14-9">}</span>
<span id="cb14-10"></span>
<span id="cb14-11"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">calculate_change_modern</span>(df, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"mmse"</span>)</span></code></pre></div>
<div class="cell-output cell-output-stdout">
<pre><code># A tibble: 6 × 5
  rid   vis    mmse mmse_bl mmse_cng
  &lt;chr&gt; &lt;chr&gt; &lt;dbl&gt;   &lt;dbl&gt;    &lt;dbl&gt;
1 001   bl       28      28        0
2 001   m06      27      28       -1
3 001   m12      25      28       -3
4 002   bl       30      30        0
5 002   m06      29      30       -1
6 002   m12      28      30       -2</code></pre>
</div>
</div>
<section id="discussion-3" class="level3">
<h3 class="anchored" data-anchor-id="discussion-3">Discussion</h3>
<p><strong>How it works:</strong> The string <code>"{measure}_bl"</code> is interpolated like <code>glue::glue()</code>, substituting the value of <code>measure</code> directly. This occurs at the level of the column name, with <code>:=</code> still required to enable left-hand side evaluation.</p>
<p><strong>Advantages:</strong></p>
<ul>
<li>Most readable of all tidyverse approaches</li>
<li>Reduces cognitive load (no need to remember <code>!!</code> semantics)</li>
<li>Consistent with glue syntax used elsewhere in the tidyverse</li>
</ul>
<p><strong>Disadvantages:</strong></p>
<ul>
<li>Requires dplyr &gt;= 1.0</li>
<li>Still requires <code>:=</code> (cannot use <code>=</code>)</li>
<li>Less flexible than <code>!!</code> for complex quasiquotation</li>
</ul>
<p><strong>When to use:</strong> New tidyverse code. This is the current recommended approach for dynamic column names in dplyr.</p>
<hr>
</section>
</section>
<section id="approach-5-rlang-expression-splicing" class="level2">
<h2 class="anchored" data-anchor-id="approach-5-rlang-expression-splicing">Approach 5: rlang Expression Splicing</h2>
<p>For more explicit metaprogramming, rlang provides <code>expr()</code> for building expressions and <code>!!!</code> for splicing them into function calls:</p>
<div class="cell">
<div class="sourceCode cell-code" id="cb16" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb16-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">library</span>(rlang)</span>
<span id="cb16-2"></span>
<span id="cb16-3">calculate_change_rlang <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">function</span>(data, measure) {</span>
<span id="cb16-4">  bl_col <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">paste0</span>(measure, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"_bl"</span>)</span>
<span id="cb16-5">  cng_col <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">paste0</span>(measure, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"_cng"</span>)</span>
<span id="cb16-6"></span>
<span id="cb16-7">  exprs <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">list</span>(</span>
<span id="cb16-8">    <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">expr</span>(.data[[<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">!!</span>measure]][vis <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"bl"</span>][<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>]),</span>
<span id="cb16-9">    <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">expr</span>(.data[[<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">!!</span>measure]] <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> .data[[<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">!!</span>bl_col]])</span>
<span id="cb16-10">  )</span>
<span id="cb16-11">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">names</span>(exprs) <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">c</span>(bl_col, cng_col)</span>
<span id="cb16-12"></span>
<span id="cb16-13">  data <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span></span>
<span id="cb16-14">    <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">group_by</span>(rid) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span></span>
<span id="cb16-15">    <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">mutate</span>(<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">!!!</span>exprs) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span></span>
<span id="cb16-16">    <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">ungroup</span>()</span>
<span id="cb16-17">}</span>
<span id="cb16-18"></span>
<span id="cb16-19"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">calculate_change_rlang</span>(df, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"mmse"</span>)</span></code></pre></div>
<div class="cell-output cell-output-stdout">
<pre><code># A tibble: 6 × 5
  rid   vis    mmse mmse_bl mmse_cng
  &lt;chr&gt; &lt;chr&gt; &lt;dbl&gt;   &lt;dbl&gt;    &lt;dbl&gt;
1 001   bl       28      28        0
2 001   m06      27      28       -1
3 001   m12      25      28       -3
4 002   bl       30      30        0
5 002   m06      29      30       -1
6 002   m12      28      30       -2</code></pre>
</div>
</div>
<section id="discussion-4" class="level3">
<h3 class="anchored" data-anchor-id="discussion-4">Discussion</h3>
<p><strong>How it works:</strong> <code>expr()</code> captures an expression without evaluating it, while <code>!!</code> inside <code>expr()</code> forces evaluation of specific parts. The result is a list of named expressions. The <code>!!!</code> (splice) operator unpacks this list into <code>mutate()</code>, equivalent to writing each expression as a separate argument.</p>
<p><strong>Advantages:</strong></p>
<ul>
<li>Build expressions programmatically before evaluation</li>
<li>Useful when the number of columns is dynamic</li>
<li>Expressions can be inspected, modified, or logged before use</li>
<li>Clear separation between expression construction and evaluation</li>
</ul>
<p><strong>Disadvantages:</strong></p>
<ul>
<li>More verbose for simple cases</li>
<li>Requires understanding quasiquotation deeply</li>
<li>Overkill for straightforward dynamic naming</li>
</ul>
<p><strong>When to use:</strong> Complex metaprogramming where expressions must be built dynamically, such as generating an unknown number of columns based on input data, or when inspecting or logging expressions before evaluation is desirable.</p>
<hr>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/rl-dynamic-column-names/media/images/ambiance2.png" class="img-fluid figure-img"></p>
<figcaption>High-performance alternatives.</figcaption>
</figure>
</div>
</section>
</section>
<section id="approach-6-data.table" class="level2">
<h2 class="anchored" data-anchor-id="approach-6-data.table">Approach 6: data.table</h2>
<p>The data.table package has its own <code>:=</code> operator that predates tidyverse adoption:</p>
<div class="cell">
<div class="sourceCode cell-code" id="cb18" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb18-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">library</span>(data.table)</span></code></pre></div>
<div class="cell-output cell-output-stderr">
<pre><code>
Attaching package: 'data.table'</code></pre>
</div>
<div class="cell-output cell-output-stderr">
<pre><code>The following object is masked from 'package:rlang':

    :=</code></pre>
</div>
<div class="cell-output cell-output-stderr">
<pre><code>The following objects are masked from 'package:dplyr':

    between, first, last</code></pre>
</div>
<div class="sourceCode cell-code" id="cb22" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb22-1">calculate_change_dt <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">function</span>(data, measure) {</span>
<span id="cb22-2">  bl_col <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">paste0</span>(measure, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"_bl"</span>)</span>
<span id="cb22-3">  cng_col <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">paste0</span>(measure, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"_cng"</span>)</span>
<span id="cb22-4"></span>
<span id="cb22-5">  dt <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">as.data.table</span>(data)</span>
<span id="cb22-6"></span>
<span id="cb22-7">  dt[, (bl_col) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span><span class="er" style="color: #AD0000;
background-color: null;
font-style: inherit;">=</span> .SD[[measure]][vis <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"bl"</span>][<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>], by <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">=</span> rid]</span>
<span id="cb22-8">  dt[, (cng_col) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span><span class="er" style="color: #AD0000;
background-color: null;
font-style: inherit;">=</span> .SD[[measure]] <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> .SD[[bl_col]], by <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">=</span> rid]</span>
<span id="cb22-9"></span>
<span id="cb22-10">  dt[]</span>
<span id="cb22-11">}</span>
<span id="cb22-12"></span>
<span id="cb22-13"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">calculate_change_dt</span>(df, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"mmse"</span>)</span></code></pre></div>
<div class="cell-output cell-output-stdout">
<pre><code>      rid    vis  mmse mmse_bl mmse_cng
   &lt;char&gt; &lt;char&gt; &lt;num&gt;   &lt;num&gt;    &lt;num&gt;
1:    001     bl    28      28        0
2:    001    m06    27      28       -1
3:    001    m12    25      28       -3
4:    002     bl    30      30        0
5:    002    m06    29      30       -1
6:    002    m12    28      30       -2</code></pre>
</div>
</div>
<section id="discussion-5" class="level3">
<h3 class="anchored" data-anchor-id="discussion-5">Discussion</h3>
<p><strong>How it works:</strong> In data.table, wrapping a variable in parentheses on the left-hand side of <code>:=</code> forces evaluation: <code>(bl_col)</code> evaluates to <code>"mmse_bl"</code>. Without parentheses, <code>bl_col := value</code> would create a column literally named “bl_col”. The <code>.SD</code> pronoun (Subset of Data) references the current group’s data.</p>
<p><strong>Advantages:</strong></p>
<ul>
<li>Extremely fast, especially for large datasets</li>
<li>Memory efficient (modifies in place by reference)</li>
<li>Mature, battle-tested codebase</li>
<li>The <code>by</code> argument handles grouping concisely</li>
</ul>
<p><strong>Disadvantages:</strong></p>
<ul>
<li>Different mental model from base R and tidyverse</li>
<li>Modify-by-reference semantics can cause subtle bugs</li>
<li>Less readable for those unfamiliar with data.table idioms</li>
<li>Parentheses syntax <code>(col)</code> is easy to forget</li>
</ul>
<p><strong>When to use:</strong> Performance-critical applications, very large datasets (millions of rows), or codebases already using data.table.</p>
<hr>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/rl-dynamic-column-names/media/images/placeholder-coffee-05.jpg" class="img-fluid figure-img"></p>
<figcaption>Minimalist high-speed data tools.</figcaption>
</figure>
</div>
</section>
</section>
<section id="approach-7-collapse" class="level2">
<h2 class="anchored" data-anchor-id="approach-7-collapse">Approach 7: collapse</h2>
<p>The collapse package offers high-performance data manipulation with its own idioms:</p>
<div class="cell">
<div class="sourceCode cell-code" id="cb24" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb24-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">library</span>(collapse)</span></code></pre></div>
<div class="cell-output cell-output-stderr">
<pre><code>collapse 2.1.6, see ?`collapse-package` or ?`collapse-documentation`</code></pre>
</div>
<div class="cell-output cell-output-stderr">
<pre><code>
Attaching package: 'collapse'</code></pre>
</div>
<div class="cell-output cell-output-stderr">
<pre><code>The following object is masked from 'package:data.table':

    fdroplevels</code></pre>
</div>
<div class="cell-output cell-output-stderr">
<pre><code>The following object is masked from 'package:stats':

    D</code></pre>
</div>
<div class="sourceCode cell-code" id="cb29" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb29-1">calculate_change_collapse <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">function</span>(data, measure) {</span>
<span id="cb29-2">  bl_col <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">paste0</span>(measure, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"_bl"</span>)</span>
<span id="cb29-3">  cng_col <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">paste0</span>(measure, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"_cng"</span>)</span>
<span id="cb29-4"></span>
<span id="cb29-5">  bl_data <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">fsubset</span>(data, vis <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"bl"</span>)</span>
<span id="cb29-6">  bl_values <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">get_vars</span>(bl_data, <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">c</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"rid"</span>, measure))</span>
<span id="cb29-7">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">names</span>(bl_values)[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>] <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> bl_col</span>
<span id="cb29-8"></span>
<span id="cb29-9">  result <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">join</span>(data, bl_values, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">on =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"rid"</span>, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">how =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"left"</span>)</span>
<span id="cb29-10">  result[[cng_col]] <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> result[[measure]] <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> result[[bl_col]]</span>
<span id="cb29-11"></span>
<span id="cb29-12">  result</span>
<span id="cb29-13">}</span>
<span id="cb29-14"></span>
<span id="cb29-15"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">calculate_change_collapse</span>(df, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"mmse"</span>)</span></code></pre></div>
<div class="cell-output cell-output-stdout">
<pre><code>left join: data[rid] 6/6 (100%) &lt;3:1st&gt; bl_values[rid] 2/2 (100%)</code></pre>
</div>
<div class="cell-output cell-output-stdout">
<pre><code># A tibble: 6 × 5
  rid   vis    mmse mmse_bl mmse_cng
  &lt;chr&gt; &lt;chr&gt; &lt;dbl&gt;   &lt;dbl&gt;    &lt;dbl&gt;
1 001   bl       28      28        0
2 001   m06      27      28       -1
3 001   m12      25      28       -3
4 002   bl       30      30        0
5 002   m06      29      30       -1
6 002   m12      28      30       -2</code></pre>
</div>
</div>
<section id="discussion-6" class="level3">
<h3 class="anchored" data-anchor-id="discussion-6">Discussion</h3>
<p><strong>How it works:</strong> collapse provides fast versions of common operations (<code>fsubset</code>, <code>get_vars</code>, <code>join</code>, etc.). Unlike dplyr, collapse does not use tidy evaluation, so dynamic column access requires base R syntax like <code>get_vars()</code> with character vectors and direct <code>[[</code> assignment. The approach above uses a join strategy rather than grouped mutation.</p>
<p><strong>Advantages:</strong></p>
<ul>
<li>Extremely fast (often faster than data.table for certain operations)</li>
<li>Low memory footprint</li>
<li>Works with both data.frames and data.tables</li>
<li>Good for time series and panel data</li>
</ul>
<p><strong>Disadvantages:</strong></p>
<ul>
<li>Smaller user community than tidyverse or data.table</li>
<li>Inconsistent tidy evaluation support across functions</li>
<li>Often requires mixing collapse functions with base R syntax</li>
<li>Less comprehensive documentation</li>
</ul>
<p><strong>When to use:</strong> Performance-critical code, especially time series or panel data analysis. Also useful when speed is needed but data.table’s reference semantics are to be avoided.</p>
<hr>
</section>
</section>
<section id="comparison-summary" class="level2">
<h2 class="anchored" data-anchor-id="comparison-summary">Comparison Summary</h2>
<table class="caption-top table">
<colgroup>
<col style="width: 16%">
<col style="width: 23%">
<col style="width: 21%">
<col style="width: 21%">
<col style="width: 16%">
</colgroup>
<thead>
<tr class="header">
<th>Approach</th>
<th>Dependencies</th>
<th>Readability</th>
<th>Performance</th>
<th>Best For</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>Base R <code>[[</code></td>
<td>None</td>
<td>Moderate</td>
<td>Low</td>
<td>No-dependency scripts</td>
</tr>
<tr class="even">
<td>Base R <code>do.call</code></td>
<td>None</td>
<td>Low</td>
<td>Moderate</td>
<td>Package development</td>
</tr>
<tr class="odd">
<td>Classic <code>!!</code> <code>:=</code></td>
<td>dplyr</td>
<td>Low</td>
<td>Moderate</td>
<td>Legacy tidyverse code</td>
</tr>
<tr class="even">
<td>Modern glue</td>
<td>dplyr &gt;= 1.0</td>
<td>High</td>
<td>Moderate</td>
<td>New tidyverse code</td>
</tr>
<tr class="odd">
<td>rlang <code>!!!</code> splice</td>
<td>rlang</td>
<td>Moderate</td>
<td>Moderate</td>
<td>Complex metaprogramming</td>
</tr>
<tr class="even">
<td>data.table</td>
<td>data.table</td>
<td>Moderate</td>
<td>High</td>
<td>Large datasets</td>
</tr>
<tr class="odd">
<td>collapse</td>
<td>collapse</td>
<td>Moderate</td>
<td>Very High</td>
<td>Performance-critical work</td>
</tr>
</tbody>
</table>
</section>
<section id="practical-example-multiple-measures" class="level2">
<h2 class="anchored" data-anchor-id="practical-example-multiple-measures">Practical Example: Multiple Measures</h2>
<p>The real power emerges when processing multiple measures:</p>
<div class="cell">
<div class="sourceCode cell-code" id="cb32" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb32-1">df_multi <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">tibble</span>(</span>
<span id="cb32-2">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">rid =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">rep</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">c</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"001"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"002"</span>), <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">each =</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>),</span>
<span id="cb32-3">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">vis =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">rep</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">c</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"bl"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"m06"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"m12"</span>), <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>),</span>
<span id="cb32-4">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">mmse =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">c</span>(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">28</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">27</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">25</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">30</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">29</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">28</span>),</span>
<span id="cb32-5">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">adas =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">c</span>(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">12</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">14</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">18</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">10</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">12</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">15</span>),</span>
<span id="cb32-6">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">cdr =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">c</span>(<span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.5</span>, <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.5</span>, <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">1.0</span>, <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.5</span>, <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.5</span>, <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.5</span>)</span>
<span id="cb32-7">)</span>
<span id="cb32-8"></span>
<span id="cb32-9">calculate_all_changes <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">function</span>(data, measures) {</span>
<span id="cb32-10">  result <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> data <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">group_by</span>(rid)</span>
<span id="cb32-11"></span>
<span id="cb32-12">  <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> (measure <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> measures) {</span>
<span id="cb32-13">    result <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> result <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span></span>
<span id="cb32-14">      <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">mutate</span>(</span>
<span id="cb32-15">        <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"{measure}_bl"</span> <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span><span class="er" style="color: #AD0000;
background-color: null;
font-style: inherit;">=</span> .data[[measure]][vis <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"bl"</span>][<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>],</span>
<span id="cb32-16">        <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"{measure}_cng"</span> <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span><span class="er" style="color: #AD0000;
background-color: null;
font-style: inherit;">=</span> .data[[measure]] <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> .data[[<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">paste0</span>(measure, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"_bl"</span>)]]</span>
<span id="cb32-17">      )</span>
<span id="cb32-18">  }</span>
<span id="cb32-19"></span>
<span id="cb32-20">  result <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">ungroup</span>()</span>
<span id="cb32-21">}</span>
<span id="cb32-22"></span>
<span id="cb32-23"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">calculate_all_changes</span>(df_multi, <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">c</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"mmse"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"adas"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"cdr"</span>))</span></code></pre></div>
<div class="cell-output cell-output-stdout">
<pre><code># A tibble: 6 × 11
  rid   vis    mmse  adas   cdr mmse_bl mmse_cng adas_bl adas_cng cdr_bl cdr_cng
  &lt;chr&gt; &lt;chr&gt; &lt;dbl&gt; &lt;dbl&gt; &lt;dbl&gt;   &lt;dbl&gt;    &lt;dbl&gt;   &lt;dbl&gt;    &lt;dbl&gt;  &lt;dbl&gt;   &lt;dbl&gt;
1 001   bl       28    12   0.5      28        0      12        0    0.5     0  
2 001   m06      27    14   0.5      28       -1      12        2    0.5     0  
3 001   m12      25    18   1        28       -3      12        6    0.5     0.5
4 002   bl       30    10   0.5      30        0      10        0    0.5     0  
5 002   m06      29    12   0.5      30       -1      10        2    0.5     0  
6 002   m12      28    15   0.5      30       -2      10        5    0.5     0  </code></pre>
</div>
</div>
</section>
<section id="common-pitfalls" class="level2">
<h2 class="anchored" data-anchor-id="common-pitfalls">Common Pitfalls</h2>
<section id="forgetting-on-the-lhs-tidyverse" class="level3">
<h3 class="anchored" data-anchor-id="forgetting-on-the-lhs-tidyverse">1. Forgetting := on the LHS (tidyverse)</h3>
<div class="sourceCode" id="cb34" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb34-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Wrong: creates column literally named "{measure}_bl"</span></span>
<span id="cb34-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">mutate</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"{measure}_bl"</span> <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">=</span> value)</span>
<span id="cb34-3"></span>
<span id="cb34-4"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Correct: evaluates the string</span></span>
<span id="cb34-5"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">mutate</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"{measure}_bl"</span> <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span><span class="er" style="color: #AD0000;
background-color: null;
font-style: inherit;">=</span> value)</span></code></pre></div>
</section>
<section id="forgetting-parentheses-data.table" class="level3">
<h3 class="anchored" data-anchor-id="forgetting-parentheses-data.table">2. Forgetting parentheses (data.table)</h3>
<div class="sourceCode" id="cb35" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb35-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Wrong: creates column named "bl_col"</span></span>
<span id="cb35-2">dt[, bl_col <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span><span class="er" style="color: #AD0000;
background-color: null;
font-style: inherit;">=</span> value]</span>
<span id="cb35-3"></span>
<span id="cb35-4"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Correct: evaluates bl_col to get "mmse_bl"</span></span>
<span id="cb35-5">dt[, (bl_col) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span><span class="er" style="color: #AD0000;
background-color: null;
font-style: inherit;">=</span> value]</span></code></pre></div>
</section>
<section id="confusing-column-references" class="level3">
<h3 class="anchored" data-anchor-id="confusing-column-references">3. Confusing column references</h3>
<div class="sourceCode" id="cb36" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb36-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Ambiguous: is 'measure' a column or variable?</span></span>
<span id="cb36-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">mutate</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"{measure}_bl"</span> <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span><span class="er" style="color: #AD0000;
background-color: null;
font-style: inherit;">=</span> measure[vis <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"bl"</span>][<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>])</span>
<span id="cb36-3"></span>
<span id="cb36-4"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Clear: .data[[measure]] references the column</span></span>
<span id="cb36-5"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">mutate</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"{measure}_bl"</span> <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span><span class="er" style="color: #AD0000;
background-color: null;
font-style: inherit;">=</span> .data[[measure]][vis <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"bl"</span>][<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>])</span></code></pre></div>
</section>
<section id="reference-semantics-in-data.table" class="level3">
<h3 class="anchored" data-anchor-id="reference-semantics-in-data.table">4. Reference semantics in data.table</h3>
<div class="sourceCode" id="cb37" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb37-1">dt <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">as.data.table</span>(df)</span>
<span id="cb37-2">dt2 <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> dt  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># This is NOT a copy!</span></span>
<span id="cb37-3">dt2[, new_col <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span><span class="er" style="color: #AD0000;
background-color: null;
font-style: inherit;">=</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>]  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Modifies both dt and dt2</span></span>
<span id="cb37-4"></span>
<span id="cb37-5"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Safe copy:</span></span>
<span id="cb37-6">dt2 <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">copy</span>(dt)</span></code></pre></div>
</section>
</section>
<section id="what-did-we-learn" class="level1">
<h1>What Did We Learn?</h1>
<section id="lessons-learnt" class="level2">
<h2 class="anchored" data-anchor-id="lessons-learnt">Lessons Learnt</h2>
<ol type="1">
<li><strong><code>:=</code> enables dynamic LHS</strong>: In both tidyverse and data.table, this operator allows the left side of an assignment to be evaluated</li>
<li><strong><code>!!</code> is the unquote operator</strong>: Forces immediate evaluation of an expression in tidy eval contexts</li>
<li><strong>Glue syntax is cleaner</strong>: <code>"{var}_suffix"</code> is more readable than <code>!!paste0(var, "_suffix")</code></li>
<li><strong>Parentheses matter in data.table</strong>: <code>(col)</code> evaluates, <code>col</code> is literal</li>
<li><strong><code>.data</code> removes ambiguity</strong>: Always use <code>.data[[col]]</code> for column references in functions</li>
<li><strong>Choose based on context</strong>: Readability (glue) vs performance (data.table) vs no dependencies (base R)</li>
</ol>
</section>
<section id="related-posts-in-this-cluster" class="level2">
<h2 class="anchored" data-anchor-id="related-posts-in-this-cluster">Related posts in this cluster</h2>
<p>This post is part of the <em>R Language and Metaprogramming</em> series. Recommended reading order:</p>
<ol type="1">
<li>Post 62: <a href="../62-rl-pipe-equivalence-myth/">The Pipe Equivalence Myth</a></li>
<li><strong>Post 63: Dynamic Column Names: Seven Approaches Compared</strong> (this post)</li>
</ol>
</section>
<section id="reproducibility" class="level2">
<h2 class="anchored" data-anchor-id="reproducibility">Reproducibility</h2>
<div class="cell">
<div class="sourceCode cell-code" id="cb38" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb38-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sessionInfo</span>()</span></code></pre></div>
<div class="cell-output cell-output-stdout">
<pre><code>R version 4.5.3 (2026-03-11)
Platform: aarch64-apple-darwin25.3.0
Running under: macOS Tahoe 26.5

Matrix products: default
BLAS:   /opt/homebrew/Cellar/openblas/0.3.32/lib/libopenblasp-r0.3.32.dylib 
LAPACK: /opt/homebrew/Cellar/r/4.5.3/lib/R/lib/libRlapack.dylib;  LAPACK version 3.12.1

locale:
[1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8

time zone: America/Los_Angeles
tzcode source: internal

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

other attached packages:
[1] collapse_2.1.6    data.table_1.17.8 rlang_1.2.0       dplyr_1.2.1      

loaded via a namespace (and not attached):
 [1] digest_0.6.37     utf8_1.2.6        R6_2.6.1          fastmap_1.2.0    
 [5] tidyselect_1.2.1  xfun_0.56         magrittr_2.0.5    glue_1.8.0       
 [9] tibble_3.3.1      knitr_1.50        parallel_4.5.3    pkgconfig_2.0.3  
[13] htmltools_0.5.8.1 generics_0.1.4    rmarkdown_2.29    lifecycle_1.0.5  
[17] cli_3.6.6         vctrs_0.7.3       compiler_4.5.3    tools_4.5.3      
[21] pillar_1.11.1     evaluate_1.0.5    Rcpp_1.1.0        yaml_2.3.10      
[25] jsonlite_2.0.0    htmlwidgets_1.6.4</code></pre>
</div>
</div>
<hr>
</section>
</section>
<section id="lets-connect" class="level1">
<h1>Let’s Connect</h1>
<p><em>Have questions, suggestions, or spot an error? Let me know.</em></p>
<ul>
<li><strong>GitHub:</strong> <a href="https://github.com/rgt47">rgt47</a></li>
<li><strong>Twitter/X:</strong> <a href="https://twitter.com/rgt47"><span class="citation" data-cites="rgt47">@rgt47</span></a></li>
<li><strong>LinkedIn:</strong> <a href="https://linkedin.com/in/rgthomaslab">Ronald Glenn Thomas</a></li>
<li><strong>Email:</strong> <a href="https://rgtlab.org/contact">Contact form</a></li>
</ul>
<p>I would enjoy hearing from readers who:</p>
<ul>
<li>Spot an error or a better approach to any of the code in this post.</li>
<li>Have suggestions for topics to cover.</li>
<li>Want to discuss R programming, data science, or reproducible research.</li>
<li>Have questions about anything in this tutorial.</li>
<li>Simply want to say hello and connect.</li>
</ul>


</section>

 ]]></description>
  <category>r</category>
  <category>metaprogramming</category>
  <category>r-language</category>
  <guid>https://rgtlab.org/posts/rl-dynamic-column-names/</guid>
  <pubDate>Mon, 16 Feb 2026 08:00:00 GMT</pubDate>
  <media:content url="https://rgtlab.org/posts/rl-dynamic-column-names/media/images/hero.png" medium="image" type="image/png" height="80" width="144"/>
</item>
<item>
  <title>Updating an R Package: A Complete Development Workflow</title>
  <dc:creator>Ronald G. Thomas</dc:creator>
  <link>https://rgtlab.org/posts/rp-package-update-workflow/</link>
  <description><![CDATA[ 




<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/rp-package-update-workflow/media/images/hero.png" class="img-fluid figure-img" style="width:80.0%"></p>
<figcaption>Terminal showing an R package build in progress</figcaption>
</figure>
</div>
<p><em>Setting up a reproducible R package development workflow.</em></p>
<section id="introduction" class="level1">
<h1>Introduction</h1>
<p>I did not really understand how much discipline goes into modifying an R package until I tried to push a change to one of my own projects and watched CI fail on three platforms simultaneously. It turns out that editing an R file is the easy part; the surrounding workflow of branching, documenting, testing, building, and integrating with GitHub Actions is what separates a quick fix from a reliable contribution.</p>
<p>The scenario is straightforward: an existing R package is hosted on GitHub and a feature must be added, a bug fixed, or error handling improved. The change itself might be small, but the process of getting it safely merged involves a surprising number of steps.</p>
<p>This post documents the complete workflow I learned while modifying my own package, <code>zzdataframe2graphic</code>. Every step from creating a feature branch to cleaning up after a merge is covered, with the goal of making the process repeatable for future changes.</p>
<p>More formally, this post documents the R-packages layer (Layer 13) of the Workflow Construct described in <a href="../../posts/52-workflow-construct/">post 52</a>, specifically the maintenance loop that keeps an existing package healthy across a working biostatistician’s many simultaneous projects. The construct’s R-packages layer holds a small set of author-maintained <code>zz*</code> packages; this post is the how-to-update-one companion to <a href="../../posts/35-simpleS3/">post 35</a>, which is the how-to-author-one entry point.</p>
<section id="motivations" class="level2">
<h2 class="anchored" data-anchor-id="motivations">Motivations</h2>
<ul>
<li>I kept making ad hoc changes to my R packages without a systematic process, and occasionally broke things on other platforms.</li>
<li>I wanted a clear mental model for the branch-edit-test-merge cycle so I could contribute to open source R projects with confidence.</li>
<li>I had GitHub Actions YAML files in my repositories but did not fully understand what they did or how to set them up from scratch.</li>
<li>I needed a reference I could return to each time I make a package change, rather than re-learning the steps every time.</li>
<li>I wanted to understand how <code>devtools</code>, <code>testthat</code>, and <code>roxygen2</code> fit together in a single coherent workflow.</li>
</ul>
</section>
<section id="objectives" class="level2">
<h2 class="anchored" data-anchor-id="objectives">Objectives</h2>
<ol type="1">
<li>Create a feature branch, make code changes, and update roxygen2 documentation in an R package.</li>
<li>Write and run unit tests locally with <code>testthat</code> and build the package with <code>devtools</code>.</li>
<li>Set up three GitHub Actions workflows (R-CMD-check, test coverage, pkgdown) and understand what each one does.</li>
<li>Execute the full pull request lifecycle: push, review, address CI failures, merge, and clean up.</li>
</ol>
<p>I am documenting my learning process here. Errors and better approaches are welcome; see the contact section.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/rp-package-update-workflow/media/images/ambiance1.png" class="img-fluid figure-img" style="width:30.0%"></p>
<figcaption>Settling in for a focused development session.</figcaption>
</figure>
</div>
<p><em>Settling in for a focused development session.</em></p>
</section>
</section>
<section id="prerequisites-and-setup" class="level1">
<h1>Prerequisites and Setup</h1>
<p>This workflow assumes familiarity with R, basic Git commands, and a GitHub account. The following tools should be installed:</p>
<div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb1-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">install.packages</span>(</span>
<span id="cb1-2">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">c</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"devtools"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"testthat"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"roxygen2"</span>,</span>
<span id="cb1-3">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"rcmdcheck"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"covr"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"pkgdown"</span>)</span>
<span id="cb1-4">)</span></code></pre></div>
<p>Git should also be configured on the system with SSH or HTTPS access to GitHub repositories. The examples below use a package called <code>zzdataframe2graphic</code>, but the process applies to any R package hosted on GitHub.</p>
</section>
<section id="what-is-an-r-package-development-workflow" class="level1">
<h1>What is an R Package Development Workflow?</h1>
<p>An R package development workflow is a structured sequence of steps that takes a code change from an idea to a tested, documented, and merged contribution. Think of it as a checklist for responsible software modification: rather than editing a file and hoping for the best, one creates an isolated branch, updates documentation automatically, runs tests on multiple platforms, and merges only when everything passes.</p>
<p>The key tools in this workflow are <code>devtools</code> (which orchestrates building, testing, and checking), <code>roxygen2</code> (which generates documentation from inline comments), <code>testthat</code> (which provides a structured testing framework), and GitHub Actions (which runs automated checks on every push). Together, they form a safety net that catches problems before they reach production.</p>
</section>
<section id="getting-started-creating-a-feature-branch" class="level1">
<h1>Getting Started: Creating a Feature Branch</h1>
<p>Every change begins with a new branch. This keeps the main branch stable during development.</p>
<div class="sourceCode" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb2-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> checkout main</span>
<span id="cb2-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> pull origin main</span>
<span id="cb2-3"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> checkout <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-b</span> feature-name</span></code></pre></div>
<p>The first two commands ensure work starts from the latest version of the codebase. The third creates a new branch and switches to it. All subsequent changes happen on this branch, isolated from <code>main</code> until the work is ready to merge.</p>
<section id="editing-the-r-code" class="level2">
<h2 class="anchored" data-anchor-id="editing-the-r-code">Editing the R Code</h2>
<p>With the branch created, navigate to the relevant source file and make your changes. For an R package, the source files live in the <code>R/</code> directory:</p>
<ol type="1">
<li>Open the target file (e.g., <code>R/zzdataframe2graphic.R</code>).</li>
<li>Make the code changes.</li>
<li>Update or add roxygen2 comments above any modified or new functions.</li>
<li>Save the file.</li>
</ol>
<p>After editing, regenerate the documentation:</p>
<div class="sourceCode" id="cb3" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb3-1">devtools<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">document</span>()</span></code></pre></div>
<p>This command reads the roxygen2 comments and updates the <code>NAMESPACE</code> file and the <code>man/</code> directory. It is important to run this every time function signatures change, parameters are added, or exported functions are modified.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/rp-package-update-workflow/media/images/ambiance2.png" class="img-fluid figure-img"></p>
<figcaption>Code review on a laptop screen</figcaption>
</figure>
</div>
<p><em>Taking a step back to review the changes before testing.</em></p>
</section>
<section id="writing-and-running-tests" class="level2">
<h2 class="anchored" data-anchor-id="writing-and-running-tests">Writing and Running Tests</h2>
<p>Every code change should be accompanied by tests. The <code>testthat</code> framework provides a clean structure for this:</p>
<div class="sourceCode" id="cb4" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb4-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">test_that</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"new feature works as expected"</span>, {</span>
<span id="cb4-2">  result <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">your_function</span>(test_input)</span>
<span id="cb4-3">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">expect_equal</span>(result, expected_output)</span>
<span id="cb4-4">})</span></code></pre></div>
<p>Add test cases to the appropriate file in <code>tests/testthat/</code>. Then run the full test suite locally:</p>
<div class="sourceCode" id="cb5" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb5-1">devtools<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">test</span>()</span></code></pre></div>
<p>Fix any failures before proceeding. If the change is substantial, consider adding multiple test cases covering edge cases and expected error conditions.</p>
</section>
<section id="building-and-checking-the-package" class="level2">
<h2 class="anchored" data-anchor-id="building-and-checking-the-package">Building and Checking the Package</h2>
<p>Once tests pass, build and check the package:</p>
<div class="sourceCode" id="cb6" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb6-1">devtools<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">build</span>()</span>
<span id="cb6-2">devtools<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">check</span>()</span></code></pre></div>
<p>The <code>check()</code> function runs <code>R CMD check</code>, which is the standard quality gate for R packages. It verifies that documentation is complete, examples run without error, tests pass, and the package can be installed cleanly.</p>
<p>For more rigorous checking, run against additional platforms:</p>
<div class="sourceCode" id="cb7" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb7-1">devtools<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">check_win_devel</span>()</span>
<span id="cb7-2">devtools<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">check_mac_release</span>()</span>
<span id="cb7-3">rcmdcheck<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">rcmdcheck</span>(<span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">args =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">c</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"--as-cran"</span>))</span></code></pre></div>
<p>The <code>--as-cran</code> flag applies the stricter checks that CRAN uses when evaluating package submissions.</p>
</section>
<section id="updating-the-description-file" class="level2">
<h2 class="anchored" data-anchor-id="updating-the-description-file">Updating the DESCRIPTION File</h2>
<p>Before committing, update the package metadata:</p>
<ol type="1">
<li>Increment the version number (e.g., from 0.2.0 to 0.2.1).</li>
<li>Update the <code>Date</code> field.</li>
<li>Add any new dependencies to <code>Imports</code> or <code>Suggests</code>.</li>
</ol>
</section>
</section>
<section id="the-git-and-github-workflow" class="level1">
<h1>The Git and GitHub Workflow</h1>
<p>With all local checks passing, it is time to commit, push, and open a pull request.</p>
<section id="staging-and-committing" class="level2">
<h2 class="anchored" data-anchor-id="staging-and-committing">Staging and Committing</h2>
<div class="sourceCode" id="cb8" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb8-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> status</span>
<span id="cb8-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> add R/zzdataframe2graphic.R</span>
<span id="cb8-3"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> add tests/testthat/test-zzdataframe2graphic.R</span>
<span id="cb8-4"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> add DESCRIPTION</span>
<span id="cb8-5"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> add man/<span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">*</span></span>
<span id="cb8-6"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> add NAMESPACE</span></code></pre></div>
<p>Review the staged changes with <code>git status</code>, then commit with a descriptive message:</p>
<div class="sourceCode" id="cb9" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb9-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> commit <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-m</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"feat: add error handling to</span></span>
<span id="cb9-2"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  zzdataframe2graphic</span></span>
<span id="cb9-3"></span>
<span id="cb9-4"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">- Added input validation for data frame columns</span></span>
<span id="cb9-5"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">- Updated roxygen2 documentation</span></span>
<span id="cb9-6"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">- Added unit tests for edge cases"</span></span></code></pre></div>
</section>
<section id="pushing-and-creating-a-pull-request" class="level2">
<h2 class="anchored" data-anchor-id="pushing-and-creating-a-pull-request">Pushing and Creating a Pull Request</h2>
<div class="sourceCode" id="cb10" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb10-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> push origin feature-name</span></code></pre></div>
<p>Then create a pull request on GitHub:</p>
<ol type="1">
<li>Navigate to the repository on GitHub.</li>
<li>Click the “Pull requests” tab.</li>
<li>Click “New pull request.”</li>
<li>Set base to <code>main</code> and compare to <code>feature-name</code>.</li>
<li>Fill in the PR description: what changed, why, how to test, and any related issues.</li>
</ol>
</section>
<section id="github-actions-automated-cicd" class="level2">
<h2 class="anchored" data-anchor-id="github-actions-automated-cicd">GitHub Actions: Automated CI/CD</h2>
<p>GitHub Actions automate the testing process on every push or pull request. A typical R package uses three workflows:</p>
<p><strong>R-CMD-check</strong> runs <code>R CMD check</code> across multiple operating systems:</p>
<ul>
<li>Windows (latest)</li>
<li>macOS (latest)</li>
<li>Ubuntu (latest, with R-release, R-devel, and R-oldrel)</li>
</ul>
<p>This workflow triggers on pushes and pull requests to the main branch.</p>
<p><strong>Test coverage</strong> runs all package tests, generates coverage reports, and identifies which parts of the code need additional testing.</p>
<p><strong>pkgdown</strong> builds the package documentation website and deploys it to GitHub Pages whenever changes are pushed to the main branch.</p>
</section>
<section id="setting-up-github-actions" class="level2">
<h2 class="anchored" data-anchor-id="setting-up-github-actions">Setting Up GitHub Actions</h2>
<p>To add these workflows to a package:</p>
<div class="sourceCode" id="cb11" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb11-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">mkdir</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-p</span> .github/workflows</span></code></pre></div>
<p>Then add three YAML configuration files:</p>
<ul>
<li><code>.github/workflows/R-CMD-check.yaml</code></li>
<li><code>.github/workflows/test-coverage.yaml</code></li>
<li><code>.github/workflows/pkgdown.yaml</code></li>
</ul>
<p>Configure repository permissions:</p>
<ol type="1">
<li>Go to Settings, then Actions, then General.</li>
<li>Set “Workflow permissions” to “Read and write.”</li>
<li>Enable GitHub Pages deployment.</li>
</ol>
<p>The <code>r-lib/actions</code> repository on GitHub provides standard workflow templates for all three.</p>
</section>
<section id="addressing-ci-failures" class="level2">
<h2 class="anchored" data-anchor-id="addressing-ci-failures">Addressing CI Failures</h2>
<p>If any workflow fails after pushing:</p>
<ol type="1">
<li>Check the “Actions” tab on GitHub.</li>
<li>Review the failure logs for each workflow.</li>
<li>Fix the issues locally.</li>
<li>Push the fixes:</li>
</ol>
<div class="sourceCode" id="cb12" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb12-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> add .</span>
<span id="cb12-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> commit <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-m</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"fix: address CI failures"</span></span>
<span id="cb12-3"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> push origin feature-name</span></code></pre></div>
<p>The pull request will automatically re-run the workflows.</p>
</section>
<section id="merging-and-cleaning-up" class="level2">
<h2 class="anchored" data-anchor-id="merging-and-cleaning-up">Merging and Cleaning Up</h2>
<p>Once all checks pass and the PR is approved:</p>
<ol type="1">
<li>Choose a merge strategy (usually “Squash and merge”).</li>
<li>Update the PR title if needed.</li>
<li>Click “Squash and merge.”</li>
</ol>
<p>Then clean up locally:</p>
<div class="sourceCode" id="cb13" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb13-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> checkout main</span>
<span id="cb13-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> pull origin main</span>
<span id="cb13-3"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> branch <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-d</span> feature-name</span></code></pre></div>
</section>
<section id="creating-a-release-optional" class="level2">
<h2 class="anchored" data-anchor-id="creating-a-release-optional">Creating a Release (Optional)</h2>
<p>For significant changes, create a GitHub release:</p>
<ol type="1">
<li>Navigate to the “Releases” section.</li>
<li>Click “Create new release.”</li>
<li>Tag the version (e.g., <code>v0.2.1</code>).</li>
<li>Add release notes summarizing the changes.</li>
<li>Publish the release.</li>
</ol>
</section>
</section>
<section id="verification" class="level1">
<h1>Verification</h1>
<p>After completing the branch-edit-test cycle, confirm that each stage of the pipeline passes.</p>
<div class="sourceCode" id="cb14" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb14-1">devtools<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">document</span>()</span>
<span id="cb14-2">devtools<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">test</span>()</span></code></pre></div>
<div class="sourceCode" id="cb15" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb15-1">devtools<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">check</span>(<span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">args =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">c</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"--as-cran"</span>))</span></code></pre></div>
<div class="sourceCode" id="cb16" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb16-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> log <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--oneline</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-5</span></span></code></pre></div>
<p>If <code>document()</code> runs without warnings, <code>test()</code> reports no failures, and <code>check()</code> returns 0 errors, 0 warnings, 0 notes, the package is ready to push.</p>
</section>
<section id="daily-workflow" class="level1">
<h1>Daily Workflow</h1>
<table class="caption-top table">
<colgroup>
<col style="width: 40%">
<col style="width: 60%">
</colgroup>
<thead>
<tr class="header">
<th style="text-align: left;">Task</th>
<th style="text-align: left;">Command</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td style="text-align: left;">Create feature branch</td>
<td style="text-align: left;"><code>git checkout -b feature-name</code></td>
</tr>
<tr class="even">
<td style="text-align: left;">Regenerate docs</td>
<td style="text-align: left;"><code>devtools::document()</code></td>
</tr>
<tr class="odd">
<td style="text-align: left;">Run tests</td>
<td style="text-align: left;"><code>devtools::test()</code></td>
</tr>
<tr class="even">
<td style="text-align: left;">Build package</td>
<td style="text-align: left;"><code>devtools::build()</code></td>
</tr>
<tr class="odd">
<td style="text-align: left;">Full check</td>
<td style="text-align: left;"><code>devtools::check(args = c("--as-cran"))</code></td>
</tr>
<tr class="even">
<td style="text-align: left;">Push branch</td>
<td style="text-align: left;"><code>git push origin feature-name</code></td>
</tr>
<tr class="odd">
<td style="text-align: left;">Clean up after merge</td>
<td style="text-align: left;"><code>git checkout main &amp;&amp; git pull &amp;&amp; git branch -d feature-name</code></td>
</tr>
</tbody>
</table>
<section id="things-to-watch-out-for" class="level2">
<h2 class="anchored" data-anchor-id="things-to-watch-out-for">Things to Watch Out For</h2>
<ol type="1">
<li><p><strong>Run <code>devtools::document()</code> before committing.</strong> I have forgotten this step more than once, leading to CI failures because the NAMESPACE file was out of date.</p></li>
<li><p><strong>Stage files explicitly rather than using <code>git add .</code></strong> The R build process creates temporary files that should not be committed. Staging specific files avoids accidental inclusion of build artifacts.</p></li>
<li><p><strong>Check on multiple platforms.</strong> A package that passes on macOS may fail on Windows due to path separators or system dependency differences. The multi-platform R-CMD-check workflow catches these issues.</p></li>
<li><p><strong>Keep commits focused.</strong> A PR that changes one function, updates its documentation, and adds its tests is easy to review. A PR that touches fifteen files across unrelated features is difficult to evaluate.</p></li>
<li><p><strong>Address CI failures immediately.</strong> Letting failures accumulate makes debugging harder. Fix each failure as it appears and push the fix before moving on.</p></li>
</ol>
</section>
</section>
<section id="uninstall-rollback" class="level1">
<h1>Uninstall / Rollback</h1>
<p>To revert a package change that has not yet been merged, delete the feature branch locally and on the remote.</p>
<div class="sourceCode" id="cb17" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb17-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> checkout main</span>
<span id="cb17-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> branch <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-D</span> feature-name</span>
<span id="cb17-3"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> push origin <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--delete</span> feature-name</span></code></pre></div>
<p>If the change has already been merged, revert the merge commit on <code>main</code>:</p>
<div class="sourceCode" id="cb18" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb18-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> revert <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span>merge-commit-hash<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span></span>
<span id="cb18-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> push origin main</span></code></pre></div>
<p>To remove the development toolchain entirely:</p>
<div class="sourceCode" id="cb19" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb19-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">remove.packages</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">c</span>(</span>
<span id="cb19-2">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"devtools"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"testthat"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"roxygen2"</span>,</span>
<span id="cb19-3">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"rcmdcheck"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"covr"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"pkgdown"</span></span>
<span id="cb19-4">))</span></code></pre></div>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/rp-package-update-workflow/media/images/ambiance3.png" class="img-fluid figure-img"></p>
<figcaption>Books and notes on a research desk</figcaption>
</figure>
</div>
<p><em>Reflecting on the lessons from a full development cycle.</em></p>
</section>
<section id="what-did-we-learn" class="level1">
<h1>What Did We Learn?</h1>
<section id="lessons-learnt" class="level2">
<h2 class="anchored" data-anchor-id="lessons-learnt">Lessons Learnt</h2>
<p><strong>Conceptual Understanding:</strong></p>
<ul>
<li>The R package structure is not just an organizational convenience; it is a contract that <code>R CMD check</code> enforces across documentation, dependencies, and test coverage.</li>
<li>Feature branches isolate risk. Working directly on <code>main</code> means every mistake is immediately visible to collaborators and CI systems.</li>
<li>Automated CI/CD on three operating systems catches platform-specific issues that local testing on a single machine cannot detect.</li>
<li>The pull request is not just a merge mechanism; it is a documentation artifact that records what changed, why, and what tests confirmed the change works.</li>
</ul>
<p><strong>Technical Skills:</strong></p>
<ul>
<li><code>devtools::document()</code> regenerates <code>NAMESPACE</code> and <code>man/</code> from roxygen2 comments, eliminating manual documentation maintenance.</li>
<li><code>devtools::check()</code> with <code>--as-cran</code> applies the strictest quality standards and is worth running before every push.</li>
<li>The <code>r-lib/actions</code> repository provides ready-to-use GitHub Actions YAML templates for R-CMD-check, test coverage, and pkgdown.</li>
<li><code>rcmdcheck::rcmdcheck()</code> provides more detailed output than <code>devtools::check()</code> and is useful for diagnosing obscure failures.</li>
</ul>
<p><strong>Gotchas and Pitfalls:</strong></p>
<ul>
<li>Forgetting to run <code>devtools::document()</code> after changing roxygen2 comments causes NAMESPACE mismatches that fail CI silently.</li>
<li>Using <code>git add .</code> can accidentally stage build artifacts, <code>.Rhistory</code>, or <code>.DS_Store</code> files that do not belong in the repository.</li>
<li>GitHub Actions workflows require “Read and write” permissions under repository settings; the default “Read” permission causes pkgdown deployment to fail without a clear error message.</li>
<li>Test coverage workflows may require LaTeX dependencies for vignette building; missing system dependencies produce cryptic errors that do not mention LaTeX directly.</li>
</ul>
</section>
<section id="limitations" class="level2">
<h2 class="anchored" data-anchor-id="limitations">Limitations</h2>
<ul>
<li>This workflow assumes a single-developer context. Multi-contributor projects require additional conventions around code review, branch protection rules, and merge conflict resolution.</li>
<li>The GitHub Actions templates from <code>r-lib/actions</code> target CRAN-style packages. Packages with non-standard system dependencies may need custom workflow modifications.</li>
<li>Test coverage reports measure which lines of code are executed during tests, not whether the tests are meaningful. High coverage does not guarantee high quality.</li>
<li>The <code>--as-cran</code> check is conservative and may flag issues that are acceptable for internal or non-CRAN packages.</li>
<li>This post does not cover continuous deployment to package repositories (e.g., r-universe or drat) or automated version bumping.</li>
</ul>
</section>
<section id="opportunities-for-improvement" class="level2">
<h2 class="anchored" data-anchor-id="opportunities-for-improvement">Opportunities for Improvement</h2>
<ol type="1">
<li>Add pre-commit hooks that run <code>devtools::document()</code> and <code>devtools::test()</code> automatically before each commit.</li>
<li>Integrate <code>lintr</code> into the CI pipeline to enforce consistent code style across all contributions.</li>
<li>Set up branch protection rules on <code>main</code> to require passing CI checks before merging.</li>
<li>Add a <code>NEWS.md</code> file to track user-facing changes with each version increment.</li>
<li>Explore <code>usethis::use_github_action()</code> to generate workflow files directly from R rather than copying YAML templates manually.</li>
<li>Consider adding a code coverage badge to the README to make test coverage visible at a glance.</li>
</ol>
</section>
</section>
<section id="wrapping-up" class="level1">
<h1>Wrapping Up</h1>
<p>The R package development workflow is more involved than simply editing code and pushing to GitHub, but each step exists for a reason. The branching strategy protects the main branch, the documentation tools keep help files synchronized with code, the testing framework catches regressions, and GitHub Actions verify everything works across platforms.</p>
<p>What I learned most from going through this process is that the overhead of a proper workflow pays for itself quickly. The first time CI catches a platform-specific bug that I would never have found on my own machine, the entire setup justified its existence.</p>
<p>For anyone starting out with R package development, my advice is to set up the full workflow once, even for a small package, and then follow it consistently. The steps become automatic after a few iterations.</p>
<p>In conclusion, four points merit emphasis. First, always branch before making changes and keep each branch focused on a single feature or fix. Second, run <code>devtools::document()</code>, <code>devtools::test()</code>, and <code>devtools::check()</code> locally before pushing. Third, set up R-CMD-check, test coverage, and pkgdown GitHub Actions workflows using the templates from <code>r-lib/actions</code>. Fourth, treat the pull request as both a merge mechanism and a documentation record of what changed and why.</p>
</section>
<section id="see-also" class="level1">
<h1>See Also</h1>
<section id="related-posts" class="level3">
<h3 class="anchored" data-anchor-id="related-posts">Related Posts</h3>
<ul>
<li><a href="../01-configtermzsh/configtermzsh/analysis/report/">Configure the Command Line for Data Science Development</a></li>
<li><a href="../02-githubarchive/githubarchive/analysis/report/">Setting Up Git for Data Science Projects</a></li>
</ul>
</section>
<section id="key-resources" class="level3">
<h3 class="anchored" data-anchor-id="key-resources">Key Resources</h3>
<ul>
<li><a href="https://r-pkgs.org/">R Packages (2e)</a> by Hadley Wickham and Jenny Bryan: the definitive guide to R package development</li>
<li><a href="https://git-scm.com/book/en/v2">Pro Git Book</a>: comprehensive guide to Git fundamentals</li>
<li><a href="https://github.com/r-lib/actions">GitHub Actions for R</a>: r-lib’s collection of workflow templates</li>
<li><a href="https://testthat.r-lib.org/">testthat documentation</a>: the standard testing framework for R</li>
<li><a href="https://devtools.r-lib.org/">devtools documentation</a>: comprehensive guide to the devtools package</li>
<li><a href="https://cran.r-project.org/doc/manuals/r-release/R-exts.html">Writing R Extensions</a>: the official R manual for package development</li>
<li><a href="https://roxygen2.r-lib.org/">roxygen2 documentation</a>: inline documentation system for R</li>
<li><a href="https://pkgdown.r-lib.org/">pkgdown documentation</a>: building package documentation websites</li>
<li><a href="https://covr.r-lib.org/">covr documentation</a>: understanding test coverage in R</li>
<li><a href="https://docs.github.com/en/actions">GitHub Actions Documentation</a>: official reference for CI/CD workflows</li>
<li><a href="https://skills.github.com/">GitHub Skills</a>: interactive courses for learning GitHub</li>
<li><a href="https://ohshitgit.com/">Oh Shit, Git!?!</a>: practical solutions for common Git mistakes</li>
</ul>
</section>
</section>
<section id="reproducibility" class="level1">
<h1>Reproducibility</h1>
<p>This post describes a workflow rather than an analysis pipeline. To follow along, one will need:</p>
<ul>
<li>R 4.4 or later</li>
<li>Git 2.30 or later</li>
<li>A GitHub account with repository access</li>
<li>The packages listed in the Prerequisites section</li>
</ul>
<p>The key commands in sequence:</p>
<div class="sourceCode" id="cb20" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb20-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> checkout <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-b</span> feature-name</span>
<span id="cb20-2"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Edit R source files and roxygen2 comments</span></span></code></pre></div>
<div class="sourceCode" id="cb21" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb21-1">devtools<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">document</span>()</span>
<span id="cb21-2">devtools<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">test</span>()</span>
<span id="cb21-3">devtools<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">build</span>()</span>
<span id="cb21-4">devtools<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">check</span>()</span></code></pre></div>
<div class="sourceCode" id="cb22" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb22-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> add <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span>specific-files<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span></span>
<span id="cb22-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> commit <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-m</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"feat: description of change"</span></span>
<span id="cb22-3"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> push origin feature-name</span>
<span id="cb22-4"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Open PR on GitHub, wait for Actions, merge</span></span>
<span id="cb22-5"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> checkout main <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">&amp;&amp;</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> pull origin main</span>
<span id="cb22-6"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> branch <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-d</span> feature-name</span></code></pre></div>
<div class="cell">
<div class="sourceCode cell-code" id="cb23" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb23-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sessionInfo</span>()</span></code></pre></div>
<div class="cell-output cell-output-stdout">
<pre><code>R version 4.5.3 (2026-03-11)
Platform: aarch64-apple-darwin25.3.0
Running under: macOS Tahoe 26.5

Matrix products: default
BLAS:   /opt/homebrew/Cellar/openblas/0.3.32/lib/libopenblasp-r0.3.32.dylib 
LAPACK: /opt/homebrew/Cellar/r/4.5.3/lib/R/lib/libRlapack.dylib;  LAPACK version 3.12.1

locale:
[1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8

time zone: America/Los_Angeles
tzcode source: internal

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

loaded via a namespace (and not attached):
 [1] htmlwidgets_1.6.4 compiler_4.5.3    fastmap_1.2.0     cli_3.6.6        
 [5] tools_4.5.3       htmltools_0.5.8.1 parallel_4.5.3    yaml_2.3.10      
 [9] rmarkdown_2.29    knitr_1.50        jsonlite_2.0.0    xfun_0.56        
[13] digest_0.6.37     rlang_1.2.0       evaluate_1.0.5   </code></pre>
</div>
</div>
</section>
<section id="lets-connect" class="level1">
<h1>Let’s Connect</h1>
<ul>
<li><strong>GitHub:</strong> <a href="https://github.com/rgt47">rgt47</a></li>
<li><strong>Twitter/X:</strong> <a href="https://twitter.com/rgt47"><span class="citation" data-cites="rgt47">@rgt47</span></a></li>
<li><strong>LinkedIn:</strong> <a href="https://linkedin.com/in/rgthomaslab">Ronald Glenn Thomas</a></li>
<li><strong>Email:</strong> <a href="https://rgtlab.org/contact">rgtlab.org/contact</a></li>
</ul>
<p>I would enjoy hearing from you if:</p>
<ul>
<li>An error or a better approach to any of the code in this post comes to mind.</li>
<li>There are suggestions for topics to see covered.</li>
<li>The interest is in discussing R programming, data science, or reproducible research.</li>
<li>There are questions about anything in this tutorial.</li>
<li>The goal is simply to say hello and connect.</li>
</ul>
<section id="related-posts-in-this-cluster" class="level2">
<h2 class="anchored" data-anchor-id="related-posts-in-this-cluster">Related posts in this cluster</h2>
<p>This post is part of the <em>R Package Development and Testing</em> series. Recommended reading order:</p>
<ol type="1">
<li><strong>Post 70: Updating an R Package: A Complete Workflow</strong> (this post)</li>
<li>Post 72: <a href="../72-rp-vim-r-repl-plugin/">Writing a Simple Vim Plugin for REPL Interaction</a></li>
<li>Post 73: <a href="../73-rp-testing-data-analysis/">Testing Data Analysis Workflows in R</a></li>
<li>Post 74: <a href="../74-rp-testthat-to-tinytest/">From testthat to tinytest</a></li>
</ol>


</section>
</section>

 ]]></description>
  <category>r</category>
  <category>package-development</category>
  <category>git</category>
  <guid>https://rgtlab.org/posts/rp-package-update-workflow/</guid>
  <pubDate>Wed, 11 Feb 2026 08:00:00 GMT</pubDate>
  <media:content url="https://rgtlab.org/posts/rp-package-update-workflow/media/images/hero.png" medium="image" type="image/png" height="96" width="144"/>
</item>
<item>
  <title>Setting Up Neovim as a Data Science IDE</title>
  <dc:creator>Ronald G. Thomas</dc:creator>
  <link>https://rgtlab.org/posts/wf-neovim-data-science-ide/</link>
  <description><![CDATA[ 




<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/wf-neovim-data-science-ide/media/images/hero.png" class="img-fluid figure-img" style="width:80.0%"></p>
<figcaption>Neovim logo and editor theme in a terminal</figcaption>
</figure>
</div>
<p><em>Neovim: a modern fork of Vim designed for extensibility through Lua.</em></p>
<section id="introduction" class="level1">
<h1>Introduction</h1>
<p>I did not really know how much a keyboard-centric text editor could reshape the way I write code until I committed to learning Neovim. For years I relied on conventional mouse-driven editors, accepting their speed as a given. The transition was not immediate, but the cumulative effect on daily productivity was substantial.</p>
<p>Neovim is a fork of Vim that retains the modal editing philosophy while adding first-class Lua scripting, an asynchronous plugin architecture, and a built-in terminal emulator. These features make it particularly well suited for data science workflows where one frequently switches between writing code, inspecting output, and editing manuscripts.</p>
<p>We describe a practical Neovim configuration for data science development on macOS. The setup covers plugin management with Lazy, core editor settings, R integration through Nvim-R, and a set of key mappings that streamline the edit-run-inspect cycle. An appendix addresses Ubuntu adjustments and working with multiple Neovim configurations.</p>
<p>More formally, we document the Editor layer (Layer 5) of the Workflow Construct described in <a href="../../posts/52-workflow-construct/">post 52</a>. Neovim is the choice of editor at this layer; the layer is substitutable (with Vim, VS Code, RStudio, or a graphical IDE), but the contract the layer holds, modal scriptable text manipulation that behaves identically locally and over SSH, is what the higher layers (Vim plugins, snippets, REPL integration) all assume. The configuration documented here is the editor-layer counterpart to the dotfiles repository (<a href="../../posts/24-setupdotfilesongithub/">post 24</a>).</p>
<section id="motivations" class="level2">
<h2 class="anchored" data-anchor-id="motivations">Motivations</h2>
<ul>
<li>I was spending too much time reaching for the mouse during analysis sessions and wanted a purely keyboard-driven workflow.</li>
<li>My existing IDE felt sluggish when editing large R scripts and Quarto documents; Neovim starts instantaneously and never lags.</li>
<li>I needed a single editor that could handle R, Python, Julia, and LaTeX without switching tools.</li>
<li>The Nvim-R plugin offered a tighter REPL integration than what I had experienced in other editors.</li>
<li>I wanted my entire editor configuration stored in version-controlled text files that I could replicate on any machine.</li>
<li>Learning Lua as a configuration language felt like a worthwhile investment given its growing adoption in the Neovim ecosystem.</li>
</ul>
</section>
<section id="objectives" class="level2">
<h2 class="anchored" data-anchor-id="objectives">Objectives</h2>
<ol type="1">
<li>Install Neovim and set up convenience shell aliases for terminal and GUI modes.</li>
<li>Build a modular Lua configuration with a clear directory hierarchy under <code>~/.config/nvim</code>.</li>
<li>Install and configure essential plugins for data science work using the Lazy plugin manager.</li>
<li>Configure Nvim-R for interactive R development with custom key mappings for sending code, inspecting objects, and rendering documents.</li>
</ol>
<p>I am documenting my learning process here. If you spot errors or have better approaches, please let me know.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/wf-neovim-data-science-ide/media/images/ambiance1.png" class="img-fluid figure-img" style="width:30.0%"></p>
<figcaption>Settling in for editor configuration.</figcaption>
</figure>
</div>
<p><em>The Neovim project logo. Neovim extends Vim with Lua scripting and an asynchronous architecture.</em></p>
</section>
</section>
<section id="prerequisites-and-setup" class="level1">
<h1>Prerequisites and Setup</h1>
<p>This guide assumes a macOS environment with Homebrew installed. The simplest approach is to install both the terminal and GUI versions of Neovim:</p>
<div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb1-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">brew</span> install neovim neovim-qt</span></code></pre></div>
<p>Convenience aliases in <code>.zshrc</code> are recommended:</p>
<div class="sourceCode" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb2-1"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">alias</span> ng=<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"nvim-qt"</span></span>
<span id="cb2-2"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">alias</span> nt=<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"nvim"</span></span></code></pre></div>
<p>The mnemonic is straightforward: <code>nt</code> for terminal, <code>ng</code> for GUI. Ubuntu Linux users should see the appendix at the end of this post for platform-specific adjustments.</p>
<p><strong>Assumed knowledge.</strong> Basic familiarity with the terminal, a general understanding of what a text editor does, and comfort navigating the file system from the command line. No prior Vim experience is required, though it helps considerably.</p>
</section>
<section id="what-is-neovim" class="level1">
<h1>What is Neovim?</h1>
<p>Neovim is a modern reimplementation of Vim, the ubiquitous Unix text editor. Where Vim relies on its own scripting language (Vimscript), Neovim adds first-class support for Lua, a lightweight and fast scripting language. This means configuration files are written in a language that is easier to read, debug, and extend than traditional Vimscript.</p>
<p>The core idea behind modal editing is that the keyboard serves different purposes depending on the current mode. In Normal mode, keys move the cursor and manipulate text. In Insert mode, keys type characters. In Visual mode, keys select regions. This separation eliminates the need for modifier-heavy shortcuts and allows rapid text manipulation once the muscle memory develops.</p>
<p>For data scientists, Neovim offers a compelling combination: a built-in terminal for running REPLs, a rich plugin ecosystem for language-specific tooling, and a configuration system that lives entirely in plain text files under version control.</p>
</section>
<section id="directory-structure-and-configuration-hierarchy" class="level1">
<h1>Directory Structure and Configuration Hierarchy</h1>
<p>The standard location for Neovim configuration files on Unix-like systems is <code>~/.config/nvim</code>. The main entry point is either <code>init.vim</code> (Vimscript) or <code>init.lua</code> (Lua). In this post we use Lua exclusively.</p>
<p>Below is the file hierarchy we construct. All the code in the <code>lua</code> subdirectory could be bundled into <code>init.lua</code>, but separating concerns into individual files is clearer and easier to maintain.</p>
<pre><code>.
|-- ginit.vim
|-- init.lua
|-- lazy-lock.json
|-- lua
|   |-- basics.lua
|   |-- leap-config.lua
|   |-- nvim-R-config.lua
|   |-- nvim-cmp-config.lua
|   |-- nvim-telescope-config.lua
|   |-- nvim-tree-config.lua
|   `-- treesitter-config.lua
|-- my_snippets
|   |-- all.snippets
|   |-- tex
|   |-- R
|   |-- python
|   |-- julia
|   |-- giles.tex.snipppets
|   |-- mail.snippets
|   |-- r.snippets
|   |-- rmd.snippets
|   |-- snippets.snippets
|   |-- tex.snippets
|   |-- text.snippets
|   `-- txt.snippets
|-- spell
|   |-- en.utf-8.add
|   `-- en.utf-8.add.spl</code></pre>
<p>Each <code>.lua</code> file under the <code>lua/</code> directory is loaded by name from <code>init.lua</code> using Lua’s <code>require()</code> function. The <code>my_snippets/</code> directory holds UltiSnips snippet files organized by language. The <code>spell/</code> directory contains custom dictionary additions for Neovim’s built-in spell checker.</p>
</section>
<section id="plugin-management-with-lazy" class="level1">
<h1>Plugin Management with Lazy</h1>
<p>Neovim on its own is useful but limited. Plugins extend its capabilities dramatically. To install plugins we need a plugin manager. Several exist; we use Lazy, which is fast, declarative, and written entirely in Lua.</p>
<p>To install Lazy, clone its repository into Neovim’s data directory:</p>
<div class="sourceCode" id="cb4" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb4-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> clone https://github.com/folke/lazy.nvim.git <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb4-2">  ~/.local/share/nvim/lazy/lazy.nvim</span></code></pre></div>
</section>
<section id="the-init.lua-entry-point" class="level1">
<h1>The init.lua Entry Point</h1>
<p>The <code>init.lua</code> file is the first file Neovim reads on startup. It sets leader keys, prepends Lazy to the runtime path, and then loads each configuration module in sequence.</p>
<div class="sourceCode" id="cb5" style="background: #f1f3f5;"><pre class="sourceCode lua code-with-copy"><code class="sourceCode lua"><span id="cb5-1"><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">vim</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">g</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">mapleader</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">","</span></span>
<span id="cb5-2"><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">vim</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">g</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">maplocalleader</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">" "</span></span>
<span id="cb5-3"><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">vim</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">opt</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">rtp</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span>prepend<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">(</span></span>
<span id="cb5-4">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"~/.local/share/nvim/lazy/lazy.nvim"</span></span>
<span id="cb5-5"><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">)</span></span>
<span id="cb5-6"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">require</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">(</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'plugins'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">)</span></span>
<span id="cb5-7"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">require</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">(</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'basics'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">)</span></span>
<span id="cb5-8"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">require</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">(</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'nvim-tree-config'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">)</span></span>
<span id="cb5-9"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">require</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">(</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'nvim-R-config'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">)</span></span>
<span id="cb5-10"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">require</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">(</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'nvim-telescope-config'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">)</span></span>
<span id="cb5-11"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">require</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">(</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'leap'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">).</span>add_default_mappings<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">()</span></span>
<span id="cb5-12"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">require</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">(</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'leap-config'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">)</span></span>
<span id="cb5-13"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">require</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">(</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'lualine'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">).</span>setup<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">()</span></span></code></pre></div>
<p>The comma leader key and space local leader are personal preferences. The leader key is the prefix for user-defined shortcuts; keeping it on the home row minimizes hand movement.</p>
</section>
<section id="plugin-list-and-descriptions" class="level1">
<h1>Plugin List and Descriptions</h1>
<p>The following block is the plugin specification passed to Lazy. Plugins are grouped by purpose: a minimal data science core, optional utilities, and Neovim-specific enhancements.</p>
<div class="sourceCode" id="cb6" style="background: #f1f3f5;"><pre class="sourceCode lua code-with-copy"><code class="sourceCode lua"><span id="cb6-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">require</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">(</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'lazy'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">).</span>setup<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">({</span></span>
<span id="cb6-2">  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-- minimal data science setup</span></span>
<span id="cb6-3">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'jalvesaq/Nvim-R'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb6-4">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'lervag/vimtex'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb6-5">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'SirVer/ultisnips'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb6-6">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'jalvesaq/vimcmdline'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb6-7">  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-- optional utilities</span></span>
<span id="cb6-8">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"nvim-lualine/lualine.nvim"</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb6-9">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"bluz71/vim-moonfly-colors"</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb6-10">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'junegunn/vim-peekaboo'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb6-11">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'tpope/vim-commentary'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb6-12">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'francoiscabrol/ranger.vim'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb6-13">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'machakann/vim-highlightedyank'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb6-14">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'tpope/vim-surround'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb6-15">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'ggandor/leap.nvim'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb6-16">  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-- neovim specific</span></span>
<span id="cb6-17">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'nvim-lua/plenary.nvim'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb6-18">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'nvim-tree/nvim-web-devicons'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb6-19">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'nvim-tree/nvim-tree.lua'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb6-20">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'nvim-telescope/telescope.nvim'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb6-21">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'nvim-treesitter/nvim-treesitter'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb6-22">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'neovim/nvim-lspconfig'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb6-23"><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">})</span></span></code></pre></div>
<p><strong>Minimal data science setup.</strong> Nvim-R provides a full R development environment with REPL integration. Vimtex handles LaTeX editing and compilation. UltiSnips manages code snippets for rapid boilerplate insertion. Vimcmdline extends REPL support to Python and Julia.</p>
<p><strong>Optional utilities.</strong> Lualine provides a status bar. Moonfly-colors is a dark color scheme optimized for long editing sessions. Peekaboo previews register contents. Commentary toggles comments. Ranger integrates a file manager. Highlighted-yank provides visual feedback on yanked text. Surround manipulates paired delimiters. Leap enables rapid cursor movement by two-character search.</p>
<p><strong>Neovim specific.</strong> Plenary provides shared Lua utilities. Web-devicons and nvim-tree deliver a file explorer with icons. Telescope is a fuzzy finder for files, buffers, and grep results. Treesitter enables syntax-aware highlighting and code navigation. Lspconfig connects to language servers for autocompletion and diagnostics.</p>
</section>
<section id="setup-basics" class="level1">
<h1>Setup Basics</h1>
<p>The <code>basics.lua</code> file contains core editor settings and key mappings. These settings apply globally regardless of file type.</p>
<div class="sourceCode" id="cb7" style="background: #f1f3f5;"><pre class="sourceCode lua code-with-copy"><code class="sourceCode lua"><span id="cb7-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">local</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">map</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">vim</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">keymap</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">set</span></span>
<span id="cb7-2"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">local</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">opts</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">noremap</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">true</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span></span>
<span id="cb7-3"><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">vim</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>cmd<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">(</span><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">[[</span></span>
<span id="cb7-4"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">"    paste registers into terminal</span></span>
<span id="cb7-5"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">tnoremap &lt;expr&gt; &lt;C-R&gt; \</span></span>
<span id="cb7-6"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">  '&lt;C-\&gt;&lt;C-N&gt;"'.nr2char(getchar()).'pi'</span></span>
<span id="cb7-7"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">set background=dark</span></span>
<span id="cb7-8"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">colorscheme moonfly</span></span>
<span id="cb7-9"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">let $FZF_DEFAULT_COMMAND = 'rg --files --hidden'</span></span>
<span id="cb7-10"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">set completeopt=menu,menuone,noinsert,noselect</span></span>
<span id="cb7-11"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">set number relativenumber</span></span>
<span id="cb7-12"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">set textwidth=80</span></span>
<span id="cb7-13"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">set cursorline</span></span>
<span id="cb7-14"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">set clipboard=unnamed</span></span>
<span id="cb7-15"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">set iskeyword-=_</span></span>
<span id="cb7-16"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">set hlsearch</span></span>
<span id="cb7-17"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">set splitright</span></span>
<span id="cb7-18"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">set hidden</span></span>
<span id="cb7-19"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">set incsearch</span></span>
<span id="cb7-20"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">set noswapfile</span></span>
<span id="cb7-21"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">set showmatch</span></span>
<span id="cb7-22"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">set ignorecase</span></span>
<span id="cb7-23"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">set smartcase</span></span>
<span id="cb7-24"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">set gdefault</span></span>
<span id="cb7-25"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">filetype plugin on</span></span>
<span id="cb7-26"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">set encoding=utf-8</span></span>
<span id="cb7-27"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">set nobackup</span></span>
<span id="cb7-28"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">set nowritebackup</span></span>
<span id="cb7-29"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">set updatetime=300</span></span>
<span id="cb7-30"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">set signcolumn=yes</span></span>
<span id="cb7-31"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">set colorcolumn=80</span></span>
<span id="cb7-32"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">set timeoutlen=1000 ttimeoutlen=10</span></span>
<span id="cb7-33"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">let g:UltiSnipsSnippetDirectories = \</span></span>
<span id="cb7-34"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">  ['~/.config/nvim/my_snippets']</span></span>
<span id="cb7-35"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">let g:UltiSnipsExpandTrigger="&lt;tab&gt;"</span></span>
<span id="cb7-36"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">let g:UltiSnipsJumpForwardTrigger="&lt;c-j&gt;"</span></span>
<span id="cb7-37"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">let g:UltiSnipsJumpBackwardTrigger="&lt;c-k&gt;"</span></span>
<span id="cb7-38"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">nnoremap &lt;leader&gt;U \</span></span>
<span id="cb7-39"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">  &lt;Cmd&gt;call UltiSnips#RefreshSnippets()&lt;CR&gt;</span></span>
<span id="cb7-40"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">autocmd BufWinEnter,WinEnter term://* startinsert</span></span>
<span id="cb7-41"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">]]</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">)</span></span></code></pre></div>
<p>A few settings worth highlighting: <code>relativenumber</code> shows line distances from the cursor, which makes vertical motions (e.g., <code>12j</code> to jump 12 lines down) intuitive. <code>colorcolumn=80</code> draws a vertical guide at 80 characters to encourage readable line lengths. <code>clipboard=unnamed</code> connects Neovim’s yank register to the system clipboard. <code>splitright</code> opens vertical splits to the right, matching the natural left-to-right reading direction.</p>
<section id="key-mappings" class="level2">
<h2 class="anchored" data-anchor-id="key-mappings">Key Mappings</h2>
<p>The following mappings are defined in <code>basics.lua</code> after the <code>vim.cmd</code> block. They use the Lua <code>vim.keymap.set</code> API.</p>
<div class="sourceCode" id="cb8" style="background: #f1f3f5;"><pre class="sourceCode lua code-with-copy"><code class="sourceCode lua"><span id="cb8-1">map<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">(</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'n'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">':'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">';'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">opts</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">)</span></span>
<span id="cb8-2">map<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">(</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'n'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">';'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">':'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">opts</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">)</span></span>
<span id="cb8-3">map<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">(</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'n'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'&lt;leader&gt;u'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb8-4">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">':UltiSnipsEdit&lt;cr&gt;'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">opts</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">)</span></span>
<span id="cb8-5">map<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">(</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'n'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'&lt;leader&gt;U'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb8-6">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'&lt;Cmd&gt;call UltiSnips#RefreshSnippets()&lt;cr&gt;'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb8-7">  <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">opts</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">)</span></span>
<span id="cb8-8">map<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">(</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'n'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'&lt;localleader&gt;&lt;localleader&gt;'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb8-9">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'&lt;C-d&gt;'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">opts</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">)</span></span>
<span id="cb8-10">map<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">(</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'n'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'-'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'$'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">opts</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">)</span></span>
<span id="cb8-11">map<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">(</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'n'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'&lt;leader&gt;w'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'vipgq'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">opts</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">)</span></span>
<span id="cb8-12">map<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">(</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'n'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'&lt;leader&gt;v'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb8-13">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">':edit ~/.config/nvim/init.lua&lt;cr&gt;'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">opts</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">)</span></span>
<span id="cb8-14">map<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">(</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'n'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'&lt;leader&gt;n'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb8-15">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">':edit ~/.config/nvim/lua/basics.lua&lt;cr&gt;'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb8-16">  <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">opts</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">)</span></span>
<span id="cb8-17">map<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">(</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'n'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'&lt;leader&gt;a'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'ggVG'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">opts</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">)</span></span>
<span id="cb8-18">map<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">(</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'n'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'&lt;leader&gt;t'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">':tab split&lt;cr&gt;'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">opts</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">)</span></span>
<span id="cb8-19">map<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">(</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'n'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'&lt;leader&gt;y'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">':vert sb3&lt;cr&gt;'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">opts</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">)</span></span>
<span id="cb8-20">map<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">(</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'n'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'&lt;leader&gt;0'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb8-21">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">':ls!&lt;CR&gt;:b&lt;Space&gt;'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">opts</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">)</span></span>
<span id="cb8-22">map<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">(</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'n'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'&lt;leader&gt;&lt;leader&gt;'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'&lt;C-w&gt;w'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">opts</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">)</span></span>
<span id="cb8-23">map<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">(</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'n'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'&lt;leader&gt;1'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'&lt;C-w&gt;:b1&lt;cr&gt;'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">opts</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">)</span></span>
<span id="cb8-24">map<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">(</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'n'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'&lt;leader&gt;2'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'&lt;C-w&gt;:b2&lt;cr&gt;'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">opts</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">)</span></span>
<span id="cb8-25">map<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">(</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'n'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'&lt;leader&gt;3'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'&lt;C-w&gt;:b3&lt;cr&gt;'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">opts</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">)</span></span>
<span id="cb8-26">map<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">(</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'t'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'ZZ'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"q('yes')&lt;CR&gt;"</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">opts</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">)</span></span>
<span id="cb8-27">map<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">(</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'t'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'ZQ'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"q('no')&lt;CR&gt;"</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">opts</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">)</span></span>
<span id="cb8-28">map<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">(</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'v'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'-'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'$'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">opts</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">)</span></span>
<span id="cb8-29">map<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">(</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'t'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'&lt;leader&gt;0'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb8-30">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'&lt;C-</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">\\</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">&gt;&lt;C-n&gt;&lt;C-w&gt;:ls!&lt;cr&gt;:b&lt;Space&gt;'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">opts</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">)</span></span>
<span id="cb8-31">map<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">(</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'t'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'&lt;Escape&gt;'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'&lt;C-</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">\\</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">&gt;&lt;C-n&gt;'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">opts</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">)</span></span>
<span id="cb8-32">map<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">(</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'t'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">',,'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb8-33">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'&lt;C-</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">\\</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">&gt;&lt;C-n&gt;&lt;C-w&gt;w'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">opts</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">)</span></span>
<span id="cb8-34">map<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">(</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'i'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'&lt;Esc&gt;'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'&lt;Esc&gt;`^'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">opts</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">)</span></span></code></pre></div>
<p>The semicolon and colon swap (<code>':'</code> to <code>';'</code> and vice versa) deserves special mention. In Vim, the colon enters command mode (one of the most frequent actions). Swapping it to the semicolon key eliminates the need to hold Shift, saving thousands of keystrokes over time.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/wf-neovim-data-science-ide/media/images/ambiance2.png" class="img-fluid figure-img"></p>
<figcaption>Soft mood lighting over a coding workspace</figcaption>
</figure>
</div>
<p><em>A well-configured development environment reduces cognitive overhead and allows sustained focus on the analytical task.</em></p>
</section>
</section>
<section id="r-development-setup-with-nvim-r" class="level1">
<h1>R Development Setup with Nvim-R</h1>
<p>The <code>nvim-R-config.lua</code> file configures the Nvim-R plugin, which transforms Neovim into a capable R IDE with REPL integration, object inspection, and document rendering.</p>
<div class="sourceCode" id="cb9" style="background: #f1f3f5;"><pre class="sourceCode lua code-with-copy"><code class="sourceCode lua"><span id="cb9-1"><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">vim</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>cmd<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">(</span><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">[[</span></span>
<span id="cb9-2"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">iabb &lt;buffer&gt; x %&gt;%</span></span>
<span id="cb9-3"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">iabb &lt;buffer&gt; z %in%</span></span>
<span id="cb9-4"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">let R_auto_start = 2</span></span>
<span id="cb9-5"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">let R_enable_comment = 1</span></span>
<span id="cb9-6"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">let R_hl_term = 0</span></span>
<span id="cb9-7"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">let R_clear_line = 1</span></span>
<span id="cb9-8"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">let R_pdfviewer = "zathura"</span></span>
<span id="cb9-9"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">let R_assign = 2</span></span>
<span id="cb9-10"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">let R_latexcmd = ['xelatex']</span></span>
<span id="cb9-11"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">augroup rmarkdown</span></span>
<span id="cb9-12"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">autocmd!</span></span>
<span id="cb9-13"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">autocmd FileType rmd,r nnoremap &lt;buffer&gt; \</span></span>
<span id="cb9-14"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">  &lt;CR&gt; :call SendLineToR("down")&lt;CR&gt;</span></span>
<span id="cb9-15"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">autocmd FileType rmd,r nnoremap &lt;buffer&gt; \</span></span>
<span id="cb9-16"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">  &lt;space&gt;' :call RMakeRmd("default")&lt;cr&gt;</span></span>
<span id="cb9-17"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">autocmd FileType rmd,r noremap \</span></span>
<span id="cb9-18"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">  &lt;space&gt;i :call RAction("dim")&lt;cr&gt;</span></span>
<span id="cb9-19"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">autocmd FileType rmd,r noremap \</span></span>
<span id="cb9-20"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">  &lt;space&gt;h :call RAction("head")&lt;cr&gt;</span></span>
<span id="cb9-21"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">autocmd FileType rmd,r noremap \</span></span>
<span id="cb9-22"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">  &lt;space&gt;p :call RAction("print")&lt;cr&gt;</span></span>
<span id="cb9-23"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">autocmd FileType rmd,r noremap \</span></span>
<span id="cb9-24"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">  &lt;space&gt;q :call RAction("length")&lt;cr&gt;</span></span>
<span id="cb9-25"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">autocmd FileType rmd,r noremap \</span></span>
<span id="cb9-26"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">  &lt;space&gt;n :call RAction("nvim.names")&lt;cr&gt;</span></span>
<span id="cb9-27"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">autocmd FileType rmd,r vmap &lt;buffer&gt; \</span></span>
<span id="cb9-28"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">  &lt;CR&gt; &lt;localleader&gt;sd</span></span>
<span id="cb9-29"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">autocmd FileType rmd,r nmap &lt;buffer&gt; \</span></span>
<span id="cb9-30"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">  &lt;space&gt;j &lt;localleader&gt;gn</span></span>
<span id="cb9-31"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">autocmd FileType rmd,r nmap &lt;buffer&gt; \</span></span>
<span id="cb9-32"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">  &lt;space&gt;k &lt;localleader&gt;gN</span></span>
<span id="cb9-33"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">autocmd FileType rmd,r nmap &lt;buffer&gt; \</span></span>
<span id="cb9-34"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">  &lt;space&gt;l &lt;localleader&gt;cd</span></span>
<span id="cb9-35"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">augroup END</span></span>
<span id="cb9-36"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">]]</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">)</span></span></code></pre></div>
<p><strong>Key configuration choices.</strong> <code>R_auto_start = 2</code> opens an R console automatically when an R file is loaded. <code>R_assign = 2</code> converts two underscores into the <code>&lt;-</code> assignment operator. The <code>iabb</code> abbreviations expand <code>x</code> to <code>%&gt;%</code> and <code>z</code> to <code>%in%</code> in insert mode, reducing keystrokes for common pipe and membership operators.</p>
<p><strong>Custom autocmds.</strong> The <code>augroup rmarkdown</code> block defines file-type-specific mappings for R and Rmd files. Pressing Enter sends the current line to the R console and moves down. Space-prefixed mappings provide quick object inspection: <code>&lt;space&gt;i</code> for dimensions, <code>&lt;space&gt;h</code> for head, <code>&lt;space&gt;p</code> for print. This tight coupling between editor and REPL is what makes Nvim-R particularly effective for interactive data analysis.</p>
</section>
<section id="ubuntu-tweaks-and-multiple-configurations" class="level1">
<h1>Ubuntu Tweaks and Multiple Configurations</h1>
<p>On Ubuntu, Neovim is available through the system package manager, though the version may lag behind the latest release. For the most current version, use the Neovim PPA or download the AppImage directly from the <a href="https://github.com/neovim/neovim/releases">Neovim releases page</a>.</p>
<div class="sourceCode" id="cb10" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb10-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sudo</span> add-apt-repository ppa:neovim-ppa/unstable</span>
<span id="cb10-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sudo</span> apt update</span>
<span id="cb10-3"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sudo</span> apt install neovim</span></code></pre></div>
<section id="working-with-multiple-configurations" class="level2">
<h2 class="anchored" data-anchor-id="working-with-multiple-configurations">Working with Multiple Configurations</h2>
<p>Neovim supports the <code>NVIM_APPNAME</code> environment variable, which allows running entirely separate configurations side by side. This is useful for testing new plugin setups without disturbing a working configuration.</p>
<p>For a thorough walkthrough, see <a href="https://michaeluloth.com/neovim-switch-configs/">Switching Configs in Neovim</a> by Michael Uloth.</p>
<p>To start Neovim with an alternative configuration stored in <code>~/.config/test_nvim</code>:</p>
<div class="sourceCode" id="cb11" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb11-1"><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">NVIM_APPNAME</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>test_nvim <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">nvim</span></span></code></pre></div>
<p>This tells Neovim to read its configuration from <code>~/.config/test_nvim/init.lua</code> instead of the default <code>~/.config/nvim/init.lua</code>. Each configuration maintains its own plugin state, cache, and data directories.</p>
</section>
</section>
<section id="verification" class="level1">
<h1>Verification</h1>
<p>After installing Neovim and copying the configuration:</p>
<div class="sourceCode" id="cb12" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb12-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 1. Version check</span></span>
<span id="cb12-2"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">nvim</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--version</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">|</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">head</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-1</span></span>
<span id="cb12-3"></span>
<span id="cb12-4"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 2. Plugin status (inside Neovim)</span></span>
<span id="cb12-5"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># :Lazy          — verify all plugins installed</span></span>
<span id="cb12-6"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># :checkhealth   — diagnose provider issues</span></span>
<span id="cb12-7"></span>
<span id="cb12-8"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 3. R integration smoke test</span></span>
<span id="cb12-9"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Open an .R file:</span></span>
<span id="cb12-10"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">nvim</span> test.R</span>
<span id="cb12-11"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Press \rf to start R console</span></span>
<span id="cb12-12"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Press \d to send the current line to R</span></span></code></pre></div>
<p>If <code>:checkhealth</code> shows red for clipboard, install <code>xclip</code> (<code>brew install xclip</code> or <code>sudo apt install xclip</code>).</p>
</section>
<section id="daily-workflow" class="level1">
<h1>Daily Workflow</h1>
<table class="caption-top table">
<thead>
<tr class="header">
<th>Keybinding / Command</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><code>\rf</code></td>
<td>Start R console</td>
</tr>
<tr class="even">
<td><code>\d</code></td>
<td>Send current line to R</td>
</tr>
<tr class="odd">
<td><code>\ss</code></td>
<td>Send selection to R</td>
</tr>
<tr class="even">
<td><code>\ro</code></td>
<td>View R object in pager</td>
</tr>
<tr class="odd">
<td><code>&lt;C-p&gt;</code></td>
<td>Fuzzy-find files (Telescope)</td>
</tr>
<tr class="even">
<td><code>&lt;C-n&gt;</code></td>
<td>Toggle file tree</td>
</tr>
<tr class="odd">
<td><code>:Lazy update</code></td>
<td>Update all plugins</td>
</tr>
<tr class="even">
<td><code>:Lazy restore</code></td>
<td>Restore pinned versions</td>
</tr>
<tr class="odd">
<td><code>NVIM_APPNAME=test nvim</code></td>
<td>Launch alternate config</td>
</tr>
</tbody>
</table>
<section id="things-to-watch-out-for" class="level2">
<h2 class="anchored" data-anchor-id="things-to-watch-out-for">Things to Watch Out For</h2>
<ol type="1">
<li><p><strong>Plugin version conflicts.</strong> Lazy locks plugin versions in <code>lazy-lock.json</code>. If a plugin update breaks something, restore the lock file from version control and run <code>:Lazy restore</code>.</p></li>
<li><p><strong>Clipboard integration.</strong> On headless Linux servers, the <code>clipboard=unnamed</code> setting requires <code>xclip</code> or <code>xsel</code> to be installed. Without these, yanking to the system clipboard silently fails.</p></li>
<li><p><strong>Terminal mode escape.</strong> The default escape sequence in Neovim’s built-in terminal is <code>&lt;C-\&gt;&lt;C-n&gt;</code>, which is awkward. The mapping <code>map('t', '&lt;Escape&gt;', '&lt;C-\\&gt;&lt;C-n&gt;', opts)</code> in our configuration fixes this, but it means a literal Escape cannot be typed in terminal mode without an alternative binding.</p></li>
<li><p><strong>R console startup.</strong> With <code>R_auto_start = 2</code>, Nvim-R opens the R console automatically. On slower machines or when R has a heavy <code>.Rprofile</code>, this can cause a noticeable delay. Set <code>R_auto_start = 0</code> to start R manually with <code>\rf</code>.</p></li>
<li><p><strong>Snippet directory paths.</strong> UltiSnips expects snippet directories to be on Neovim’s runtime path. If snippets are not expanding, verify the path in <code>g:UltiSnipsSnippetDirectories</code> matches the actual directory location.</p></li>
</ol>
</section>
</section>
<section id="uninstall-rollback" class="level1">
<h1>Uninstall / Rollback</h1>
<p>To remove Neovim and its configuration:</p>
<div class="sourceCode" id="cb13" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb13-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 1. Remove configuration</span></span>
<span id="cb13-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">rm</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-ri</span> ~/.config/nvim</span>
<span id="cb13-3"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">rm</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-ri</span> ~/.local/share/nvim   <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># plugin data</span></span>
<span id="cb13-4"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">rm</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-ri</span> ~/.cache/nvim          <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># cache</span></span>
<span id="cb13-5"></span>
<span id="cb13-6"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 2. Uninstall Neovim</span></span>
<span id="cb13-7"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">brew</span> uninstall neovim          <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># macOS</span></span>
<span id="cb13-8"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sudo</span> apt remove neovim         <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Ubuntu / Debian</span></span></code></pre></div>
<p>To keep Neovim but reset to defaults, rename the config:</p>
<div class="sourceCode" id="cb14" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb14-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">mv</span> ~/.config/nvim ~/.config/nvim.backup</span></code></pre></div>
<p>Neovim will start with no plugins and default settings.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/wf-neovim-data-science-ide/media/images/ambiance3.png" class="img-fluid figure-img"></p>
<figcaption>UCSD Geisel Library at dusk</figcaption>
</figure>
</div>
<p><em>The pursuit of an efficient development environment parallels the broader scholarly commitment to refining one’s tools and methods.</em></p>
</section>
<section id="what-did-we-learn" class="level1">
<h1>What Did We Learn?</h1>
<section id="lessons-learnt" class="level2">
<h2 class="anchored" data-anchor-id="lessons-learnt">Lessons Learnt</h2>
<p><strong>Conceptual Understanding:</strong></p>
<ul>
<li>Modal editing is not merely a different interface; it represents a fundamentally different model of text manipulation where composable commands replace mouse-driven selection.</li>
<li>The separation of configuration into modular Lua files mirrors the separation-of-concerns principle in software engineering and makes each component independently testable.</li>
<li>Neovim’s asynchronous architecture means plugins run without blocking the editor, which is why operations like fuzzy search and tree-sitter parsing feel instantaneous even on large files.</li>
<li>The REPL-integrated workflow (edit code, send to console, inspect output) collapses the feedback loop in interactive data analysis to a single keystroke.</li>
</ul>
<p><strong>Technical Skills:</strong></p>
<ul>
<li>Writing Lua configuration files from scratch, including understanding the <code>vim.cmd</code>, <code>vim.keymap.set</code>, and <code>vim.opt</code> APIs.</li>
<li>Installing and managing plugins through the Lazy plugin manager, including reading <code>lazy-lock.json</code> for reproducible plugin states.</li>
<li>Configuring Nvim-R for interactive R development with custom autocmds, abbreviations, and file-type-specific key mappings.</li>
<li>Setting up UltiSnips with language-specific snippet directories for rapid code template insertion.</li>
</ul>
<p><strong>Gotchas and Pitfalls:</strong></p>
<ul>
<li>Forgetting to set <code>noremap = true</code> on key mappings can cause recursive mapping chains that freeze the editor or produce unexpected behavior.</li>
<li>The <code>iskeyword-=_</code> setting (which treats underscores as word boundaries) improves <code>snake_case</code> navigation but breaks word completion for identifiers containing underscores.</li>
<li>Neovim’s built-in terminal uses terminal mode, not normal mode. Mappings defined for normal mode do not apply until one escapes back to normal mode with the configured escape binding.</li>
<li>The <code>gdefault</code> setting applies substitutions globally by default (no need for the <code>/g</code> flag), which is convenient but reverses the meaning of <code>/g</code> in substitute commands — adding <code>/g</code> now means “first match only.”</li>
</ul>
</section>
<section id="limitations" class="level2">
<h2 class="anchored" data-anchor-id="limitations">Limitations</h2>
<ul>
<li>This configuration targets macOS as the primary platform. While most settings transfer directly to Linux, clipboard integration, font rendering, and GUI behavior may require platform-specific adjustments.</li>
<li>The plugin selection reflects one practitioner’s workflow. Users working primarily in Python or Julia may need different language server configurations and REPL integrations.</li>
<li>Nvim-R connects to a single R session. Users who need multiple concurrent R sessions or remote R connections may find this limiting compared to RStudio’s session management.</li>
<li>The configuration does not include a debugger setup. Interactive debugging in R and Python requires additional plugins (nvim-dap) and configuration not covered here.</li>
<li>Snippet management through UltiSnips requires learning a domain-specific snippet syntax. Users unfamiliar with snippet engines face an additional learning curve.</li>
<li>This post does not address Neovim’s built-in LSP (Language Server Protocol) configuration in depth. Full autocompletion and diagnostics require language-server setup that varies by language.</li>
</ul>
</section>
<section id="opportunities-for-improvement" class="level2">
<h2 class="anchored" data-anchor-id="opportunities-for-improvement">Opportunities for Improvement</h2>
<ol type="1">
<li>Add LSP configuration for R (languageserver), Python (pyright), and Julia (LanguageServer.jl) to enable autocompletion, go-to-definition, and inline diagnostics.</li>
<li>Replace UltiSnips with LuaSnip, a pure-Lua snippet engine that integrates more naturally with Neovim’s Lua ecosystem and does not require Python.</li>
<li>Configure nvim-cmp for intelligent autocompletion that combines LSP suggestions, buffer words, and snippet expansions in a unified menu.</li>
<li>Add a DAP (Debug Adapter Protocol) configuration for step-through debugging of R and Python scripts directly within Neovim.</li>
<li>Create a custom Quarto filetype plugin with mappings for rendering, previewing, and navigating between code chunks in <code>.qmd</code> files.</li>
<li>Explore the Which-Key plugin to provide a discoverable popup menu of available key mappings, reducing the memorization burden for new users.</li>
</ol>
</section>
</section>
<section id="wrapping-up" class="level1">
<h1>Wrapping Up</h1>
<p>Configuring Neovim for data science is an investment that pays dividends over time. The initial learning curve is real — modal editing requires deliberate practice before it becomes natural. However, once the muscle memory develops, the speed and precision of keyboard-driven editing fundamentally changes the coding experience.</p>
<p>The configuration presented here provides a functional starting point: Lazy manages plugins, basics.lua establishes sensible defaults, and Nvim-R connects the editor to an interactive R console. The modular structure means each component can be modified independently as the workflow evolves.</p>
<p>In conclusion, four points merit emphasis. First, Neovim’s Lua-based configuration is readable, version-controllable, and modular, which makes it practical to maintain across machines. Second, the Lazy plugin manager provides reproducible plugin states through its lock file, enabling consistent environments after cloning. Third, Nvim-R transforms Neovim into a capable R IDE with single-keystroke code execution and object inspection. Fourth, investing time in key mappings and snippets yields compounding productivity gains across every editing session.</p>
<p>Anyone considering the switch should start with a minimal configuration and add plugins gradually. The Neovim ecosystem is large, and configuring everything at once is overwhelming. Actual workflow demands should dictate what to add next.</p>
</section>
<section id="see-also" class="level1">
<h1>See Also</h1>
<p><strong>Related posts:</strong></p>
<ul>
<li><a href="../../posts/01-configtermzsh/configtermzsh/analysis/report/index.qmd">Configuring the Command Line for Data Science Development</a> (terminal and Zsh setup that complements this Neovim configuration)</li>
<li><a href="../../posts/24-setupdotfilesongithub/setupdotfilesongithub/analysis/report/index.qmd">Setting Up a GitHub Dotfiles Repository</a> (version control for configuration files)</li>
</ul>
<p><strong>Key resources:</strong></p>
<ul>
<li><a href="https://neovim.io/doc/">Neovim Official Documentation</a></li>
<li><a href="https://github.com/jalvesaq/Nvim-R">Nvim-R GitHub Repository</a></li>
<li><a href="https://github.com/folke/lazy.nvim">Lazy.nvim Plugin Manager</a></li>
<li><a href="https://neovim.io/doc/user/lua-guide.html">Neovim Lua Guide</a></li>
<li><a href="https://michaeluloth.com/neovim-switch-configs/">Switching Neovim Configs</a> by Michael Uloth</li>
</ul>
</section>
<section id="reproducibility" class="level1">
<h1>Reproducibility</h1>
<p>This post describes a configuration-only workflow with no data analysis pipeline. The configuration files reside in <code>~/.config/nvim/</code> and are managed through version control.</p>
<p><strong>Software versions used:</strong></p>
<div class="sourceCode" id="cb15" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb15-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">nvim</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--version</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">|</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">head</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-1</span></span>
<span id="cb15-2"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">brew</span> list <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--versions</span> neovim</span></code></pre></div>
<p><strong>Configuration files described in this post:</strong></p>
<ul>
<li><code>~/.config/nvim/init.lua</code>: Entry point, leader keys, module loading</li>
<li><code>~/.config/nvim/lua/basics.lua</code>: Core settings and key mappings</li>
<li><code>~/.config/nvim/lua/nvim-R-config.lua</code>: R development configuration</li>
<li><code>~/.config/nvim/lua/plugins.lua</code>: Lazy plugin specification</li>
<li><code>~/.config/nvim/my_snippets/</code>: UltiSnips snippet files by language</li>
</ul>
<p>To replicate this setup, clone the dotfiles repository (or copy the files above) into <code>~/.config/nvim/</code>, then open Neovim. Lazy will automatically install all specified plugins on first launch.</p>
</section>
<section id="lets-connect" class="level1">
<h1>Let’s Connect</h1>
<ul>
<li><strong>GitHub:</strong> <a href="https://github.com/rgt47">rgt47</a></li>
<li><strong>Twitter/X:</strong> <a href="https://twitter.com/rgt47"><span class="citation" data-cites="rgt47">@rgt47</span></a></li>
<li><strong>LinkedIn:</strong> <a href="https://linkedin.com/in/rgthomaslab">Ronald Glenn Thomas</a></li>
<li><strong>Email:</strong> <a href="https://rgtlab.org/contact">rgtlab.org/contact</a></li>
</ul>
<p>I would enjoy hearing from you if:</p>
<ul>
<li>You spot an error or a better approach to any of the code in this post.</li>
<li>You have suggestions for topics you would like to see covered.</li>
<li>You want to discuss R programming, data science, or reproducible research.</li>
<li>You have questions about anything in this tutorial.</li>
<li>You just want to say hello and connect.</li>
</ul>
<section id="related-posts-in-this-cluster" class="level2">
<h2 class="anchored" data-anchor-id="related-posts-in-this-cluster">Related posts in this cluster</h2>
<p>This post is part of the <em>Workflow Construct</em> series. Recommended reading order:</p>
<ol type="1">
<li>Post 15: <a href="../15-wf-construct-overview-anchor/">A Workflow Construct for the Modern Data Scientist</a></li>
<li>Post 16: <a href="../16-wf-unix-workspace-config/">Unix Command-Line Workspace Setup for Data Science</a></li>
<li>Post 17: <a href="../17-wf-multi-laptop-dotfiles-bootstrap/">Multi-Laptop macOS Bootstrap</a></li>
<li>Post 18: <a href="../18-wf-git-for-data-science/">Setting Up Git for Data Science Workflows</a></li>
<li><strong>Post 19: Setting Up Neovim as a Data Science IDE</strong> (this post)</li>
<li>Post 20: <a href="../20-wf-r-vim-latex-workflow/">Extending the R-Vim Workflow with LaTeX</a></li>
<li>Post 21: <a href="../21-wf-modern-cli-tools/">Modern CLI Replacements for the Shell Layer</a></li>
<li>Post 22: <a href="../22-wf-claude-code-in-shell/">LLM-Augmented Editing for the Workflow Construct</a></li>
<li>Post 23: <a href="../23-wf-yabai-tiling-window-manager/">Configuring Yabai as a Tiling Window Manager</a></li>
<li>Post 24: <a href="../24-wf-pocket-terminal-ttyd-tailscale/">A pocket terminal with ttyd and Tailscale</a></li>
<li>Post 25: <a href="../25-wf-linux-mint-on-macbook/">Install Linux Mint on a MacBook Air</a></li>
</ol>


</section>
</section>

 ]]></description>
  <category>neovim</category>
  <category>vim</category>
  <category>r</category>
  <category>python</category>
  <guid>https://rgtlab.org/posts/wf-neovim-data-science-ide/</guid>
  <pubDate>Wed, 11 Feb 2026 08:00:00 GMT</pubDate>
  <media:content url="https://rgtlab.org/posts/wf-neovim-data-science-ide/media/images/hero.png" medium="image" type="image/png" height="96" width="144"/>
</item>
<item>
  <title>Reproducible Blog Posts with ZZCOLLAB: A Quarto Workflow</title>
  <dc:creator>Ronald G. Thomas</dc:creator>
  <link>https://rgtlab.org/posts/zc-quarto-compendium-intro/</link>
  <description><![CDATA[ 




<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/zc-quarto-compendium-intro/media/images/hero.png" class="img-fluid figure-img" style="width:80.0%"></p>
<figcaption>A Quarto-themed workspace representing reproducible document authoring</figcaption>
</figure>
</div>
<p><em>Quarto provides the rendering engine; ZZCOLLAB provides the reproducible scaffolding around it.</em></p>
<section id="introduction" class="level1">
<h1>Introduction</h1>
<p>I did not really know how much infrastructure a blog post needed until I returned to one of my own R-based posts six months after writing it and watched it fail to render. A package had updated, a system library had shifted, and the code that once produced clean diagnostic plots now threw cryptic errors. The rendered HTML was frozen in time, but the source code was dead.</p>
<p>Technical blog posts that embed R code face a well-documented fragility problem. Package updates introduce breaking changes, data sources move or disappear, and system-level dependencies shift beneath the analysis. A post that rendered correctly in 2024 may fail silently in 2025, producing different results or refusing to compile entirely.</p>
<p>The conventional response is to freeze rendered output and accept that the source code is a snapshot rather than a living document. We describe an alternative: treating each blog post as a standalone ZZCOLLAB reproducible research project, complete with its own Docker environment, pinned package versions, and continuous integration pipeline.</p>
<section id="motivations" class="level2">
<h2 class="anchored" data-anchor-id="motivations">Motivations</h2>
<ul>
<li>I was frustrated that returning to a six-month-old post required hours of debugging broken package dependencies before I could even render it again.</li>
<li>I wanted each blog post to be independently clonable so that a collaborator could reproduce the results without asking me what versions of R and ggplot2 I originally used.</li>
<li>I needed a workflow that scaled to 40+ posts without requiring a monorepo that coupled unrelated analyses together.</li>
<li>I was already using ZZCOLLAB for research projects and wanted a consistent mental model across all my computational work, whether a journal article or a tutorial post.</li>
<li>I wanted CI/CD to catch breakage immediately on push rather than discovering it months later when a reader reports a dead link or a wrong figure.</li>
</ul>
</section>
<section id="objectives" class="level2">
<h2 class="anchored" data-anchor-id="objectives">Objectives</h2>
<ol type="1">
<li>Document the lead-in directory pattern that separates git repositories from archival material.</li>
<li>Explain the dual-symlink architecture that reconciles Quarto discovery with rrtools/ZZCOLLAB conventions.</li>
<li>Walk through the steps for creating, initializing, and publishing a new blog post project.</li>
<li>Describe the five-pillar reproducibility framework as applied to the blog post context.</li>
</ol>
<p>I am documenting my learning process here. If you spot errors or have better approaches, please let me know.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/zc-quarto-compendium-intro/media/images/ambiance1.png" class="img-fluid figure-img"></p>
<figcaption>A workspace ready for reproducible publishing.</figcaption>
</figure>
</div>
</section>
</section>
<section id="what-is-a-reproducible-blog-post" class="level1">
<h1>What is a Reproducible Blog Post?</h1>
<p>A reproducible blog post is a technical document whose computational results can be regenerated from source by anyone, on any machine, at any point in the future. It goes beyond simply sharing code alongside prose. Concretely, a reader can clone a single repository, build a Docker container, and produce an identical HTML document without installing anything beyond Docker itself.</p>
<p>The distinction matters because most technical blog posts are reproducible only in theory. They share code snippets but omit the system libraries, the exact package versions, and the data preparation steps that produced the published figures. A truly reproducible blog post packages all of these elements together, much as a research compendium packages the materials needed to reproduce a journal article.</p>
<section id="the-problem" class="level2">
<h2 class="anchored" data-anchor-id="the-problem">The Problem</h2>
<p>Consider a blog post that fits a linear model to the Palmer Penguins dataset and generates diagnostic plots. The post depends on:</p>
<ul>
<li>A specific R version (4.5.1)</li>
<li>The <code>palmerpenguins</code> package (for data)</li>
<li><code>ggplot2</code>, <code>broom</code>, <code>patchwork</code> (for analysis)</li>
<li>System libraries for PNG rendering (<code>libpng</code>, <code>cairo</code>)</li>
<li>Quarto itself (for HTML generation)</li>
</ul>
<p>Any of these may change independently. Without explicit version management, reproducing the post requires reconstructing the original environment through trial and error.</p>
</section>
<section id="the-solution" class="level2">
<h2 class="anchored" data-anchor-id="the-solution">The Solution</h2>
<p>Each blog post becomes a self-contained project with five components:</p>
<ol type="1">
<li><strong>Dockerfile</strong>: defines the computational environment</li>
<li><strong>renv.lock</strong>: pins exact R package versions</li>
<li><strong>.Rprofile</strong>: configures R session behavior</li>
<li><strong>Source code</strong>: analysis scripts and narrative</li>
<li><strong>Data</strong>: raw and derived datasets</li>
</ol>
<p>A collaborator (or the author, six months later) can clone the repository, run <code>make docker-build &amp;&amp; make r</code>, and reproduce the post in an identical environment.</p>
</section>
</section>
<section id="the-lead-in-pattern" class="level1">
<h1>The Lead-In Pattern</h1>
<p>Blog post projects follow the same directory convention used across all research software projects in this lab:</p>
<pre><code>~/Dropbox/prj/qblog/posts/
+-- 29-setupquarto/          # lead-in directory
|   +-- archive/             # old drafts, artifacts
|   +-- setupquarto/         # git repo (the project)
|       +-- .git/
|       +-- Dockerfile
|       +-- index.qmd -&gt; analysis/report/index.qmd
|       +-- ...</code></pre>
<p>The <strong>lead-in directory</strong> (<code>29-setupquarto/</code>) is not managed by git. It serves as a container for the git repository and an <code>archive/</code> directory for superseded drafts, exploratory notebooks, and other material that does not belong in version control.</p>
<p>The <strong>project directory</strong> (<code>setupquarto/</code>) is the git repository. Its name matches the GitHub repository name and omits the numeric prefix.</p>
<p>This pattern mirrors the layout used for research software under <code>~/prj/sfw/</code>:</p>
<pre><code>~/prj/sfw/07-zzcollab/
|   +-- archive/
|   +-- zzcollab/            # git repo</code></pre>
<p>The numeric prefix provides chronological ordering in file browsers without polluting repository names.</p>
</section>
<section id="creating-a-new-post" class="level1">
<h1>Creating a New Post</h1>
<section id="step-1-create-the-lead-in" class="level2">
<h2 class="anchored" data-anchor-id="step-1-create-the-lead-in">Step 1: Create the Lead-In</h2>
<div class="sourceCode" id="cb3" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb3-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">mkdir</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-p</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb3-2">  ~/Dropbox/prj/qblog/posts/43-newpost/archive</span>
<span id="cb3-3"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">cd</span> ~/Dropbox/prj/qblog/posts/43-newpost</span></code></pre></div>
</section>
<section id="step-2-initialize-the-zzcollab-project" class="level2">
<h2 class="anchored" data-anchor-id="step-2-initialize-the-zzcollab-project">Step 2: Initialize the ZZCOLLAB Project</h2>
<p>Copy the scaffold from an existing post or use the ZZCOLLAB CLI:</p>
<div class="sourceCode" id="cb4" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb4-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Option A: Copy from template</span></span>
<span id="cb4-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">cp</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-r</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb4-3">  ~/Dropbox/prj/qblog/posts/39-templatepost/<span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb4-4">templatepost <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb4-5">  ./newpost</span>
<span id="cb4-6"></span>
<span id="cb4-7"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Option B: Use zzcollab CLI</span></span>
<span id="cb4-8"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">zzc</span> analysis newpost</span></code></pre></div>
<p>Both approaches produce the same directory structure.</p>
</section>
<section id="step-3-write-the-post" class="level2">
<h2 class="anchored" data-anchor-id="step-3-write-the-post">Step 3: Write the Post</h2>
<p>The blog post content lives at <code>analysis/report/index.qmd</code>. A root-level symlink provides Quarto compatibility:</p>
<div class="sourceCode" id="cb5" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb5-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Already created by scaffold:</span></span>
<span id="cb5-2"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># newpost/index.qmd -&gt; analysis/report/index.qmd</span></span></code></pre></div>
<p>Open <code>analysis/report/index.qmd</code> and write. Analysis code that produces figures or derived data belongs in <code>analysis/scripts/</code>. Reusable utility functions belong in <code>R/</code>.</p>
</section>
<section id="step-4-initialize-git-and-push" class="level2">
<h2 class="anchored" data-anchor-id="step-4-initialize-git-and-push">Step 4: Initialize Git and Push</h2>
<div class="sourceCode" id="cb6" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb6-1"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">cd</span> newpost</span>
<span id="cb6-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> init <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-b</span> main</span>
<span id="cb6-3"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> add .</span>
<span id="cb6-4"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> commit <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-m</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-5">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Initial scaffold for blog post: newpost"</span></span>
<span id="cb6-6"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">gh</span> repo create rgt47/newpost <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--public</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-7">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--source</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>. <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--remote</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>origin</span>
<span id="cb6-8"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> remote set-url origin <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb6-9">    git@github.com:rgt47/newpost.git</span>
<span id="cb6-10"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> push <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-u</span> origin main</span></code></pre></div>
<p>Each blog post is its own GitHub repository. Collaborators clone a single post and receive the complete reproducible environment.</p>
</section>
</section>
<section id="connecting-to-quarto" class="level1">
<h1>Connecting to Quarto</h1>
<p>The Quarto site lives at <code>~/Dropbox/prj/qblog/</code>. It discovers blog posts through a glob pattern in <code>blog/index.qmd</code>:</p>
<div class="sourceCode" id="cb7" style="background: #f1f3f5;"><pre class="sourceCode yaml code-with-copy"><code class="sourceCode yaml"><span id="cb7-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># blog/index.qmd</span></span>
<span id="cb7-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">listing</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span></span>
<span id="cb7-3"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">  </span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">contents</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> </span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"../posts/*/*/index.qmd"</span></span>
<span id="cb7-4"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">  </span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">type</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> default</span></span>
<span id="cb7-5"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">  </span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sort</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> </span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"date desc"</span></span>
<span id="cb7-6"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">  </span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">categories</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> </span><span class="ch" style="color: #20794D;
background-color: null;
font-style: inherit;">true</span></span></code></pre></div>
<p>The double-wildcard <code>posts/*/*/index.qmd</code> matches the lead-in/project nesting. Quarto resolves the <code>index.qmd</code> symlink transparently and reads the YAML front matter to populate the blog listing.</p>
<section id="how-quarto-discovers-posts" class="level2">
<h2 class="anchored" data-anchor-id="how-quarto-discovers-posts">How Quarto Discovers Posts</h2>
<p>The resolution chain for a blog post:</p>
<pre><code>blog/index.qmd
  +-- glob: ../posts/*/*/index.qmd
      +-- matches:
          posts/29-setupquarto/setupquarto/index.qmd
          +-- symlink: analysis/report/index.qmd
              +-- actual file with YAML front matter</code></pre>
<p>Quarto reads the <code>title</code>, <code>date</code>, <code>categories</code>, <code>description</code>, <code>image</code>, and <code>draft</code> fields from the YAML header. Posts with <code>draft: false</code> are excluded from the listing.</p>
</section>
<section id="site-configuration" class="level2">
<h2 class="anchored" data-anchor-id="site-configuration">Site Configuration</h2>
<p>The site-level <code>_quarto.yml</code> defines navigation, theme, and rendering defaults. It does not enumerate posts; the listing glob handles discovery automatically. Adding a new post requires only creating the lead-in directory and project, with no modification to <code>_quarto.yml</code> or any other site-level file.</p>
</section>
</section>
<section id="github-integration" class="level1">
<h1>GitHub Integration</h1>
<section id="one-repository-per-post" class="level2">
<h2 class="anchored" data-anchor-id="one-repository-per-post">One Repository per Post</h2>
<p>Each blog post is a standalone GitHub repository under the <code>rgt47</code> organization. This design provides:</p>
<ul>
<li><strong>Independent version history</strong>: each post tracks its own commits without polluting a monorepo log</li>
<li><strong>Isolated CI/CD</strong>: a failing build in one post does not block others</li>
<li><strong>Focused collaboration</strong>: a contributor clones only the post they are reviewing</li>
<li><strong>Independent dependency management</strong>: each post pins its own package versions</li>
</ul>
</section>
<section id="cicd-pipeline" class="level2">
<h2 class="anchored" data-anchor-id="cicd-pipeline">CI/CD Pipeline</h2>
<p>Each repository includes <code>.github/workflows/blog-render.yml</code>, which executes on push:</p>
<ol type="1">
<li>Build the Docker image from <code>Dockerfile</code></li>
<li>Restore R packages from <code>renv.lock</code></li>
<li>Run unit tests (<code>tests/testthat/</code>)</li>
<li>Execute analysis scripts (<code>analysis/scripts/</code>)</li>
<li>Render the Quarto report to HTML</li>
<li>Upload the rendered artifact</li>
</ol>
<p>The pipeline confirms that the post renders successfully in a clean environment. If a package update breaks the build, the failure is detected immediately rather than months later.</p>
</section>
<section id="collaborator-workflow" class="level2">
<h2 class="anchored" data-anchor-id="collaborator-workflow">Collaborator Workflow</h2>
<p>A collaborator reproducing or reviewing a post:</p>
<div class="sourceCode" id="cb9" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb9-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> clone <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb9-2">  git@github.com:rgt47/setupquarto.git</span>
<span id="cb9-3"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">cd</span> setupquarto</span>
<span id="cb9-4"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">make</span> docker-build    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># build the Docker image</span></span>
<span id="cb9-5"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">make</span> r               <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># enter the container</span></span>
<span id="cb9-6"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Inside the container:</span></span>
<span id="cb9-7"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">Rscript</span> analysis/scripts/01_prepare_data.R</span>
<span id="cb9-8"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">quarto</span> render analysis/report/index.qmd</span></code></pre></div>
<p>The <code>Makefile</code> automates common operations. <code>make r</code> mounts the project directory into the container and drops the user into an R-ready shell.</p>
</section>
</section>
<section id="the-five-pillars-in-blog-context" class="level1">
<h1>The Five Pillars in Blog Context</h1>
<p>ZZCOLLAB’s reproducibility framework rests on five components. Each serves a specific role in the blog post context:</p>
<dl>
<dt><strong>1. Dockerfile</strong></dt>
<dd>
Defines the base R version (rocker/tidyverse:4.5.1), system dependencies, and the <code>analyst</code> user. Ensures the computational environment is identical across machines and CI/CD runners.
</dd>
<dt><strong>2. renv.lock</strong></dt>
<dd>
Pins exact package versions. A minimal lockfile (172 bytes) specifies only the R version and CRAN repository; <code>renv::install()</code> populates it from the <code>DESCRIPTION</code> file on first use. A full lockfile captures the complete dependency tree.
</dd>
<dt><strong>3. .Rprofile</strong></dt>
<dd>
Configures R session behavior. Activates <code>renv</code>, sets repository mirrors, enables auto-snapshot, and detects whether the session is running inside a ZZCOLLAB container.
</dd>
<dt><strong>4. Source code</strong></dt>
<dd>
Analysis scripts in <code>analysis/scripts/</code> produce figures and derived data. The narrative in <code>analysis/report/index.qmd</code> loads pre-computed results rather than running analysis inline. This separation ensures that rendering the narrative is fast and that analysis code is independently testable.
</dd>
<dt><strong>5. Data</strong></dt>
<dd>
Raw data in <code>analysis/data/raw_data/</code> is immutable. Derived data in <code>analysis/data/derived_data/</code> is produced by analysis scripts and may be regenerated. Each dataset includes a <code>README.md</code> documenting its provenance.
</dd>
</dl>
</section>
<section id="directory-structure-reference" class="level1">
<h1>Directory Structure Reference</h1>
<p>Complete annotated structure of a blog post project:</p>
<pre><code>setupquarto/                     # git repo root
+-- .git/
+-- .github/
|   +-- workflows/
|       +-- blog-render.yml      # CI/CD pipeline
+-- .gitignore
+-- .Rbuildignore
+-- .Rprofile                    # renv activation
+-- .zzcollab/                   # zzcollab metadata
+-- analysis/
|   +-- data/
|   |   +-- raw_data/            # immutable inputs
|   |   +-- derived_data/        # script outputs
|   +-- figures/                 # generated plots
|   +-- media/
|   |   +-- images/              # photos, diagrams
|   +-- report/
|   |   +-- index.qmd            # the blog post
|   |   +-- data -&gt; ../data      # convenience
|   |   +-- figures -&gt; ../figures
|   |   +-- media -&gt; ../media
|   +-- scripts/
|       +-- 01_prepare_data.R
|       +-- 02_fit_models.R
|       +-- 03_generate_figures.R
+-- R/                           # reusable functions
+-- tests/
|   +-- testthat/                # unit tests
|   +-- integration/             # pipeline tests
+-- Dockerfile
+-- Makefile
+-- DESCRIPTION                  # R package metadata
+-- NAMESPACE
+-- renv.lock
+-- renv/
|   +-- activate.R
+-- index.qmd -&gt; analysis/report/index.qmd
+-- data -&gt; analysis/data
+-- figures -&gt; analysis/figures
+-- media -&gt; analysis/media</code></pre>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/zc-quarto-compendium-intro/media/images/ambiance2.png" class="img-fluid figure-img"></p>
<figcaption>A quiet study environment for focused work</figcaption>
</figure>
</div>
<p><em>Reproducible workflows require careful architecture, much like building a well-organized library.</em></p>
</section>
<section id="the-symlink-architecture" class="level1">
<h1>The Symlink Architecture</h1>
<p>The project uses two layers of symlinks to reconcile three competing requirements: (1) Quarto expects <code>index.qmd</code> at the directory root it discovers via glob, (2) the rrtools/ZZCOLLAB convention places the narrative at <code>analysis/report/index.qmd</code>, and (3) relative paths in the <code>.qmd</code> file must resolve to <code>analysis/data/</code>, <code>analysis/figures/</code>, and <code>analysis/media/</code> regardless of which directory Quarto considers the “working directory” during rendering.</p>
<section id="layer-1-root-level-symlinks" class="level2">
<h2 class="anchored" data-anchor-id="layer-1-root-level-symlinks">Layer 1: Root-Level Symlinks</h2>
<pre><code>setupquarto/                  # git repo root
+-- index.qmd -&gt; analysis/report/index.qmd
+-- data -&gt; analysis/data
+-- figures -&gt; analysis/figures
+-- media -&gt; analysis/media</code></pre>
<p><strong>Purpose</strong>: Quarto’s blog listing glob (<code>../posts/*/*/index.qmd</code>) matches files at the project root. The <code>index.qmd</code> symlink makes the post discoverable without duplicating the file.</p>
<p>The <code>data/</code>, <code>figures/</code>, and <code>media/</code> symlinks allow Quarto to resolve relative image and data paths when it renders from the project root. If the <code>.qmd</code> references <code>figures/eda-overview.png</code>, Quarto resolves it through the root-level <code>figures/</code> symlink to <code>analysis/figures/eda-overview.png</code>.</p>
<p><strong>Git behavior</strong>: Git stores symlinks as text files containing the relative target path. On clone, git recreates the symlinks. The <code>git add .</code> command captures symlinks automatically:</p>
<pre><code>$ git ls-files -s index.qmd
120000 &lt;hash&gt; 0   index.qmd</code></pre>
<p>The <code>120000</code> mode indicates a symbolic link.</p>
</section>
<section id="layer-2-report-level-symlinks" class="level2">
<h2 class="anchored" data-anchor-id="layer-2-report-level-symlinks">Layer 2: Report-Level Symlinks</h2>
<pre><code>analysis/report/
+-- index.qmd            # the actual blog post
+-- data -&gt; ../data      # resolves to analysis/data
+-- figures -&gt; ../figures # resolves to
|                         # analysis/figures
+-- media -&gt; ../media     # resolves to
                          # analysis/media</code></pre>
<p><strong>Purpose</strong>: When an author edits <code>index.qmd</code> inside <code>analysis/report/</code> and uses a relative path like <code>data/derived_data/results.csv</code>, the <code>data</code> symlink resolves to <code>../data</code> (i.e., <code>analysis/data</code>). This ensures paths work correctly whether Quarto renders from the project root (following the root-level symlink) or from <code>analysis/report/</code> directly.</p>
<p>Without these symlinks, an <code>![](figures/plot.png)</code> reference in <code>index.qmd</code> would resolve differently depending on the rendering context:</p>
<ul>
<li>From root: <code>setupquarto/figures/plot.png</code> (works via root symlink)</li>
<li>From <code>analysis/report/</code>: <code>analysis/report/figures/plot.png</code> (fails: no such directory)</li>
</ul>
<p>The report-level symlinks make both contexts resolve to <code>analysis/figures/plot.png</code>.</p>
</section>
<section id="layer-3-the-quarto-site-glob" class="level2">
<h2 class="anchored" data-anchor-id="layer-3-the-quarto-site-glob">Layer 3: The Quarto Site Glob</h2>
<p>At the site level, Quarto discovers posts through the listing in <code>blog/index.qmd</code>:</p>
<div class="sourceCode" id="cb14" style="background: #f1f3f5;"><pre class="sourceCode yaml code-with-copy"><code class="sourceCode yaml"><span id="cb14-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">listing</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span></span>
<span id="cb14-2"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">  </span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">contents</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> </span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"../posts/*/*/index.qmd"</span></span></code></pre></div>
<p>The resolution chain:</p>
<pre><code>blog/index.qmd (listing page)
  |
  +-- glob: ../posts/*/*/index.qmd
      |
      +-- posts/29-setupquarto/    &lt;- lead-in
      |   +-- setupquarto/         &lt;- project
      |       +-- index.qmd        &lt;- symlink
      |           +-- target:
      |               analysis/report/index.qmd
      |               +-- YAML: title, date, ...
      |
      +-- posts/08-palmerpenguins.../
          +-- palmerpenguinspart1/
              +-- index.qmd -&gt;
                  analysis/report/index.qmd</code></pre>
<p>A critical constraint discovered during development: <strong>Quarto’s listing glob does not follow symlinked directories.</strong> If <code>posts/29-setupquarto/</code> were itself a symlink to an external path, Quarto would skip it entirely. The lead-in directory must be a real directory; only the <code>index.qmd</code> file inside may be a symlink. This is why the project lives inside <code>qblog/posts/</code> rather than being symlinked from an external location.</p>
</section>
<section id="why-not-a-single-symlink" class="level2">
<h2 class="anchored" data-anchor-id="why-not-a-single-symlink">Why Not a Single Symlink?</h2>
<p>An earlier design placed project repositories outside the Quarto site (at <code>~/prj/blog/NN-name/name/</code>) and used directory-level symlinks:</p>
<pre><code># DOES NOT WORK:
# Quarto skips symlinked directories
posts/29-setupquarto -&gt;
  ~/prj/blog/29-setupquarto/setupquarto/</code></pre>
<p>Quarto’s internal file discovery skips symlinked directories during glob matching. Individual posts could still be rendered by explicit path (<code>quarto render posts/29-setupquarto/index.qmd</code>), but the blog listing page would not discover them. This forced the current architecture where project directories reside physically inside the qblog tree.</p>
</section>
<section id="image-path-resolution" class="level2">
<h2 class="anchored" data-anchor-id="image-path-resolution">Image Path Resolution</h2>
<p>Images referenced in the <code>.qmd</code> file demonstrate the symlink chain in action. For project-local images stored in <code>analysis/media/images/</code>:</p>
<div class="sourceCode" id="cb17" style="background: #f1f3f5;"><pre class="sourceCode markdown code-with-copy"><code class="sourceCode markdown"><span id="cb17-1"><span class="al" style="color: #AD0000;
background-color: null;
font-style: inherit;">![](media/images/diagram.png)</span></span></code></pre></div>
<p>This resolves through the report-level symlink: <code>media -&gt; ../media -&gt; analysis/media/images/diagram.png</code>.</p>
</section>
<section id="creating-the-symlinks" class="level2">
<h2 class="anchored" data-anchor-id="creating-the-symlinks">Creating the Symlinks</h2>
<p>The migration script creates both layers:</p>
<div class="sourceCode" id="cb18" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb18-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Root-level symlinks</span></span>
<span id="cb18-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">ln</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-s</span> analysis/report/index.qmd index.qmd</span>
<span id="cb18-3"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">ln</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-s</span> analysis/data data</span>
<span id="cb18-4"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">ln</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-s</span> analysis/figures figures</span>
<span id="cb18-5"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">ln</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-s</span> analysis/media media</span>
<span id="cb18-6"></span>
<span id="cb18-7"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Report-level symlinks</span></span>
<span id="cb18-8"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">cd</span> analysis/report</span>
<span id="cb18-9"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">ln</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-s</span> ../data data</span>
<span id="cb18-10"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">ln</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-s</span> ../figures figures</span>
<span id="cb18-11"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">ln</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-s</span> ../media media</span></code></pre></div>
<p>These are relative symlinks, which is critical for portability. An absolute symlink (<code>/Users/zenn/Dropbox/...</code>) would break on any other machine. Relative symlinks survive cloning, moving the project, and CI/CD environments.</p>
</section>
<section id="verifying-symlink-integrity" class="level2">
<h2 class="anchored" data-anchor-id="verifying-symlink-integrity">Verifying Symlink Integrity</h2>
<p>To confirm symlinks are correctly configured:</p>
<div class="sourceCode" id="cb19" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb19-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># From the project root:</span></span>
<span id="cb19-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">ls</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-la</span> index.qmd data figures media</span>
<span id="cb19-3"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Should show -&gt; targets</span></span>
<span id="cb19-4"></span>
<span id="cb19-5"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># From analysis/report/:</span></span>
<span id="cb19-6"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">ls</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-la</span> data figures media</span>
<span id="cb19-7"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Should show -&gt; ../data, ../figures, ../media</span></span>
<span id="cb19-8"></span>
<span id="cb19-9"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Verify resolution:</span></span>
<span id="cb19-10"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">file</span> index.qmd</span>
<span id="cb19-11"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Should show: "symbolic link to ..."</span></span>
<span id="cb19-12"></span>
<span id="cb19-13"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Verify the target exists:</span></span>
<span id="cb19-14"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">readlink</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-f</span> index.qmd</span>
<span id="cb19-15"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Should show the absolute path to the actual file</span></span></code></pre></div>
</section>
<section id="things-to-watch-out-for" class="level2">
<h2 class="anchored" data-anchor-id="things-to-watch-out-for">Things to Watch Out For</h2>
<ol type="1">
<li><p><strong>Symlinks on Windows.</strong> Windows requires developer mode or administrative privileges to create symbolic links. If collaborators use Windows, document this requirement prominently or provide a setup script that checks permissions.</p></li>
<li><p><strong>Dropbox follows symlinks.</strong> Dropbox does not sync symlinks as symlinks; it follows them and syncs the target content. The <code>index.qmd</code> symlink at the project root becomes a regular file on other Dropbox-connected machines. Treat git as the canonical source, not Dropbox.</p></li>
<li><p><strong>Quarto skips symlinked directories.</strong> This was the most time-consuming discovery in the entire project. Directory-level symlinks in <code>posts/</code> are invisible to the blog listing glob. Only file-level symlinks work.</p></li>
<li><p><strong><code>git config core.symlinks</code>.</strong> Some git configurations on Windows disable symlink support. Collaborators may need to run <code>git config core.symlinks true</code> to restore correct behavior after cloning.</p></li>
<li><p><strong>Freeze cache and Docker.</strong> Quarto’s <code>freeze</code> feature caches rendered output outside the Docker container. If a post executes R code during rendering (rather than loading pre-computed results), the freeze cache may produce inconsistent results between local and CI environments.</p></li>
</ol>
</section>
</section>
<section id="daily-workflow" class="level1">
<h1>Daily Workflow</h1>
<table class="caption-top table">
<colgroup>
<col style="width: 52%">
<col style="width: 47%">
</colgroup>
<thead>
<tr class="header">
<th>Command</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><code>make r</code></td>
<td>Enter the Docker container</td>
</tr>
<tr class="even">
<td><code>make docker-build</code></td>
<td>Rebuild the Docker image</td>
</tr>
<tr class="odd">
<td><code>make docker-post-render</code></td>
<td>Render the post inside Docker</td>
</tr>
<tr class="even">
<td><code>make check-renv</code></td>
<td>Validate renv.lock against code</td>
</tr>
<tr class="odd">
<td><code>quarto render index.qmd --to html</code></td>
<td>Render locally (outside Docker)</td>
</tr>
<tr class="even">
<td><code>quarto preview</code></td>
<td>Live preview with auto-reload</td>
</tr>
</tbody>
</table>
</section>
<section id="uninstall-rollback" class="level1">
<h1>Uninstall / Rollback</h1>
<p>To remove the zzcollab scaffolding from a post:</p>
<div class="sourceCode" id="cb20" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb20-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 1. Remove Docker artifacts</span></span>
<span id="cb20-2"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">docker</span> rmi <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$(</span><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">docker</span> images <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-q</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span>post-image<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">)</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span>/dev/null</span>
<span id="cb20-3"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">rm</span> Dockerfile Makefile</span>
<span id="cb20-4"></span>
<span id="cb20-5"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 2. Remove renv</span></span>
<span id="cb20-6"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">rm</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-ri</span> renv/ renv.lock .Rprofile</span>
<span id="cb20-7"></span>
<span id="cb20-8"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 3. Remove symlinks (keeps the target files intact)</span></span>
<span id="cb20-9"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">rm</span> index.qmd data figures media</span>
<span id="cb20-10"></span>
<span id="cb20-11"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 4. Move index.qmd out of analysis/report/ to the root</span></span>
<span id="cb20-12"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">cp</span> analysis/report/index.qmd ./index.qmd</span></code></pre></div>
<p>The post’s <code>.qmd</code> content is unchanged; only the reproducibility scaffolding is removed.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://rgtlab.org/posts/zc-quarto-compendium-intro/media/images/ambiance3.png" class="img-fluid figure-img"></p>
<figcaption>UCSD Geisel Library, a space for focused research</figcaption>
</figure>
</div>
<p><em>Academic research and technical writing share a common requirement: disciplined organization of complex materials.</em></p>
</section>
<section id="what-did-we-learn" class="level1">
<h1>What Did We Learn?</h1>
<section id="lessons-learnt" class="level2">
<h2 class="anchored" data-anchor-id="lessons-learnt">Lessons Learnt</h2>
<p><strong>Conceptual Understanding:</strong></p>
<ul>
<li>Reproducibility for blog posts is not merely about sharing code. It requires packaging the entire computational environment: R version, system libraries, package versions, and data provenance.</li>
<li>The lead-in pattern (numbered directory containing a git repository and an archive folder) provides a clean separation between version-controlled and exploratory material.</li>
<li>Quarto’s glob-based post discovery is powerful but has a critical limitation: it does not follow symlinked directories. Understanding this constraint drove the entire architecture.</li>
<li>Each blog post as its own repository provides independence at the cost of administrative overhead. For a lab producing dozens of posts, this trade-off is worth explicit consideration.</li>
</ul>
<p><strong>Technical Skills:</strong></p>
<ul>
<li>Creating and verifying dual-layer relative symlinks that work across rendering contexts (project root vs.&nbsp;<code>analysis/report/</code>).</li>
<li>Configuring Quarto listing globs with the double wildcard pattern (<code>posts/*/*/index.qmd</code>) to match the lead-in/project nesting.</li>
<li>Setting up CI/CD pipelines that build Docker images, restore <code>renv</code> environments, and render Quarto reports in a single workflow.</li>
<li>Using <code>git ls-files -s</code> to verify that git stores symlinks with the <code>120000</code> mode and the correct relative target path.</li>
</ul>
<p><strong>Gotchas and Pitfalls:</strong></p>
<ul>
<li>Absolute symlinks break portability. Always use relative paths when creating symlinks, even if the absolute path seems more readable during initial setup.</li>
<li>A full site render across 42+ posts is prohibitively slow. In practice, render only the modified post locally and rely on CI/CD for validation.</li>
<li>The <code>renv.lock</code> must be committed to each post repository individually. A shared lockfile across posts defeats the purpose of independent dependency management.</li>
<li>Dropbox silently converts symlinks to regular files on sync. Never rely on Dropbox as the primary synchronization mechanism for projects that use symlinks.</li>
</ul>
</section>
<section id="limitations" class="level2">
<h2 class="anchored" data-anchor-id="limitations">Limitations</h2>
<ul>
<li><strong>Symlink fragility across operating systems.</strong> macOS and Linux handle symlinks transparently; Windows requires special configuration. This limits cross-platform collaboration without additional setup documentation.</li>
<li><strong>Repository proliferation.</strong> With 42 posts, this workflow creates 42 GitHub repositories. Actions minutes, Dependabot alerts, and repository settings must be managed individually.</li>
<li><strong>Quarto freeze interaction.</strong> The freeze cache lives outside the Docker container and may produce inconsistent results if posts execute R code during rendering rather than loading pre-computed outputs.</li>
<li><strong>Dropbox and symlinks are incompatible.</strong> Dropbox follows symlinks and syncs target content, making git the only reliable synchronization mechanism.</li>
<li><strong>Rendering cost.</strong> A full site render requires entering each post’s Docker container, restoring packages, and executing Quarto. For 42 posts, this is prohibitively slow without selective rendering.</li>
<li><strong>Initial setup overhead.</strong> The scaffold, symlinks, CI/CD workflow, and renv initialization require non-trivial effort for each new post, though this is amortized over the post’s lifetime.</li>
</ul>
</section>
<section id="opportunities-for-improvement" class="level2">
<h2 class="anchored" data-anchor-id="opportunities-for-improvement">Opportunities for Improvement</h2>
<ol type="1">
<li><strong>Automate post scaffolding.</strong> A single CLI command (<code>zzc blog 43-newpost</code>) could create the lead-in directory, initialize the ZZCOLLAB project, create both symlink layers, and push an initial commit to GitHub.</li>
<li><strong>Shared Dockerfile caching.</strong> Posts that use the same base profile (e.g., <code>ubuntu_x11_analysis</code>) could share a pre-built Docker image from a container registry, eliminating redundant builds.</li>
<li><strong>Selective site rendering.</strong> A script that detects which posts have changed since the last render and re-renders only those would make full-site builds feasible.</li>
<li><strong>Symlink validation hook.</strong> A pre-commit git hook could verify that all expected symlinks exist and point to valid targets, catching broken links before they reach CI/CD.</li>
<li><strong>Centralized dependency dashboard.</strong> A monitoring tool that scans all 42 renv.lock files and flags posts using outdated or vulnerable package versions would simplify maintenance.</li>
<li><strong>Template compliance checker.</strong> An automated script that validates YAML front matter fields, symlink integrity, and directory structure against the ZZCOLLAB blog post specification.</li>
</ol>
</section>
</section>
<section id="wrapping-up" class="level1">
<h1>Wrapping Up</h1>
<p>This post documented a workflow for treating each blog post as a standalone reproducible research project using ZZCOLLAB, Docker, renv, and Quarto. The architecture emerged from a concrete frustration: returning to a post that no longer rendered and having no reliable way to reconstruct the environment that originally produced it.</p>
<p>The workflow is not without costs. Each post generates its own GitHub repository, requires its own Docker image, and demands its own CI/CD pipeline. The initial setup overhead is real. But the payoff is substantial: any post can be cloned and reproduced by anyone, at any time, without guessing at package versions or system dependencies.</p>
<p>In conclusion, five points merit emphasis. First, the lead-in pattern (<code>NN-name/name/</code>) cleanly separates git repositories from archival material. Second, dual-layer relative symlinks reconcile Quarto’s discovery mechanism with the rrtools/ZZCOLLAB directory convention. Third, Quarto’s listing glob does not follow symlinked directories: this constraint drove the entire architecture. Fourth, the five pillars (Dockerfile, renv.lock, .Rprofile, source code, data) provide a complete reproducibility contract for each post. Fifth, CI/CD catches breakage on push rather than months later when a reader reports a problem.</p>
</section>
<section id="appendix-rrtools-as-the-predecessor-pattern" class="level1">
<h1>Appendix: rrtools as the Predecessor Pattern</h1>
<p>ZZCOLLAB did not appear in a vacuum. The compendium pattern documented above is a refinement of the <strong>rrtools</strong> package introduced by Marwick, Boettiger, and Mullen (2018), which in turn operationalised the ‘research compendium’ construct introduced by Gentleman and Temple Lang (2007). This appendix preserves the rrtools framing for readers who encounter the older pattern in the literature or in existing project repositories, and clarifies what zzcollab adds.</p>
<section id="the-problem-rrtools-and-zzcollab-tried-to-solve" class="level2">
<h2 class="anchored" data-anchor-id="the-problem-rrtools-and-zzcollab-tried-to-solve">The problem rrtools (and zzcollab) tried to solve</h2>
<p>When sharing R code with a collaborator, several predictable failure modes arise: different R versions on each machine; mismatched package versions; missing system dependencies (pandoc, LaTeX, image libraries); missing supplementary files referenced by the analysis (bibliography files, LaTeX preambles, datasets, images); and collaborator-specific R startup configurations (<code>.Rprofile</code>, <code>.Renviron</code>).</p>
<p>A real-world scenario unfolds like this:</p>
<ol type="1">
<li>The author emails an R Markdown file to a colleague, Joe.</li>
<li>Joe attempts to run it with <code>R -e "source('peng1.Rmd')"</code>.</li>
<li>R is not installed on Joe’s system.</li>
<li>After installing R, Joe gets an error: ‘could not find function <code>render</code>’.</li>
<li>Joe installs the rmarkdown package.</li>
<li>Now pandoc is missing.</li>
<li>After installing pandoc, a required package is missing.</li>
<li>After installing the package, supplementary files are missing (bibliography, images).</li>
<li>The cycle continues until both parties give up or one party invests an afternoon in environment debugging.</li>
</ol>
<p>The rrtools framework addressed this by defining a fixed research-compendium directory layout (<code>R/</code>, <code>data/</code>, <code>vignettes/</code>, <code>analysis/</code>) and pairing it with a Dockerfile that pinned the R version, the system libraries, and the package versions. The compendium directory was itself an R package, which gave it a <code>DESCRIPTION</code> file as a canonical manifest of dependencies.</p>
</section>
<section id="what-zzcollab-adds" class="level2">
<h2 class="anchored" data-anchor-id="what-zzcollab-adds">What zzcollab adds</h2>
<p>The zzcollab framework retains rrtools’s directory layout and Dockerfile-plus-DESCRIPTION pattern, and extends it on three axes:</p>
<ol type="1">
<li><strong>Profile-based base images.</strong> rrtools assumed each project would author its own Dockerfile from <code>rocker/r-ver</code> or similar. zzcollab provides named profiles (<code>minimal</code>, <code>analysis</code>, <code>modeling</code>, <code>publishing</code>, <code>shiny</code>) with pre-built base images, reducing the per-project Docker work to selecting a profile.</li>
<li><strong>Renv integration as a first-class layer.</strong> The original rrtools approach used <code>DESCRIPTION</code> for dependency declaration; zzcollab additionally pins exact package versions via <code>renv.lock</code>. This is a stricter reproducibility contract.</li>
<li><strong>Make-driven workflow.</strong> zzcollab projects ship a <code>Makefile</code> that exposes <code>make r</code> (enter the container), <code>make check-renv</code> (validate package state), <code>make test</code>, and <code>make docker-render-qmd</code> as the canonical entry points, rather than requiring the analyst to remember per-project Docker commands.</li>
</ol>
<p>For readers maintaining an existing rrtools project, the migration to zzcollab is mechanical (copy the project’s <code>R/</code>, <code>analysis/</code>, <code>data/</code> into a new zzcollab scaffold) and reversible. The two patterns coexist: a project on rrtools still satisfies the compendium-tier requirements of the Workflow Construct described in <a href="../../posts/52-workflow-construct/">post 52</a>; zzcollab is the construct’s recommended implementation of that tier in 2026, but rrtools remains a valid alternative.</p>
</section>
</section>
<section id="see-also" class="level1">
<h1>See Also</h1>
<section id="related-posts" class="level3">
<h3 class="anchored" data-anchor-id="related-posts">Related Posts</h3>
<ul>
<li><a href="../../posts/39-templatepost/templatepost/">Template Post: The ZZCOLLAB Blog Post Exemplar</a> (the reference implementation for this workflow)</li>
<li><a href="../../posts/01-configtermzsh/configtermzsh/">Configure the Command Line for Data Science Development</a> (terminal and shell setup for the development environment)</li>
<li><a href="../../posts/24-setupdotfilesongithub/setupdotfilesongithub/">Setting Up Dotfiles on GitHub</a> (configuration management for development tools)</li>
</ul>
</section>
<section id="key-resources" class="level3">
<h3 class="anchored" data-anchor-id="key-resources">Key Resources</h3>
<ul>
<li>Marwick, B., Boettiger, C., &amp; Mullen, L. (2018). Packaging Data Analytical Work Reproducibly Using R (and Friends). <em>The American Statistician</em>, 72(1), 80–88.</li>
<li>ZZCOLLAB documentation: <code>~/prj/sfw/07-zzcollab/zzcollab/docs/</code></li>
<li><a href="https://quarto.org/docs/websites/website-blog.html">Quarto Blog Documentation</a></li>
<li><a href="https://rocker-project.org/">The rocker Project</a></li>
<li><a href="https://rstudio.github.io/renv/">renv documentation</a></li>
</ul>
</section>
</section>
<section id="reproducibility" class="level1">
<h1>Reproducibility</h1>
<p><strong>Environment</strong>: R 4.5.1, Quarto 1.6+, Docker (rocker/tidyverse:4.5.1)</p>
<p><strong>Project template</strong>: <code>posts/39-templatepost/</code></p>
<p><strong>Site configuration</strong>: <code>_quarto.yml</code> at repository root</p>
<p><strong>Migration script</strong>: <code>migrate_posts.sh</code> at repository root (converts flat post directories to lead-in/project structure)</p>
<div class="cell">
<div class="sourceCode cell-code" id="cb21" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb21-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sessionInfo</span>()</span></code></pre></div>
<div class="cell-output cell-output-stdout">
<pre><code>R version 4.5.3 (2026-03-11)
Platform: aarch64-apple-darwin25.3.0
Running under: macOS Tahoe 26.5

Matrix products: default
BLAS:   /opt/homebrew/Cellar/openblas/0.3.32/lib/libopenblasp-r0.3.32.dylib 
LAPACK: /opt/homebrew/Cellar/r/4.5.3/lib/R/lib/libRlapack.dylib;  LAPACK version 3.12.1

locale:
[1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8

time zone: America/Los_Angeles
tzcode source: internal

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

loaded via a namespace (and not attached):
 [1] htmlwidgets_1.6.4 compiler_4.5.3    fastmap_1.2.0     cli_3.6.6        
 [5] tools_4.5.3       htmltools_0.5.8.1 parallel_4.5.3    yaml_2.3.10      
 [9] rmarkdown_2.29    knitr_1.50        jsonlite_2.0.0    xfun_0.56        
[13] digest_0.6.37     rlang_1.2.0       evaluate_1.0.5   </code></pre>
</div>
</div>
</section>
<section id="lets-connect" class="level1">
<h1>Let’s Connect</h1>
<ul>
<li><strong>GitHub:</strong> <a href="https://github.com/rgt47">rgt47</a></li>
<li><strong>Twitter/X:</strong> <a href="https://twitter.com/rgt47"><span class="citation" data-cites="rgt47">@rgt47</span></a></li>
<li><strong>LinkedIn:</strong> <a href="https://linkedin.com/in/rgthomaslab">Ronald Glenn Thomas</a></li>
<li><strong>Email:</strong> <a href="https://rgtlab.org/contact">rgtlab.org/contact</a></li>
</ul>
<p>I would enjoy hearing from you if:</p>
<ul>
<li>You spot an error or a better approach to any of the code in this post.</li>
<li>You have suggestions for topics you would like to see covered.</li>
<li>You want to discuss R programming, data science, or reproducible research.</li>
<li>You have questions about anything in this tutorial.</li>
<li>You just want to say hello and connect.</li>
</ul>
<section id="related-posts-in-this-cluster" class="level2">
<h2 class="anchored" data-anchor-id="related-posts-in-this-cluster">Related posts in this cluster</h2>
<p>This post is part of the <em>ZZCOLLAB Reproducible Compendia</em> series. Recommended reading order:</p>
<ol type="1">
<li><strong>Post 01: Reproducible Blog Posts with ZZCOLLAB</strong> (this post)</li>
<li>Post 02: <a href="../02-zc-blog-post-template/">Constructing a reproducible blog post using zzcollab tools</a></li>
<li>Post 03: <a href="../03-zc-markdown-to-blog-workflow/">From Markdown to Blog Post: A ZZCOLLAB workflow</a></li>
<li>Post 04: <a href="../04-zc-share-rmd-via-docker/">Sharing R Code via Docker: R Markdown Reports</a></li>
<li>Post 05: <a href="../05-zc-analysis-initiation-checklist/">A 55-Item Initiation Checklist for zzcollab Data Analyses</a></li>
<li>Post 06: <a href="../06-zc-manuscript-report-elements/">Seven Required Elements for a zzc Manuscript report.Rmd</a></li>
<li>Post 07: <a href="../07-zc-tiered-ci-strategy/">A tiered CI strategy for zzcollab research compendia</a></li>
<li>Post 08: <a href="../08-zc-github-actions-workflows/">GitHub Actions workflows for zzcollab research compendia</a></li>
</ol>


</section>
</section>

 ]]></description>
  <category>quarto</category>
  <category>r</category>
  <category>docker</category>
  <category>reproducibility</category>
  <category>zzcollab-compendia</category>
  <guid>https://rgtlab.org/posts/zc-quarto-compendium-intro/</guid>
  <pubDate>Tue, 10 Feb 2026 08:00:00 GMT</pubDate>
  <media:content url="https://rgtlab.org/posts/zc-quarto-compendium-intro/media/images/hero.png" medium="image" type="image/png" height="80" width="144"/>
</item>
</channel>
</rss>
