<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>emre şahin's digital garden 🍃 - CLI</title>
    <link>https://emresahin.net/tags/cli/</link>
    <description>Posts in the CLI tag</description>
    <language>en</language>
    <managingEditor>contact@emresahin.net (Emre Şahin)</managingEditor>
    <lastBuildDate>Tue, 15 Sep 2026 19:46:32 +0000</lastBuildDate>
    <atom:link href="https://emresahin.net/tags/cli/rss.xml" rel="self" type="application/rss+xml"/>
    <item>
      <title>devlog 24</title>
      <published>2025-04-24T02:54:54+00:00</published>
      <updated>2025-04-24T02:54:54+00:00</updated>
      <author>Emre Şahin</author>
      <pubDate>Thu, 24 Apr 2025 02:54:54 +0000</pubDate>
      <link>https://emresahin.net/devlog-24/</link>
      <guid isPermaLink="true">https://emresahin.net/devlog-24/</guid>
      <description>🐇 Is there a way to implement Default for CLI structs? 🐢 There is a way if we start from the configuration, not just files. The default configuration is a TOML document. We can make it an XvcConfiguration struct and load and store it with confy . 🐇 We have a cascading set of configurations, but m...</description>
      <category>XVC</category>
      <category>Development</category>
      <category>XVC</category>
      <category>Rust</category>
      <category>Configuration</category>
      <category>confy</category>
      <category>CLI</category>
      <content:encoded><![CDATA[<p>🐇 Is there a way to implement <code>Default</code> for CLI structs?</p>
<p>🐢 There is a way if we start from the configuration, not just files. The default
configuration is a TOML document. We can make it an <code>XvcConfiguration</code> struct and
load and store it with <code>confy</code>.</p>
<p>🐇 We have a cascading set of configurations, but maybe we can start from the
struct and serialize/deserialize it on demand.</p>
<p>🐲 I don’t think we need to update <code>xvc-config</code> at the moment. It’s working, and
we don’t need to alter its inner workings in the near future.</p>
<p>🐢 I agree. We can use the <a href="https://docs.rs/config/"><code>config</code></a> crate when we need to update and have
enough time to work on this.</p>]]></content:encoded>
    </item>
    <item>
      <title>bits 15</title>
      <published>2025-03-27T09:52:06+00:00</published>
      <updated>2025-03-27T09:52:06+00:00</updated>
      <author>Emre Şahin</author>
      <pubDate>Thu, 27 Mar 2025 09:52:06 +0000</pubDate>
      <link>https://emresahin.net/bits-15/</link>
      <guid isPermaLink="true">https://emresahin.net/bits-15/</guid>
      <description>How to use a Python virtual environment with Nushell: virtualenv .venv overlay use .venv/bin/activate.nu</description>
      <category>bits</category>
      <category>shell</category>
      <category>nushell</category>
      <category>Python</category>
      <category>venv</category>
      <category>virtualenv</category>
      <category>cli</category>
      <content:encoded><![CDATA[<p>How to use a Python virtual environment with Nushell:</p>
<pre><code class="language-nu">virtualenv .venv
overlay use .venv/bin/activate.nu
</code></pre>]]></content:encoded>
    </item>
    <item>
      <title>My Nushell aliases for `gh` command</title>
      <published>2025-01-29T08:04:19+00:00</published>
      <updated>2025-01-29T08:04:19+00:00</updated>
      <author>Emre Şahin</author>
      <pubDate>Wed, 29 Jan 2025 08:04:19 +0000</pubDate>
      <link>https://emresahin.net/my-nushell-aliases-for--gh--command/</link>
      <guid isPermaLink="true">https://emresahin.net/my-nushell-aliases-for--gh--command/</guid>
      <description>Almost every day, I start my work by playing with my configuration. Today, I added aliases for gh (GitHub CLI) to Nushell. # List issues related to me export def ghil [] { (gh issue list --search "involves:iesahin" --json title,url) | from json } # Add a comment to an issue export alias ghic = gh...</description>
      <category>Shell</category>
      <category>Nushell</category>
      <category>nushell</category>
      <category>gh</category>
      <category>GitHub</category>
      <category>CLI</category>
      <content:encoded><![CDATA[<p>Almost every day, I start my work by playing with my configuration. Today, I added aliases for <code>gh</code> (GitHub CLI) to Nushell.</p>
<pre><code class="language-nu"># List issues related to me
export def ghil [] { 
  (gh issue list --search "involves:iesahin" --json title,url) | from json
}

# Add a comment to an issue
export alias ghic = gh issue comment

# Create an issue
export alias ghiC = gh issue create $in

# View an issue
export alias ghiv = gh issue view

# View an issue in the browser
export alias ghiw = gh issue view --web

# List PRs in a table
export def ghpl [] {
  (gh pr list --json "title,url,headRefName" ) | from json
}

# View a PR
export alias ghpv = gh pr view

# View a PR in GitHub
export alias ghpw = gh pr view --web

# Checkout a PR
export alias ghco = gh pr checkout

# Add a comment to a PR
export alias ghpc = gh pr comment

# Show changes in a PR
export alias ghpd = gh pr diff

# Add a review to a PR
export alias ghpR = gh pr review

# Create a PR
export alias ghpC = gh pr create

# Merge a PR to main
export alias ghpM = gh pr merge

# List GH CI runs in a table
export def ghrl [] {
  (gh run list --json conclusion,displayTitle,headBranch,url ) | from json 
}

# View a GH CI run
export alias ghrv = gh run view

# View a GH CI run in the browser
export alias ghrw = gh run view --web 

# View output from a failed run
export alias ghrf = gh run view --log-failed

# Search code and get results in a table
export def ghsc [] { 
  (gh search code $in --json repository,path,textMatches,url) | from json
}
</code></pre>
<p>Aliases don’t allow pipes in Nushell, so I used <code>def</code> for those that require them.</p>
<p>When there is an <code>$in</code> parameter in an alias, it expects the input from a pipe.
An issue can be created like this, for example:</p>
<pre><code class="language-nu">"Your software sucks" | ghiC
</code></pre>
<p>To search for the code copied to the clipboard and get only the URLs, you can use:</p>
<pre><code class="language-nu">pbpaste | ghsc | get url
</code></pre>
<p>To open them in different browser windows:</p>
<pre><code class="language-nu">pbpaste | ghsc | get url | each { |u| start $u }
</code></pre>
<p>I’ll add a shortcut key for this last one. It’s so quick! :D</p>]]></content:encoded>
    </item>
    <item>
      <title>devlog 13</title>
      <published>2025-01-19T09:55:05+00:00</published>
      <updated>2025-01-19T09:55:05+00:00</updated>
      <author>Emre Şahin</author>
      <pubDate>Sun, 19 Jan 2025 09:55:05 +0000</pubDate>
      <link>https://emresahin.net/devlog-13/</link>
      <guid isPermaLink="true">https://emresahin.net/devlog-13/</guid>
      <description>🐇 So, what’s next? 🐲 We can work on completions or the GUI. 🐢 Completions are similar to the Homebrew work. It’s in another ecosystem and feels boring. 🐲 We can try to make it in Rust: “Writing shell completions in Rust.” 🐢 There are two solutions: One is clap_complete (https://docs.rs/clap_compl...</description>
      <category>xvc</category>
      <category>completions</category>
      <category>clap_complete</category>
      <category>subcommands</category>
      <category>xvc pipeline step dependency</category>
      <category>xvc aliases</category>
      <category>Rust</category>
      <category>rust-analyzer</category>
      <category>shell completion</category>
      <category>CLI</category>
      <category>auto-save.nvim</category>
      <content:encoded><![CDATA[<p>🐇 So, what’s next?</p>
<p>🐲 We can work on completions or the GUI.</p>
<p>🐢 Completions are similar to the Homebrew work. It’s in another ecosystem and feels boring.</p>
<p>🐲 We can try to make it in Rust: “Writing shell completions in Rust.”</p>
<p>🐢 There are two solutions: One is <code>clap_complete</code> (https://docs.rs/clap_complete/latest/clap_complete/) and the other is <code>shell_completion</code> (https://github.com/JoshMcguigan/shell_completion). The latter is very new but may be what we need: writing completions only with Rust.</p>
<p>🐲 The <code>shell_completion</code> crate is very bare-bones. It doesn’t have anything, actually. https://github.com/JoshMcguigan/shell_completion/issues/1</p>
<p>🐇 The feature that we need is dynamic completion. We’ll write something similar to <a href="https://docs.rs/clap_complete/latest/clap_complete/engine/struct.ArgValueCompleter.html"><code>ArgValueCompleter</code></a> for this.</p>
<p>🦊 I think we can just start a new branch.</p>
<p>🐲 We have a branch for Homebrew; should we merge it?</p>
<p>🐢 I think, yes, we can merge it. We’ll fix it if it breaks. It’s a separate workflow file anyway.</p>
<p>🦊 Created the PR.</p>
<pre><code class="language-sh">ghpl
264	add brew tap	add-brew-tap	OPEN	2025-01-03T05:14:45Z
</code></pre>
<p>🐢 Merged it.</p>
<p>🐇 Now let’s create a new branch and add <code>clap-complete</code> to <code>Cargo.toml</code>.</p>
<p>🐲 There are some packages that we need to take a look at. <code>thiserror</code> now has v2.</p>
<p>🐢 Upgraded packages and compiled. It works.</p>
<p>🐲 Let’s update the version.</p>
<p>🐢 Done. Now we can add <code>clap-complete</code>.</p>
<p>🦊 Added with the <code>unstable-dynamic</code> command. Can we build it again?</p>
<p>🐢 Built it. Now we can add a <code>completions</code> subcommand to Xvc.</p>
<p>🐲 Is this the right name for this?</p>
<p>🐢 It will output a shell script that we can source in the shell.</p>
<p>🐲 Okay, let’s add the subcommand now.</p>
<p>🐢 The example doesn’t work with the <code>clap</code> builder.</p>
<p>🐇 Let’s search for it.</p>
<hr>
<p>🐢 I think we’re extending ourselves a bit when trying to add all dynamic features of completion at once. We can just start by adding completion to <code>xvc-test-helper</code>.</p>
<p>🐇 Yep, that’s a better idea. We can have many more examples, and it’s certainly more straightforward.</p>
<p>🐢 One thing I noticed when working yesterday is that using auto-save was actually preventing many of these swap errors.</p>
<p>🐲 It was a bit slow. Can we take a look at a few others?</p>
<p>🐢 https://github.com/okuuva/auto-save.nvim seems a bit more polished.</p>
<p>🐇 Installed it, but haven’t seen an effect yet.</p>
<p>🐢 We may need to restart this.</p>
<p>🐲 Now, we can get into the <code>test-helper</code> completions.</p>
<p>🐢 Done. Added these quickly as you thought. Now adding this to <code>main</code> seems much easier.</p>
<p>🦊 That was a nice approach. Let’s dive into adding this to <code>main</code>.</p>
<p>🐇 Should we go with a separate command or just an option?</p>
<p>🐲 Our commands have distinct initial letters, allowing them to be used with just those letters. Adding another top-level command for this will make <code>c</code> useless.</p>
<p>🐢 I don’t think that’s a valid concern. We can create aliases for all commands and <code>completions</code> shouldn’t have to have an alias. But I agree that completions should not be a top-level command. It will be run once for installation at most.</p>
<p>🦊 I agree. Let’s call it <code>--completions</code>. It will be run as <code>xvc --completions zsh</code> and will print out the completions.</p>
<p>🐢 Okay. Let’s do this.</p>
<p>🦊 No errors left. Let’s install this version.</p>
<p>🐢 We get:</p>
<pre><code class="language-sh">error: 'xvc' requires a subcommand but one was not provided
  [subcommands: file, init, pipeline, storage, root, check-ignore, aliases, help]

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

For more information, try '--help'.
</code></pre>
<p>🦊 Umm, okay. I think we don’t have an option to create a command like that. Then we’ll have to print completions with a subcommand.</p>
<p>🐲 The above discussion is now moot. ❌</p>
<p>🐢 We can try to push, but probably it’s not worth it.</p>
<p>🐲 Let’s not lose time on this. I think we can remove the <code>aliases</code> command and replace it with <code>completions</code>, and add single-letter aliases to subcommands.</p>
<p>🐢 We can have an option in the <code>completions</code> command to print aliases. That will work.</p>
<p>🦊 I don’t think people will use the <code>aliases</code> command if we have single-letter command aliases.</p>
<p>🐢 You may be right. No need to try to maintain that at this time.</p>
<hr>
<p>🐢 Good morning, and I’m fed up with these <code>blink.cmp</code> errors, you know.</p>
<p>🐇 Reinstalling <code>blink</code> works. It’s interesting to rely on such unreliable software.</p>
<p>🐢 Oh, yeah. It’s <em>interesting</em>. Where were we yesterday?</p>
<p>🐲 We decided to rename the <code>aliases</code> command to <code>completions</code>.</p>
<p>🐇 And waiting for <code>rust-analyzer</code> to complete its analysis.</p>
<p>🐢 Uh, yeah. I see.</p>
<p>🦊 The code itself is very short, actually.</p>
<pre><code class="language-rust">    if let Some(shell) = cli_opts.completions {
        let mut cmd = XvcCLI::command();
        generate(shell, &amp;mut cmd, "xvc", &amp;mut io::stdout());
        return Ok(None);
    }</code></pre>
<p>🐲 We’ll use the <code>output!</code> macro instead of writing to <code>io::stdout()</code>, right?</p>
<p>🐢 Yes, we can rely on the usual output system. It will be slower to create an output thread but shouldn’t matter for outputting a shell script.</p>
<p>🐇 It takes a while for <code>rust-analyzer</code> to scan all the directories, it looks like. When I close the project, it just cannot reload it immediately.</p>
<p>🐢 We can use <code>cargo clean</code> from time to time.</p>
<p>🦊 <code>rust-analyzer</code> finished scanning; let’s try to rename <code>AliasesCLI</code>.</p>
<p>🐲 Let’s keep these here; maybe we’ll need them in our scripts:</p>
<pre><code class="language-sh"># Standard Xvc command aliases for longer commands.
alias xls='xvc file list'
alias pvc='xvc pipeline'
alias fvc='xvc file'
alias xvcf='xvc file'
alias xvcft='xvc file track'
alias xvcfl='xvc file list'
alias xvcfs='xvc file send'
alias xvcfb='xvc file bring'
alias xvcfh='xvc file hash'
alias xvcfco='xvc file checkout'
alias xvcfr='xvc file recheck'
alias xvcp='xvc pipeline'
alias xvcpr='xvc pipeline run'
alias xvcps='xvc pipeline step'
alias xvcpsn='xvc pipeline step new'
alias xvcpsd='xvc pipeline step dependency'
alias xvcpso='xvc pipeline step output'
alias xvcpi='xvc pipeline import'
alias xvcpe='xvc pipeline export'
alias xvcpl='xvc pipeline list'
alias xvcpn='xvc pipeline new'
alias xvcpu='xvc pipeline update'
alias xvcpd='xvc pipeline dag'
alias xvcs='xvc storage'
alias xvcsn='xvc storage new'
alias xvcsl='xvc storage list'
alias xvcsr='xvc storage remove'
</code></pre>
<p>🦊 We can use them when adding single-letter aliases.</p>
<p>🐇 There is a <a href="https://docs.rs/clap_complete/latest/clap_complete/aot/enum.Shell.html#method.from_env"><code>from_env</code></a> method for <code>Shell</code>; will we support it?</p>
<p>🦊 I think we can support it. It’s much easier to use if we omit the shell.</p>
<p>🐲 We had <code>aliases</code> in <code>xvc-core</code>, but we need to access <code>XvcCLI</code> from completions. The completions module must be moved to <code>xvc</code>.</p>
<p>🦊 We added the <code>clap_complete</code> dependency to the <code>xvc-pipeline</code> and <code>xvc-file</code> crates, but I don’t think they are necessary. Let’s remove them now.</p>
<p>🐢 Now only the <code>test-helper</code> and <code>xvc</code> crates have the <code>clap_complete</code> dependency.</p>
<p>🐇 Are we ready to test?</p>
<p>🐢 Completions are working. 🎉</p>
<p>🐲 Now we need to update the docs and doc tests, I believe.</p>
<p>🐢 There are also tests to update.</p>
<p>🐇 Tests are running now. In the meantime, can we take a look at the <code>blink</code> configuration?</p>
<p>🐲 When we removed <code>xvc aliases</code>, we also removed <code>pvc</code>, <code>xls</code>, and other aliases. Maybe we can add these to the docs.</p>
<p>🐢 We can put them in the <code>xvc completions</code> reference for now.</p>
<hr>
<p>🐢 It takes a while for <code>rust-analyzer</code> to finish analyzing the codebase.</p>
<p>🦊 Added aliases for <code>xvc pipeline</code> commands. Do you think we need to repeat root-level flags in <code>xvc-pipeline</code>?</p>
<p>🐢 No need to divert attention, I believe. Also, I still think there is an easier way to do that.</p>
<p>🐇 Okay. Do you think we should add easier subcommands to <code>step</code>? Like, <code>step new</code> becoming <code>xvc p s n</code> instead of <code>xvc p s n</code>? (Wait, that’s the same). I mean, more concise.</p>
<p>🐢 I think we can extend these even to the top-level, but shouldn’t make them visible. It will pollute the help text. We can add <code>xvc fl</code> for <code>file list</code> to avoid the space, but hide these from the help text.</p>
<p>🐲 That’s a good idea, but it will require including sub-crate level modules at the top level. We must test it first.</p>
<p>🐢 Let’s finish up the current changes and release them first.</p>
<p>🐇 By the way, we didn’t add two-letter abbreviations to the <code>xvc storage new</code> subcommands. Do you think we need them?</p>
<p>🐢 When we think about the frequency of these commands, no, I don’t think we need them. Users won’t add a new storage every day.</p>
<p>🐇 By that logic, we shouldn’t need an <code>n</code> for <code>xvc storage new</code>.</p>
<p>🐢 Actually, yeah. Maybe we should remove even <code>s</code>.</p>
<p>🦊 <code>storage list</code> may be useful.</p>
<p>🐢 <code>s</code> is a very common letter, though. We can have other uses for that letter.</p>
<p>🐇  Updating <code>xvc p s dependency</code> options, but it looks like making dependency options subcommands is a better way.</p>
<p>🦊 We didn’t do it because currently we can supply multiple options with a single command. If we go the subcommand route, we’ll have to write all dependencies one by one. Adding multiple subcommands with <code>clap</code> is a bit tricky.</p>
<p>🐢 Umm. Yeah, I see.</p>
<p>🐇 Maybe we can provide a separate command to add multiple dependencies. Like, <code>xvc p s d add 'lines=myfile.csv::10-20; file=myimage.jpg; glob=dir/image-10*'</code></p>
<p>🐲 That looks like the start of a language. We need a parser for those strings. They will be freeform.</p>
<p>🦊 Another option is to keep the current options and add commands with the same names.</p>
<p>🐢 That will be confusing. The user will have both <code>--param</code> and <code>param</code>, and they will work differently.</p>
<p>🐇 We can have an <code>add</code> command that accepts the current options. Parsing will be done by <code>clap</code> just like now, but for a subcommand of <code>dependency</code>.</p>
<p>🐢 The full command will be something like <code>xvc pipeline step --step-name preprocessing dependency add --params 'params.json::batch_size'</code></p>
<p>🐇 With shorter commands, it’s like <code>xvc p s -s preprocessing d a --params 'params.json::batch_size'</code>, and this doesn’t look like <code>ffmpeg</code> monstrosities.</p>
<p>🐲 In any case, this is a backward-incompatible change. This should wait for v0.7 along with ECS changes.</p>
<p>🐢 ECS changes are not user-visible, but these are. Certainly needs a minor version update.</p>
<p>🐇 Then, okay, let’s keep the current ones for this version and think about updating them in a future version.</p>
<p>🐢 Okay. Let’s finish up and we’ll create a separate invisible subcommand to ask questions to the Xvc repo for completions.</p>
<p>🐇 We completed completions for the common command structure. Now, we need a way to show certain info after certain commands. A tab after <code>xvc p r -p</code> should show pipeline names, for example.</p>
<p>🐢 Umm, yeah. And these should be as quick as possible. They shouldn’t check Git or any other things.</p>
<p>🐇 I looked here and there, and the only working example I found is here: https://github.com/clap-rs/clap/blob/master/clap_complete/tests/testsuite/zsh.rs#L248 in <code>clap_complete</code> tests.</p>
<p>🐢 Let’s try to dive in. We can start by cloning the repo, I believe.</p>]]></content:encoded>
    </item>
    <item>
      <title>Poor man's secrets manager with pass</title>
      <published>2025-01-02T15:21:45+00:00</published>
      <updated>2025-01-02T15:21:45+00:00</updated>
      <author>Emre Şahin</author>
      <pubDate>Thu, 02 Jan 2025 15:21:45 +0000</pubDate>
      <link>https://emresahin.net/poor-man-s-secrets-manager-with-pass/</link>
      <guid isPermaLink="true">https://emresahin.net/poor-man-s-secrets-manager-with-pass/</guid>
      <description>I recently restarted using pass , the standard Unix password manager, to store my passwords. At work, we use Doppler to manage our secrets. It injects environment variables before running commands. I thought, why can’t I do the same with a simple script? The following script evaluates all passwor...</description>
      <category>Security</category>
      <category>Tools</category>
      <category>pass</category>
      <category>secrets-management</category>
      <category>cli</category>
      <category>shell-script</category>
      <category>zsh</category>
      <content:encoded><![CDATA[<p>I recently restarted using <code>pass</code>, the standard Unix password manager, to store my passwords.</p>
<p>At work, we use Doppler to manage our secrets. It injects environment variables before running commands. I thought, why can’t I do the same with a simple script?</p>
<p>The following script evaluates all password files with <code>env</code> in their names. I keep my environment variables in files named <code>env-aws</code>, etc., in the following format:</p>
<pre><code class="language-bash">export AWS_ACCESS_KEY_ID="123456...."
</code></pre>
<p>The following script, which I called <code>rws</code> (short for <code>run-with-secrets</code>), allows me to run a command like <code>rws s3cmd</code> and use <code>pass</code> to inject the variables. So far, I’m happy with it.</p>
<pre><code class="language-zsh">#!/bin/zsh

cmd="$@"

fd -F env $HOME/.password-store | while read file ; do
  bb="${file:t:r}"
  eval $(pass show ${bb})
done

exec $cmd
</code></pre>]]></content:encoded>
    </item>
    <item>
      <title>devlog 6</title>
      <published>2024-07-12T08:53:14+00:00</published>
      <updated>2024-07-12T08:53:14+00:00</updated>
      <author>Emre Şahin</author>
      <pubDate>Fri, 12 Jul 2024 08:53:14 +0000</pubDate>
      <link>https://emresahin.net/devlog-6/</link>
      <guid isPermaLink="true">https://emresahin.net/devlog-6/</guid>
      <description>I have a habit of testing against the CLI’s help string output. It allows me to keep the documentation up to date and makes me aware of any undocumented options. When new features are added, the help text changes and the test fails, which prompts me to add those options to the documentation. I tr...</description>
      <category>devlog</category>
      <category>Software Development</category>
      <category>xvc</category>
      <category>Python</category>
      <category>pytest</category>
      <category>clap</category>
      <category>CLI</category>
      <category>Testing</category>
      <content:encoded><![CDATA[<p>I have a habit of testing against the CLI’s help string output. It allows me to
keep the documentation up to date and makes me aware of any undocumented options.
When new features are added, the help text changes and the test fails, which
prompts me to add those options to the documentation.</p>
<p>I tried the same approach when testing Python bindings with Pytest:</p>
<pre><code class="language-python">def test_pipeline_step_dependency(empty_xvc_repo):
    dep_help = empty_xvc_repo.pipeline().step().dependency(help=True)
    expected = """
Usage: xvc pipeline step dependency [OPTIONS] --step-name &lt;STEP_NAME&gt;

Options:
  -s, --step-name &lt;STEP_NAME&gt;
          Name of the step to add the dependency to
"""
    assert dep_help == expected
</code></pre>
<p>This doesn’t work because the help text is generated by <a href="https://docs.rs/clap/latest/clap/">clap</a> and skips the usual
thread-based output handler. All command output and errors in Xvc are returned
as strings from the command, except for the help text that’s generated by <a href="https://docs.rs/clap/latest/clap/">clap</a>
automatically.</p>
<p>There are probably workarounds for this, but I won’t pursue them further as I don’t
test the actual functionality in the Python bindings anyway.</p>]]></content:encoded>
    </item>
    <item>
      <title>devlog 4</title>
      <published>2024-06-05T10:04:54+00:00</published>
      <updated>2024-06-05T10:04:54+00:00</updated>
      <author>Emre Şahin</author>
      <pubDate>Wed, 05 Jun 2024 10:04:54 +0000</pubDate>
      <link>https://emresahin.net/devlog-4/</link>
      <guid isPermaLink="true">https://emresahin.net/devlog-4/</guid>
      <description>Let’s start by looking at the debug output issue. We can start by replacing the eprintln! macros with println! , perhaps. I replaced the eprintln! s with println! , but it didn’t make any difference. Maybe we should remove those statements completely. I can’t really find the place that kills the ...</description>
      <category>devlog</category>
      <category>xvc</category>
      <category>xvc storage</category>
      <category>python</category>
      <category>clap</category>
      <category>debug</category>
      <category>rust</category>
      <category>s3</category>
      <category>cli</category>
      <content:encoded><![CDATA[<p>Let’s start by looking at the debug output issue. We can start by replacing the <code>eprintln!</code> macros with <code>println!</code>, perhaps.</p>
<p>I replaced the <code>eprintln!</code>s with <code>println!</code>, but it didn’t make any difference. Maybe we should remove those statements completely.</p>
<p>I can’t really find the place that kills the kernel. The last command to run is <code>xvc file list</code>:</p>
<pre><code class="language-python">print(xvc_test_data.file().list("test-data/dir-0002"))
</code></pre>
<p>and the output it produces is:</p>
<pre><code>[src/output.rs:144:13] &amp;output_str = "SS         131 2024-06-05 08:57:11 41e16be7          test-data/dir-0002/file-0003.bin\nSS         131 2024-06-05 08:57:11 27f0
efd0          test-data/dir-0002/file-0002.bin\nSS         131 2024-06-05 08:57:11 66de5084          test-data/dir-0002/file-0001.bin\nTotal #: 3 Workspace Size:
      393 Cached Size:        6006\n"
</code></pre>
<p><code>print</code> may be causing the crash, but the more likely cause is the command that comes after this:</p>
<pre><code>!ls -l test-data/dir-0001/
</code></pre>
<p>I replaced this with <code>lsd</code>, which also failed. Maybe it’s actually a Python crash or bug.</p>
<p>The way to understand is to create a notebook file with only that cell and try to run it.</p>
<p>The <code>ls</code> line runs fine with a new notebook. It even runs on the <code>README</code> file when run at the beginning. The line that makes the kernel crash is:</p>
<pre><code class="language-python">xvc_test_data.storage().new_s3(name="backup", bucket_name="xvc-test", region="eu-central-1", storage_prefix="xvc-storage")
</code></pre>
<p>We can start by removing the <code>new_s3</code> part.</p>
<p>The <code>storage()</code> method runs fine. It returns an <code>XvcStorage()</code> object, as it should.</p>
<p>When I run <code>storage().list()</code>, it takes a very long time. The bug is likely related to <code>storage()</code>.</p>
<p>It looks like the <code>storage</code> object was adding <code>file</code> instead of <code>storage</code> as a subcommand. I’ve fixed it now.</p>
<p>That was the bug. The <code>README</code> notebook now creates the S3 storage.</p>
<p>What was the reason behind this?</p>
<p>Parsing the CLI to the <code>XvcCLI</code> object was perhaps the culprit. Let’s look at it more clearly.</p>
<p>Let’s try <code>xvc file new s3</code> as a command to see how it behaves.</p>
<p>It says <em>unrecognized subcommand</em> for <code>new</code>.</p>
<p>This is how it should be, but I wonder why it doesn’t work for the <code>XvcCLI</code> parser.</p>
<p>Anyway, it’s already 13:00, so let’s stop here for today.</p>]]></content:encoded>
    </item>
    <item>
      <title>bits 6</title>
      <published>2024-05-23T16:33:49+00:00</published>
      <updated>2024-05-23T16:33:49+00:00</updated>
      <author>Emre Şahin</author>
      <pubDate>Thu, 23 May 2024 16:33:49 +0000</pubDate>
      <link>https://emresahin.net/bits-6/</link>
      <guid isPermaLink="true">https://emresahin.net/bits-6/</guid>
      <description>We’re using Graphite at work. Today, I configured a tmux shortcut to open a new pane and run gt create , which creates a new branch and commits all staged Git files. bind-key -n S-F12 split-window -h -c "#{pane_current_path}" "gt c" I just hit Shift-F12, and it opens a new pane in the current dir...</description>
      <category>bits</category>
      <category>tmux</category>
      <category>graphite</category>
      <category>cli</category>
      <category>git</category>
      <category>workflow</category>
      <category>productivity</category>
      <content:encoded><![CDATA[<p>We’re using <a href="https://graphite.dev">Graphite</a> at work. Today, I configured a tmux
shortcut to open a new pane and run <code>gt create</code>, which creates a new branch and
commits all staged Git files.</p>
<pre><code>bind-key -n S-F12 split-window -h -c "#{pane_current_path}" "gt c"
</code></pre>
<p>I just hit Shift-F12, and it opens a new pane in the current directory and runs the
command.</p>]]></content:encoded>
    </item>
    <item>
      <title>TIL: Get the Latest Git Commit SHA-1</title>
      <published>2024-02-13T09:58:12+00:00</published>
      <updated>2024-02-13T09:58:12+00:00</updated>
      <author>Emre Şahin</author>
      <pubDate>Tue, 13 Feb 2024 09:58:12 +0000</pubDate>
      <link>https://emresahin.net/til-6/</link>
      <guid isPermaLink="true">https://emresahin.net/til-6/</guid>
      <description>This command retrieves the SHA-1 hash of the latest commit: git rev-parse HEAD</description>
      <category>Shell</category>
      <category>Version Control</category>
      <category>Git</category>
      <category>CLI</category>
      <category>SHA-1</category>
      <content:encoded><![CDATA[<p>This command retrieves the SHA-1 hash of the latest commit:</p>
<pre><code class="language-bash">git rev-parse HEAD
</code></pre>]]></content:encoded>
    </item>
    <item>
      <title>airmux</title>
      <published>2024-01-25T09:01:54+00:00</published>
      <updated>2024-01-25T09:01:54+00:00</updated>
      <author>Emre Şahin</author>
      <pubDate>Thu, 25 Jan 2024 09:01:54 +0000</pubDate>
      <link>https://emresahin.net/airmux/</link>
      <guid isPermaLink="true">https://emresahin.net/airmux/</guid>
      <description>Airmux is a session manager for tmux that allows defining and managing multiple tmux sessions through project configuration files. Projects are defined in YAML or JSON files that specify settings such as commands to run, window/pane layouts, and other session settings. The start command loads and...</description>
      <category>tools</category>
      <category>tools</category>
      <category>tmux</category>
      <category>cli</category>
      <category>rust</category>
      <category>airmux</category>
      <category>terminal</category>
      <content:encoded><![CDATA[<ul>
<li><a href="https://github.com/dermoumi/airmux">Airmux</a> is a session manager for tmux that allows defining and managing multiple tmux sessions through project configuration files.</li>
<li>Projects are defined in YAML or JSON files that specify settings such as commands to run, window/pane layouts, and other session settings.</li>
<li>The <code>start</code> command loads and attaches to an existing project, while <code>edit</code> creates or modifies project files.</li>
<li>Projects support environment variable expansion, and command-line arguments passed to <code>start</code> are available as <code>$1</code>, <code>$2</code>, etc., in the project file.</li>
<li>Local project files can be used without specifying a name if they are found by searching parent directories.</li>
<li>Commands like <code>list</code>, <code>remove</code>, and <code>freeze</code> can manage existing projects.</li>
<li><code>Debug</code> prints the tmux commands to create a session without attaching, which is useful for troubleshooting.</li>
<li>Fields like <code>on_start</code> and <code>on_stop</code> allow running commands before or after session startup/shutdown.</li>
<li>Windows and panes have their own settings, such as layouts and commands to run, and they can be nested in the project file.</li>
<li>Airmux aims to provide an easy and organized way to define and manage multiple related tmux sessions through project files.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title>Developing a gitignore crate</title>
      <published>2022-06-09T02:48:03+00:00</published>
      <updated>2022-06-09T02:48:03+00:00</updated>
      <author>Emre Şahin</author>
      <pubDate>Thu, 09 Jun 2022 02:48:03 +0000</pubDate>
      <link>https://emresahin.net/developing-a-gitignore-crate/</link>
      <guid isPermaLink="true">https://emresahin.net/developing-a-gitignore-crate/</guid>
      <description>I needed a file system ignore library for a utility I was writing. This is similar to Git’s .gitignore , but the files containing the rules can have different names, and ignore rules may be defined programmatically. I was using burntsushi’s ignore crate , which in turn uses globset by the same au...</description>
      <category>Rust</category>
      <category>Software Development</category>
      <category>rust</category>
      <category>gitignore</category>
      <category>architecture</category>
      <category>cli</category>
      <category>xvc</category>
      <content:encoded><![CDATA[<p>I needed a file system <em>ignore</em> library for a utility I was writing. This is similar
to Git’s <code>.gitignore</code>, but the files containing the rules can have different
names, and ignore rules may be defined programmatically.</p>
<p>I was using burntsushi’s <a href="https://crates.io/crates/ignore">ignore crate</a>, which in turn uses <a href="https://crates.io/crates/globset">globset</a> by the
same author. In a directory hierarchy, I was first collecting all <code>.ignore</code>
files, then trying to decide which files are ignored. Ripgrep’s <code>from</code>
parameter led me to believe this.</p>
<p>However, it looks like it has a bug that doesn’t consider the source directory of
the ignore file. Git has different semantics when <code>a/.gitignore</code> and
<code>a/b/.gitignore</code> have a line <code>mydir/</code>. In the second case, Git
doesn’t ignore <code>a/mydir/</code>, but Ripgrep seems to consider all elements from all
files as if they are from the root directory.</p>
<p>In practice, most people may be using ignore files in their root directory, but
there are valid use cases (like moving around directories with ignore patterns
in them) that are affected by such behavior.</p>
<p>I decided to tackle this. Ripgrep’s <a href="https://crates.io/crates/globset">globset</a> seems nice and fast, so
it was better to base the solution on top of that. My basic use case is walking
a directory hierarchy. There are solutions to this like <a href="https://docs.rs/walkdir/latest/walkdir/">walkdir</a> or <a href="https://docs.rs/jwalk/latest/jwalk/">jwalk</a>,
but they use ripgrep’s gitignore; hence, they have the same bug.</p>
<p>I usually begin by setting up data structures to understand the problem. This
allows me to understand the domain formally.</p>
<p>First, what are we trying to accomplish? Basically, we want to decide if a path
should be ignored (a) in the results and (b) in the directory traversal.
The gitignore specification not only allows <em>ignores</em>, but also has a way to add
paths to a whitelist to <em>unignore</em>. Hence, deciding whether to ignore a path is
like:</p>
<pre><code class="language-rust">let include = |path| {
    if whitelist_globs.match(path) {
       true
    } else if ignore_globs.match(path) {
       false
    } else {
       true }
}</code></pre>
<p>We also need to decide whether to traverse a directory:</p>
<pre><code class="language-rust">let traverse = |path| {
    if path.is_dir() &amp;&amp; !include(path) { false } else { true }
}</code></pre>
<p>We have the following possible <code>MatchResult</code>s between the ignore rules and a
path:</p>
<pre><code class="language-rust">pub enum MatchResult {
    NoMatch,
    Ignore,
    Whitelist,
}</code></pre>
<p>We need a comparison function to get a <code>MatchResult</code>. One side of the
comparison is <code>path</code>, and the other is a set of <code>GlobSet</code>s from the <a href="https://crates.io/crates/globset">globset</a>
crate. We’ll have two globsets: one for <code>ignores</code> and the other for
<code>whitelists</code>.</p>
<pre><code class="language-rust">pub struct IgnoreRules {
    root: PathBuf,

    patterns: Arc&lt;Vec&lt;Pattern&lt;Glob&gt;&gt;&gt;,

    whitelist_set: Arc&lt;GlobSet&gt;,
    ignore_set: Arc&lt;GlobSet&gt;,
}</code></pre>
<p>The data types for the sets are <code>Arc&lt;GlobSet&gt;</code>, as I planned to make the traversal
concurrent for each directory. However, as only one thread will be modifying
the rules (one <code>.gitignore</code> or <code>.ignore</code> file per directory), I felt no need to
wrap the sets with <code>Mutex</code>.</p>
<p>We also keep track of each pattern separately. <code>patterns</code> is a <code>Pattern</code> vector
that contains all globs individually. One reason is to rebuild the <code>GlobSet</code> as
we traverse directories: since we cannot add any more rules to the sets once they
have been built, we keep the patterns separately to add them to the sets of the
child directories.</p>
<p>Another reason is to keep track of the source, as this can be used for
debugging or reporting, similar to <a href="https://git-scm.com/docs/git-check-ignore"><code>git check-ignore</code></a>.</p>
<p><code>root</code> is the point where this traversal began.</p>
<p>Patterns are defined in a generic way to be instantiated with globs, strings,
regexes, or any other type.</p>
<pre><code class="language-rust">pub struct Pattern&lt;T&gt; {
    pattern: T,
    original: String,
    source: Source,
    effect: PatternEffect,
    relativity: PatternRelativity,
    path_kind: PathKind,
}</code></pre>
<p>The rationale for this generic approach is that the patterns need some kind of
transformation. They can be read from files as strings, then compiled into
globs or regexes.</p>
<pre class="mermaid">flowchart LR
     String--&gt;Ok(Glob)--&gt;Glob--&gt;Regex
</pre>

<p>The <code>source</code> field shows where this pattern was retrieved. It’s an enum with two
possible values for the time being:</p>
<pre><code class="language-rust">enum Source {
    File { path: PathBuf, line: usize },
    Global,
}</code></pre>
<p>The <a href="https://git-scm.com/docs/gitignore">gitignore specification</a> says:</p>
<blockquote>
<p>An optional prefix “!” which negates the pattern; any matching file excluded
by a previous pattern will become included again.</p>
</blockquote>
<p>We use the <code>effect</code> field to tag the pattern as either an <code>Ignore</code> or a
<code>Whitelist</code> pattern.</p>
<pre><code class="language-rust">pub enum PatternEffect {
    Ignore,
    Whitelist,
}</code></pre>
<p>The spec also makes a distinction between files and directories:</p>
<blockquote>
<p>If there is a separator at the end of the pattern then the pattern will only
match directories, otherwise the pattern can match both files and
directories.</p>
</blockquote>
<p>So we use <code>path_kind</code> to reflect this distinction:</p>
<pre><code class="language-rust">pub enum PathKind {
    Any,
    Directory,
}</code></pre>
<p>Some patterns can be relative to the directory they are found in:</p>
<blockquote>
<p>If there is a separator at the beginning or middle (or both) of the pattern,
then the pattern is relative to the directory level of the particular
.gitignore file itself. Otherwise the pattern may also match at any level
below the .gitignore level.</p>
</blockquote>
<p>And we show this with the <code>relativity</code> field:</p>
<pre><code class="language-rust">pub enum PatternRelativity {
    Anywhere,
    RelativeTo { directory: String },
}</code></pre>
<p>Note that we are converting from the specification to Rust types. In the future,
these enums can be extended to cover cases where these patterns are used in
other places, or we can merge some of them to reduce complexity. Currently, this
level of abstraction seems adequate.</p>
<p>To convert <code>Pattern&lt;String&gt;</code> to <code>Pattern&lt;Glob&gt;</code> (or <code>Pattern&lt;Result&lt;Glob, Error&gt;&gt;</code>), or any other type, we can use a <code>map</code> function similar to
<code>Option::map</code>:</p>
<pre><code class="language-rust">    fn map&lt;U, F&gt;(self, f: F) -&gt; Pattern&lt;U&gt;
    where
        F: FnOnce(T) -&gt; U,
    {
        let pat = Pattern::&lt;U&gt; {
            pattern: f(self.pattern),
            original: self.original,
            source: self.source,
            effect: self.effect,
            relativity: self.relativity,
            path_kind: self.path_kind,
        };
        pat
    }</code></pre>
<p><code>map</code> will make it easy to convert between various types of patterns.</p>
<p>Now, we change the perspective to a higher level. Let’s begin by writing a <em>file tree walker.</em></p>
<pre><code class="language-rust">pub fn walk_serial(
    given: IgnoreRules,
    dir: &amp;Path,
    walk_options: &amp;WalkOptions,
    sender: &amp;Sender&lt;WalkerResult&lt;PathMetadata&gt;&gt;,
) -&gt; WalkerResult&lt;()&gt; { todo!() }</code></pre>
<p>This function uses the given <code>IgnoreRules</code> and returns files and directories
via the channel. <code>PathMetadata</code> is just a bundle of <code>PathBuf</code> and its
<code>Metadata</code>.</p>
<pre><code class="language-rust">pub struct PathMetadata {
    path: PathBuf,
    metadata: Metadata,
}</code></pre>
<p><code>WalkOptions</code> will contain various options about this traversal. Currently, it only
has <code>ignore_filename</code> and <code>include_dirs</code>, which set whether only files are sent
through the channel, or if directories should be included too.</p>
<pre><code class="language-rust">pub struct WalkOptions&lt;'a&gt; {
    ignore_filename: Option&lt;&amp;'a str&gt;,
    include_dirs: bool,
}</code></pre>
<p>Initially, <code>IgnoreRules</code> contains only the files that should be ignored (or
whitelisted) globally. When a directory contains a <code>.gitignore</code> file (or any
file name that was set in <code>WalkOptions</code>), it checks for new ignore rules and sends
all files through the channel. If a directory is not ignored, it then calls
<code>walk_serial</code> recursively with it.</p>
<p>The final lines of the function are as follows:</p>
<pre><code class="language-rust">    for child_dir in child_dirs {
        walk_serial(dir_with_ignores.clone(), &amp;child_dir, walk_options, sender)?;
    }</code></pre>
<p>As it calls itself as a final operation after collecting and sending all child
files, this is an example of <em>tail recursion.</em></p>
<p>The output is sent through a channel because our final goal is to make all
this concurrent. Each new directory will create a new thread, and all files
will be collected from the channel. At this point, we are creating a serial
version of the walker with identical data structures to test it easily.</p>
<p>One gotcha using the channels in the serial version is that the channel size must
be enough to contain all file names. Effectively, this means it will be
<code>crossbeam_channel::unbounded</code> for <code>walk_serial</code>. We will (ab)use the channel’s
<code>Sender&lt;PathMetadata&gt;</code> as a <code>Vec&lt;PathMetadata&gt;</code> in <em>no parallel</em> mode. In
parallel settings, using <code>unbounded</code> channels is not recommended; if the
consumers of the channel don’t work as quickly, you may fill up the memory,
especially for longer-running processes.</p>
<p><code>walk_serial</code> will basically do four things:</p>
<ul>
<li>List all the elements in the given directory.</li>
<li>Check if there is a new ignore file and update the rules if necessary.</li>
<li>Filter the elements by ignore rules and send them to the channel.</li>
<li>Run <code>walk_serial</code> recursively for child directories if they are not ignored.</li>
</ul>
<h3 id="read-the-directory">Read the directory</h3>
<p>The first is done using <code>read_dir</code> of <code>Path</code>. It returns a set of elements.
Then the list is processed, and elements’ metadata is retrieved. This requires a
bit of error handling: <code>read_dir</code> may return an <code>Error</code>, each element in the
result can be an <code>Error</code>, and getting <code>metadata</code> may also result in an <code>Error</code>.</p>
<pre><code class="language-rust">    let elements = dir
        .read_dir()
        .map_err(|e| anyhow!("Error reading directory: {:?}, {:?}", dir, e))?;
    let mut child_dirs = Vec::&lt;PathBuf&gt;::new();
    let mut child_paths = Vec::&lt;PathMetadata&gt;::new();

    for entry in elements {
        match entry {
            Err(err) =&gt; sender.send(Err(WalkerError::from(anyhow!(
                "Error reading entry in dir {:?} {:?}",
                dir,
                err
            ))))?,
            Ok(entry) =&gt; match entry.metadata() {
                Err(err) =&gt; sender.send(Err(WalkerError::from(anyhow!(
                    "Error getting metadata {:?} {}",
                    entry,
                    err
                ))))?,
                Ok(md) =&gt; {
                    child_paths.push(PathMetadata {
                        path: entry.path(),
                        metadata: md.clone(),
                    });
                    if md.is_dir() {
                        child_dirs.push(entry.path());
                    }
                }
            },
        }
    }</code></pre>
<p>Note that we collect <code>PathMetadata</code> objects in the <code>child_paths</code> vector. If
there happens to be an error while retrieving, it’s sent to the caller to ignore or
to report to the user.</p>
<p>I usually handle errors first in <code>match</code> expressions because they are usually
more straightforward.</p>
<h3 id="check-if-there-are-new-ignore-rules">Check if there are new ignore rules</h3>
<p>After reading all the files in the directory, it checks if there actually is an
ignore filename in <code>given</code> and that a file exists with that name in the list we
just created. If there is no such file, we can use <code>given</code> rules to decide the
ignored elements.</p>
<p>If there is a file that may change the ignore rules, it reads it and updates
<code>patterns</code>. Then, if there are <em>new</em> ignore or whitelist rules, the respective
globsets are recreated. It checks whether we have new rules because compiling
globs is possibly an expensive operation.</p>
<h3 id="filtering-directory-elements">Filtering directory elements</h3>
<p>Filtering files and directories from the list checks two globsets. If an
element matches a whitelist rule, it’s included in the results without checking
if it <em>also</em> matches ignore rules. Hence, whitelisting has a priority. Otherwise,
it checks the ignore rules, and if they don’t match, the element is included in
the results.</p>
<p>If there are directories matching the ignore rules, they are not traversed, and
any possible <em>whitelisting</em> rule that was specified <em>within</em> that directory is
not taken into consideration.</p>
<h3 id="listing-child-directories">Listing child directories</h3>
<p>We saw how <code>walk_serial</code> calls itself for child directories above. After
sending all results to the channel, it calls itself for the child directories
and repeats.</p>
<h2 id="converting-patterns-from-string-to-glob">Converting patterns from String to Glob</h2>
<p>The <em>interesting</em> part about ignoring files is that we should modify patterns
into globs to add to a <code>GlobSet</code>. We cannot add the patterns directly to a <code>GlobSet</code>
because some of the rules make them <em>context dependent.</em> We parsed the patterns
to get <code>PatternRelativity</code> and <code>PathKind</code>, and these also affect how we add the
patterns to a <code>GlobSet</code>.</p>
<p>The heart of the conversion is a function that receives a <code>String</code>, filters it,
and transforms it to a list of <code>Pattern&lt;Glob&gt;</code>s.</p>
<pre><code class="language-rust">fn content_to_patterns(
    ignore_root: &amp;Path,
    source: Option&lt;&amp;Path&gt;,
    contents: &amp;str,
) -&gt; Vec&lt;Pattern&lt;WalkerResult&lt;Glob&gt;&gt;&gt; {</code></pre>
<p>As we also need to report the line number for patterns, we use <code>enumerate</code> on
the iterator:</p>
<pre><code class="language-rust">    let patterns: Vec&lt;Pattern&lt;WalkerResult&lt;Glob&gt;&gt;&gt; = content
        .lines()
        .enumerate()
        // A line starting with # serves as a comment. Put a backslash ("\") in front of the first hash for patterns that begin with a hash.
        .filter(|(_, line)| !(line.trim().is_empty() || line.starts_with("#")))</code></pre>
<p>The <code>filter</code> line is used to check for blank and comment lines. Then we trim the
trailing space unless the pattern ends with <code>\</code>.</p>
<pre><code class="language-rust">        .map(|(i, line)| {
            if !line.ends_with("\\ ") {
                (i, line.trim_end())
            } else {
                (i, line)
            }
        })</code></pre>
<p>The function receives the source file name as a parameter. We create the
<code>Source</code> using it and the line number:</p>
<pre><code class="language-rust">        .map(|(i, line)| {
            (
                line,
                match source {
                    Some(p) =&gt; Source::File {
                        path: p
                            .strip_prefix(ignore_root)
                            .expect("path must be within ignore_root")
                            .to_path_buf(),
                        line: (i + 1).into(),
                    },
                    None =&gt; Source::Global,
                },
            )
        })</code></pre>
<p>Then we build the pattern object, update the pattern to add it before the globset,
and build the globs. Building the glob may cause errors if it’s not well-formed.
They are handled as well.</p>
<pre><code class="language-rust">        .map(|(line, source)| build_pattern(source, line))
        .map(transform_pattern_for_glob)
        .map(|pc| pc.map(|s| Glob::new(&amp;s).map_err(WalkerError::from)))</code></pre>
<p>Two functions are left that may need attention. One is <code>build_pattern</code> that
converts a pattern line to a <code>Pattern&lt;String&gt;</code> object. The other is the
<code>transform_pattern_for_glob</code> function that modifies <code>Pattern&lt;String&gt;</code> before
building the glob.</p>
<p>After writing <code>walk_serial</code>, we’ll use all the machinery to write a parallel
version of it.</p>
<h3 id="parsing-pattern-strings-for-pattern-objects">Parsing pattern strings for pattern objects</h3>
<p>We create pattern objects from pattern strings following the <a href="https://git-scm.com/docs/gitignore">spec</a>. There are
three basic rules that we will follow:</p>
<ul>
<li>If the pattern starts with <code>!</code>, it’s a whitelist rule.</li>
<li>If the pattern ends with <code>/</code>, it matches only to directories.</li>
<li>If the pattern contains a non-final <code>/</code>, it’s relative to the directory.</li>
</ul>
<p>We’ll also check whether the line starts with <code>/</code> and remove that to prevent
double-slashes <code>//</code> in patterns. The resulting pattern in the object won’t
contain any of the artifacts that change its semantics, and we’ll add them when
building the globsets.</p>
<p>Let’s start by checking whether the line starts with <code>!</code>.</p>
<pre><code class="language-rust">    let begin_exclamation = original.starts_with("!");
    let line = if begin_exclamation || original.starts_with("\\!") {
        original[1..].to_owned()
    } else {
        original.to_owned()
    };</code></pre>
<p>Checking the presence of <code>/</code> at the beginning, at the end, and in intermediate
positions is also straightforward:</p>
<pre><code class="language-rust">    let end_slash = line.ends_with("/");
    let line = if end_slash {
        &amp;line[..line.len() - 1]
    } else {
        line
    };

    let begin_slash = line.starts_with("/");
    let non_final_slash = if line.len() &gt; 0 {
        line[..line.len() - 1].chars().find(|c| *c == '/').is_some()
    } else {
        false
    };</code></pre>
<p>We check the non-final slash separately from the beginning slash because we’ll
remove the initial slash if it exists.</p>
<pre><code class="language-rust">    let line = if begin_slash { &amp;line[1..] } else { line };</code></pre>
<p>Although there is almost 1-1 correspondence between the boolean conditions and
the enums, I create enums in separate statements for <em>self-documenting</em> code.</p>
<pre><code class="language-rust">    let effect = if begin_exclamation {
        PatternEffect::Whitelist
    } else {
        PatternEffect::Ignore
    };

    let path_kind = if end_slash {
        PathKind::Directory
    } else {
        PathKind::Any
    };

    let relativity = if non_final_slash {
        PatternRelativity::RelativeTo {
            directory: current_dir.to_owned(),
        }
    } else {
        PatternRelativity::Anywhere
    };</code></pre>
<p>After building these attributes of the pattern and stripping the pattern from the
initial and final <code>/</code>, and other semantic-changing elements, we build a pattern:</p>
<pre><code class="language-rust">
    let pattern = Pattern::&lt;String&gt; {
        pattern: line.to_owned(),
        original: original.to_owned(),
        source,
        effect,
        relativity,
        path_kind,
    };
</code></pre>
<h2 id="transforming-a-pattern-string-to-a-suitable-glob">Transforming a pattern string to a suitable glob</h2>
<p>Now, another transformation is needed to convert <code>Pattern&lt;String&gt;</code> to
<code>Pattern&lt;Glob&gt;</code>. In this case, we’ll need to recreate the pattern based on the
rules.</p>
<p>The transformation considers <code>relativity</code> and <code>path_kind</code> to generate a glob
that can be added to a <code>GlobSet</code>. Note that the function returns not a
compiled <code>Pattern&lt;Glob&gt;</code>, but a <code>Pattern&lt;String&gt;</code>, because after this
transformation, building the glob requires <code>Glob::new</code>.</p>
<pre><code class="language-rust">fn transform_pattern_for_glob(pattern: Pattern&lt;String&gt;) -&gt; Pattern&lt;String&gt; {
    let anything_anywhere = |p| format!("**/{p}");
    let anything_relative = |p, directory| format!("{directory}/**/{p}");
    let directory_anywhere = |p| format!("**{p}/");
    let directory_relative = |p, directory| format!("{directory}/**/{p}/");

    let transformed_pattern = match (&amp;pattern.path_kind, &amp;pattern.relativity) {
        (PathKind::Any, PatternRelativity::Anywhere) =&gt; anything_anywhere(pattern.pattern),
        (PathKind::Any, PatternRelativity::RelativeTo { directory }) =&gt; {
            anything_relative(pattern.pattern, directory)
        }
        (PathKind::Directory, PatternRelativity::Anywhere) =&gt; directory_anywhere(pattern.pattern),
        (PathKind::Directory, PatternRelativity::RelativeTo { directory }) =&gt; {
            directory_relative(pattern.pattern, directory)
        }
    };

    Pattern {
        pattern: transformed_pattern,
        ..pattern
    }
}</code></pre>
<p>I could merge the closures into the <code>match</code> and lower the number of lines, but adding
the closures increased <em>self-documentation</em>. Instead of multiple <code>if/else</code>
branches, I tend to use <code>match</code> with tuples to have a clearer picture. The
<em>corresponding</em> <code>if/else</code> code without closures would be like:</p>
<pre><code class="language-rust">
let transformed_pattern = if pattern.path_kind == PathKind::Any {
    if pattern.relativity == PatternRelativity::Anywhere {
        format!("**/{p}")
        } else if ...
        }
...</code></pre>
<p>And in my opinion, this latter format is more error-prone and less readable.</p>
<h2 id="combining-it-all-together">Combining it all together</h2>
<p>At this point, we can revisit <code>content_to_patterns</code> again:</p>
<pre><code class="language-rust linenos hl_lines=[&quot;13-14&quot;]">fn content_to_patterns(
    ignore_root: &amp;Path,
    source: Option&lt;&amp;Path&gt;,
    content: &amp;str,
) -&gt; Vec&lt;Pattern&lt;Result&lt;Glob&gt;&gt;&gt; {
    watch!(source);
    let patterns: Vec&lt;Pattern&lt;Result&lt;Glob&gt;&gt;&gt; = content
        .lines()
        .enumerate()
        ...
        .map(|(line, source)| build_pattern(source, line))
        .map(transform_pattern_for_glob)
        .map(|pc| pc.map(|s|
            Glob::new(&amp;s).map_err(Error::from)))
        .collect();

    patterns
}
</code></pre>
<p>The last step in the transformation is creating the <code>Glob</code> object, as in the
highlighted lines above. It runs <code>Pattern&lt;String&gt;.map</code> with <code>Glob::new</code> to get
a <code>Pattern&lt;Result&lt;Glob&gt;&gt;</code>. <code>Error</code> and <code>Result</code> are custom types defined within
the crate using the <a href="https://crates.io/crates/thiserror/latest/thiserror/"><code>thiserror</code></a> crate.</p>
<p>After we have a vector of <code>Pattern&lt;Result&lt;Glob&gt;&gt;</code>, we report the errors in
<code>clear_glob_errors</code> to get <code>Vec&lt;Pattern&lt;Glob&gt;&gt;</code> from this vector. The function is defined as:</p>
<pre><code class="language-rust linenos">fn clear_glob_errors(
    sender: &amp;Sender&lt;Result&lt;PathMetadata&gt;&gt;,
    new_patterns: Vec&lt;Pattern&lt;Result&lt;Glob&gt;&gt;&gt;,
) -&gt; Vec&lt;Pattern&lt;Glob&gt;&gt; {
    let new_glob_patterns: Vec&lt;Pattern&lt;Glob&gt;&gt; = new_patterns
        .into_iter()
        .filter_map(|p| match p.transpose() {
            Ok(p) =&gt; Some(p),
            Err(e) =&gt; {
                sender
                    .send(Err(Error::from(anyhow!("Error in glob pattern: {:?}", e))))
                    .expect("Error in channel");
                None
            }
        })
        .collect();
    new_glob_patterns
}</code></pre>
<p>The <code>sender</code> is the same channel that we use to send the results. This
communication channel is used to report <em>only the errors</em>, not <code>PathMetadata</code>;
hence, when we encounter an <code>Err(e)</code> in <code>new_patterns</code>, it’s reported to the same
channel that the results are expected. Handling the errors first seems like a
common theme here as well.</p>
<p>This is also one of the reasons <code>walk_serial</code> receives a channel instead of a
vector. Our usual use case is traversing all the directories in parallel, and
without channel-based communication in the serial version, we would need to
duplicate this function.</p>
<p>At this point, I completed the <code>walk_serial</code> functionality. I wrote tests. I’m using the
<a href="https://crates.io/crates/test-case"><code>test-case</code></a> crate for writing many tests encompassing different use cases.
I’m omitting them here for brevity.</p>
<h2 id="writing-a-parallel-walker">Writing a Parallel Walker</h2>
<p>Converting <code>walk_serial</code> to a parallel one is quite trivial. As we already used
channels in the serial version, we have all the components of a parallel
walker. For the sake of simplicity, I won’t use a thread pool and instead spawn a new
thread for each <em>task.</em> One question is what’s the unit task that we’ll use to
create tasks.</p>
<p><a href="https://docs.rs/jwalk/latest/jwalk/">jwalk</a> uses directories as points to spawn new tasks. That’s a sensible
approach, and I’ll use this as well. This means a single directory will be
processed by a single thread, even if it contains one million files, and 10
directories will be processed by 10 threads even if each contains one file. In
practice, I think we can assume <em>directories</em> are natural chunks for this
problem.</p>
<p>The interface for the parallel walker is identical to the serial version. This is
to replace one with the other when the need arises.<sup class="footnote-reference" id="fr-1-1"><a href="#footnote-1">1</a></sup></p>
<p>The only difference is in the final loop. <code>walk_serial</code> calls itself
recursively at the end, while <code>walk_parallel</code> calls itself in <em>separate threads</em>.</p>
<pre><code class="language-rust">
pub fn walk_parallel(
    given: IgnoreRules,
    dir: &amp;Path,
    walk_options: WalkOptions,
    sender: Sender&lt;Result&lt;PathMetadata&gt;&gt;,
) -&gt; Result&lt;()&gt; {
    ....
    crossbeam::scope(|s| {
        for child_dir in child_dirs {
            let dwi = dir_with_ignores.clone();
            let walk_options = walk_options.clone();
            let sender = sender.clone();
            s.spawn(move |_| walk_parallel(dwi, &amp;child_dir.path, walk_options, sender));
        }
    })
    .expect("Error in crossbeam scope in walk_parallel");

    Ok(())
}

</code></pre>
<h2 id="code">Code</h2>
<p>The code is soon to be released, and I’ll add a link to it. You can check <a href="https://github.com/iesahin">my
GitHub profile</a> if I forget.</p>
<hr>
<ol class="footnote-definition">
<li id="footnote-1">
<p>Update: I decided to change the <code>walk_serial</code> interface to use <code>Vec</code>
instead of <code>Sender</code> for simplicity. <a href="#fr-1-1">↩</a></p>
</li>
</ol>]]></content:encoded>
    </item>
    <item>
      <title>TIL: Recursive vs. Non-recursive Mappings in Vim</title>
      <published>2021-01-04T18:43:07+00:00</published>
      <updated>2021-01-04T18:43:07+00:00</updated>
      <author>Emre Şahin</author>
      <pubDate>Mon, 04 Jan 2021 18:43:07 +0000</pubDate>
      <link>https://emresahin.net/til-7/</link>
      <guid isPermaLink="true">https://emresahin.net/til-7/</guid>
      <description>The difference between imap / inoremap , vmap / vnoremap , and nmap / nnoremap is the recursive definition. When we map using nmap w dd , the original function of w is lost. We cannot remap nmap v w and expect the functionality of the original w . A Vim Guide for Intermediate Users</description>
      <category>Vim</category>
      <category>map</category>
      <category>noremap</category>
      <category>commands</category>
      <category>recursive</category>
      <category>CLI</category>
      <content:encoded><![CDATA[<p>The difference between <code>imap</code>/<code>inoremap</code>, <code>vmap</code>/<code>vnoremap</code>, and <code>nmap</code>/<code>nnoremap</code> is the recursive definition. When we map using <code>nmap w dd</code>, the original function of <code>w</code> is lost. We cannot remap <code>nmap v w</code> and expect the functionality of the original <code>w</code>.</p>
<p><a href="https://thevaluable.dev/vim-intermediate/">A Vim Guide for Intermediate Users</a></p>]]></content:encoded>
    </item>
    <item>
      <title>TIL: Enabling Spell Checking in Vim</title>
      <published>2020-12-26T01:07:26+00:00</published>
      <updated>2020-12-26T01:07:26+00:00</updated>
      <author>Emre Şahin</author>
      <pubDate>Sat, 26 Dec 2020 01:07:26 +0000</pubDate>
      <link>https://emresahin.net/til-8/</link>
      <guid isPermaLink="true">https://emresahin.net/til-8/</guid>
      <description>This command enables spell checking: :setlocal spell spelllang=en_us This enables the spell option and configures it to check for US English. Unrecognized words are highlighted with one of the following: SpellBad word not recognized |hl-SpellBad| SpellCap word not capitalised |hl-SpellCap| SpellR...</description>
      <category>Vim</category>
      <category>Spell Check</category>
      <category>English</category>
      <category>CLI</category>
      <content:encoded><![CDATA[<p>This command enables spell checking:</p>
<pre><code>:setlocal spell spelllang=en_us
</code></pre>
<p>This enables the <code>spell</code> option and configures it to check for US English.</p>
<p>Unrecognized words are highlighted with one of the following:</p>
<pre><code class="language-text">	SpellBad	word not recognized			|hl-SpellBad|
	SpellCap	word not capitalised			|hl-SpellCap|
	SpellRare	rare word				|hl-SpellRare|
	SpellLocal	wrong spelling for selected region	|hl-SpellLocal|
</code></pre>
<p>Vim only checks for spelling; it does not perform grammar checks.</p>]]></content:encoded>
    </item>
    <item>
      <title>Creating AWS S3 buckets from the command line</title>
      <published>2020-06-11T16:29:14+00:00</published>
      <updated>2020-06-11T16:29:14+00:00</updated>
      <author>Emre Şahin</author>
      <pubDate>Thu, 11 Jun 2020 16:29:14 +0000</pubDate>
      <link>https://emresahin.net/creating-aws-s3-buckets-from-command-line/</link>
      <guid isPermaLink="true">https://emresahin.net/creating-aws-s3-buckets-from-command-line/</guid>
      <description>If you have the necessary AWS credentials: export AWS_ACCESS_KEY_ID="XXXX" export AWS_SECRET_ACCESS_KEY="YYYY" you can install the AWS CLI with: pip3 install --user awscli and create an S3 bucket from the command line without using a web browser: aws s3api create-bucket --acl public-read --bucket...</description>
      <category>Cloud</category>
      <category>AWS</category>
      <category>S3</category>
      <category>CLI</category>
      <category>DevOps</category>
      <content:encoded><![CDATA[<p>If you have the necessary AWS credentials:</p>
<pre><code class="language-sh">export AWS_ACCESS_KEY_ID="XXXX"
export AWS_SECRET_ACCESS_KEY="YYYY"
</code></pre>
<p>you can install the AWS CLI with:</p>
<pre><code class="language-sh">pip3 install --user awscli
</code></pre>
<p>and create an S3 bucket from the command line without using a web browser:</p>
<pre><code class="language-sh">aws s3api create-bucket --acl public-read --bucket my-unique-bucket-name --region eu-central-1 --create-bucket-configuration LocationConstraint=eu-central-1
</code></pre>
<p>This will create a publicly readable bucket at <code>http://my-unique-bucket-name.s3.amazonaws.com</code>.</p>
<p>Then you can copy your files with:</p>
<pre><code class="language-sh">aws s3 cp my-file.zip s3://my-unique-bucket-name/subdir/my-file.zip
</code></pre>
<p>and share the link <code>http://my-unique-bucket-name.s3.amazonaws.com/subdir/my-file.zip</code> with anyone you want.</p>]]></content:encoded>
    </item>
    <item>
      <title>TIL May 1</title>
      <published>2020-05-01T22:19:07+00:00</published>
      <updated>2020-05-01T22:19:07+00:00</updated>
      <author>Emre Şahin</author>
      <pubDate>Fri, 01 May 2020 22:19:07 +0000</pubDate>
      <link>https://emresahin.net/til-may-1--17222/</link>
      <guid isPermaLink="true">https://emresahin.net/til-may-1--17222/</guid>
      <description>Nota seems like a nice command-line calculator. It converts what you type into ASCII art formulas. In[1]: 10 + 10 Out[1]: 20.0 _____ In[2]: ╲╱ 100 Out[2]: 10.0 ┌ ┐ In[3]: Max │ 10 , 1 , 21 , -3 │ └ ┘ Out[3]: 21.0 In[4]: ⟨Emre's Number⟩ ≡ 79 Out[4]: 79.0 _______________ In[5]: ╲╱ Emre's Number Out...</description>
      <category>TIL</category>
      <category>Nota</category>
      <category>CLI</category>
      <category>Calculator</category>
      <category>Haskell</category>
      <category>Intention</category>
      <category>Productivity</category>
      <category>Git</category>
      <category>Bash</category>
      <category>Zsh</category>
      <content:encoded><![CDATA[<ul>
<li><a href="https://kary.us/nota/">Nota</a> seems like a nice command-line calculator. It converts what you type into ASCII art formulas.</li>
</ul>
<pre><code>  In[1]: 10 + 10

 Out[1]: 20.0


           _____
  In[2]: ╲╱ 100

 Out[2]: 10.0


             ┌                  ┐
  In[3]: Max │ 10 , 1 , 21 , -3 │
             └                  ┘

 Out[3]: 21.0


  In[4]: ⟨Emre's Number⟩ ≡  79

 Out[4]: 79.0


           _______________
  In[5]: ╲╱ Emre's Number

 Out[5]: 8.888194417315589


                      2
  In[6]: Emre's Number

 Out[6]: 6241.0


                      Emre's Number
  In[7]: Emre's Number

 Out[7]: 8.1759873707105095e149

</code></pre>
<pre><code>It looks a bit heavy for a CLI calculator, as it is written in Haskell and downloads 100+ MB of libraries, but when you need ASCII art to display your calculations or want to use spaces in variable names, it may prove useful.
</code></pre>
<ul>
<li>
<p>I began to use <a href="https://www.getintention.com/">Intention</a> to limit my Twitter time. It allows you to set a limited time (1, 5, 10, or 15 minutes) for yourself and tracks the total time you spend on <em>addictive sites.</em> When this total time is lower than your goal for a period, you get a streak. It looks visually and psychologically nicer than LeechBlock.</p>
</li>
<li>
<p>I read the <a href="http://www.git-scm.com/book/en/v2/Git-Tools-Reset-Demystified"><code>git reset</code></a> section in the Git book. It details how <code>git reset</code> behaves with its <code>--soft</code>, <code>--mixed</code>, and <code>--hard</code> parameters. The first resets only the <code>HEAD</code>; the second resets both the index and <code>HEAD</code>; and the third resets the working tree and copies files back from the current <code>HEAD</code> to the working tree.</p>
<p>One important point: Contrasting <code>git checkout master</code> and <code>git reset master</code>: the first moves <code>HEAD</code> to the <code>master</code> branch, while the second moves the current branch to <code>master</code>.</p>
<p><code>git reset</code> can also be used to squash commits. Basically, you <code>git reset --mixed</code> to an earlier commit like <code>HEAD~3</code> and recommit. This creates a new commit, taking <code>HEAD~3</code> as the parent and skipping <code>HEAD~2</code> and <code>HEAD~1</code>, resulting in a new <code>HEAD</code>.</p>
</li>
<li>
<p><a href="https://github.com/dylanaraps/pure-bash-bible">Here</a> are many useful pure Bash functions to be used in scripts. I’m a Zsh person, but writing Bash scripts is more portable, of course.</p>
</li>
</ul>]]></content:encoded>
    </item>
  </channel>
</rss>
