<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-us"><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://ofek.dev/feed.xml" rel="self" type="application/atom+xml" /><link href="https://ofek.dev/" rel="alternate" type="text/html" hreflang="en-us" /><updated>2026-04-05T18:26:50+00:00</updated><id>https://ofek.dev/feed.xml</id><title type="html">Ofek Lev</title><subtitle>Software Engineer from Knowhere</subtitle><author><name>Ofek Lev</name><email>human@ofek.dev</email></author><entry><title type="html">Distributing command line tools for macOS</title><link href="https://ofek.dev/words/guides/2025-05-13-distributing-command-line-tools-for-macos/" rel="alternate" type="text/html" title="Distributing command line tools for macOS" /><published>2025-05-13T19:30:00+00:00</published><updated>2026-04-05T18:25:56+00:00</updated><id>https://ofek.dev/words/guides/distributing-command-line-tools-for-macos</id><content type="html" xml:base="https://ofek.dev/words/guides/2025-05-13-distributing-command-line-tools-for-macos/"><![CDATA[<p>In this post I’ll show how to properly distribute a command line tool for macOS.</p>

<ul id="markdown-toc">
  <li><a href="#background" id="markdown-toc-background">Background</a></li>
  <li><a href="#prerequisites" id="markdown-toc-prerequisites">Prerequisites</a></li>
  <li><a href="#code-signing-certificates" id="markdown-toc-code-signing-certificates">Code signing certificates</a>    <ul>
      <li><a href="#developer-id-application" id="markdown-toc-developer-id-application">Developer ID Application</a></li>
      <li><a href="#developer-id-installer" id="markdown-toc-developer-id-installer">Developer ID Installer</a></li>
    </ul>
  </li>
  <li><a href="#app-store-connect-api-key" id="markdown-toc-app-store-connect-api-key">App Store Connect API key</a></li>
  <li><a href="#process-binaries" id="markdown-toc-process-binaries">Process binaries</a></li>
  <li><a href="#create-installer-package" id="markdown-toc-create-installer-package">Create installer package</a>    <ul>
      <li><a href="#universal-binary" id="markdown-toc-universal-binary">Universal binary (optional)</a></li>
      <li><a href="#preparation" id="markdown-toc-preparation">Preparation</a></li>
      <li><a href="#component-package" id="markdown-toc-component-package">Component package</a></li>
      <li><a href="#distribution-package" id="markdown-toc-distribution-package">Distribution package</a></li>
    </ul>
  </li>
  <li><a href="#process-installer-package" id="markdown-toc-process-installer-package">Process installer package</a></li>
  <li><a href="#secret-storage" id="markdown-toc-secret-storage">Secret storage</a></li>
</ul>

<h2 id="background">Background</h2>

<p>I maintain many command line tools at work and for personal projects, like <a href="https://github.com/pypa/hatch">Hatch</a>. A good user experience requires that installation is easy and reliable on all platforms, and I’ve found that macOS is the most difficult to support.</p>

<p>This guide will not cover how to get your project on <a href="https://brew.sh">Homebrew</a> as that is already fairly well documented with plenty of examples. Instead, I will assume you want to distribute your project as both a standalone binary and an <a href="https://developer.apple.com/documentation/xcode/packaging-mac-software-for-distribution">installer package</a> (<code class="language-plaintext highlighter-rouge">.pkg</code> file).</p>

<p>I’ll assume the CLI has already been built and is called <code class="language-plaintext highlighter-rouge">rusty</code> (harkening back to a <a href="/words/guides/2022-11-19-writing-a-cli-in-rust/">previous post</a>).</p>

<h2 id="prerequisites">Prerequisites</h2>

<ul>
  <li>An <a href="https://developer.apple.com">Apple developer account</a></li>
  <li><a href="https://github.com/openssl/openssl">OpenSSL</a> available on PATH as <code class="language-plaintext highlighter-rouge">openssl</code></li>
  <li><a href="https://gregoryszorc.com/docs/apple-codesign/stable/">apple-codesign</a> available on PATH as <code class="language-plaintext highlighter-rouge">rcodesign</code></li>
  <li>A macOS machine with <a href="https://developer.apple.com/xcode/resources/">Command Line Tools for Xcode</a></li>
</ul>

<p class="note"><a href="#create-installer-package">Creating the installer package</a> is the only part that requires being on macOS.</p>

<h2 id="code-signing-certificates">Code signing certificates</h2>

<p>The standalone binary and installer package each need to be signed or else <a href="https://en.wikipedia.org/wiki/Gatekeeper_(macOS)">Gatekeeper</a> will block them.</p>

<p>We will need to create two types of signing certificates:</p>

<ul>
  <li>One for the binary itself called a <code class="language-plaintext highlighter-rouge">Developer ID Application</code></li>
  <li>One for the installer package called a <code class="language-plaintext highlighter-rouge">Developer ID Installer</code></li>
</ul>

<p>The process is the same for both:</p>

<ol>
  <li>Create a <a href="https://en.wikipedia.org/wiki/Certificate_signing_request">Certificate Signing Request</a> (CSR) from a private key</li>
  <li>Upload the CSR to Apple</li>
  <li>Download the certificate from Apple</li>
</ol>

<p>Go to the <a href="https://developer.apple.com/account">Apple Developer Portal</a> and click on the <code class="language-plaintext highlighter-rouge">Certificates</code> link:</p>

<p><img src="/assets/img/posts/cli-macos/portal-cert.png" alt="Apple Developer Portal landing page - certificates" loading="lazy" /></p>

<h3 id="developer-id-application">Developer ID Application</h3>

<p>Create a private key:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>openssl genrsa -out private_key_application.pem 2048
</code></pre></div></div>

<p>Create a CSR from the private key:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rcodesign generate-certificate-signing-request --pem-source private_key_application.pem --csr-pem-path csr.pem
</code></pre></div></div>

<p>Start creating the <code class="language-plaintext highlighter-rouge">Developer ID Application</code> certificate:</p>

<p><img src="/assets/img/posts/cli-macos/cert-create.png" alt="Create certificate" loading="lazy" /></p>

<p><img src="/assets/img/posts/cli-macos/cert-id-application.png" alt="Choose Developer ID Application" loading="lazy" /></p>

<p>Choose the <code class="language-plaintext highlighter-rouge">G2 Sub-CA</code> option and upload the <code class="language-plaintext highlighter-rouge">csr.pem</code> file:</p>

<p><img src="/assets/img/posts/cli-macos/cert-csr.png" alt="Upload CSR" loading="lazy" /></p>

<p>You should see the following page with the ability to download the certificate:</p>

<p><img src="/assets/img/posts/cli-macos/cert-final-id-application.png" alt="Download Developer ID Application certificate" loading="lazy" /></p>

<p>The certificate you download is in the binary <a href="https://en.wikipedia.org/wiki/X.690#DER_encoding">DER format</a> (<code class="language-plaintext highlighter-rouge">.cer</code>), which isn’t very portable. Let’s turn it into a <a href="https://en.wikipedia.org/wiki/Privacy-Enhanced_Mail">PEM</a> file:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>openssl x509 -in developerID_application.cer -inform DER -out certificate_application.pem -outform PEM
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">private_key_application.pem</code> and <code class="language-plaintext highlighter-rouge">certificate_application.pem</code> files are all we need, permanently delete the following files:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">csr.pem</code></li>
  <li><code class="language-plaintext highlighter-rouge">developerID_application.cer</code></li>
</ul>

<h3 id="developer-id-installer">Developer ID Installer</h3>

<p>The process is the same as above, first create a CSR:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>openssl genrsa -out private_key_installer.pem 2048
rcodesign generate-certificate-signing-request --pem-source private_key_installer.pem --csr-pem-path csr.pem
</code></pre></div></div>

<p>Then start the <code class="language-plaintext highlighter-rouge">Developer ID Installer</code> certificate creation process:</p>

<p><img src="/assets/img/posts/cli-macos/cert-create.png" alt="Create certificate" loading="lazy" /></p>

<p><img src="/assets/img/posts/cli-macos/cert-id-installer.png" alt="Choose Developer ID Installer" loading="lazy" /></p>

<p>Upload the CSR:</p>

<p><img src="/assets/img/posts/cli-macos/cert-csr.png" alt="Upload CSR" loading="lazy" /></p>

<p>Download the certificate:</p>

<p><img src="/assets/img/posts/cli-macos/cert-final-id-installer.png" alt="Download Developer ID Installer certificate" loading="lazy" /></p>

<p>Finalize the certificate:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>openssl x509 -in developerID_installer.cer -inform DER -out certificate_installer.pem -outform PEM
</code></pre></div></div>

<p>Permanently delete the following files:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">csr.pem</code></li>
  <li><code class="language-plaintext highlighter-rouge">developerID_installer.cer</code></li>
</ul>

<h2 id="app-store-connect-api-key">App Store Connect API key</h2>

<p>Next we need to create an API key for Apple’s <a href="https://developer.apple.com/documentation/notaryapi/submitting-software-for-notarization-over-the-web">notary service</a>. Like code signing, notarization is also required by <a href="https://en.wikipedia.org/wiki/Gatekeeper_(macOS)">Gatekeeper</a>.</p>

<p>Go back to the <a href="https://developer.apple.com/account">Apple Developer Portal</a> and click on the <code class="language-plaintext highlighter-rouge">Users and Access</code> link:</p>

<p><img src="/assets/img/posts/cli-macos/portal-api-key.png" alt="Apple Developer Portal landing page - users and access" loading="lazy" /></p>

<p>Click on the <code class="language-plaintext highlighter-rouge">Integrations</code> tab and then the <code class="language-plaintext highlighter-rouge">App Store Connect API</code> option under the <code class="language-plaintext highlighter-rouge">Keys</code> section to begin the creation process. Make sure to copy the <code class="language-plaintext highlighter-rouge">Issuer ID</code> as we will need that later.</p>

<p><img src="/assets/img/posts/cli-macos/api-key-create.png" alt="App Store Connect API key creation start" loading="lazy" /></p>

<p>Choose a name for the key and make sure it has <code class="language-plaintext highlighter-rouge">Developer</code> access:</p>

<p><img src="/assets/img/posts/cli-macos/api-key-form.png" alt="App Store Connect API key form" loading="lazy" /></p>

<p>Download the key and also copy the <code class="language-plaintext highlighter-rouge">Key ID</code> as we will need that later:</p>

<p><img src="/assets/img/posts/cli-macos/api-key-download.png" alt="App Store Connect API key download" loading="lazy" /></p>

<p>The downloaded <a href="https://en.wikipedia.org/wiki/PKCS_8">private key</a> will be named <code class="language-plaintext highlighter-rouge">AuthKey_&lt;Key ID&gt;.p8</code>. Run the following command to save the information thus far into a single JSON file for better portability, with the <code class="language-plaintext highlighter-rouge">&lt;Issuer ID&gt;</code> and <code class="language-plaintext highlighter-rouge">&lt;Key ID&gt;</code> placeholders replaced by the values you copied earlier:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rcodesign encode-app-store-connect-api-key -o app_store_connect_api_key.json "&lt;Issuer ID&gt;" "&lt;Key ID&gt;" "AuthKey_&lt;Key ID&gt;.p8"
</code></pre></div></div>

<p>This will create a file called <code class="language-plaintext highlighter-rouge">app_store_connect_api_key.json</code> with the following content:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"issuer_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"&lt;Issuer ID&gt;"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"key_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"&lt;Key ID&gt;"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"private_key"</span><span class="p">:</span><span class="w"> </span><span class="s2">"..."</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Permanently delete the <code class="language-plaintext highlighter-rouge">AuthKey_&lt;Key ID&gt;.p8</code> file and wherever you noted the <code class="language-plaintext highlighter-rouge">Issuer ID</code> and <code class="language-plaintext highlighter-rouge">Key ID</code> (although the latter two are not sensitive data).</p>

<h2 id="process-binaries">Process binaries</h2>

<p>Now that we have the necessary Apple credentials, we can sign and notarize the binaries. All binaries must go through the following steps.</p>

<p>First, sign each binary in-place using the <code class="language-plaintext highlighter-rouge">Developer ID Application</code> certificate and associated private key:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rcodesign sign --pem-source certificate_application.pem --pem-source private_key_application.pem --code-signature-flags runtime rusty
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">--code-signature-flags runtime</code> enables the <a href="https://developer.apple.com/documentation/security/hardened-runtime">Hardened Runtime</a> capability, which is a requirement for notarization.</p>

<p>Then, submit the binary to Apple’s notary service:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rcodesign notary-submit --api-key-path app_store_connect_api_key.json rusty
</code></pre></div></div>

<p class="note" title="Tip">I’ve encountered a fair amount of flakiness with Apple’s notary service so I’d recommend setting the <code class="language-plaintext highlighter-rouge">--max-wait-seconds</code> to something quite long like 3600 seconds (1 hour). Additionally, you may even want to run such notarization steps in CI multiple times after you get everything set up once.</p>

<h2 id="create-installer-package">Create installer package</h2>

<p class="note">These steps require being on macOS.</p>

<h3 id="universal-binary">Universal binary (optional)</h3>

<p>If you wish to support users on both <a href="https://en.wikipedia.org/wiki/Apple_silicon">Apple Silicon</a> and the older Intel architectures, you will need to build a <a href="https://en.wikipedia.org/wiki/Universal_binary">universal binary</a>. Assuming you have the following binaries:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">rusty-aarch64-apple-darwin</code></li>
  <li><code class="language-plaintext highlighter-rouge">rusty-x86_64-apple-darwin</code></li>
</ul>

<p>You can create a universal binary with the following <a href="https://www.unix.com/man_page/osx/1/lipo/"><code class="language-plaintext highlighter-rouge">lipo</code></a> command:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>lipo -create -output rusty-universal rusty-aarch64-apple-darwin rusty-x86_64-apple-darwin
</code></pre></div></div>

<p>This will create a <code class="language-plaintext highlighter-rouge">rusty-universal</code> binary that can be used on both architectures and is what the installer package will contain.</p>

<p>Finally, ensure the binary has the correct permissions:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>chmod 755 rusty-universal
</code></pre></div></div>

<h3 id="preparation">Preparation</h3>

<p>The following values will be used to create the installer package:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">&lt;IDENTIFIER&gt;</code>: The ID of the distribution, usually the <a href="https://en.wikipedia.org/wiki/Reverse_domain_name_notation">reverse-DNS</a> of the project e.g. <code class="language-plaintext highlighter-rouge">com.example.rusty</code>. This can be whatever you want as long as it uniquely refers to the project.</li>
  <li><code class="language-plaintext highlighter-rouge">&lt;VERSION&gt;</code>: The version of the project e.g. <code class="language-plaintext highlighter-rouge">1.2.3</code>.</li>
</ul>

<p>If your project has a logo, I’d recommend creating an image that will be displayed on the installation window’s sidebar. The image’s height should be approximately 2.5x its width. The dimensions I use for Hatch and other projects’ macOS installer image are 1390x3680 (<a href="https://raw.githubusercontent.com/pypa/hatch/refs/heads/master/release/macos/pkg/resources/icon.png">example</a>).</p>

<p>Finally, create a temporary directory whose absolute path we will refer to as <code class="language-plaintext highlighter-rouge">&lt;TEMP_DIR&gt;</code>.</p>

<h3 id="component-package">Component package</h3>

<p>We need to create a component <a href="https://en.wikipedia.org/wiki/Package_(macOS)">package</a> with a structure that mimics the desired installation structure. A component package is a file ending in <code class="language-plaintext highlighter-rouge">.pkg</code> that will be nested inside the actual <code class="language-plaintext highlighter-rouge">.pkg</code> installer package.</p>

<p>Create a directory <code class="language-plaintext highlighter-rouge">&lt;TEMP_DIR&gt;/root</code>, whose absolute path we will refer to as <code class="language-plaintext highlighter-rouge">&lt;ROOT_DIR&gt;</code>. The structure should be as follows:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>root
├── etc
│   └── paths.d
│       └── rusty
└── usr
    └── local
        └── bin
            └── rusty
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">&lt;ROOT_DIR&gt;/usr/local/bin/rusty</code> file should be the standalone binary users will run (or the <a href="#universal-binary">universal binary</a> if you created one).</p>

<p>The <code class="language-plaintext highlighter-rouge">&lt;ROOT_DIR&gt;/etc/paths.d/rusty</code> file should contain the path to the binary on the user’s machine:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/usr/local/bin/rusty
</code></pre></div></div>

<p>Adding this file to <code class="language-plaintext highlighter-rouge">/etc/paths.d</code> will ensure the binary is found when the user runs <code class="language-plaintext highlighter-rouge">rusty</code> from the command line.</p>

<p class="note">Under some circumstances, a user’s shell may require extra configuration to properly respect <a href="https://www.unix.com/man_page/osx/8/path_helper"><code class="language-plaintext highlighter-rouge">path_helper</code></a>. I’ve seen that with <a href="https://github.com/NixOS/nix">Nix</a> users a few times.</p>

<p>Create a directory <code class="language-plaintext highlighter-rouge">&lt;TEMP_DIR&gt;/components</code>, whose absolute path we will refer to as <code class="language-plaintext highlighter-rouge">&lt;COMPONENTS_DIR&gt;</code>. Then run the following command to create the component package, with the placeholders replaced by the values from earlier:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pkgbuild --root "&lt;ROOT_DIR&gt;" --identifier "&lt;IDENTIFIER&gt;" --version "&lt;VERSION&gt;" --install-location / "&lt;COMPONENTS_DIR&gt;/&lt;IDENTIFIER&gt;.pkg"
</code></pre></div></div>

<p>This will create the component package file named <code class="language-plaintext highlighter-rouge">&lt;IDENTIFIER&gt;.pkg</code>.</p>

<h3 id="distribution-package">Distribution package</h3>

<p>The distribution package is the actual installer package that users will download and install. It will contain the <a href="#component-package">component package</a> as well as some metadata.</p>

<p>Create a directory <code class="language-plaintext highlighter-rouge">&lt;TEMP_DIR&gt;/resources</code>, whose absolute path we will refer to as <code class="language-plaintext highlighter-rouge">&lt;RESOURCES_DIR&gt;</code>. The structure should be as follows:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>resources
├── LICENSE.txt
├── README.html
└── icon.png
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">LICENSE.txt</code> file (named however you like) should be the license of what you distribute. I’d recommend simply copying the project’s license file to this location.</p>

<p>The <code class="language-plaintext highlighter-rouge">README.html</code> file will be rendered for the user upon installation. It might contain something like this:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">&lt;!-- file: "index.html" --&gt;</span>
<span class="cp">&lt;!DOCTYPE html&gt;</span>
<span class="nt">&lt;html&gt;</span>
<span class="nt">&lt;head&gt;&lt;/head&gt;</span>
<span class="nt">&lt;body&gt;</span>
  <span class="nt">&lt;p&gt;</span>This will install Rusty v1.2.3 globally.<span class="nt">&lt;/p&gt;</span>

  <span class="nt">&lt;p&gt;</span>For more information, see our <span class="nt">&lt;a</span> <span class="na">href=</span><span class="s">"https://example.com/setup/"</span><span class="nt">&gt;</span>Installation Guide<span class="nt">&lt;/a&gt;</span>.<span class="nt">&lt;/p&gt;</span>
<span class="nt">&lt;/body&gt;</span>
<span class="nt">&lt;/html&gt;</span>
</code></pre></div></div>

<p>The optional <code class="language-plaintext highlighter-rouge">icon.png</code> file should be the image that was <a href="#preparation">noted earlier</a>.</p>

<p>Next, create a <a href="https://developer.apple.com/library/archive/documentation/DeveloperTools/Reference/DistributionDefinitionRef/Chapters/Distribution_XML_Ref.html">distribution definition file</a> <code class="language-plaintext highlighter-rouge">&lt;TEMP_DIR&gt;/distribution.xml</code>. It should contain the following content with the placeholders replaced by the values from earlier:</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">&lt;!-- file: "distribution.xml" --&gt;</span>
<span class="cp">&lt;?xml version="1.0" encoding="utf-8"?&gt;</span>
<span class="nt">&lt;installer-gui-script</span> <span class="na">minSpecVersion=</span><span class="s">"1"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;title&gt;</span>Rusty<span class="nt">&lt;/title&gt;</span>
  <span class="nt">&lt;license</span> <span class="na">file=</span><span class="s">"LICENSE.txt"</span> <span class="na">mime-type=</span><span class="s">"text/plain"</span><span class="nt">/&gt;</span>
  <span class="nt">&lt;readme</span> <span class="na">file=</span><span class="s">"README.html"</span> <span class="na">mime-type=</span><span class="s">"text/html"</span><span class="nt">/&gt;</span>
  <span class="nt">&lt;background</span> <span class="na">mime-type=</span><span class="s">"image/png"</span> <span class="na">file=</span><span class="s">"icon.png"</span> <span class="na">alignment=</span><span class="s">"left"</span> <span class="na">scaling=</span><span class="s">"proportional"</span><span class="nt">/&gt;</span>
  <span class="nt">&lt;background-darkAqua</span> <span class="na">mime-type=</span><span class="s">"image/png"</span> <span class="na">file=</span><span class="s">"icon.png"</span> <span class="na">alignment=</span><span class="s">"left"</span> <span class="na">scaling=</span><span class="s">"proportional"</span><span class="nt">/&gt;</span>
  <span class="nt">&lt;options</span> <span class="na">hostArchitectures=</span><span class="s">"arm64,x86_64"</span> <span class="na">customize=</span><span class="s">"never"</span> <span class="na">require-scripts=</span><span class="s">"false"</span><span class="nt">/&gt;</span>
  <span class="nt">&lt;domains</span> <span class="na">enable_localSystem=</span><span class="s">"true"</span><span class="nt">/&gt;</span>

  <span class="nt">&lt;choices-outline&gt;</span>
    <span class="nt">&lt;line</span> <span class="na">choice=</span><span class="s">"&lt;IDENTIFIER&gt;.choice"</span><span class="nt">/&gt;</span>
  <span class="nt">&lt;/choices-outline&gt;</span>
  <span class="nt">&lt;choice</span> <span class="na">title=</span><span class="s">"Rusty (universal)"</span> <span class="na">id=</span><span class="s">"&lt;IDENTIFIER&gt;.choice"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;pkg-ref</span> <span class="na">id=</span><span class="s">"&lt;IDENTIFIER&gt;.pkg"</span><span class="nt">/&gt;</span>
  <span class="nt">&lt;/choice&gt;</span>

  <span class="nt">&lt;pkg-ref</span> <span class="na">id=</span><span class="s">"&lt;IDENTIFIER&gt;.pkg"</span><span class="nt">&gt;&lt;IDENTIFIER&gt;</span>.pkg<span class="nt">&lt;/pkg-ref&gt;</span>
<span class="nt">&lt;/installer-gui-script&gt;</span>
</code></pre></div></div>

<p class="note">If there is no image, the <code class="language-plaintext highlighter-rouge">background</code> and <code class="language-plaintext highlighter-rouge">background-darkAqua</code> elements can be omitted. The <code class="language-plaintext highlighter-rouge">hostArchitectures</code> attribute of the <code class="language-plaintext highlighter-rouge">options</code> element assumes a <a href="#universal-binary">universal binary</a> is being distributed. If that is not the case, then modify the value to refer to a single architecture.</p>

<p>Now, create the installer package:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>productbuild --distribution "&lt;TEMP_DIR&gt;/distribution.xml" --resources "&lt;RESOURCES_DIR&gt;" --package-path "&lt;COMPONENTS_DIR&gt;" rusty.pkg
</code></pre></div></div>

<p>This will create the final installer package file named <code class="language-plaintext highlighter-rouge">rusty.pkg</code>.</p>

<h2 id="process-installer-package">Process installer package</h2>

<p>Sign the installer package using the <code class="language-plaintext highlighter-rouge">Developer ID Installer</code> certificate and associated private key:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rcodesign sign --pem-source certificate_installer.pem --pem-source private_key_installer.pem rusty.pkg
</code></pre></div></div>

<p>Then, submit the installer package to Apple’s notary service:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rcodesign notary-submit --api-key-path app_store_connect_api_key.json --staple rusty.pkg
</code></pre></div></div>

<p class="note">The <code class="language-plaintext highlighter-rouge">--staple</code> flag will <a href="https://developer.apple.com/documentation/security/customizing-the-notarization-workflow#Staple-the-ticket-to-your-distribution">staple</a> the certificate to the installer package. The binaries were not stabled because Apple currently does not support that.</p>

<h2 id="secret-storage">Secret storage</h2>

<p>In order to go through this process again (like in CI) be sure to securely save the following five credentials:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">certificate_application.pem</code></li>
  <li><code class="language-plaintext highlighter-rouge">private_key_application.pem</code></li>
  <li><code class="language-plaintext highlighter-rouge">certificate_installer.pem</code></li>
  <li><code class="language-plaintext highlighter-rouge">private_key_installer.pem</code></li>
  <li><code class="language-plaintext highlighter-rouge">app_store_connect_api_key.json</code></li>
</ul>]]></content><author><name>Ofek Lev</name><email>human@ofek.dev</email></author><category term="guides" /><summary type="html"><![CDATA[In this post I’ll show how to properly distribute a command line tool for macOS.]]></summary></entry><entry><title type="html">Writing a CLI in Rust</title><link href="https://ofek.dev/words/guides/2022-11-19-writing-a-cli-in-rust/" rel="alternate" type="text/html" title="Writing a CLI in Rust" /><published>2022-11-19T16:09:00+00:00</published><updated>2026-04-05T18:25:56+00:00</updated><id>https://ofek.dev/words/guides/writing-a-cli-in-rust</id><content type="html" xml:base="https://ofek.dev/words/guides/2022-11-19-writing-a-cli-in-rust/"><![CDATA[<p>In this post I’ll walk through the setup of an <a href="https://github.com/ofek/rusty">example project</a> to show how to build a modern CLI in Rust.</p>

<ul id="markdown-toc">
  <li><a href="#background" id="markdown-toc-background">Background</a></li>
  <li><a href="#setup" id="markdown-toc-setup">Setup</a></li>
  <li><a href="#argument-passing" id="markdown-toc-argument-passing">Argument passing</a></li>
  <li><a href="#verbosity" id="markdown-toc-verbosity">Verbosity</a>    <ul>
      <li><a href="#flags" id="markdown-toc-flags">Flags</a></li>
      <li><a href="#access" id="markdown-toc-access">Access</a></li>
    </ul>
  </li>
  <li><a href="#output-macros" id="markdown-toc-output-macros">Output macros</a></li>
  <li><a href="#colors" id="markdown-toc-colors">Colors</a></li>
  <li><a href="#configuration" id="markdown-toc-configuration">Configuration</a>    <ul>
      <li><a href="#loading" id="markdown-toc-loading">Loading</a></li>
      <li><a href="#windows-canonical-paths" id="markdown-toc-windows-canonical-paths">Windows canonical paths</a></li>
      <li><a href="#commands" id="markdown-toc-commands">Commands</a></li>
    </ul>
  </li>
  <li><a href="#shell-completion" id="markdown-toc-shell-completion">Shell completion</a></li>
  <li><a href="#conclusion" id="markdown-toc-conclusion">Conclusion</a></li>
</ul>

<h2 id="background">Background</h2>

<p>I recently embedded on another team at <a href="https://www.datadoghq.com">work</a> in order to improve their testing processes. In doing so I created a CLI to provide a better developer experience and eventually a unified interface for interacting with their <a href="https://github.com/vectordotdev/vector">repository</a>.</p>

<p>I have a fair amount of experience developing CLIs, such as Python’s <a href="https://github.com/pypa/hatch">Hatch</a> and the tool my current team uses to maintain <a href="https://github.com/DataDog/integrations-core">hundreds of packages</a> while staying lean.</p>

<p>My go-to framework in Python is <a href="https://github.com/pallets/click">Click</a>, which allows for a declarative approach to defining commands. Using their example:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># file: "hello.py"
</span><span class="kn">import</span> <span class="n">click</span>

<span class="nd">@click.command</span><span class="p">()</span>
<span class="nd">@click.option</span><span class="p">(</span><span class="sh">"</span><span class="s">--count</span><span class="sh">"</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="nb">help</span><span class="o">=</span><span class="sh">"</span><span class="s">Number of greetings.</span><span class="sh">"</span><span class="p">)</span>
<span class="nd">@click.option</span><span class="p">(</span><span class="sh">"</span><span class="s">--name</span><span class="sh">"</span><span class="p">,</span> <span class="n">prompt</span><span class="o">=</span><span class="sh">"</span><span class="s">Your name</span><span class="sh">"</span><span class="p">,</span> <span class="nb">help</span><span class="o">=</span><span class="sh">"</span><span class="s">The person to greet.</span><span class="sh">"</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">hello</span><span class="p">(</span><span class="n">count</span><span class="p">,</span> <span class="n">name</span><span class="p">):</span>
    <span class="sh">"""</span><span class="s">Simple program that greets NAME for a total of COUNT times.</span><span class="sh">"""</span>
    <span class="k">for</span> <span class="n">_</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="n">count</span><span class="p">):</span>
        <span class="n">click</span><span class="p">.</span><span class="nf">echo</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Hello, </span><span class="si">{</span><span class="n">name</span><span class="si">}</span><span class="s">!</span><span class="sh">"</span><span class="p">)</span>

<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="sh">'</span><span class="s">__main__</span><span class="sh">'</span><span class="p">:</span>
    <span class="nf">hello</span><span class="p">()</span>
</code></pre></div></div>

<p>I quite like this style and the overall development velocity granted by Python, so I was worried about losing one or both of these. Fortunately, <a href="https://github.com/clap-rs/clap">Clap</a> provides similar functionality and I’m just as productive developing in Rust.</p>

<h2 id="setup">Setup</h2>

<p>Install <a href="https://www.rust-lang.org/tools/install">Rust</a> then create an app called <code class="language-plaintext highlighter-rouge">rusty</code>:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cargo new rusty
cd rusty
</code></pre></div></div>

<p>We’ll add Clap with the <a href="https://docs.rs/clap/latest/clap/_derive/_tutorial/index.html"><code class="language-plaintext highlighter-rouge">derive</code></a> feature for the actual CLI and <a href="https://github.com/dtolnay/anyhow">anyhow</a> for easy error propagation:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cargo add clap -F derive
cargo add anyhow
</code></pre></div></div>

<p>The commands themselves, which are each just a <code class="language-plaintext highlighter-rouge">struct</code>, will be stored in the <code class="language-plaintext highlighter-rouge">src/commands</code> directory. We want every command group/namespace to have its own directory with a <code class="language-plaintext highlighter-rouge">cli</code> module, including the root <code class="language-plaintext highlighter-rouge">rusty</code> command group. All commands will have an <code class="language-plaintext highlighter-rouge">exec</code> method that provides the actual implementation, which in the case of command groups will simply be calling sub-commands.</p>

<p>Create the following files:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// file: "src/commands/mod.rs"</span>
<span class="k">pub</span> <span class="k">mod</span> <span class="n">cli</span><span class="p">;</span>
</code></pre></div></div>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// file: "src/commands/cli.rs"</span>
<span class="k">use</span> <span class="nn">anyhow</span><span class="p">::</span><span class="nb">Result</span><span class="p">;</span>
<span class="k">use</span> <span class="nn">clap</span><span class="p">::</span><span class="n">Parser</span><span class="p">;</span>

<span class="cd">/// Rusty example app</span>
<span class="nd">#[derive(Parser,</span> <span class="nd">Debug)]</span>
<span class="nd">#[command(version,</span> <span class="nd">bin_name</span> <span class="nd">=</span> <span class="s">"rusty"</span><span class="nd">,</span> <span class="nd">disable_help_subcommand</span> <span class="nd">=</span> <span class="kc">true</span><span class="nd">)]</span>
<span class="k">pub</span> <span class="k">struct</span> <span class="n">Cli</span> <span class="p">{}</span>

<span class="k">impl</span> <span class="n">Cli</span> <span class="p">{</span>
    <span class="k">pub</span> <span class="k">fn</span> <span class="nf">exec</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Result</span><span class="o">&lt;</span><span class="p">()</span><span class="o">&gt;</span> <span class="p">{</span>
        <span class="nd">println!</span><span class="p">(</span><span class="s">"Hello, World!"</span><span class="p">);</span>

        <span class="nf">Ok</span><span class="p">(())</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Then update the binary entry point:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// file: "src/main.rs"</span>
<span class="k">mod</span> <span class="n">commands</span><span class="p">;</span>

<span class="k">use</span> <span class="nn">anyhow</span><span class="p">::</span><span class="nb">Result</span><span class="p">;</span>
<span class="k">use</span> <span class="nn">clap</span><span class="p">::</span><span class="n">Parser</span><span class="p">;</span>

<span class="k">use</span> <span class="k">crate</span><span class="p">::</span><span class="nn">commands</span><span class="p">::</span><span class="nn">cli</span><span class="p">::</span><span class="n">Cli</span><span class="p">;</span>

<span class="k">fn</span> <span class="nf">main</span><span class="p">()</span> <span class="k">-&gt;</span> <span class="nb">Result</span><span class="o">&lt;</span><span class="p">()</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="k">let</span> <span class="n">cli</span> <span class="o">=</span> <span class="nn">Cli</span><span class="p">::</span><span class="nf">parse</span><span class="p">();</span>

    <span class="n">cli</span><span class="nf">.exec</span><span class="p">()</span>
<span class="p">}</span>
</code></pre></div></div>

<p>At this point the project’s structure should look like:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>src
├── commands
│   ├── cli.rs
│   └── mod.rs
└── main.rs
Cargo.lock
Cargo.toml
</code></pre></div></div>

<p>Now let’s test the app:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>❯ cargo run -q -- --help
Rusty example app

Usage: rusty

Options:
  -h, --help     Print help information
  -V, --version  Print version information
</code></pre></div></div>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>❯ cargo run -q -- --version
rusty 0.1.0
</code></pre></div></div>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>❯ cargo run -q
Hello, World!
</code></pre></div></div>

<p class="note">The help text is derived from the documentation on the command we defined and the version comes from the <code class="language-plaintext highlighter-rouge">Cargo.toml</code> file.</p>

<h2 id="argument-passing">Argument passing</h2>

<p>Very often you’ll want to pass arguments through to other executables, like when working in Kubernetes environments. To illustrate this, we’ll add an <code class="language-plaintext highlighter-rouge">exec</code> sub-command that will spawn a process with the supplied arguments.</p>

<p>Create the following file:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// file: "src/commands/exec.rs"</span>
<span class="k">use</span> <span class="nn">anyhow</span><span class="p">::</span><span class="nb">Result</span><span class="p">;</span>
<span class="k">use</span> <span class="nn">clap</span><span class="p">::</span><span class="n">Args</span><span class="p">;</span>
<span class="k">use</span> <span class="nn">std</span><span class="p">::</span><span class="nn">process</span><span class="p">::{</span><span class="n">exit</span><span class="p">,</span> <span class="n">Command</span><span class="p">};</span>

<span class="cd">/// Execute an arbitrary command</span>
<span class="cd">///</span>
<span class="cd">/// All arguments are passed through unless --help is first</span>
<span class="nd">#[derive(Args,</span> <span class="nd">Debug)]</span>
<span class="nd">#[command()]</span>
<span class="k">pub</span> <span class="k">struct</span> <span class="n">Cli</span> <span class="p">{</span>
    <span class="nd">#[arg(required</span> <span class="nd">=</span> <span class="kc">true</span><span class="nd">,</span> <span class="nd">trailing_var_arg</span> <span class="nd">=</span> <span class="kc">true</span><span class="nd">,</span> <span class="nd">allow_hyphen_values</span> <span class="nd">=</span> <span class="kc">true</span><span class="nd">)]</span>
    <span class="n">args</span><span class="p">:</span> <span class="nb">Vec</span><span class="o">&lt;</span><span class="nb">String</span><span class="o">&gt;</span><span class="p">,</span>
<span class="p">}</span>

<span class="k">impl</span> <span class="n">Cli</span> <span class="p">{</span>
    <span class="k">pub</span> <span class="k">fn</span> <span class="nf">exec</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Result</span><span class="o">&lt;</span><span class="p">()</span><span class="o">&gt;</span> <span class="p">{</span>
        <span class="k">let</span> <span class="k">mut</span> <span class="n">command</span> <span class="o">=</span> <span class="nn">Command</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="py">.args</span><span class="p">[</span><span class="mi">0</span><span class="p">]);</span>
        <span class="k">if</span> <span class="k">self</span><span class="py">.args</span><span class="nf">.len</span><span class="p">()</span> <span class="o">&gt;</span> <span class="mi">1</span> <span class="p">{</span>
            <span class="n">command</span><span class="nf">.args</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="py">.args</span><span class="p">[</span><span class="mi">1</span><span class="o">..</span><span class="p">]);</span>
        <span class="p">}</span>

        <span class="k">let</span> <span class="n">status</span> <span class="o">=</span> <span class="n">command</span><span class="nf">.status</span><span class="p">()</span><span class="o">?</span><span class="p">;</span>
        <span class="nf">exit</span><span class="p">(</span><span class="n">status</span><span class="nf">.code</span><span class="p">()</span><span class="nf">.unwrap_or</span><span class="p">(</span><span class="mi">1</span><span class="p">));</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Then add the new module:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// file: "src/commands/mod.rs"</span>
<span class="k">pub</span> <span class="k">mod</span> <span class="n">cli</span><span class="p">;</span>
<span class="k">pub</span> <span class="k">mod</span> <span class="n">exec</span><span class="p">;</span>
</code></pre></div></div>

<p>Finally, modify the root command:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// file: "src/commands/cli.rs"</span>
<span class="k">use</span> <span class="nn">anyhow</span><span class="p">::</span><span class="nb">Result</span><span class="p">;</span>
<span class="k">use</span> <span class="nn">clap</span><span class="p">::{</span><span class="n">Parser</span><span class="p">,</span> <span class="n">Subcommand</span><span class="p">};</span>

<span class="cd">/// Rusty example app</span>
<span class="nd">#[derive(Parser,</span> <span class="nd">Debug)]</span>
<span class="nd">#[command(version,</span> <span class="nd">bin_name</span> <span class="nd">=</span> <span class="s">"rusty"</span><span class="nd">,</span> <span class="nd">disable_help_subcommand</span> <span class="nd">=</span> <span class="kc">true</span><span class="nd">)]</span>
<span class="k">pub</span> <span class="k">struct</span> <span class="n">Cli</span> <span class="p">{</span>
    <span class="nd">#[command(subcommand)]</span>
    <span class="n">command</span><span class="p">:</span> <span class="n">Commands</span><span class="p">,</span>
<span class="p">}</span>

<span class="nd">#[derive(Subcommand,</span> <span class="nd">Debug)]</span>
<span class="k">enum</span> <span class="n">Commands</span> <span class="p">{</span>
    <span class="nf">Exec</span><span class="p">(</span><span class="k">super</span><span class="p">::</span><span class="nn">exec</span><span class="p">::</span><span class="n">Cli</span><span class="p">),</span>
<span class="p">}</span>

<span class="k">impl</span> <span class="n">Cli</span> <span class="p">{</span>
    <span class="k">pub</span> <span class="k">fn</span> <span class="nf">exec</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Result</span><span class="o">&lt;</span><span class="p">()</span><span class="o">&gt;</span> <span class="p">{</span>
        <span class="k">match</span> <span class="o">&amp;</span><span class="k">self</span><span class="py">.command</span> <span class="p">{</span>
            <span class="nn">Commands</span><span class="p">::</span><span class="nf">Exec</span><span class="p">(</span><span class="n">cli</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="n">cli</span><span class="nf">.exec</span><span class="p">(),</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>At this point the project’s structure should look like:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>src
├── commands
│   ├── cli.rs
│   ├── exec.rs
│   └── mod.rs
└── main.rs
Cargo.lock
Cargo.toml
</code></pre></div></div>

<p>Let’s try it:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>❯ cargo run -q -- --help
Rusty example app

Usage: rusty &lt;COMMAND&gt;

Commands:
  exec  Execute an arbitrary command

Options:
  -h, --help     Print help information
  -V, --version  Print version information
</code></pre></div></div>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>❯ cargo run -q -- exec --help
Execute an arbitrary command

All arguments are passed through unless --help is first

Usage: rusty exec &lt;ARGS&gt;...

Arguments:
  &lt;ARGS&gt;...


Options:
  -h, --help
          Print help information (use `-h` for a summary)
</code></pre></div></div>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>❯ cargo run -q -- exec ls -a
.
..
.git
.gitignore
Cargo.lock
Cargo.toml
src
target
</code></pre></div></div>

<h2 id="verbosity">Verbosity</h2>

<p>Let’s add an option to influence the app’s verbosity.</p>

<h3 id="flags">Flags</h3>

<p>We’ll use the <a href="https://github.com/clap-rs/clap-verbosity-flag">clap-verbosity-flag</a> crate:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cargo add clap-verbosity-flag
</code></pre></div></div>

<p>Modify the root command:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// file: "src/commands/cli.rs"</span>
<span class="k">use</span> <span class="nn">anyhow</span><span class="p">::</span><span class="nb">Result</span><span class="p">;</span>
<span class="k">use</span> <span class="nn">clap</span><span class="p">::{</span><span class="n">Parser</span><span class="p">,</span> <span class="n">Subcommand</span><span class="p">};</span>
<span class="k">use</span> <span class="nn">clap_verbosity_flag</span><span class="p">::{</span><span class="n">InfoLevel</span><span class="p">,</span> <span class="n">Verbosity</span><span class="p">};</span>

<span class="cd">/// Rusty example app</span>
<span class="nd">#[derive(Parser,</span> <span class="nd">Debug)]</span>
<span class="nd">#[command(version,</span> <span class="nd">bin_name</span> <span class="nd">=</span> <span class="s">"rusty"</span><span class="nd">,</span> <span class="nd">disable_help_subcommand</span> <span class="nd">=</span> <span class="kc">true</span><span class="nd">)]</span>
<span class="k">pub</span> <span class="k">struct</span> <span class="n">Cli</span> <span class="p">{</span>
    <span class="nd">#[clap(flatten)]</span>
    <span class="k">pub</span> <span class="n">verbose</span><span class="p">:</span> <span class="n">Verbosity</span><span class="o">&lt;</span><span class="n">InfoLevel</span><span class="o">&gt;</span><span class="p">,</span>

    <span class="nd">#[command(subcommand)]</span>
    <span class="n">command</span><span class="p">:</span> <span class="n">Commands</span><span class="p">,</span>
<span class="p">}</span>

<span class="nd">#[derive(Subcommand,</span> <span class="nd">Debug)]</span>
<span class="k">enum</span> <span class="n">Commands</span> <span class="p">{</span>
    <span class="nf">Exec</span><span class="p">(</span><span class="k">super</span><span class="p">::</span><span class="nn">exec</span><span class="p">::</span><span class="n">Cli</span><span class="p">),</span>
<span class="p">}</span>

<span class="k">impl</span> <span class="n">Cli</span> <span class="p">{</span>
    <span class="k">pub</span> <span class="k">fn</span> <span class="nf">exec</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Result</span><span class="o">&lt;</span><span class="p">()</span><span class="o">&gt;</span> <span class="p">{</span>
        <span class="k">match</span> <span class="o">&amp;</span><span class="k">self</span><span class="py">.command</span> <span class="p">{</span>
            <span class="nn">Commands</span><span class="p">::</span><span class="nf">Exec</span><span class="p">(</span><span class="n">cli</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="n">cli</span><span class="nf">.exec</span><span class="p">(),</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p class="note">The <code class="language-plaintext highlighter-rouge">&lt;InfoLevel&gt;</code> indicates that the default level will show informational output.</p>

<p>Now every command can influence the output level:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>❯ cargo run -q -- --help
Rusty example app

Usage: rusty [OPTIONS] &lt;COMMAND&gt;

Commands:
  exec  Execute an arbitrary command

Options:
  -v, --verbose...  More output per occurrence
  -q, --quiet...    Less output per occurrence
  -h, --help        Print help information
  -V, --version     Print version information
</code></pre></div></div>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>❯ cargo run -q -- exec --help
Execute an arbitrary command

All arguments are passed through unless --help is first

Usage: rusty exec [OPTIONS] &lt;ARGS&gt;...

Arguments:
  &lt;ARGS&gt;...


Options:
  -v, --verbose...
          More output per occurrence

  -q, --quiet...
          Less output per occurrence

  -h, --help
          Print help information (use `-h` for a summary)
</code></pre></div></div>

<h3 id="access">Access</h3>

<p>Next we’ll provide a way to access the configured verbosity level globally using the <a href="https://github.com/matklad/once_cell">once_cell</a> crate:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cargo add once_cell
</code></pre></div></div>

<p>The <a href="https://github.com/rust-lang/log">log</a> crate is also required for the level enums:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cargo add log
</code></pre></div></div>

<p>Create the following file:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// file: "src/app.rs"</span>
<span class="k">use</span> <span class="k">log</span><span class="p">::</span><span class="n">LevelFilter</span><span class="p">;</span>
<span class="k">use</span> <span class="nn">once_cell</span><span class="p">::</span><span class="nn">sync</span><span class="p">::</span><span class="n">OnceCell</span><span class="p">;</span>

<span class="k">static</span> <span class="n">VERBOSITY</span><span class="p">:</span> <span class="n">OnceCell</span><span class="o">&lt;</span><span class="n">LevelFilter</span><span class="o">&gt;</span> <span class="o">=</span> <span class="nn">OnceCell</span><span class="p">::</span><span class="nf">new</span><span class="p">();</span>

<span class="k">pub</span> <span class="k">fn</span> <span class="nf">verbosity</span><span class="p">()</span> <span class="k">-&gt;</span> <span class="o">&amp;</span><span class="k">'static</span> <span class="n">LevelFilter</span> <span class="p">{</span>
    <span class="n">VERBOSITY</span><span class="nf">.get</span><span class="p">()</span><span class="nf">.expect</span><span class="p">(</span><span class="s">"verbosity is not initialized"</span><span class="p">)</span>
<span class="p">}</span>

<span class="k">pub</span> <span class="k">fn</span> <span class="nf">set_global_verbosity</span><span class="p">(</span><span class="n">verbosity</span><span class="p">:</span> <span class="n">LevelFilter</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">VERBOSITY</span><span class="nf">.set</span><span class="p">(</span><span class="n">verbosity</span><span class="p">)</span><span class="nf">.expect</span><span class="p">(</span><span class="s">"could not set verbosity"</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Then update the binary entry point:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// file: "src/main.rs"</span>
<span class="k">mod</span> <span class="n">app</span><span class="p">;</span>
<span class="k">mod</span> <span class="n">commands</span><span class="p">;</span>

<span class="k">use</span> <span class="nn">anyhow</span><span class="p">::</span><span class="nb">Result</span><span class="p">;</span>
<span class="k">use</span> <span class="nn">clap</span><span class="p">::</span><span class="n">Parser</span><span class="p">;</span>

<span class="k">use</span> <span class="k">crate</span><span class="p">::</span><span class="nn">commands</span><span class="p">::</span><span class="nn">cli</span><span class="p">::</span><span class="n">Cli</span><span class="p">;</span>

<span class="k">fn</span> <span class="nf">main</span><span class="p">()</span> <span class="k">-&gt;</span> <span class="nb">Result</span><span class="o">&lt;</span><span class="p">()</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="k">let</span> <span class="n">cli</span> <span class="o">=</span> <span class="nn">Cli</span><span class="p">::</span><span class="nf">parse</span><span class="p">();</span>
    <span class="nn">app</span><span class="p">::</span><span class="nf">set_global_verbosity</span><span class="p">(</span><span class="n">cli</span><span class="py">.verbose</span><span class="nf">.log_level_filter</span><span class="p">());</span>

    <span class="n">cli</span><span class="nf">.exec</span><span class="p">()</span>
<span class="p">}</span>
</code></pre></div></div>

<p>At this point the project’s structure should look like:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>src
├── commands
│   ├── cli.rs
│   ├── exec.rs
│   └── mod.rs
├── app.rs
└── main.rs
Cargo.lock
Cargo.toml
</code></pre></div></div>

<p>Now the entire app can access the configured verbosity level with:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">*</span><span class="k">crate</span><span class="p">::</span><span class="nn">app</span><span class="p">::</span><span class="nf">verbosity</span><span class="p">()</span>
</code></pre></div></div>

<h2 id="output-macros">Output macros</h2>

<p>With the verbosity set globally, we can create <a href="https://doc.rust-lang.org/book/ch19-06-macros.html#declarative-macros-with-macro_rules-for-general-metaprogramming">declarative macros</a> that will be available throughout the app for conditionally displaying text based on the verbosity.</p>

<p>Create the following file:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// file: "src/macros.rs"</span>
<span class="nd">macro_rules!</span> <span class="n">define_display_macro</span> <span class="p">{</span>
    <span class="p">(</span><span class="nv">$name:ident</span><span class="p">,</span> <span class="nv">$level:ident</span><span class="p">,</span> <span class="nv">$d:tt</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="p">{</span>
        <span class="nd">macro_rules!</span> <span class="nv">$name</span> <span class="p">{</span>
            <span class="p">(</span><span class="nv">$d</span><span class="p">(</span><span class="nv">$d</span> <span class="n">arg</span><span class="p">:</span><span class="n">tt</span><span class="p">)</span><span class="o">*</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="p">{</span>
                <span class="k">if</span> <span class="k">log</span><span class="p">::</span><span class="nn">Level</span><span class="p">::</span><span class="nv">$level</span> <span class="o">&lt;=</span> <span class="o">*</span><span class="nv">$crate</span><span class="p">::</span><span class="nn">app</span><span class="p">::</span><span class="nf">verbosity</span><span class="p">()</span> <span class="p">{</span>
                    <span class="nd">eprintln!</span><span class="p">(</span><span class="nv">$d</span><span class="p">(</span><span class="nv">$d</span> <span class="n">arg</span><span class="p">)</span><span class="o">*</span><span class="p">);</span>
                <span class="p">}</span>
            <span class="p">};</span>
        <span class="p">}</span>
    <span class="p">};</span>
<span class="p">}</span>

<span class="nd">define_display_macro!</span><span class="p">(</span><span class="n">trace</span><span class="p">,</span> <span class="n">Trace</span><span class="p">,</span> $<span class="p">);</span>
<span class="nd">define_display_macro!</span><span class="p">(</span><span class="n">debug</span><span class="p">,</span> <span class="n">Debug</span><span class="p">,</span> $<span class="p">);</span>
<span class="nd">define_display_macro!</span><span class="p">(</span><span class="n">info</span><span class="p">,</span> <span class="n">Info</span><span class="p">,</span> $<span class="p">);</span>
<span class="nd">define_display_macro!</span><span class="p">(</span><span class="n">warn</span><span class="p">,</span> <span class="n">Warn</span><span class="p">,</span> $<span class="p">);</span>
<span class="nd">define_display_macro!</span><span class="p">(</span><span class="n">error</span><span class="p">,</span> <span class="n">Error</span><span class="p">,</span> $<span class="p">);</span>
</code></pre></div></div>

<p>Here we’re maximizing boilerplate reduction by defining a single macro that will generate the macros the app will actually use.</p>

<p class="note">The <code class="language-plaintext highlighter-rouge">$</code> hack is a workaround for a limitation that is being worked on (see <a href="https://github.com/rust-lang/rust/issues/35853#issuecomment-415993963">rust-lang/rust#35853</a> and <a href="https://github.com/rust-lang/rust/issues/83527#issuecomment-1281176235">rust-lang/rust#83527</a>).</p>

<p>Then add the module to the very top of the binary entry point preceded by <code class="language-plaintext highlighter-rouge">#[macro_use]</code>:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// file: "src/main.rs"</span>
<span class="nd">#[macro_use]</span>
<span class="k">mod</span> <span class="n">macros</span><span class="p">;</span>
<span class="k">mod</span> <span class="n">app</span><span class="p">;</span>
<span class="k">mod</span> <span class="n">commands</span><span class="p">;</span>

<span class="k">use</span> <span class="nn">anyhow</span><span class="p">::</span><span class="nb">Result</span><span class="p">;</span>
<span class="k">use</span> <span class="nn">clap</span><span class="p">::</span><span class="n">Parser</span><span class="p">;</span>

<span class="k">use</span> <span class="k">crate</span><span class="p">::</span><span class="nn">commands</span><span class="p">::</span><span class="nn">cli</span><span class="p">::</span><span class="n">Cli</span><span class="p">;</span>

<span class="k">fn</span> <span class="nf">main</span><span class="p">()</span> <span class="k">-&gt;</span> <span class="nb">Result</span><span class="o">&lt;</span><span class="p">()</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="k">let</span> <span class="n">cli</span> <span class="o">=</span> <span class="nn">Cli</span><span class="p">::</span><span class="nf">parse</span><span class="p">();</span>
    <span class="nn">app</span><span class="p">::</span><span class="nf">set_global_verbosity</span><span class="p">(</span><span class="n">cli</span><span class="py">.verbose</span><span class="nf">.log_level_filter</span><span class="p">());</span>

    <span class="n">cli</span><span class="nf">.exec</span><span class="p">()</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Now let’s create a hidden sub-command to test the macros:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// file: "src/commands/test.rs"</span>
<span class="k">use</span> <span class="nn">anyhow</span><span class="p">::</span><span class="nb">Result</span><span class="p">;</span>
<span class="k">use</span> <span class="nn">clap</span><span class="p">::</span><span class="n">Args</span><span class="p">;</span>

<span class="cd">/// Test conditional output</span>
<span class="nd">#[derive(Args,</span> <span class="nd">Debug)]</span>
<span class="nd">#[command(hide</span> <span class="nd">=</span> <span class="kc">true</span><span class="nd">)]</span>
<span class="k">pub</span> <span class="k">struct</span> <span class="n">Cli</span> <span class="p">{</span>
    <span class="n">text</span><span class="p">:</span> <span class="nb">String</span><span class="p">,</span>
<span class="p">}</span>

<span class="k">impl</span> <span class="n">Cli</span> <span class="p">{</span>
    <span class="k">pub</span> <span class="k">fn</span> <span class="nf">exec</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Result</span><span class="o">&lt;</span><span class="p">()</span><span class="o">&gt;</span> <span class="p">{</span>
        <span class="nd">trace!</span><span class="p">(</span><span class="s">"trace {}"</span><span class="p">,</span> <span class="k">self</span><span class="py">.text</span><span class="p">);</span>
        <span class="nd">debug!</span><span class="p">(</span><span class="s">"debug {}"</span><span class="p">,</span> <span class="k">self</span><span class="py">.text</span><span class="p">);</span>
        <span class="nd">info!</span><span class="p">(</span><span class="s">"info {}"</span><span class="p">,</span> <span class="k">self</span><span class="py">.text</span><span class="p">);</span>
        <span class="nd">warn!</span><span class="p">(</span><span class="s">"warn {}"</span><span class="p">,</span> <span class="k">self</span><span class="py">.text</span><span class="p">);</span>
        <span class="nd">error!</span><span class="p">(</span><span class="s">"error {}"</span><span class="p">,</span> <span class="k">self</span><span class="py">.text</span><span class="p">);</span>

        <span class="nf">Ok</span><span class="p">(())</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Then add the new module:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// file: "src/commands/mod.rs"</span>
<span class="k">pub</span> <span class="k">mod</span> <span class="n">cli</span><span class="p">;</span>
<span class="k">pub</span> <span class="k">mod</span> <span class="n">exec</span><span class="p">;</span>
<span class="k">pub</span> <span class="k">mod</span> <span class="n">test</span><span class="p">;</span>
</code></pre></div></div>

<p>Finally, add it to the root command:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// file: "src/commands/cli.rs"</span>
<span class="k">use</span> <span class="nn">anyhow</span><span class="p">::</span><span class="nb">Result</span><span class="p">;</span>
<span class="k">use</span> <span class="nn">clap</span><span class="p">::{</span><span class="n">Parser</span><span class="p">,</span> <span class="n">Subcommand</span><span class="p">};</span>
<span class="k">use</span> <span class="nn">clap_verbosity_flag</span><span class="p">::{</span><span class="n">InfoLevel</span><span class="p">,</span> <span class="n">Verbosity</span><span class="p">};</span>

<span class="cd">/// Rusty example app</span>
<span class="nd">#[derive(Parser,</span> <span class="nd">Debug)]</span>
<span class="nd">#[command(version,</span> <span class="nd">bin_name</span> <span class="nd">=</span> <span class="s">"rusty"</span><span class="nd">,</span> <span class="nd">disable_help_subcommand</span> <span class="nd">=</span> <span class="kc">true</span><span class="nd">)]</span>
<span class="k">pub</span> <span class="k">struct</span> <span class="n">Cli</span> <span class="p">{</span>
    <span class="nd">#[clap(flatten)]</span>
    <span class="k">pub</span> <span class="n">verbose</span><span class="p">:</span> <span class="n">Verbosity</span><span class="o">&lt;</span><span class="n">InfoLevel</span><span class="o">&gt;</span><span class="p">,</span>

    <span class="nd">#[command(subcommand)]</span>
    <span class="n">command</span><span class="p">:</span> <span class="n">Commands</span><span class="p">,</span>
<span class="p">}</span>

<span class="nd">#[derive(Subcommand,</span> <span class="nd">Debug)]</span>
<span class="k">enum</span> <span class="n">Commands</span> <span class="p">{</span>
    <span class="nf">Exec</span><span class="p">(</span><span class="k">super</span><span class="p">::</span><span class="nn">exec</span><span class="p">::</span><span class="n">Cli</span><span class="p">),</span>
    <span class="nf">Test</span><span class="p">(</span><span class="k">super</span><span class="p">::</span><span class="nn">test</span><span class="p">::</span><span class="n">Cli</span><span class="p">),</span>
<span class="p">}</span>

<span class="k">impl</span> <span class="n">Cli</span> <span class="p">{</span>
    <span class="k">pub</span> <span class="k">fn</span> <span class="nf">exec</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Result</span><span class="o">&lt;</span><span class="p">()</span><span class="o">&gt;</span> <span class="p">{</span>
        <span class="k">match</span> <span class="o">&amp;</span><span class="k">self</span><span class="py">.command</span> <span class="p">{</span>
            <span class="nn">Commands</span><span class="p">::</span><span class="nf">Exec</span><span class="p">(</span><span class="n">cli</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="n">cli</span><span class="nf">.exec</span><span class="p">(),</span>
            <span class="nn">Commands</span><span class="p">::</span><span class="nf">Test</span><span class="p">(</span><span class="n">cli</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="n">cli</span><span class="nf">.exec</span><span class="p">(),</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>At this point the project’s structure should look like:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>src
├── commands
│   ├── cli.rs
│   ├── exec.rs
│   ├── mod.rs
│   └── test.rs
├── app.rs
├── macros.rs
└── main.rs
Cargo.lock
Cargo.toml
</code></pre></div></div>

<p>Let’s try it out:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>❯ cargo run -q -- --help
Rusty example app

Usage: rusty [OPTIONS] &lt;COMMAND&gt;

Commands:
  exec  Execute an arbitrary command

Options:
  -v, --verbose...  More output per occurrence
  -q, --quiet...    Less output per occurrence
  -h, --help        Print help information
  -V, --version     Print version information
</code></pre></div></div>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>❯ cargo run -q -- test hello
info hello
warn hello
error hello
</code></pre></div></div>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>❯ cargo run -q -- test hello -vv
trace hello
debug hello
info hello
warn hello
error hello
</code></pre></div></div>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>❯ cargo run -q -- test hello -qq
error hello
</code></pre></div></div>

<h2 id="colors">Colors</h2>

<p>Now let’s apply styling to the output levels using <a href="https://github.com/jam1garner/owo-colors">owo-colors</a> with the <code class="language-plaintext highlighter-rouge">supports-colors</code> feature for conditional use based on TTY detection:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cargo add owo-colors -F supports-colors
</code></pre></div></div>

<p>Update the macros and the test command:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// file: "src/macros.rs"</span>
<span class="nd">macro_rules!</span> <span class="n">display</span> <span class="p">{</span>
    <span class="p">(</span><span class="nv">$</span><span class="p">(</span><span class="nv">$arg:tt</span><span class="p">)</span><span class="o">*</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="p">{{</span>
        <span class="k">use</span> <span class="nn">owo_colors</span><span class="p">::</span><span class="n">OwoColorize</span><span class="p">;</span>
        <span class="nd">println!</span><span class="p">(</span>
            <span class="s">"{}"</span><span class="p">,</span>
            <span class="nd">format!</span><span class="p">(</span><span class="nv">$</span><span class="p">(</span><span class="nv">$arg</span><span class="p">)</span><span class="o">*</span><span class="p">)</span>
                <span class="nf">.if_supports_color</span><span class="p">(</span><span class="nn">owo_colors</span><span class="p">::</span><span class="nn">Stream</span><span class="p">::</span><span class="n">Stdout</span><span class="p">,</span> <span class="p">|</span><span class="n">text</span><span class="p">|</span> <span class="n">text</span><span class="nf">.bold</span><span class="p">())</span>
        <span class="p">);</span>
    <span class="p">}};</span>
<span class="p">}</span>

<span class="nd">macro_rules!</span> <span class="n">critical</span> <span class="p">{</span>
    <span class="p">(</span><span class="nv">$</span><span class="p">(</span><span class="nv">$arg:tt</span><span class="p">)</span><span class="o">*</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="p">{{</span>
        <span class="k">use</span> <span class="nn">owo_colors</span><span class="p">::</span><span class="n">OwoColorize</span><span class="p">;</span>
        <span class="nd">eprintln!</span><span class="p">(</span>
            <span class="s">"{}"</span><span class="p">,</span>
            <span class="nd">format!</span><span class="p">(</span><span class="nv">$</span><span class="p">(</span><span class="nv">$arg</span><span class="p">)</span><span class="o">*</span><span class="p">)</span>
                <span class="nf">.if_supports_color</span><span class="p">(</span><span class="nn">owo_colors</span><span class="p">::</span><span class="nn">Stream</span><span class="p">::</span><span class="n">Stderr</span><span class="p">,</span> <span class="p">|</span><span class="n">text</span><span class="p">|</span> <span class="n">text</span><span class="nf">.bright_red</span><span class="p">())</span>
        <span class="p">);</span>
    <span class="p">}};</span>
<span class="p">}</span>

<span class="nd">macro_rules!</span> <span class="n">define_display_macro</span> <span class="p">{</span>
    <span class="p">(</span><span class="nv">$name:ident</span><span class="p">,</span> <span class="nv">$level:ident</span><span class="p">,</span> <span class="nv">$style:ident</span><span class="p">,</span> <span class="nv">$d:tt</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="p">(</span>
        <span class="nd">macro_rules!</span> <span class="nv">$name</span> <span class="p">{</span>
            <span class="p">(</span><span class="nv">$d</span><span class="p">(</span><span class="nv">$d</span> <span class="n">arg</span><span class="p">:</span><span class="n">tt</span><span class="p">)</span><span class="o">*</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="p">{{</span>
                <span class="k">use</span> <span class="nn">owo_colors</span><span class="p">::</span><span class="n">OwoColorize</span><span class="p">;</span>
                <span class="k">if</span> <span class="k">log</span><span class="p">::</span><span class="nn">Level</span><span class="p">::</span><span class="nv">$level</span> <span class="o">&lt;=</span> <span class="o">*</span><span class="nv">$crate</span><span class="p">::</span><span class="nn">app</span><span class="p">::</span><span class="nf">verbosity</span><span class="p">()</span> <span class="p">{</span>
                    <span class="nd">eprintln!</span><span class="p">(</span>
                        <span class="s">"{}"</span><span class="p">,</span>
                        <span class="nd">format!</span><span class="p">(</span><span class="nv">$d</span><span class="p">(</span><span class="nv">$d</span> <span class="n">arg</span><span class="p">)</span><span class="o">*</span><span class="p">)</span>
                            <span class="nf">.if_supports_color</span><span class="p">(</span><span class="nn">owo_colors</span><span class="p">::</span><span class="nn">Stream</span><span class="p">::</span><span class="n">Stderr</span><span class="p">,</span> <span class="p">|</span><span class="n">text</span><span class="p">|</span> <span class="n">text</span>.<span class="nv">$style</span><span class="p">())</span>
                    <span class="p">);</span>
                <span class="p">}</span>
            <span class="p">}};</span>
        <span class="p">}</span>
    <span class="p">);</span>
<span class="p">}</span>

<span class="nd">define_display_macro!</span><span class="p">(</span><span class="n">trace</span><span class="p">,</span> <span class="n">Trace</span><span class="p">,</span> <span class="n">underline</span><span class="p">,</span> $<span class="p">);</span>
<span class="nd">define_display_macro!</span><span class="p">(</span><span class="n">debug</span><span class="p">,</span> <span class="n">Debug</span><span class="p">,</span> <span class="n">italic</span><span class="p">,</span> $<span class="p">);</span>
<span class="nd">define_display_macro!</span><span class="p">(</span><span class="n">info</span><span class="p">,</span> <span class="n">Info</span><span class="p">,</span> <span class="n">bold</span><span class="p">,</span> $<span class="p">);</span>
<span class="nd">define_display_macro!</span><span class="p">(</span><span class="n">success</span><span class="p">,</span> <span class="n">Info</span><span class="p">,</span> <span class="n">bright_cyan</span><span class="p">,</span> $<span class="p">);</span>
<span class="nd">define_display_macro!</span><span class="p">(</span><span class="n">waiting</span><span class="p">,</span> <span class="n">Info</span><span class="p">,</span> <span class="n">bright_magenta</span><span class="p">,</span> $<span class="p">);</span>
<span class="nd">define_display_macro!</span><span class="p">(</span><span class="n">warn</span><span class="p">,</span> <span class="n">Warn</span><span class="p">,</span> <span class="n">bright_yellow</span><span class="p">,</span> $<span class="p">);</span>
<span class="nd">define_display_macro!</span><span class="p">(</span><span class="n">error</span><span class="p">,</span> <span class="n">Error</span><span class="p">,</span> <span class="n">bright_red</span><span class="p">,</span> $<span class="p">);</span>
</code></pre></div></div>

<p class="note">For standard or informational output we use bold rather than bright white for terminals with white backgrounds.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// file: "src/commands/test.rs"</span>
<span class="k">use</span> <span class="nn">anyhow</span><span class="p">::</span><span class="nb">Result</span><span class="p">;</span>
<span class="k">use</span> <span class="nn">clap</span><span class="p">::</span><span class="n">Args</span><span class="p">;</span>

<span class="cd">/// Test conditional output</span>
<span class="nd">#[derive(Args,</span> <span class="nd">Debug)]</span>
<span class="nd">#[command(hide</span> <span class="nd">=</span> <span class="kc">true</span><span class="nd">)]</span>
<span class="k">pub</span> <span class="k">struct</span> <span class="n">Cli</span> <span class="p">{</span>
    <span class="n">text</span><span class="p">:</span> <span class="nb">String</span><span class="p">,</span>
<span class="p">}</span>

<span class="k">impl</span> <span class="n">Cli</span> <span class="p">{</span>
    <span class="k">pub</span> <span class="k">fn</span> <span class="nf">exec</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Result</span><span class="o">&lt;</span><span class="p">()</span><span class="o">&gt;</span> <span class="p">{</span>
        <span class="nd">trace!</span><span class="p">(</span><span class="s">"trace {}"</span><span class="p">,</span> <span class="k">self</span><span class="py">.text</span><span class="p">);</span>
        <span class="nd">debug!</span><span class="p">(</span><span class="s">"debug {}"</span><span class="p">,</span> <span class="k">self</span><span class="py">.text</span><span class="p">);</span>
        <span class="nd">info!</span><span class="p">(</span><span class="s">"info {}"</span><span class="p">,</span> <span class="k">self</span><span class="py">.text</span><span class="p">);</span>
        <span class="nd">success!</span><span class="p">(</span><span class="s">"success {}"</span><span class="p">,</span> <span class="k">self</span><span class="py">.text</span><span class="p">);</span>
        <span class="nd">waiting!</span><span class="p">(</span><span class="s">"waiting {}"</span><span class="p">,</span> <span class="k">self</span><span class="py">.text</span><span class="p">);</span>
        <span class="nd">warn!</span><span class="p">(</span><span class="s">"warn {}"</span><span class="p">,</span> <span class="k">self</span><span class="py">.text</span><span class="p">);</span>
        <span class="nd">error!</span><span class="p">(</span><span class="s">"error {}"</span><span class="p">,</span> <span class="k">self</span><span class="py">.text</span><span class="p">);</span>
        <span class="nd">display!</span><span class="p">(</span><span class="s">"display {}"</span><span class="p">,</span> <span class="k">self</span><span class="py">.text</span><span class="p">);</span>
        <span class="nd">critical!</span><span class="p">(</span><span class="s">"critical {}"</span><span class="p">,</span> <span class="k">self</span><span class="py">.text</span><span class="p">);</span>

        <span class="nf">Ok</span><span class="p">(())</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">display!</code>/<code class="language-plaintext highlighter-rouge">critical!</code> macros provide a way to always output using <code class="language-plaintext highlighter-rouge">println!</code>/<code class="language-plaintext highlighter-rouge">eprintln!</code> but with color.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>❯ cargo run -q -- test hello -qqq
display hello
critical hello
</code></pre></div></div>

<h2 id="configuration">Configuration</h2>

<h3 id="loading">Loading</h3>

<p>Now we’ll persist app settings using <a href="https://github.com/rust-cli/confy">confy</a> and <a href="https://github.com/serde-rs/serde">serde</a> with the <code class="language-plaintext highlighter-rouge">derive</code> feature:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cargo add serde -F derive
cargo add confy
</code></pre></div></div>

<p>Create the following file:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// file: "src/config.rs"</span>
<span class="k">use</span> <span class="nn">anyhow</span><span class="p">::{</span><span class="n">Context</span><span class="p">,</span> <span class="nb">Result</span><span class="p">};</span>
<span class="k">use</span> <span class="n">confy</span><span class="p">;</span>
<span class="k">use</span> <span class="nn">serde</span><span class="p">::{</span><span class="n">Deserialize</span><span class="p">,</span> <span class="n">Serialize</span><span class="p">};</span>
<span class="k">use</span> <span class="nn">std</span><span class="p">::</span><span class="nn">path</span><span class="p">::</span><span class="n">PathBuf</span><span class="p">;</span>

<span class="k">const</span> <span class="n">APP_NAME</span><span class="p">:</span> <span class="o">&amp;</span><span class="nb">str</span> <span class="o">=</span> <span class="s">"rusty"</span><span class="p">;</span>
<span class="k">const</span> <span class="n">FILE_STEM</span><span class="p">:</span> <span class="o">&amp;</span><span class="nb">str</span> <span class="o">=</span> <span class="s">"config"</span><span class="p">;</span>

<span class="nd">#[derive(Deserialize,</span> <span class="nd">Serialize,</span> <span class="nd">Clone,</span> <span class="nd">Debug)]</span>
<span class="k">pub</span> <span class="k">struct</span> <span class="n">Config</span> <span class="p">{</span>
    <span class="k">pub</span> <span class="n">repo</span><span class="p">:</span> <span class="nb">String</span><span class="p">,</span>
<span class="p">}</span>

<span class="k">impl</span> <span class="nb">Default</span> <span class="k">for</span> <span class="n">Config</span> <span class="p">{</span>
    <span class="k">fn</span> <span class="nf">default</span><span class="p">()</span> <span class="k">-&gt;</span> <span class="k">Self</span> <span class="p">{</span>
        <span class="k">Self</span> <span class="p">{</span>
            <span class="n">repo</span><span class="p">:</span> <span class="s">""</span><span class="nf">.into</span><span class="p">(),</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="k">pub</span> <span class="k">fn</span> <span class="nf">path</span><span class="p">()</span> <span class="k">-&gt;</span> <span class="nb">Result</span><span class="o">&lt;</span><span class="n">PathBuf</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="nn">confy</span><span class="p">::</span><span class="nf">get_configuration_file_path</span><span class="p">(</span><span class="n">APP_NAME</span><span class="p">,</span> <span class="n">FILE_STEM</span><span class="p">)</span>
        <span class="nf">.with_context</span><span class="p">(||</span> <span class="s">"unable to find the config file"</span><span class="p">)</span>
<span class="p">}</span>

<span class="k">pub</span> <span class="k">fn</span> <span class="nf">load</span><span class="p">()</span> <span class="k">-&gt;</span> <span class="nb">Result</span><span class="o">&lt;</span><span class="n">Config</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="nn">confy</span><span class="p">::</span><span class="nf">load</span><span class="p">(</span><span class="n">APP_NAME</span><span class="p">,</span> <span class="n">FILE_STEM</span><span class="p">)</span><span class="nf">.with_context</span><span class="p">(||</span> <span class="s">"unable to load config"</span><span class="p">)</span>
<span class="p">}</span>

<span class="k">pub</span> <span class="k">fn</span> <span class="nf">save</span><span class="p">(</span><span class="n">config</span><span class="p">:</span> <span class="n">Config</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Result</span><span class="o">&lt;</span><span class="p">()</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="nn">confy</span><span class="p">::</span><span class="nf">store</span><span class="p">(</span><span class="n">APP_NAME</span><span class="p">,</span> <span class="n">FILE_STEM</span><span class="p">,</span> <span class="n">config</span><span class="p">)</span><span class="nf">.with_context</span><span class="p">(||</span> <span class="s">"unable to save config"</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Here we defined a single option named <code class="language-plaintext highlighter-rouge">repo</code>. Now let’s load the configuration and provide global access to it like we did for the verbosity.</p>

<p>Edit the following file:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// file: "src/app.rs"</span>
<span class="k">use</span> <span class="k">log</span><span class="p">::</span><span class="n">LevelFilter</span><span class="p">;</span>
<span class="k">use</span> <span class="nn">once_cell</span><span class="p">::</span><span class="nn">sync</span><span class="p">::</span><span class="n">OnceCell</span><span class="p">;</span>

<span class="k">use</span> <span class="k">crate</span><span class="p">::</span><span class="nn">config</span><span class="p">::</span><span class="n">Config</span><span class="p">;</span>

<span class="k">static</span> <span class="n">VERBOSITY</span><span class="p">:</span> <span class="n">OnceCell</span><span class="o">&lt;</span><span class="n">LevelFilter</span><span class="o">&gt;</span> <span class="o">=</span> <span class="nn">OnceCell</span><span class="p">::</span><span class="nf">new</span><span class="p">();</span>
<span class="k">static</span> <span class="n">CONFIG</span><span class="p">:</span> <span class="n">OnceCell</span><span class="o">&lt;</span><span class="n">Config</span><span class="o">&gt;</span> <span class="o">=</span> <span class="nn">OnceCell</span><span class="p">::</span><span class="nf">new</span><span class="p">();</span>

<span class="k">pub</span> <span class="k">fn</span> <span class="nf">verbosity</span><span class="p">()</span> <span class="k">-&gt;</span> <span class="o">&amp;</span><span class="k">'static</span> <span class="n">LevelFilter</span> <span class="p">{</span>
    <span class="n">VERBOSITY</span><span class="nf">.get</span><span class="p">()</span><span class="nf">.expect</span><span class="p">(</span><span class="s">"verbosity is not initialized"</span><span class="p">)</span>
<span class="p">}</span>

<span class="k">pub</span> <span class="k">fn</span> <span class="nf">config</span><span class="p">()</span> <span class="k">-&gt;</span> <span class="o">&amp;</span><span class="k">'static</span> <span class="n">Config</span> <span class="p">{</span>
    <span class="n">CONFIG</span><span class="nf">.get</span><span class="p">()</span><span class="nf">.expect</span><span class="p">(</span><span class="s">"config is not initialized"</span><span class="p">)</span>
<span class="p">}</span>

<span class="k">pub</span> <span class="k">fn</span> <span class="nf">set_global_verbosity</span><span class="p">(</span><span class="n">verbosity</span><span class="p">:</span> <span class="n">LevelFilter</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">VERBOSITY</span><span class="nf">.set</span><span class="p">(</span><span class="n">verbosity</span><span class="p">)</span><span class="nf">.expect</span><span class="p">(</span><span class="s">"could not set verbosity"</span><span class="p">)</span>
<span class="p">}</span>

<span class="k">pub</span> <span class="k">fn</span> <span class="nf">set_global_config</span><span class="p">(</span><span class="n">config</span><span class="p">:</span> <span class="n">Config</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">CONFIG</span><span class="nf">.set</span><span class="p">(</span><span class="n">config</span><span class="p">)</span><span class="nf">.expect</span><span class="p">(</span><span class="s">"could not set config"</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Then update the binary entry point:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// file: "src/main.rs"</span>
<span class="nd">#[macro_use]</span>
<span class="k">mod</span> <span class="n">macros</span><span class="p">;</span>
<span class="k">mod</span> <span class="n">app</span><span class="p">;</span>
<span class="k">mod</span> <span class="n">commands</span><span class="p">;</span>
<span class="k">mod</span> <span class="n">config</span><span class="p">;</span>

<span class="k">use</span> <span class="nn">anyhow</span><span class="p">::</span><span class="nb">Result</span><span class="p">;</span>
<span class="k">use</span> <span class="nn">clap</span><span class="p">::</span><span class="n">Parser</span><span class="p">;</span>

<span class="k">use</span> <span class="k">crate</span><span class="p">::</span><span class="nn">commands</span><span class="p">::</span><span class="nn">cli</span><span class="p">::</span><span class="n">Cli</span><span class="p">;</span>

<span class="k">fn</span> <span class="nf">main</span><span class="p">()</span> <span class="k">-&gt;</span> <span class="nb">Result</span><span class="o">&lt;</span><span class="p">()</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="k">let</span> <span class="n">cli</span> <span class="o">=</span> <span class="nn">Cli</span><span class="p">::</span><span class="nf">parse</span><span class="p">();</span>
    <span class="nn">app</span><span class="p">::</span><span class="nf">set_global_verbosity</span><span class="p">(</span><span class="n">cli</span><span class="py">.verbose</span><span class="nf">.log_level_filter</span><span class="p">());</span>
    <span class="nn">app</span><span class="p">::</span><span class="nf">set_global_config</span><span class="p">(</span><span class="nn">config</span><span class="p">::</span><span class="nf">load</span><span class="p">()</span><span class="o">?</span><span class="p">);</span>

    <span class="n">cli</span><span class="nf">.exec</span><span class="p">()</span>
<span class="p">}</span>
</code></pre></div></div>

<p>At this point the project’s structure should look like:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>src
├── commands
│   ├── cli.rs
│   ├── exec.rs
│   ├── mod.rs
│   └── test.rs
├── app.rs
├── config.rs
├── macros.rs
└── main.rs
Cargo.lock
Cargo.toml
</code></pre></div></div>

<p>Now the entire app can access the loaded configuration with:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">crate</span><span class="p">::</span><span class="nn">app</span><span class="p">::</span><span class="nf">config</span><span class="p">()</span>
</code></pre></div></div>

<h3 id="windows-canonical-paths">Windows canonical paths</h3>

<p>Before we procede, let’s add a utility for resolving a path that works around a <a href="https://github.com/rust-lang/rust/issues/42869">long-standing issue</a> on Windows.</p>

<p>We’ll use the <a href="https://crates.io/crates/dunce">dunce</a> crate:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cargo add dunce
</code></pre></div></div>

<p>Create the following file:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// file: "src/platform.rs"</span>
<span class="k">pub</span> <span class="k">fn</span> <span class="nf">canonicalize_path</span><span class="p">(</span><span class="n">path</span><span class="p">:</span> <span class="o">&amp;</span><span class="nb">String</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">String</span> <span class="p">{</span>
    <span class="k">match</span> <span class="nn">dunce</span><span class="p">::</span><span class="nf">canonicalize</span><span class="p">(</span><span class="n">path</span><span class="p">)</span> <span class="p">{</span>
        <span class="nf">Ok</span><span class="p">(</span><span class="n">p</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="n">p</span><span class="nf">.display</span><span class="p">()</span><span class="nf">.to_string</span><span class="p">(),</span>
        <span class="nf">Err</span><span class="p">(</span><span class="n">_</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="n">path</span><span class="nf">.to_string</span><span class="p">(),</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Then add the module to the binary entry point:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// file: "src/main.rs"</span>
<span class="nd">#[macro_use]</span>
<span class="k">mod</span> <span class="n">macros</span><span class="p">;</span>
<span class="k">mod</span> <span class="n">app</span><span class="p">;</span>
<span class="k">mod</span> <span class="n">commands</span><span class="p">;</span>
<span class="k">mod</span> <span class="n">config</span><span class="p">;</span>
<span class="k">mod</span> <span class="n">platform</span><span class="p">;</span>

<span class="k">use</span> <span class="nn">anyhow</span><span class="p">::</span><span class="nb">Result</span><span class="p">;</span>
<span class="k">use</span> <span class="nn">clap</span><span class="p">::</span><span class="n">Parser</span><span class="p">;</span>

<span class="k">use</span> <span class="k">crate</span><span class="p">::</span><span class="nn">commands</span><span class="p">::</span><span class="nn">cli</span><span class="p">::</span><span class="n">Cli</span><span class="p">;</span>

<span class="k">fn</span> <span class="nf">main</span><span class="p">()</span> <span class="k">-&gt;</span> <span class="nb">Result</span><span class="o">&lt;</span><span class="p">()</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="k">let</span> <span class="n">cli</span> <span class="o">=</span> <span class="nn">Cli</span><span class="p">::</span><span class="nf">parse</span><span class="p">();</span>
    <span class="nn">app</span><span class="p">::</span><span class="nf">set_global_verbosity</span><span class="p">(</span><span class="n">cli</span><span class="py">.verbose</span><span class="nf">.log_level_filter</span><span class="p">());</span>
    <span class="nn">app</span><span class="p">::</span><span class="nf">set_global_config</span><span class="p">(</span><span class="nn">config</span><span class="p">::</span><span class="nf">load</span><span class="p">()</span><span class="o">?</span><span class="p">);</span>

    <span class="n">cli</span><span class="nf">.exec</span><span class="p">()</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="commands">Commands</h3>

<p>Let’s now add an interface for managing the configuration.</p>

<p>First create the following files:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// file: "src/commands/config/mod.rs"</span>
<span class="k">pub</span> <span class="k">mod</span> <span class="n">cli</span><span class="p">;</span>
<span class="k">pub</span> <span class="k">mod</span> <span class="n">find</span><span class="p">;</span>
<span class="k">pub</span> <span class="k">mod</span> <span class="n">set</span><span class="p">;</span>
</code></pre></div></div>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// file: "src/commands/config/cli.rs"</span>
<span class="k">use</span> <span class="nn">anyhow</span><span class="p">::</span><span class="nb">Result</span><span class="p">;</span>
<span class="k">use</span> <span class="nn">clap</span><span class="p">::{</span><span class="n">Args</span><span class="p">,</span> <span class="n">Subcommand</span><span class="p">};</span>

<span class="cd">/// Manage the config file</span>
<span class="nd">#[derive(Args,</span> <span class="nd">Debug)]</span>
<span class="nd">#[command()]</span>
<span class="k">pub</span> <span class="k">struct</span> <span class="n">Cli</span> <span class="p">{</span>
    <span class="nd">#[command(subcommand)]</span>
    <span class="n">command</span><span class="p">:</span> <span class="n">Commands</span><span class="p">,</span>
<span class="p">}</span>

<span class="nd">#[derive(Subcommand,</span> <span class="nd">Debug)]</span>
<span class="k">enum</span> <span class="n">Commands</span> <span class="p">{</span>
    <span class="nf">Find</span><span class="p">(</span><span class="k">super</span><span class="p">::</span><span class="nn">find</span><span class="p">::</span><span class="n">Cli</span><span class="p">),</span>
    <span class="nf">Set</span><span class="p">(</span><span class="k">super</span><span class="p">::</span><span class="nn">set</span><span class="p">::</span><span class="nn">cli</span><span class="p">::</span><span class="n">Cli</span><span class="p">),</span>
<span class="p">}</span>

<span class="k">impl</span> <span class="n">Cli</span> <span class="p">{</span>
    <span class="k">pub</span> <span class="k">fn</span> <span class="nf">exec</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Result</span><span class="o">&lt;</span><span class="p">()</span><span class="o">&gt;</span> <span class="p">{</span>
        <span class="k">match</span> <span class="o">&amp;</span><span class="k">self</span><span class="py">.command</span> <span class="p">{</span>
            <span class="nn">Commands</span><span class="p">::</span><span class="nf">Find</span><span class="p">(</span><span class="n">cli</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="n">cli</span><span class="nf">.exec</span><span class="p">(),</span>
            <span class="nn">Commands</span><span class="p">::</span><span class="nf">Set</span><span class="p">(</span><span class="n">cli</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="n">cli</span><span class="nf">.exec</span><span class="p">(),</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// file: "src/commands/config/find.rs"</span>
<span class="k">use</span> <span class="nn">anyhow</span><span class="p">::</span><span class="nb">Result</span><span class="p">;</span>
<span class="k">use</span> <span class="nn">clap</span><span class="p">::</span><span class="n">Args</span><span class="p">;</span>

<span class="k">use</span> <span class="k">crate</span><span class="p">::</span><span class="n">config</span><span class="p">;</span>

<span class="cd">/// Locate the config file</span>
<span class="nd">#[derive(Args,</span> <span class="nd">Debug)]</span>
<span class="nd">#[command()]</span>
<span class="k">pub</span> <span class="k">struct</span> <span class="n">Cli</span> <span class="p">{}</span>

<span class="k">impl</span> <span class="n">Cli</span> <span class="p">{</span>
    <span class="k">pub</span> <span class="k">fn</span> <span class="nf">exec</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Result</span><span class="o">&lt;</span><span class="p">()</span><span class="o">&gt;</span> <span class="p">{</span>
        <span class="nd">display!</span><span class="p">(</span><span class="s">"{}"</span><span class="p">,</span> <span class="nn">config</span><span class="p">::</span><span class="nf">path</span><span class="p">()</span><span class="o">?</span><span class="nf">.display</span><span class="p">());</span>

        <span class="nf">Ok</span><span class="p">(())</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// file: "src/commands/config/set/mod.rs"</span>
<span class="k">pub</span> <span class="k">mod</span> <span class="n">cli</span><span class="p">;</span>
<span class="k">pub</span> <span class="k">mod</span> <span class="n">repo</span><span class="p">;</span>
</code></pre></div></div>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// file: "src/commands/config/set/cli.rs"</span>
<span class="k">use</span> <span class="nn">anyhow</span><span class="p">::</span><span class="nb">Result</span><span class="p">;</span>
<span class="k">use</span> <span class="nn">clap</span><span class="p">::{</span><span class="n">Args</span><span class="p">,</span> <span class="n">Subcommand</span><span class="p">};</span>

<span class="cd">/// Modify the config file</span>
<span class="nd">#[derive(Args,</span> <span class="nd">Debug)]</span>
<span class="nd">#[command()]</span>
<span class="k">pub</span> <span class="k">struct</span> <span class="n">Cli</span> <span class="p">{</span>
    <span class="nd">#[command(subcommand)]</span>
    <span class="n">command</span><span class="p">:</span> <span class="n">Commands</span><span class="p">,</span>
<span class="p">}</span>

<span class="nd">#[derive(Subcommand,</span> <span class="nd">Debug)]</span>
<span class="k">enum</span> <span class="n">Commands</span> <span class="p">{</span>
    <span class="nf">Repo</span><span class="p">(</span><span class="k">super</span><span class="p">::</span><span class="nn">repo</span><span class="p">::</span><span class="n">Cli</span><span class="p">),</span>
<span class="p">}</span>

<span class="k">impl</span> <span class="n">Cli</span> <span class="p">{</span>
    <span class="k">pub</span> <span class="k">fn</span> <span class="nf">exec</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Result</span><span class="o">&lt;</span><span class="p">()</span><span class="o">&gt;</span> <span class="p">{</span>
        <span class="k">match</span> <span class="o">&amp;</span><span class="k">self</span><span class="py">.command</span> <span class="p">{</span>
            <span class="nn">Commands</span><span class="p">::</span><span class="nf">Repo</span><span class="p">(</span><span class="n">cli</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="n">cli</span><span class="nf">.exec</span><span class="p">(),</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// file: "src/commands/config/set/repo.rs"</span>
<span class="k">use</span> <span class="nn">anyhow</span><span class="p">::</span><span class="nb">Result</span><span class="p">;</span>
<span class="k">use</span> <span class="nn">clap</span><span class="p">::</span><span class="n">Args</span><span class="p">;</span>

<span class="k">use</span> <span class="k">crate</span><span class="p">::{</span><span class="n">app</span><span class="p">,</span> <span class="n">config</span><span class="p">,</span> <span class="n">platform</span><span class="p">};</span>

<span class="cd">/// Set the path to the repository</span>
<span class="nd">#[derive(Args,</span> <span class="nd">Debug)]</span>
<span class="nd">#[command()]</span>
<span class="k">pub</span> <span class="k">struct</span> <span class="n">Cli</span> <span class="p">{</span>
    <span class="n">path</span><span class="p">:</span> <span class="nb">String</span><span class="p">,</span>
<span class="p">}</span>

<span class="k">impl</span> <span class="n">Cli</span> <span class="p">{</span>
    <span class="k">pub</span> <span class="k">fn</span> <span class="nf">exec</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Result</span><span class="o">&lt;</span><span class="p">()</span><span class="o">&gt;</span> <span class="p">{</span>
        <span class="k">let</span> <span class="n">path</span> <span class="o">=</span> <span class="nn">platform</span><span class="p">::</span><span class="nf">canonicalize_path</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="py">.path</span><span class="p">);</span>
        <span class="nd">debug!</span><span class="p">(</span><span class="s">"Setting repository path to: {}"</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">path</span><span class="p">);</span>

        <span class="k">let</span> <span class="k">mut</span> <span class="n">config</span> <span class="o">=</span> <span class="nn">app</span><span class="p">::</span><span class="nf">config</span><span class="p">()</span><span class="nf">.clone</span><span class="p">();</span>
        <span class="n">config</span><span class="py">.repo</span> <span class="o">=</span> <span class="n">path</span><span class="p">;</span>
        <span class="nn">config</span><span class="p">::</span><span class="nf">save</span><span class="p">(</span><span class="n">config</span><span class="p">)</span><span class="o">?</span><span class="p">;</span>

        <span class="nf">Ok</span><span class="p">(())</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Then add the new module and update the root command:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// file: "src/commands/mod.rs"</span>
<span class="k">pub</span> <span class="k">mod</span> <span class="n">cli</span><span class="p">;</span>
<span class="k">pub</span> <span class="k">mod</span> <span class="n">config</span><span class="p">;</span>
<span class="k">pub</span> <span class="k">mod</span> <span class="n">exec</span><span class="p">;</span>
<span class="k">pub</span> <span class="k">mod</span> <span class="n">test</span><span class="p">;</span>
</code></pre></div></div>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// file: "src/commands/cli.rs"</span>
<span class="k">use</span> <span class="nn">anyhow</span><span class="p">::</span><span class="nb">Result</span><span class="p">;</span>
<span class="k">use</span> <span class="nn">clap</span><span class="p">::{</span><span class="n">Parser</span><span class="p">,</span> <span class="n">Subcommand</span><span class="p">};</span>
<span class="k">use</span> <span class="nn">clap_verbosity_flag</span><span class="p">::{</span><span class="n">InfoLevel</span><span class="p">,</span> <span class="n">Verbosity</span><span class="p">};</span>

<span class="cd">/// Rusty example app</span>
<span class="nd">#[derive(Parser,</span> <span class="nd">Debug)]</span>
<span class="nd">#[command(version,</span> <span class="nd">bin_name</span> <span class="nd">=</span> <span class="s">"rusty"</span><span class="nd">,</span> <span class="nd">disable_help_subcommand</span> <span class="nd">=</span> <span class="kc">true</span><span class="nd">)]</span>
<span class="k">pub</span> <span class="k">struct</span> <span class="n">Cli</span> <span class="p">{</span>
    <span class="nd">#[clap(flatten)]</span>
    <span class="k">pub</span> <span class="n">verbose</span><span class="p">:</span> <span class="n">Verbosity</span><span class="o">&lt;</span><span class="n">InfoLevel</span><span class="o">&gt;</span><span class="p">,</span>

    <span class="nd">#[command(subcommand)]</span>
    <span class="n">command</span><span class="p">:</span> <span class="n">Commands</span><span class="p">,</span>
<span class="p">}</span>

<span class="nd">#[derive(Subcommand,</span> <span class="nd">Debug)]</span>
<span class="k">enum</span> <span class="n">Commands</span> <span class="p">{</span>
    <span class="nf">Config</span><span class="p">(</span><span class="k">super</span><span class="p">::</span><span class="nn">config</span><span class="p">::</span><span class="nn">cli</span><span class="p">::</span><span class="n">Cli</span><span class="p">),</span>
    <span class="nf">Exec</span><span class="p">(</span><span class="k">super</span><span class="p">::</span><span class="nn">exec</span><span class="p">::</span><span class="n">Cli</span><span class="p">),</span>
    <span class="nf">Test</span><span class="p">(</span><span class="k">super</span><span class="p">::</span><span class="nn">test</span><span class="p">::</span><span class="n">Cli</span><span class="p">),</span>
<span class="p">}</span>

<span class="k">impl</span> <span class="n">Cli</span> <span class="p">{</span>
    <span class="k">pub</span> <span class="k">fn</span> <span class="nf">exec</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Result</span><span class="o">&lt;</span><span class="p">()</span><span class="o">&gt;</span> <span class="p">{</span>
        <span class="k">match</span> <span class="o">&amp;</span><span class="k">self</span><span class="py">.command</span> <span class="p">{</span>
            <span class="nn">Commands</span><span class="p">::</span><span class="nf">Config</span><span class="p">(</span><span class="n">cli</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="n">cli</span><span class="nf">.exec</span><span class="p">(),</span>
            <span class="nn">Commands</span><span class="p">::</span><span class="nf">Exec</span><span class="p">(</span><span class="n">cli</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="n">cli</span><span class="nf">.exec</span><span class="p">(),</span>
            <span class="nn">Commands</span><span class="p">::</span><span class="nf">Test</span><span class="p">(</span><span class="n">cli</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="n">cli</span><span class="nf">.exec</span><span class="p">(),</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>At this point the project’s structure should look like:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>src
├── commands
│   ├── config
│   │   ├── set
│   │   │   ├── cli.rs
│   │   │   ├── mod.rs
│   │   │   └── repo.rs
│   │   ├── cli.rs
│   │   ├── find.rs
│   │   └── mod.rs
│   ├── cli.rs
│   ├── exec.rs
│   ├── mod.rs
│   └── test.rs
├── app.rs
├── config.rs
├── macros.rs
├── main.rs
└── platform.rs
Cargo.lock
Cargo.toml
</code></pre></div></div>

<p>Let’s try it out:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>❯ cargo run -q -- --help
Rusty example app

Usage: rusty [OPTIONS] &lt;COMMAND&gt;

Commands:
  config  Manage the config file
  exec    Execute an arbitrary command

Options:
  -v, --verbose...  More output per occurrence
  -q, --quiet...    Less output per occurrence
  -h, --help        Print help information
  -V, --version     Print version information
</code></pre></div></div>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>❯ cargo run -q -- config --help
Manage the config file

Usage: rusty config [OPTIONS] &lt;COMMAND&gt;

Commands:
  find  Locate the config file
  set   Modify the config file

Options:
  -v, --verbose...  More output per occurrence
  -q, --quiet...    Less output per occurrence
  -h, --help        Print help information
</code></pre></div></div>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>❯ cargo run -q -- config set repo . -v
Setting repository path to: C:\Users\ofek\Desktop\code\rusty
</code></pre></div></div>

<h2 id="shell-completion">Shell completion</h2>

<p>Let’s offer shell completion using <a href="https://crates.io/crates/clap_complete">clap_complete</a>:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cargo add clap_complete
</code></pre></div></div>

<p>Create the following file:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// file: "src/commands/complete.rs"</span>
<span class="k">use</span> <span class="nn">anyhow</span><span class="p">::</span><span class="nb">Result</span><span class="p">;</span>
<span class="k">use</span> <span class="nn">clap</span><span class="p">::{</span><span class="n">Args</span><span class="p">,</span> <span class="n">CommandFactory</span><span class="p">};</span>
<span class="k">use</span> <span class="nn">clap_complete</span><span class="p">::{</span><span class="n">generate</span><span class="p">,</span> <span class="n">Shell</span><span class="p">};</span>
<span class="k">use</span> <span class="nn">std</span><span class="p">::</span><span class="n">io</span><span class="p">;</span>

<span class="k">use</span> <span class="k">crate</span><span class="p">::</span><span class="nn">commands</span><span class="p">::</span><span class="nn">cli</span><span class="p">::</span><span class="n">Cli</span> <span class="k">as</span> <span class="n">RootCli</span><span class="p">;</span>

<span class="cd">/// Display the completion file for a given shell</span>
<span class="nd">#[derive(Args,</span> <span class="nd">Debug)]</span>
<span class="nd">#[command()]</span>
<span class="k">pub</span> <span class="k">struct</span> <span class="n">Cli</span> <span class="p">{</span>
    <span class="nd">#[arg(value_enum)]</span>
    <span class="n">shell</span><span class="p">:</span> <span class="n">Shell</span><span class="p">,</span>
<span class="p">}</span>

<span class="k">impl</span> <span class="n">Cli</span> <span class="p">{</span>
    <span class="k">pub</span> <span class="k">fn</span> <span class="nf">exec</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Result</span><span class="o">&lt;</span><span class="p">()</span><span class="o">&gt;</span> <span class="p">{</span>
        <span class="k">let</span> <span class="k">mut</span> <span class="n">cmd</span> <span class="o">=</span> <span class="nn">RootCli</span><span class="p">::</span><span class="nf">command</span><span class="p">();</span>
        <span class="k">let</span> <span class="n">bin_name</span> <span class="o">=</span> <span class="n">cmd</span><span class="nf">.get_name</span><span class="p">()</span><span class="nf">.to_string</span><span class="p">();</span>
        <span class="nf">generate</span><span class="p">(</span><span class="k">self</span><span class="py">.shell</span><span class="p">,</span> <span class="o">&amp;</span><span class="k">mut</span> <span class="n">cmd</span><span class="p">,</span> <span class="n">bin_name</span><span class="p">,</span> <span class="o">&amp;</span><span class="k">mut</span> <span class="nn">io</span><span class="p">::</span><span class="nf">stdout</span><span class="p">());</span>

        <span class="nf">Ok</span><span class="p">(())</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Then add the new module and update the root command:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// file: "src/commands/mod.rs"</span>
<span class="k">pub</span> <span class="k">mod</span> <span class="n">cli</span><span class="p">;</span>
<span class="k">pub</span> <span class="k">mod</span> <span class="n">complete</span><span class="p">;</span>
<span class="k">pub</span> <span class="k">mod</span> <span class="n">config</span><span class="p">;</span>
<span class="k">pub</span> <span class="k">mod</span> <span class="n">exec</span><span class="p">;</span>
<span class="k">pub</span> <span class="k">mod</span> <span class="n">test</span><span class="p">;</span>
</code></pre></div></div>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// file: "src/commands/cli.rs"</span>
<span class="k">use</span> <span class="nn">anyhow</span><span class="p">::</span><span class="nb">Result</span><span class="p">;</span>
<span class="k">use</span> <span class="nn">clap</span><span class="p">::{</span><span class="n">Parser</span><span class="p">,</span> <span class="n">Subcommand</span><span class="p">};</span>
<span class="k">use</span> <span class="nn">clap_verbosity_flag</span><span class="p">::{</span><span class="n">InfoLevel</span><span class="p">,</span> <span class="n">Verbosity</span><span class="p">};</span>

<span class="cd">/// Rusty example app</span>
<span class="nd">#[derive(Parser,</span> <span class="nd">Debug)]</span>
<span class="nd">#[command(version,</span> <span class="nd">bin_name</span> <span class="nd">=</span> <span class="s">"rusty"</span><span class="nd">,</span> <span class="nd">disable_help_subcommand</span> <span class="nd">=</span> <span class="kc">true</span><span class="nd">)]</span>
<span class="k">pub</span> <span class="k">struct</span> <span class="n">Cli</span> <span class="p">{</span>
    <span class="nd">#[clap(flatten)]</span>
    <span class="k">pub</span> <span class="n">verbose</span><span class="p">:</span> <span class="n">Verbosity</span><span class="o">&lt;</span><span class="n">InfoLevel</span><span class="o">&gt;</span><span class="p">,</span>

    <span class="nd">#[command(subcommand)]</span>
    <span class="n">command</span><span class="p">:</span> <span class="n">Commands</span><span class="p">,</span>
<span class="p">}</span>

<span class="nd">#[derive(Subcommand,</span> <span class="nd">Debug)]</span>
<span class="k">enum</span> <span class="n">Commands</span> <span class="p">{</span>
    <span class="nf">Complete</span><span class="p">(</span><span class="k">super</span><span class="p">::</span><span class="nn">complete</span><span class="p">::</span><span class="n">Cli</span><span class="p">),</span>
    <span class="nf">Config</span><span class="p">(</span><span class="k">super</span><span class="p">::</span><span class="nn">config</span><span class="p">::</span><span class="nn">cli</span><span class="p">::</span><span class="n">Cli</span><span class="p">),</span>
    <span class="nf">Exec</span><span class="p">(</span><span class="k">super</span><span class="p">::</span><span class="nn">exec</span><span class="p">::</span><span class="n">Cli</span><span class="p">),</span>
    <span class="nf">Test</span><span class="p">(</span><span class="k">super</span><span class="p">::</span><span class="nn">test</span><span class="p">::</span><span class="n">Cli</span><span class="p">),</span>
<span class="p">}</span>

<span class="k">impl</span> <span class="n">Cli</span> <span class="p">{</span>
    <span class="k">pub</span> <span class="k">fn</span> <span class="nf">exec</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Result</span><span class="o">&lt;</span><span class="p">()</span><span class="o">&gt;</span> <span class="p">{</span>
        <span class="k">match</span> <span class="o">&amp;</span><span class="k">self</span><span class="py">.command</span> <span class="p">{</span>
            <span class="nn">Commands</span><span class="p">::</span><span class="nf">Complete</span><span class="p">(</span><span class="n">cli</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="n">cli</span><span class="nf">.exec</span><span class="p">(),</span>
            <span class="nn">Commands</span><span class="p">::</span><span class="nf">Config</span><span class="p">(</span><span class="n">cli</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="n">cli</span><span class="nf">.exec</span><span class="p">(),</span>
            <span class="nn">Commands</span><span class="p">::</span><span class="nf">Exec</span><span class="p">(</span><span class="n">cli</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="n">cli</span><span class="nf">.exec</span><span class="p">(),</span>
            <span class="nn">Commands</span><span class="p">::</span><span class="nf">Test</span><span class="p">(</span><span class="n">cli</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="n">cli</span><span class="nf">.exec</span><span class="p">(),</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Let’s see:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>❯ cargo run -q -- --help
Rusty example app

Usage: rusty [OPTIONS] &lt;COMMAND&gt;

Commands:
  complete  Display the completion file for a given shell
  config    Manage the config file
  exec      Execute an arbitrary command

Options:
  -v, --verbose...  More output per occurrence
  -q, --quiet...    Less output per occurrence
  -h, --help        Print help information
  -V, --version     Print version information
</code></pre></div></div>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>❯ cargo run -q -- complete --help
Display the completion file for a given shell

Usage: rusty complete [OPTIONS] &lt;SHELL&gt;

Arguments:
  &lt;SHELL&gt;  [possible values: bash, elvish, fish, powershell, zsh]

Options:
  -v, --verbose...  More output per occurrence
  -q, --quiet...    Less output per occurrence
  -h, --help        Print help information
</code></pre></div></div>

<h2 id="conclusion">Conclusion</h2>

<p>Now you should be all set to implement your own command line applications using the same strategies and directory structure described. The final code lives <a href="https://github.com/ofek/rusty">here</a>.</p>]]></content><author><name>Ofek Lev</name><email>human@ofek.dev</email></author><category term="guides" /><summary type="html"><![CDATA[In this post I’ll walk through the setup of an example project to show how to build a modern CLI in Rust.]]></summary></entry><entry><title type="html">Site is finally live!</title><link href="https://ofek.dev/words/misc/2019-12-08-site-finally-up/" rel="alternate" type="text/html" title="Site is finally live!" /><published>2019-12-08T17:48:42+00:00</published><updated>2026-04-05T18:25:56+00:00</updated><id>https://ofek.dev/words/misc/site-finally-up</id><content type="html" xml:base="https://ofek.dev/words/misc/2019-12-08-site-finally-up/"><![CDATA[<p>Welcome travelers! I’ve been procrastinating putting together a web site/portfolio/blog for the better part of a year and now the first version is finished.</p>

<p>Do not expect regular content; I will only dedicate blog posts to tutorials or to topics that are particularly interesting 😀</p>]]></content><author><name>Ofek Lev</name><email>human@ofek.dev</email></author><category term="misc" /><summary type="html"><![CDATA[Welcome travelers! I’ve been procrastinating putting together a web site/portfolio/blog for the better part of a year and now the first version is finished.]]></summary></entry></feed>